* 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:
@@ -299,6 +299,13 @@ interface EmitOk {
|
||||
summary: {
|
||||
waves: number;
|
||||
plans: number;
|
||||
/**
|
||||
* #3302 — the number of record-agent entries the orchestrator must end up
|
||||
* with in WAVE_WORKTREE_MANIFEST after the run (plans with `use_worktree`
|
||||
* not `false`). The loud count check: fewer entries than this after the
|
||||
* Workflow run means metadata capture failed and the wave must HALT.
|
||||
*/
|
||||
worktreePlans: number;
|
||||
stagesByWave: string[][][]; // wave → stage → planId[]
|
||||
resumeRunId: string;
|
||||
budgetTokens: number | null;
|
||||
@@ -564,14 +571,44 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
// #3302: the generated script must hand the per-agent executor results back
|
||||
// to the orchestrator so it can feed the wave merge chain. Emitted right
|
||||
// after the header comments (meta stays the first statement): a helper that
|
||||
// extracts the executor's <worktree_metadata> JSON (agents/gsd-executor.md
|
||||
// <worktree_metadata_capture>) from one agent() result, plus the outcomes
|
||||
// accumulator the stage barriers below push into. `metadata` is null when
|
||||
// the result carried no parseable block — the LOUD-failure input the
|
||||
// orchestrator halts on for expects_worktree plans (never a silent skip).
|
||||
lines.push('// #3302: extract the executor-returned <worktree_metadata> JSON so the');
|
||||
lines.push('// orchestrator can record it into WAVE_WORKTREE_MANIFEST after the run');
|
||||
lines.push('// (worktree.record-agent -> worktree.cleanup-wave, the same manifest-scoped');
|
||||
lines.push('// merge chain inline dispatch feeds). null = absent/unparseable/interrupted.');
|
||||
lines.push('function gsdWorktreeMetadata(agentResult) {');
|
||||
lines.push(' if (typeof agentResult !== \'string\') return null;');
|
||||
lines.push(' const m = agentResult.match(/<worktree_metadata>([\\s\\S]*?)<\\/worktree_metadata>/);');
|
||||
lines.push(' if (m === null) return null;');
|
||||
lines.push(' try {');
|
||||
lines.push(' const parsed = JSON.parse(m[1]);');
|
||||
lines.push(' return (parsed !== null && typeof parsed === \'object\') ? parsed : null;');
|
||||
lines.push(' } catch (e) {');
|
||||
lines.push(' return null;');
|
||||
lines.push(' }');
|
||||
lines.push('}');
|
||||
lines.push('const gsdAgentOutcomes = [];');
|
||||
lines.push('');
|
||||
|
||||
const stagesByWave: string[][][] = [];
|
||||
let totalPlans = 0;
|
||||
let worktreePlans = 0;
|
||||
|
||||
for (let wi = 0; wi < waves.length; wi++) {
|
||||
const wave = waves[wi];
|
||||
const stages = partitionStages(wave.plans);
|
||||
stagesByWave.push(stages);
|
||||
totalPlans += wave.plans.length;
|
||||
for (const p of wave.plans) {
|
||||
if (p.use_worktree !== false) worktreePlans += 1;
|
||||
}
|
||||
|
||||
lines.push('// Wave ' + wave.id);
|
||||
// Title must match this wave's meta.phases entry EXACTLY.
|
||||
@@ -587,17 +624,37 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
|
||||
// threw "parallel() expects an array of functions" (#2590). Passing
|
||||
// agent() results directly would also start every agent eagerly, before
|
||||
// parallel() could bound concurrency.
|
||||
lines.push('await parallel([');
|
||||
// #3302: capture the barrier's resolved results (one per thunk, in thunk
|
||||
// order — the documented parallel() contract) so each plan's outcome can
|
||||
// be tagged and returned below. Discarding them stranded every
|
||||
// worktree-wf_* branch: the merge chain had no input (#3302).
|
||||
lines.push('const gsdStage_' + wi + '_' + si + ' = await parallel([');
|
||||
for (const p of stagePlans) {
|
||||
lines.push(' () => agent(' + quoteString(p.brief) + ', ' + agentOptions(p, executorModel) + '),');
|
||||
}
|
||||
lines.push('])');
|
||||
// Positional tagging is decided at EMIT time from the validated manifest,
|
||||
// so attribution survives out-of-order completion and needs no runtime
|
||||
// introspection. expects_worktree mirrors agentOptions' own per-plan
|
||||
// decision (use_worktree !== false).
|
||||
lines.push('gsdAgentOutcomes.push(');
|
||||
for (let pi = 0; pi < stagePlans.length; pi++) {
|
||||
const p = stagePlans[pi];
|
||||
const tail = pi < stagePlans.length - 1 ? ',' : '';
|
||||
lines.push(' { plan: ' + quoteString(p.id) + ', expects_worktree: ' + (p.use_worktree !== false) + ', metadata: gsdWorktreeMetadata(gsdStage_' + wi + '_' + si + '[' + pi + ']) }' + tail);
|
||||
}
|
||||
lines.push(')');
|
||||
}
|
||||
if (wi < waves.length - 1) lines.push('');
|
||||
}
|
||||
|
||||
lines.push('// Each agent writes SUMMARY.md on its worktree branch; commits land there');
|
||||
lines.push('// and are merged by the orchestrator exactly as in inline wave dispatch.');
|
||||
// #3302: the script's top-level return value is what the Workflow tool hands
|
||||
// back to the orchestrator. One { plan, expects_worktree, metadata } entry
|
||||
// per dispatched plan; the orchestrator records every worktree entry via
|
||||
// `gsd_run query worktree.record-agent` and HALTS on a null metadata entry
|
||||
// for an expects_worktree plan (see the execute:wave:pre fragment) — a
|
||||
// silently-empty manifest is the exact #3302 failure mode.
|
||||
lines.push('return gsdAgentOutcomes');
|
||||
|
||||
const script = lines.join('\n');
|
||||
|
||||
@@ -607,6 +664,9 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
|
||||
summary: {
|
||||
waves: waves.length,
|
||||
plans: totalPlans,
|
||||
// #3302: the number of record-agent entries the orchestrator must end up
|
||||
// with in WAVE_WORKTREE_MANIFEST after the run — the loud count check.
|
||||
worktreePlans,
|
||||
stagesByWave,
|
||||
resumeRunId: runId,
|
||||
budgetTokens,
|
||||
|
||||
Reference in New Issue
Block a user