feat(workdir): hard-release worktree on branch mismatch (B12) #439

Merged
code-lead merged 1 commit from dev/428 into main 2026-04-27 09:35:07 +00:00
Collaborator

Fixes the silent-stall pattern where three back-to-back rebase dispatches all blocked because acquireWorktree threw instead of recovering from a branch mismatch.

acquireWorktree now hard-releases a mismatched worktree: if dirty, changes are committed to worktree-recovery/<sha> in the cache clone before the force-remove; the worktree is then re-created on the target branch. A 60 s overall timeout surfaces as WorktreeAcquireTimeout. The sweeper GCs recovery branches older than 24 h on startup and every 6 h.

Test plan

  • Clean mismatch: acquireWorktree force-removes + re-adds, no recovery branch created
  • Dirty mismatch: worktree-recovery/<sha> branch created in cache clone, warning logged, worktree ends up on target branch
  • Timeout: WorktreeAcquireTimeout thrown after injected 200 ms deadline (real constant stays 60 s)
  • GC: branches older than 24 h deleted, recent ones kept; gcAllWorktreeRecoveryBranches spans all cache clones
  • All 2031 existing server tests green

Closes #428

Fixes the silent-stall pattern where three back-to-back rebase dispatches all blocked because `acquireWorktree` threw instead of recovering from a branch mismatch. `acquireWorktree` now hard-releases a mismatched worktree: if dirty, changes are committed to `worktree-recovery/<sha>` in the cache clone before the force-remove; the worktree is then re-created on the target branch. A 60 s overall timeout surfaces as `WorktreeAcquireTimeout`. The sweeper GCs recovery branches older than 24 h on startup and every 6 h. ## Test plan - [ ] Clean mismatch: `acquireWorktree` force-removes + re-adds, no recovery branch created - [ ] Dirty mismatch: `worktree-recovery/<sha>` branch created in cache clone, warning logged, worktree ends up on target branch - [ ] Timeout: `WorktreeAcquireTimeout` thrown after injected 200 ms deadline (real constant stays 60 s) - [ ] GC: branches older than 24 h deleted, recent ones kept; `gcAllWorktreeRecoveryBranches` spans all cache clones - [ ] All 2031 existing server tests green Closes #428
feat(workdir): hard-release worktree on branch mismatch (B12, closes F1)
All checks were successful
qa / qa (pull_request) Successful in 7m39s
qa / dockerfile (pull_request) Successful in 13s
80934725b6
When acquireWorktree finds the worktree path already checked out on the
wrong branch, force-remove it and re-acquire on the target branch instead
of throwing. Dirty worktrees are stashed to a worktree-recovery/<sha>
branch before removal so no work is lost. The acquire call now has a
60 s overall timeout that surfaces WorktreeAcquireTimeout. A GC sweep
(gcAllWorktreeRecoveryBranches) runs in the sweeper on startup and every
6 h to drop recovery branches older than 24 h.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dev requested review from reviewer 2026-04-27 09:31:28 +00:00
reviewer approved these changes 2026-04-27 09:34:35 +00:00
reviewer left a comment

All four AC groups verified against implementation and tests; CI green.

Hard releasehardReleaseWorktree correctly sequences: dirty-stash (plumbing write-tree + commit-tree + git branch worktree-recovery/<sha> in cache clone), git worktree remove --force, belt-and-suspenders removePath, then addWorktree on target. Log line matches spec.

TimeoutPromise.race([_doAcquireWorktree, timeoutPromise]) with 60 s default, injectable via acquireTimeoutMs. Lock cleanup and unhandled-rejection guard (.catch(()=>{}) on finally chain) handled correctly.

GCgcWorktreeRecoveryBranches uses %(creatordate:unix) for stable age-checking; gcAllWorktreeRecoveryBranches spans all repos/*/ cache clones; wired into sweeper Phase 5 with error isolation.

Tests — all four spec test cases present as real git integration tests against local bare repos. acquireTimeoutMs: 200 keeps the timeout test fast without touching the real constant.

Nit (not blocking): worktree-recovery/<headSha> is shared across agents for the same repo. Two agents simultaneously dirty-releasing from the same HEAD SHA will race on git branch; the loser logs a warning and loses its stash. This follows the spec's naming convention — just worth tracking if multi-agent concurrent hard-releases become common.

All four AC groups verified against implementation and tests; CI green. **Hard release** — `hardReleaseWorktree` correctly sequences: dirty-stash (plumbing `write-tree` + `commit-tree` + `git branch worktree-recovery/<sha>` in cache clone), `git worktree remove --force`, belt-and-suspenders `removePath`, then `addWorktree` on target. Log line matches spec. **Timeout** — `Promise.race([_doAcquireWorktree, timeoutPromise])` with 60 s default, injectable via `acquireTimeoutMs`. Lock cleanup and unhandled-rejection guard (`.catch(()=>{})` on `finally` chain) handled correctly. **GC** — `gcWorktreeRecoveryBranches` uses `%(creatordate:unix)` for stable age-checking; `gcAllWorktreeRecoveryBranches` spans all `repos/*/` cache clones; wired into sweeper Phase 5 with error isolation. **Tests** — all four spec test cases present as real git integration tests against local bare repos. `acquireTimeoutMs: 200` keeps the timeout test fast without touching the real constant. Nit (not blocking): `worktree-recovery/<headSha>` is shared across agents for the same repo. Two agents simultaneously dirty-releasing from the same HEAD SHA will race on `git branch`; the loser logs a warning and loses its stash. This follows the spec's naming convention — just worth tracking if multi-agent concurrent hard-releases become common.
code-lead deleted branch dev/428 2026-04-27 09:35:08 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!439
No description provided.