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

@@ -553,7 +553,12 @@ increases monotonically across waves. `{status}` is `complete` (success),
EXPECTED_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "${USE_WORKTREES_FOR_PLAN:-true}" != "false" ] && [ -z "${WAVE_WORKTREE_MANIFEST:-}" ]; then
WAVE_WORKTREE_MANIFEST=$(mktemp "${TMPDIR:-/tmp}/gsd-worktree-wave-XXXXXX.json")
printf '{"worktrees":[]}\n' > "$WAVE_WORKTREE_MANIFEST"
# Persist the dispatch-time orchestrator worktree root so wave-cleanup can pin back to the
# orchestrator's OWN worktree — NOT `git worktree list`'s first entry (always the main
# checkout), which pins a non-primary (per-phase lane) orchestrator off its branch (#630).
# Dispatch runs from the orchestrator's lane, so show-toplevel here is the correct root.
ORCH_ROOT=$(git rev-parse --show-toplevel)
ORCH_ROOT="$ORCH_ROOT" MANIFEST="$WAVE_WORKTREE_MANIFEST" node -e 'const fs=require("fs");fs.writeFileSync(process.env.MANIFEST,JSON.stringify({orchestrator_root:process.env.ORCH_ROOT||null,worktrees:[]})+"\n")'
export WAVE_WORKTREE_MANIFEST
fi
```
@@ -761,10 +766,15 @@ increases monotonically across waves. `{status}` is `complete` (success),
exit 1
}
# Guard: pin cleanup back to the primary worktree and fail on branch drift (#3174).
PRIMARY_WT=$(git worktree list --porcelain | awk '/^worktree /{print substr($0,10); exit}')
# Guard: pin cleanup back to the orchestrator's OWN worktree and fail on branch drift (#3174, #630).
# Resolve from the dispatch-time orchestrator root persisted in the manifest — NOT `git worktree
# list`'s first entry, which is always the main checkout and would pin a non-primary (per-phase
# lane) orchestrator off its own branch, tripping the #3174 assertion below (#630). Byte-identical
# for a primary orchestrator (its root IS the first entry); the fallback covers pre-#630 manifests.
PRIMARY_WT=$(MANIFEST="$WAVE_WORKTREE_MANIFEST" node -e 'const fs=require("fs");try{const j=JSON.parse(fs.readFileSync(process.env.MANIFEST,"utf8"));if(j&&j.orchestrator_root)process.stdout.write(String(j.orchestrator_root))}catch(e){}')
[ -n "$PRIMARY_WT" ] || PRIMARY_WT=$(git worktree list --porcelain | awk '/^worktree /{print substr($0,10); exit}')
if [ -z "$PRIMARY_WT" ]; then
echo "FATAL: could not resolve primary worktree before cleanup" >&2
echo "FATAL: could not resolve orchestrator worktree before cleanup" >&2
exit 1
fi
if [ -n "$PRIMARY_WT" ] && [ "$(pwd -P 2>/dev/null)" != "$(cd "$PRIMARY_WT" 2>/dev/null && pwd -P)" ]; then echo "⚠ Orchestrator CWD drifted to $(pwd) — pinning to $PRIMARY_WT before worktree cleanup (#3174)"; cd "$PRIMARY_WT" || { echo "FATAL: cannot cd to primary worktree $PRIMARY_WT" >&2; exit 1; }; fi
@@ -780,8 +790,11 @@ increases monotonically across waves. `{status}` is `complete` (success),
If the orchestrator deviated from the standard wave merge path (e.g., custom inter-worktree base-update merges with `merge: bring …` style messages), run this snippet after the custom merges are complete. It reads only `WAVE_WORKTREE_MANIFEST`; do not discover unrelated `worktree-agent-*` worktrees.
```bash
# Cleanup-tail: pin orchestrator CWD to primary worktree before cleanup-tail (#3174).
PRIMARY_WT=$(git worktree list --porcelain | awk '/^worktree /{print substr($0,10); exit}')
# Cleanup-tail: pin orchestrator CWD to its OWN worktree before cleanup-tail (#3174, #630).
# Same fix as the templated path: resolve the dispatch-time orchestrator root from the manifest,
# not `git worktree list`'s first entry (always the main checkout — wrong for a lane orchestrator).
PRIMARY_WT=$(MANIFEST="$WAVE_WORKTREE_MANIFEST" node -e 'const fs=require("fs");try{const j=JSON.parse(fs.readFileSync(process.env.MANIFEST,"utf8"));if(j&&j.orchestrator_root)process.stdout.write(String(j.orchestrator_root))}catch(e){}')
[ -n "$PRIMARY_WT" ] || PRIMARY_WT=$(git worktree list --porcelain | awk '/^worktree /{print substr($0,10); exit}')
if [ -n "$PRIMARY_WT" ] && [ "$(pwd -P 2>/dev/null)" != "$(cd "$PRIMARY_WT" 2>/dev/null && pwd -P)" ]; then echo "⚠ Orchestrator CWD drifted to $(pwd) — pinning to $PRIMARY_WT before cleanup-tail (#3174)"; cd "$PRIMARY_WT" || { echo "FATAL: cannot cd to primary worktree $PRIMARY_WT" >&2; exit 1; }; fi
# Cleanup-tail: remove residual agent worktrees after a cross-wave-dependency deviation.
# Uses only the current wave manifest to avoid touching unrelated active agents (#3384).