From 9540fe43b974da8e8e83fed2810d0832b8dbf2f8 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 16 Jun 2026 14:19:08 -0400 Subject: [PATCH] fix: have executor self-report worktree metadata (#1349) --- .changeset/1297-worktree-metadata.md | 6 ++++++ agents/gsd-executor.md | 22 ++++++++++++++++++++++ gsd-core/workflows/execute-phase.md | 3 ++- tests/agent-size-baseline.json | 2 +- tests/workflow-size-baseline.json | 2 +- tests/worktree-cleanup.test.cjs | 23 +++++++++++++++++++++++ 6 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 .changeset/1297-worktree-metadata.md diff --git a/.changeset/1297-worktree-metadata.md b/.changeset/1297-worktree-metadata.md new file mode 100644 index 000000000..c4cb0c5ae --- /dev/null +++ b/.changeset/1297-worktree-metadata.md @@ -0,0 +1,6 @@ +--- +type: Fixed +pr: 1349 +--- + +**Parallel worktree execution now has executor-authored cleanup metadata** — executor agents capture their worktree path, branch, and expected base before task commits and return a parseable metadata block for execute-phase to prefer over runtime harness metadata. (#1297) diff --git a/agents/gsd-executor.md b/agents/gsd-executor.md index 90739fc76..d86e4a990 100644 --- a/agents/gsd-executor.md +++ b/agents/gsd-executor.md @@ -103,6 +103,24 @@ PLAN_START_EPOCH=$(date +%s) ``` + +If running inside a git worktree, capture authoritative worktree identity before +any task commit changes HEAD. The execute-phase orchestrator consumes this from +your final `` return block to build the wave cleanup manifest +without relying on runtime harness metadata (#1297). + +```bash +GSD_WORKTREE_PATH="" +GSD_WORKTREE_BRANCH="" +GSD_WORKTREE_EXPECTED_BASE="" +if [ -f .git ]; then + GSD_WORKTREE_PATH=$(git rev-parse --show-toplevel) + GSD_WORKTREE_BRANCH=$(git rev-parse --abbrev-ref HEAD) + GSD_WORKTREE_EXPECTED_BASE=$(git rev-parse HEAD) +fi +``` + + ```bash grep -n "type=\"checkpoint" [plan-path] @@ -761,6 +779,10 @@ into the user's project history. **Tasks:** {completed}/{total} **SUMMARY:** {path to SUMMARY.md} + +{"agent_id":"{phase}-{plan}","worktree_path":"${GSD_WORKTREE_PATH:-}","branch":"${GSD_WORKTREE_BRANCH:-}","expected_base":"${GSD_WORKTREE_EXPECTED_BASE:-}"} + + **Commits:** - {hash}: {message} - {hash}: {message} diff --git a/gsd-core/workflows/execute-phase.md b/gsd-core/workflows/execute-phase.md index f220d8840..1685336d4 100644 --- a/gsd-core/workflows/execute-phase.md +++ b/gsd-core/workflows/execute-phase.md @@ -635,6 +635,7 @@ increases monotonically across waves. `{status}` is `complete` (success), this commit — the orchestrator force-removes the worktree after you return, and any uncommitted SUMMARY.md will be permanently lost (#2070). REQUIRED ORDER: Write SUMMARY.md → commit → only then any narration. No text between Write and commit (truncation risk; #2070 rescue is not primary defense). + @@ -682,7 +683,7 @@ increases monotonically across waves. `{status}` is `complete` (success), ) ``` - Immediately after each worktree `Agent()` spawn returns metadata, atomically append `{agent_id, worktree_path, branch, expected_base}` to `WAVE_WORKTREE_MANIFEST`. If any field is missing, stop and ask for recovery instead of scanning all agent worktrees. + After each `Agent()` returns, parse executor-returned worktree metadata (``) before harness metadata, then atomically append `{agent_id, worktree_path, branch, expected_base}` to `WAVE_WORKTREE_MANIFEST`. Missing: stop and ask for recovery instead of scanning worktrees. > **Worktree recovery policy (#48 + #1292):** See `execute-phase/steps/worktree-recovery-policy.md` — FAIL-CLOSED rule for base/HEAD-namespace mismatches AND isolated-run fail-safe recovery. diff --git a/tests/agent-size-baseline.json b/tests/agent-size-baseline.json index e320d007c..0c96ebb56 100644 --- a/tests/agent-size-baseline.json +++ b/tests/agent-size-baseline.json @@ -14,7 +14,7 @@ "gsd-domain-researcher.md": 6938, "gsd-eval-auditor.md": 7761, "gsd-eval-planner.md": 7008, - "gsd-executor.md": 42519, + "gsd-executor.md": 43343, "gsd-framework-selector.md": 6778, "gsd-integration-checker.md": 15148, "gsd-intel-updater.md": 18122, diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index 13d07faf6..258c964a0 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -24,7 +24,7 @@ "docs-update.md": 54770, "edit-phase.md": 12883, "eval-review.md": 9923, - "execute-phase.md": 93122, + "execute-phase.md": 93157, "execute-plan.md": 31365, "explore.md": 10497, "extract-learnings.md": 12849, diff --git a/tests/worktree-cleanup.test.cjs b/tests/worktree-cleanup.test.cjs index 5a87ef393..19f45cb53 100644 --- a/tests/worktree-cleanup.test.cjs +++ b/tests/worktree-cleanup.test.cjs @@ -684,6 +684,29 @@ describe('bug #3384: worktree cleanup workflow contracts', () => { assert.doesNotMatch(content, /done < <\(git worktree list --porcelain \| grep "\^worktree " \| grep "\\\.claude\/worktrees\/agent-"/); }); + test('#1297 gsd-executor self-reports authoritative worktree metadata', () => { + const content = fs.readFileSync(EXECUTOR_AGENT_PATH, 'utf8'); + assert.match(content, //); + assert.match(content, /git rev-parse --show-toplevel/); + assert.match(content, /git rev-parse --abbrev-ref HEAD/); + assert.match(content, /GSD_WORKTREE_EXPECTED_BASE=\$\(git rev-parse HEAD\)/); + assert.match(content, //); + assert.match(content, /"worktree_path":/); + assert.match(content, /"branch":/); + assert.match(content, /"expected_base":/); + }); + + test('#1297 execute-phase consumes executor-returned worktree metadata before harness metadata', () => { + const content = fs.readFileSync(EXECUTE_PHASE_PATH, 'utf8'); + assert.match(content, //); + assert.match(content, /executor-returned worktree metadata/i); + assert.match(content, /harness metadata/i); + assert.ok( + content.indexOf('executor-returned worktree metadata') < content.indexOf('harness metadata'), + 'execute-phase must prefer executor-returned worktree metadata before runtime harness metadata (#1297)' + ); + }); + test('quick contract requires a cleanup manifest instead of global worktree discovery', () => { const content = fs.readFileSync(QUICK_PATH, 'utf8'); assert.match(content, /WAVE_WORKTREE_MANIFEST|QUICK_WORKTREE_MANIFEST/);