fix(#3302): return per-agent worktree metadata from workflow scripts (#3450)

* fix(#3302): return per-agent worktree metadata from workflow scripts

* fix(#3302): link changeset fragment to pr 3450

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-14 09:28:22 -04:00
committed by GitHub
parent 29c27e948b
commit 9dd240f634
5 changed files with 366 additions and 14 deletions

View File

@@ -168,10 +168,79 @@ worktree isolation applied PER PLAN from the manifest's `use_worktree` field
Omitting it from the tool invocation silently regresses phase-resume to a
no-op: an interrupted phase re-runs completed plans.
The orchestrator still runs steps 4–5.8 (wait for completion, worktree cleanup,
post-merge gate, tracking update) exactly as it does for inline dispatch — the
Workflow backend only replaces HOW agents are spawned for this wave, not what
happens after they return.
### After the run: manifest bridge into the merge chain (#3302)
The single Workflow tool call replaces step 3's per-plan `Agent()` loop — which also
means step 3's manifest bookkeeping (creation + per-agent recording) does NOT happen on
this path. The orchestrator MUST bridge the run's per-agent results into the SAME
manifest-scoped merge chain inline dispatch uses, before steps 4–5.8, which then run
unchanged:
1. **Create the manifest BEFORE invoking the tool** (this is step 3's creation block,
which this path skips). When ANY plan in the wave has `use_worktree` not `false`:
```bash
if [ -z "${WAVE_WORKTREE_MANIFEST:-}" ]; then
M=$(mktemp "${TMPDIR:-/tmp}/gsd-worktree-wave-XXXXXX") && mv "$M" "$M.json" && WAVE_WORKTREE_MANIFEST="$M.json" || exit 1 # XXXXXX must be path-final on BSD/macOS (#1520)
# Persist the dispatch-time orchestrator worktree root so wave-cleanup pins back
# to the orchestrator's OWN worktree (#630), exactly as inline dispatch does.
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
```
2. **Invoke the Workflow tool with the emitted script and
`resumeFromRunId: summary.resumeRunId`.** The script top-level `return`s one entry
per dispatched plan: `{ plan, expects_worktree, metadata }`. `metadata` is that
plan's executor `<worktree_metadata>` JSON (`{agent_id, worktree_path, branch,
expected_base}` — captured by the executor itself per
`agents/gsd-executor.md`), or `null` when the agent's result carried none
(interrupted agent, resumed-from-cache plan, or a non-worktree plan).
3. **Record every worktree plan** exactly as inline dispatch does at step 3's
"After each `Agent()` returns" — one `worktree.record-agent` per returned entry
with `expects_worktree: true` and complete metadata:
```bash
gsd_run query worktree.record-agent --manifest "$WAVE_WORKTREE_MANIFEST" \
--agent-id "<metadata.agent_id>" --path "<metadata.worktree_path>" \
--branch "<metadata.branch>" --base "<metadata.expected_base>" \
--files "<plan files_modified, space-separated>"
```
The verb's write-strict validation applies as inline: on a non-zero exit or any
missing field, stop and ask for recovery — do not append an under-populated entry.
4. **HALT on uncapturable metadata — never a silently-empty manifest (#3302).**
After recording, the manifest must hold one entry per `expects_worktree: true`
outcome (`summary.worktreePlans` from `resolve-wave-dispatch` is the expected
count). Any shortfall — a `null` `metadata`, a missing/empty field, or a count
mismatch — means commits are stranded on their `worktree-wf_*` branches and
`worktree.cleanup-wave` would merge nothing while the phase looks green. STOP the
phase with the failing plan id and the recovery hint below; do NOT run
`worktree.cleanup-wave` and do NOT proceed to step 4.
**Recovery hint:** the unmerged `worktree-wf_*` branch still holds the work. Recover
the missing metadata from the run's per-agent result journal (`journal.jsonl` — one
`{"type":"result",…}` line per agent — in the Workflow run's transcript dir), re-run
`worktree.record-agent` by hand, then re-run cleanup. If the journal cannot be
recovered either, merge the branch manually after review — never discard it.
5. **Resume (`resumeFromRunId`).** Cached/resumed agents do not re-emit their final
messages, so a previously-completed plan can return with `metadata: null`. Recover
that plan's metadata from the ORIGINAL run's journal (same hint as above). If it
cannot be recovered, fail loudly per rule 4 — a resumed run must never report
success over silently-dropped agent work.
6. **Non-worktree plans** (`expects_worktree: false` — `use_worktree: false` in the
manifest): they ran without isolation; their commits are already on the main working
tree. No record-agent entry, no manifest write.
With the manifest populated, steps 4–5.8 (wait/completion bookkeeping, step 5.5's
manifest-scoped `worktree.cleanup-wave`, post-merge gate, tracking update) run
UNCHANGED — the Workflow backend replaces HOW agents are spawned and returns their
metadata; the merge chain itself is the inline path's own, now with real input.
**If `backend == "inline"`** (any gate miss, or `resolve-wave-dispatch` itself
unavailable/erroring): proceed to step 3's standard per-message `Agent()`