fix(#630): pin wave-cleanup to orchestrator root via manifest, not worktree-list first-entry (#643)

* fix(#630): pin wave-cleanup to orchestrator root via manifest, not list first-entry

Follow-up to #590. #590 fixed the dispatch-side orchestrator cwd anchor
(ORCHESTRATOR_WT via git rev-parse --show-toplevel) but the two
wave-cleanup guards still resolved PRIMARY_WT from `git worktree list
--porcelain`'s first entry — always the main checkout. An orchestrator
running from a non-primary (per-phase lane) worktree was therefore cd'd
off its own lane at cleanup, tripping the #3174 branch-drift assertion
(ORCH_BRANCH != EXPECTED_BRANCH) and refusing merge-back — the same
failure #590 set out to fix, surviving on the cleanup side.

Persist the dispatch-time orchestrator root (show-toplevel, captured from
the lane the orchestrator dispatches from) into WAVE_WORKTREE_MANIFEST as
`orchestrator_root`, and resolve PRIMARY_WT from it at both cleanup sites.
The git-worktree-list first entry survives only as a guarded fallback for
pre-#630 manifests. Byte-identical for a primary orchestrator (its root
IS the first entry); unblocks the non-primary-orchestrator topology.

Regression test (bug-630-...): behaviorally proves the pivot by running
the shipped manifest-reader one-liner against a real non-primary-worktree
git topology — it resolves to the lane while first-entry resolves to main
— plus contract assertions. Updates the #3425 worktree-cleanup contract
tests to the new manifest-based resolution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#630): add changeset for wave-cleanup orchestrator-root fix

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#630): canonicalize paths with realpathSync.native for Windows 8.3 parity

On Windows the CI runner's os.tmpdir() yields an 8.3 short name (RUNNER~1)
while `git worktree list` reports the long form (runneradmin); plain
realpathSync preserved each input's form, so the first-entry/main sanity
comparison mismatched. Canonicalize both sides (and the reader output)
via fs.realpathSync.native, which reconciles 8.3 and long forms. Test-only;
the shipped manifest reader is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-03 08:07:16 -04:00
committed by GitHub
parent 56a815c722
commit c89197972e
4 changed files with 207 additions and 9 deletions

View File

@@ -706,8 +706,11 @@ describe('bug #3384: worktree cleanup workflow contracts', () => {
test('#3425: helper cleanup path pins orchestrator CWD to primary worktree and checks EXPECTED_BRANCH', () => {
const content = fs.readFileSync(EXECUTE_PHASE_PATH, 'utf8');
assert.match(content, /PRIMARY_WT=\$\(git worktree list --porcelain \| awk '\/\^worktree \/\{print substr\(\$0,10\); exit\}'\)/);
assert.match(content, /if \[ -z "\$PRIMARY_WT" \]; then\s+echo "FATAL: could not resolve primary worktree before cleanup" >&2\s+exit 1\s+fi/);
// #630: the orchestrator root is now resolved from the manifest's orchestrator_root; the
// git-worktree-list first entry survives only as a guarded fallback for pre-#630 manifests.
assert.match(content, /PRIMARY_WT=\$\(MANIFEST="\$WAVE_WORKTREE_MANIFEST" node -e '[^']*orchestrator_root[^']*'\)/);
assert.match(content, /\[ -n "\$PRIMARY_WT" \] \|\| PRIMARY_WT=\$\(git worktree list --porcelain \| awk '\/\^worktree \/\{print substr\(\$0,10\); exit\}'\)/);
assert.match(content, /if \[ -z "\$PRIMARY_WT" \]; then\s+echo "FATAL: could not resolve orchestrator worktree before cleanup" >&2\s+exit 1\s+fi/);
assert.match(content, /cd "\$PRIMARY_WT" \|\| \{ echo "FATAL: cannot cd to primary worktree \$PRIMARY_WT" >&2; exit 1; \}/);
assert.match(content, /ORCH_BRANCH=\$\(git rev-parse --abbrev-ref HEAD\)/);
assert.match(content, /FATAL: orchestrator on '\$ORCH_BRANCH' but expected '\$EXPECTED_BRANCH' before worktree cleanup — refusing to merge \(#3174-class drift\)/);
@@ -718,7 +721,9 @@ test('#3425: helper cleanup path pins orchestrator CWD to primary worktree and c
test('#3425: cleanup-tail snippet carries the same primary-worktree pin before removal', () => {
const content = fs.readFileSync(EXECUTE_PHASE_PATH, 'utf8');
assert.match(content, /Cleanup-tail: pin orchestrator CWD to primary worktree before cleanup-tail \(#3174\)\./);
assert.match(content, /Cleanup-tail: pin orchestrator CWD to its OWN worktree before cleanup-tail \(#3174, #630\)\./);
// #630: cleanup-tail resolves the orchestrator root from the manifest, with first-entry fallback.
assert.match(content, /PRIMARY_WT=\$\(MANIFEST="\$WAVE_WORKTREE_MANIFEST" node -e '[^']*orchestrator_root[^']*'\)/);
assert.match(content, /FATAL: cannot cd to primary worktree \$PRIMARY_WT/);
assert.match(content, /# Cleanup-tail: remove residual agent worktrees after a cross-wave-dependency deviation\./);
});