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/);