Skip to content

fix: let a merge follow-up take over the worktree that already holds its PR branch - #4239

Merged
atomantic merged 1 commit into
mainfrom
cos/task-mstolkdd/agent-1e904405
Aug 15, 2026
Merged

fix: let a merge follow-up take over the worktree that already holds its PR branch#4239
atomantic merged 1 commit into
mainfrom
cos/task-mstolkdd/agent-1e904405

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

A review-loop/merge follow-up must run in a worktree attached to its PR branch. When another worktree already has that branch checked out, git worktree add refuses and the task takes the timed worktree-busy pause added in #4227 — correct when the holder is a cleanup seconds from teardown, useless when it is a tree removeWorktree REFUSED to delete (uncommitted changes). Nothing is coming to release that branch, so the follow-up burns its five waits, blocks as worktree-failed, and the PR it exists to land sits open forever. That is what stranded PR #4216.

Two fixes:

Adopt the holder instead of waiting it out. findAdoptableWorktreeForBranch (new, worktreeManager.js) locates the worktree holding a branch and refuses every holder PortOS doesn't own outright — adoption MOVES the directory, so it rejects the primary checkout, anything outside data/cos/worktrees/, a human claim-* tree, a locked worktree, and any tree belonging to a running or paused agent (a paused agent's tree is deliberately preserved as resume context and is absent from the in-process maps). The protected-id set unions the in-process maps with persisted running/paused status, the same definition the daily worktree reap uses, so a run that survived a server restart still counts. An unreadable agent list fails closed to the existing pause rather than reading as "nothing is running".

Stop the follow-up losing the branch it is pointed at. updateTask strips existingBranch on a terminal status unless the task is a follow-up — keyed on resumedFromAgentId. But resumePointerMetadata stamps that key onto any task whose run left work behind, so a follow-up that fails once looks exactly like a resume from then on and gets stripped. It would then cut a worktree fresh off main and push its fix to a branch the PR never heard of. resolveTaskExistingBranch falls back to reviewLoopPRBranch — the same value under the follow-up's own namespace, never treated as a resume pointer.

Test plan

  • server/services/worktreeManager.test.js — 7 new tests for findAdoptableWorktreeForBranch: finds the CoS tree holding the branch; returns null when nothing holds it; refuses the primary checkout and any tree outside the managed root; refuses a claim-* tree; refuses a running agent's tree; refuses a locked worktree; returns null rather than throwing when the listing fails.
  • server/services/agentWorkspacePrep.test.js — adoption instead of pausing; fallback to the pause when adoption is refused; no holder lookup when the task has no branch to attach to; running and paused agents both protected; refuses to adopt at all when the agent list can't be read; plus 4 tests for resolveTaskExistingBranch including a stripped follow-up attaching to its PR branch.
  • Full server suite green: 1378 files, 16432 tests passed (services/ run clean end to end).

…its PR branch

A review-loop/merge follow-up must run attached to its PR branch. When some
other worktree already has that branch checked out, `git worktree add` refuses
and the task pauses on a cooldown — correct when the holder is a cleanup
seconds from teardown, useless when it is a tree `removeWorktree` REFUSED to
delete (uncommitted changes). Nothing is coming to release that branch, so the
follow-up burns its five waits, blocks as `worktree-failed`, and the pull
request it exists to land sits open forever (sys-rl-msto5qab, PR #4216).

Adopt the holder instead. `findAdoptableWorktreeForBranch` locates the tree
holding the branch and refuses every holder PortOS doesn't own outright —
adoption MOVES the directory, so the primary checkout, anything outside
data/cos/worktrees/, a human `claim-*` tree, a locked worktree, and any tree
belonging to a running OR paused agent are all rejected (paused agents keep
their tree as resume context and are absent from the in-process maps). An
unreadable agent list fails closed to the existing pause rather than reading as
"nothing is running".

Also fixes the follow-up losing the branch it is pointed at. `updateTask`
strips `existingBranch` on a terminal status unless the task is a follow-up,
keyed on `resumedFromAgentId` — but `resumePointerMetadata` stamps that key
onto ANY task whose run left work behind, so a follow-up that fails once looks
exactly like a resume from then on and gets stripped. It would then cut a
worktree fresh off main and push its fix to a branch the PR never heard of.
`resolveTaskExistingBranch` falls back to `reviewLoopPRBranch`, the same value
under the follow-up's own namespace, which is never treated as a resume pointer.
@atomantic
atomantic merged commit 88c4977 into main Aug 15, 2026
7 checks passed
@atomantic
atomantic deleted the cos/task-mstolkdd/agent-1e904405 branch August 15, 2026 02:58
atomantic added a commit that referenced this pull request Aug 15, 2026
## Summary

Consolidates the "which worktrees may PortOS touch" and "what branch
does this task target" logic that was independently duplicated across
`worktreeManager.js`, `branchReconcile.js`, `agentWorkspacePrep.js`,
`agentWorktreeCleanup.js`, `agentManagement.js`, and `cosTaskStore.js`
into two pure, unit-tested modules:

- `server/lib/worktreeOwnership.js` — `worktreeOwnershipReason()` is the
single ownership gate for destructive worktree operations
(adopt/reap/cleanup), taking explicit options for the differences that
are intentional (a reaper may include `.claude/worktrees/`; stale
`/claim` worktrees may be reclaimed only by branch reconciliation;
liveness requirements differ by caller).
- `server/lib/taskTargetBranch.js` — `resolveTaskTargetBranch()` /
`shouldStripTaskTargetBranch()` is the single reader/writer contract for
a task's `existingBranch` (retry) vs `reviewLoopPRBranch` (review-loop
follow-up) pointer, closing a gap where three call sites read
`metadata.existingBranch` raw and so weren't healed the same way
`agentWorkspacePrep.js`'s resolver already was.

Also collapses `prepareAgentWorkspace`'s two-step "try create, then
discover the holder on failure" flow into resolving the branch holder
once up front (pointer if still valid, else discovery), giving a resume
retry the same safe worktree-takeover behavior a review-loop follow-up
already had, and retiring the `branchStillClaimed` fail-open.

Follow-up from #4239's review; addresses #4241.

Also includes a `/simplify` pass: dropped `worktreeOwnershipReason`'s
unused `allowLocked` option and the unexercised bare-string root form
(no caller ever used either), and parallelized `prepareAgentWorkspace`'s
independent base-branch-detection and branch-holder-resolution I/O
instead of awaiting them serially.

One deferred cleanup filed as #4284 (out of scope here — touches an
externally-asserted test vocabulary): `reapMergedWorktrees`'s inline
remap from the shared module's canonical `worktree-*` skip-reason codes
back to a private legacy vocabulary.

## Test plan

- `cd server && NODE_ENV=test npx vitest run
lib/worktreeOwnership.test.js lib/taskTargetBranch.test.js
services/agentManagement.test.js services/agentWorkspacePrep.test.js
services/branchReconcile.test.js services/cleanupAgentWorktree.test.js
services/cosTaskStore.test.js services/worktreeManager.test.js` — 624
tests passing
- Manually traced `isAbandonedAgentWorktree`,
`worktreeProtectionReason`, and `resolveLiveOwnerReason` against their
pre-refactor implementations to confirm the shared predicate preserves
each of their previously-intentional differences (locked handling,
stale-claim reclaim, liveness-unknown fail-closed behavior)
- Manually traced `spawnReviewLoopFollowUp` to confirm
`reviewLoopFollowUp`/`reviewLoopPRBranch` are still both stamped so
`resolveTaskTargetBranch`'s fallback still resolves a follow-up's branch
after the `existingBranch` write was removed
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