fix: stop CoS agent worktree branches from tracking the default branch - #4182
Conversation
#4172) `git worktree add -b <branch> <path> origin/main` does not leave the new branch untracked: with git's default `branch.autoSetupMerge`, branching off a remote-tracking ref records `branch.<name>.merge = refs/heads/main`. `/do:pr` deliberately derives its push destination from that config (`git push $REMOTE HEAD:$MERGE`) so a branch whose upstream is named differently still pushes to the right ref — which here resolves to `HEAD:refs/heads/main` and lands the agent's commits straight on main, reporting success while no PR is ever opened. Neither existing carve-out fires: the branch HAS an upstream, and the remote is a real remote rather than `.`. Every new-branch `worktree add` now passes `--no-track`, and a new `lib/branchUpstreamGuard.js` asserts the invariant after the fact — an agent branch's upstream is either absent or names its own ref. A foreign upstream is dropped and logged loudly (which also repairs branches created before this fix and re-attached by a review-loop agent); it throws only if the repair does not take, because at that point every downstream push helper is still aimed at the wrong ref.
…closed on an unreadable upstream (#4172) Both review passes landed on the same gap: enforceSafeBranchUpstream throws AFTER `git worktree add` has already succeeded, and that throw was outside the `cleanupOrphanBranch` catch that only wraps the add itself — so a refusal returned a failed create while leaving a registered worktree (and, for a fresh `-b` add, an orphan branch) on disk. Worst on the persistent feature-agent path: it lives outside WORKTREES_DIR, so no reaper sweeps it, its only caller does not catch, and a retry's add then fails 'already exists' until a human prunes. enforceUpstreamOrUndoAdd now removes the tree on refusal, deleting the branch only where this add created it. Second finding: readBranchUpstream collapsed a failed config read into '', which reads as 'no upstream' — so a wedged git or an unreadable repo would wave through a branch that really does track main, at the one moment the guard most needs to hold. It now distinguishes '' (unset, exit 1) from null (could not read), isSafeBranchUpstream rejects null, and the guard refuses rather than guessing. Mirrors the sentinel-and-validate rule in CLAUDE.md.
|
Review pass complete — 1. The guard's throw could strand a worktree. 2. Both reviewers confirmed no flow relied on the auto-set upstream (the only Also closed the coverage gap Affected suites: 137 passed. CI is green on the rerun — the earlier |
|
Round 2 (confirmation pass on the fix commit):
Reviewers satisfied. Merging once CI finishes. |
Summary
A CoS agent worktree branch was being created with
branch.<name>.merge = refs/heads/main. That is not a stray--trackanywhere in PortOS — it is git's defaultbranch.autoSetupMerge, which auto-tracks whenever a new branch is cut from a remote-tracking ref, andgit worktree add -b <branch> <path> origin/mainis exactly that shape.It matters because
/do:prdeliberately derives its push destination from that config rather than from the local branch name (git push "$PUSH_REMOTE" "HEAD:$PUSH_BRANCH"), so a branch whose upstream is named differently still pushes to the right ref. Withmerge=refs/heads/mainthe same guard resolves toHEAD:refs/heads/mainand pushes the agent's work straight to main, reporting success while the branch is never published and no PR is opened. Neither existing carve-out catches it: the branch has an upstream, and the remote is a real remote, not..Fixed at the creation sites, per the issue's decision:
--no-trackon every new-branchgit worktree add—createWorktree(CoS agent worktrees) andcreatePersistentWorktree's new-branch arm. The--track -b … origin/<branchName>arm is left alone: it tracks the branch's own ref, which is correct.server/lib/branchUpstreamGuard.js— the assertion the issue asked for, as a reusable guard. The invariant is "an agent branch's upstream is either absent or names its own ref" (deliberately broader than "is it the default branch" — any foreign ref is a mis-aimed push, main is just the worst instance). It is repair-then-verify rather than refuse: it drops the bogus upstream and logs loudly, and throws only if the repair does not take. Repair matters because branches created before this fix keep their bad upstream, and a review-loop agent re-attaching to one would inherit it — so the guard also runs on the existing-branch attach path.agentWorkspacePrep.js— the JIRA feature-branch bootstrap runs the same check.checkout -boff a local branch does not auto-track under git's default, but a repo configuredbranch.autoSetupMerge=alwaysrecords the base branch as the new branch's upstream.Acceptance criteria: a freshly created agent worktree branch now has no upstream ✓;
git push "$(git config --get branch.$BR.remote)" "HEAD:$(git config --get branch.$BR.merge)"cannot land on main ✓ (executed as a test, below); the config shape is covered by tests ✓.Test plan
server/lib/branchUpstreamGuard.test.jsruns against real git repositories in a temp dir with a real bare remote — the whole premise is a git default, and a mockedexecGitwould only assert what the test author believedgit configprints.worktree add -b … origin/mainand asserts git recordsrefs/heads/main, so the--no-trackfix is demonstrably load-bearing; the paired test shows--no-trackleaves it empty.origin/mainis unmoved andorigin/<branch>carries the commit.mergewith noremote, which--unset-upstreamrefuses to clear).server/services/worktreeManager.test.jsadds wiring coverage: the add carries--no-trackbefore-b, the guard fires on both the new-branch and existing-branch paths, and a healthy branch is left alone.Full server suite: 28453 passed. Five files failed under parallel load (
setup-data-drift,chiptuneRender,settings.secretsStrip,imageGenQuota,privacyNeverFederates,imageGen.*) with 10s-timeout errors; all 8 pass when re-run in isolation, and none touch this code.Closes #4172