Skip to content

fix: stop branch-reconcile probing origin on a repo that has none - #6080

Merged
atomantic merged 1 commit into
mainfrom
cos/task-mtlon5dg/agent-4fe6e23c
Sep 3, 2026
Merged

fix: stop branch-reconcile probing origin on a repo that has none#6080
atomantic merged 1 commit into
mainfrom
cos/task-mtlon5dg/agent-4fe6e23c

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

The server log was emitting an unactionable error on a loop:

❌ branch-reconcile: git ls-remote origin failed — remote branch state unknown this cycle

Root cause. gatherBranchState resolves origin and computes hasOrigin, then fired git ls-remote --heads origin in its read fan-out regardless of that verdict. reconcile() gates both of its own remote reads on hasOrigin precisely because "probing one every cycle would log a failure forever" — the gather never got that gate.

The caller that made it visible is the read-only leftover-branch detector (userActionDetectors.js:75), which calls gatherBranchState once for every managed app. An app's repoPath can be a directory that was never a clone (a static-site folder, a checkout whose remote was never added), so ls-remote exited 128 there on every detector cycle — for a repo with no remote to read and zero candidate branches to judge. gatherBranchState doesn't throw on a non-repo (it returns 0 inputs), so the detector's own try/catch never fired and nothing named the offending app.

What changed

  • gatherBranchState gates the remote read on the hasOrigin it already has — the same gate reconcile() applies at both of its remote-reading sites. null there is the established "could not ask" value that reconcile() already passes explicitly for an origin-less repo, so the sole consumer (upstreamGone) is unaffected: it requires false for both senses of null, and an origin-less repo has no origin/* upstream to begin with. Fail-safe direction — nothing reads as "shipped", nothing gets reaped.
  • The failure line now names the repo and git's own reason. One log line serves three call sites across every managed app; a bare "ls-remote failed" told you neither which repo went unread nor whether it was a network blip, an unauthenticated remote, or no remote at all:
    ❌ branch-reconcile: git ls-remote origin failed in <repo> (fatal: 'origin' does not appear to be a git repository) — remote branch state unknown this cycle
    
    A rejected execGit (30s timeout, spawn failure) and a non-zero exit previously collapsed into the same message; they are now distinguishable.

Test plan

  • Two new tests, each verified failing before the fix:
    • gatherBranchState "skips the remote probe on a repo with no origin" — asserts no ls-remote spawn and no error log (failed pre-fix: ls-remote was called).
    • reapOrphanedRemotes "names the repo and git's own reason when the remote cannot be read" (failed pre-fix: the line contained neither).
  • server/services/branchReconcile.test.js + userActionDetectors.test.js: 156 passed.
  • Full npm test (server + client): 1908 files / 38506 tests passed, 0 failures.
  • Verified against the live instance: calling gatherBranchState on the real non-git app path no longer logs, PortOS's own gather still resolves its branches, and a direct listRemoteHeads call still reports — now naming the repo and the cause.

https://claude.ai/code/session_01FBgckWpLV6qzytK9vKnVvU

The read-only leftover-branch detector calls gatherBranchState once per
managed app, and an app's repoPath can be a directory that was never a
clone (a static-site folder, a checkout whose remote was never added).
gatherBranchState resolved origin and computed hasOrigin, then ran
`git ls-remote --heads origin` anyway — so every detector cycle logged
`❌ branch-reconcile: git ls-remote origin failed` for that app, for a
repo with no remote to read and no candidate branches to judge. It does
not throw on a non-repo either, so the detector's own try/catch never
fired and nothing named the app.

Gate the remote read on the hasOrigin the gather already has, matching
the gate reconcile() applies at both of its own remote reads for exactly
this reason. `null` there is the established "could not ask" value that
reconcile() already passes explicitly for an origin-less repo.

Also name the repo and git's own reason in the failure line: one log
line serves three call sites across every managed app, so a bare
"ls-remote failed" told you neither which repo went unread nor whether
it was a network blip, an unauthenticated remote, or no remote at all.

Claude-Session: https://claude.ai/code/session_01FBgckWpLV6qzytK9vKnVvU
@atomantic
atomantic merged commit 062a83a into main Sep 3, 2026
7 checks passed
@atomantic
atomantic deleted the cos/task-mtlon5dg/agent-4fe6e23c branch September 3, 2026 15:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant