fix(#2285): wire claude-orchestration Workflow backend into execute-phase (#2314)

The claude-orchestration capability (#1143) shipped registered 'active'
but fully inert: detectWorkflowBackend/emitWorkflowScript had no caller
outside their own CLI router, and execute-phase.md declared an
execute:wave:pre hook point that the workflow body never rendered — so
claude_orchestration.enabled:true had zero effect on real runs.

Approach B (maintainer-chosen):
- execute-phase.md now renders the execute:wave:pre hook
  (gsd_run loop render-hooks execute:wave:pre) at a new step 2.75,
  immediately before each wave's Agent() dispatch — fixing the latent
  dead-hook gap for any pre-wave capability.
- Move the claude-orchestration contribution execute:wave:post ->
  execute:wave:pre (a pre-wave backend selector belongs before dispatch,
  not after); rename fragments/execute-wave-post.md -> execute-wave-pre.md
  with prose instructing the orchestrator to call resolve-wave-dispatch
  before step 3. Unrelated wave:post contributions (ui.safety-gate, drift,
  external-job, mempalace) untouched.
- New .cts seam resolveWaveDispatch(input) composes detectWorkflowBackend
  + emitWorkflowScript into one {backend:'inline'|'workflow', ...} result;
  exposed as gsd-tools claude-orchestration resolve-wave-dispatch. This is
  a real non-CLI-router, non-test caller of both functions.

Fail-closed: any gate miss (disabled, non-Claude runtime, Workflow tool
absent, SDK below floor, execution_backend:inline, malformed input) or an
emit failure resolves to inline with a byte-identical result shape — no
regression to the default-off execute-phase path.

Regression tests (tests/fix-2285-*) cover happy-path activation + SDK-floor
BVA, the fail-closed gate-miss table with detectWorkflowBackend parity, a
fast-check composition property, capability.json contribution assertions,
and a source-contract guard that execute:wave:pre is now actually rendered.
Dependent registry-shape assertions updated in-scope.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-15 19:18:28 -04:00
committed by GitHub
parent 75bedf16fd
commit ff9cb6069f
35 changed files with 1486 additions and 203 deletions

View File

@@ -475,6 +475,105 @@ describe('emitWorkflowScript', () => {
});
});
// ─── 3.5. Per-plan use_worktree (#2772 / #2285 finding 1) ─────────────────────
//
// The Workflow backend must NEVER force worktree isolation on a plan the
// inline path (execute-phase.md step 2.5's USE_WORKTREES_FOR_PLAN) keeps out
// of worktrees — e.g. a submodule-touching plan, where the executor commit
// protocol cannot correctly handle submodule commits inside an isolated
// worktree. `use_worktree` is the per-plan signal that threads that decision
// into the emitted script.
describe('emitWorkflowScript — per-plan use_worktree (#2772 / #2285 finding 1)', () => {
test('[happy] use_worktree omitted (default) -> isolation: "worktree" (backward-compatible default)', () => {
const r = emitWorkflowScript(singleWaveManifest());
assert.strictEqual(r.ok, true);
assert.match(r.script, /agent\("Implement the foo module", \{ agentType: "gsd-executor", isolation: "worktree" \}\)/);
});
test('[happy] use_worktree: true explicit -> isolation: "worktree" (same as default)', () => {
const r = emitWorkflowScript({
phaseDir: '.p', runId: 'r',
waves: [{ id: 'w1', plans: [{ id: 'p1', brief: 'b', files_modified: ['a.cts'], use_worktree: true }] }],
});
assert.strictEqual(r.ok, true);
assert.match(r.script, /agent\("b", \{ agentType: "gsd-executor", isolation: "worktree" \}\)/);
});
test('[negative] use_worktree: false -> isolation OMITTED entirely for that plan', () => {
const r = emitWorkflowScript({
phaseDir: '.p', runId: 'r',
waves: [{ id: 'w1', plans: [{ id: 'p1', brief: 'submodule plan', files_modified: ['vendor/lib.c'], use_worktree: false }] }],
});
assert.strictEqual(r.ok, true);
assert.match(r.script, /agent\("submodule plan", \{ agentType: "gsd-executor" \}\)/);
assert.ok(!/agent\("submodule plan"[^)]*isolation/.test(r.script), 'isolation must not appear for this plan\'s agent() call');
});
test('[happy] mixed wave: one worktree plan + one non-worktree plan in the SAME parallel() batch — each carries its own isolation independently', () => {
const r = emitWorkflowScript({
phaseDir: '.p', runId: 'r',
waves: [{ id: 'w1', plans: [
{ id: 'p1', brief: 'normal plan', files_modified: ['src/a.ts'] },
{ id: 'p2', brief: 'submodule plan', files_modified: ['vendor/b.c'], use_worktree: false },
] }],
});
assert.strictEqual(r.ok, true);
// Both plans have disjoint files_modified -> coalesce into ONE parallel() stage.
assert.strictEqual(r.summary.stagesByWave[0].length, 1, 'non-overlapping plans share one stage');
assert.match(r.script, /agent\("normal plan", \{ agentType: "gsd-executor", isolation: "worktree" \}\)/);
assert.match(r.script, /agent\("submodule plan", \{ agentType: "gsd-executor" \}\)/);
assert.ok(!/agent\("submodule plan"[^)]*isolation/.test(r.script), 'the submodule plan must never gain isolation from being batched with a worktree plan');
});
test('[negative] use_worktree with a non-boolean value -> ok:false (strict typing, no silent coercion)', () => {
const r = emitWorkflowScript({
phaseDir: '.p', runId: 'r',
waves: [{ id: 'w1', plans: [{ id: 'p1', brief: 'b', files_modified: ['a.cts'], use_worktree: 'false' }] }],
});
assert.strictEqual(r.ok, false);
assert.match(r.reason, /use_worktree/);
});
test('property: use_worktree never flips to isolation:"worktree" when explicitly false, across random plan shapes', () => {
fc.assert(fc.property(
fc.array(
fc.record({
id: fc.integer({ min: 0, max: 999 }).map((n) => 'p' + n),
brief: fc.string({ minLength: 1, maxLength: 20 }).filter((s) => !/[\r\n"\\]/.test(s)),
useWorktree: fc.boolean(),
}),
{ minLength: 1, maxLength: 4 },
).filter((plans) => new Set(plans.map((p) => p.id)).size === plans.length), // unique ids
(planSpecs) => {
const waves = [{
id: 'w1',
plans: planSpecs.map((p, i) => ({
id: p.id,
brief: p.brief,
files_modified: ['src/file' + i + '.cts'], // disjoint -> no overlap-driven staging noise
use_worktree: p.useWorktree,
})),
}];
const r = emitWorkflowScript({ phaseDir: '.p', runId: 'r', waves });
assert.strictEqual(r.ok, true);
for (const p of planSpecs) {
const briefEsc = JSON.stringify(p.brief);
const idx = r.script.indexOf('agent(' + briefEsc + ',');
assert.ok(idx !== -1, 'agent() call for plan must exist');
const lineEnd = r.script.indexOf('\n', idx);
const line = r.script.slice(idx, lineEnd === -1 ? undefined : lineEnd);
if (p.useWorktree === false) {
assert.ok(!line.includes('isolation'), 'use_worktree:false must never carry isolation');
} else {
assert.ok(line.includes('isolation: "worktree"'), 'use_worktree:true must carry isolation: "worktree"');
}
}
},
));
});
});
// ─── 4. Capability declaration validation ─────────────────────────────────────
describe('capability declaration (capabilities/claude-orchestration/capability.json)', () => {
@@ -520,16 +619,19 @@ describe('capability declaration (capabilities/claude-orchestration/capability.j
assert.strictEqual(slice.default, 'auto');
});
test('registers at WIRED points only (execute:wave:post, plan:post)', () => {
test('registers at WIRED points only (execute:wave:pre, plan:post)', () => {
const cap = loadCap();
const points = cap.contributions.map((c) => c.point);
for (const p of points) {
assert.ok(
['discuss:pre', 'discuss:post', 'plan:pre', 'plan:post', 'execute:post', 'execute:wave:post', 'verify:post', 'ship:pre', 'ship:post'].includes(p),
['discuss:pre', 'discuss:post', 'plan:pre', 'plan:post', 'execute:pre', 'execute:wave:pre', 'execute:post', 'verify:post', 'ship:pre', 'ship:post'].includes(p),
'contribution point ' + p + ' must be a wired point',
);
}
assert.ok(points.includes('execute:wave:post'), 'registers the execute wave hook');
// #2285: the dispatch-backend selector moved from execute:wave:post (fires
// AFTER the wave already dispatched inline — too late to select a backend)
// to execute:wave:pre (fires BEFORE step 3's Agent() dispatch).
assert.ok(points.includes('execute:wave:pre'), 'registers the pre-wave dispatch-selector hook');
assert.ok(points.includes('plan:post'), 'declares plan:* ownership for ultraplan (criterion 5)');
});
@@ -563,13 +665,21 @@ describe('registry integration', () => {
assert.strictEqual(registry.configSchema['claude_orchestration.execution_backend'].default, 'auto');
});
test('byLoopPoint[execute:wave:post].contributions includes our capability', () => {
test('byLoopPoint[execute:wave:pre].contributions includes our capability (#2285)', () => {
const { capMap } = loadAndValidate(new Set());
const registry = buildRegistry(capMap);
const contribs = registry.byLoopPoint['execute:wave:pre'].contributions;
const ours = contribs.find((c) => c.capId === 'claude-orchestration');
assert.ok(ours, 'our execute:wave:pre contribution is registered');
assert.strictEqual(ours.into, 'executor');
});
test('byLoopPoint[execute:wave:post] no longer carries our contribution (#2285 moved it to wave:pre)', () => {
const { capMap } = loadAndValidate(new Set());
const registry = buildRegistry(capMap);
const contribs = registry.byLoopPoint['execute:wave:post'].contributions;
const ours = contribs.find((c) => c.capId === 'claude-orchestration');
assert.ok(ours, 'our execute:wave:post contribution is registered');
assert.strictEqual(ours.into, 'executor');
assert.strictEqual(ours, undefined, 'claude-orchestration must not remain at execute:wave:post');
});
test('committed registry is in sync (gen-capability-registry --check)', () => {

View File

@@ -629,17 +629,29 @@ describe('F. Real registry execute:wave:post shape — guard against accidental
`ui.safety-gate onError must be 'halt'; got ${uiGate.onError}`);
});
test('[happy] real registry: execute:wave:post has no steps and 3 contributions (claude-orchestration executor + external-job executor + mempalace capture-problems)', () => {
test('[happy] real registry: execute:wave:post has no steps and 2 contributions (external-job executor + mempalace capture-problems)', () => {
const point = realRegistry.byLoopPoint['execute:wave:post'];
assert.strictEqual(point.steps.length, 0,
`execute:wave:post steps must be empty; got ${point.steps.length}`);
// #1143: claude-orchestration registers an execute:wave:post contribution
// providing the Workflow-tool backend guidance (default-off, claude-only).
assert.strictEqual(point.contributions.length, 3,
`execute:wave:post must have 3 contributions (claude-orchestration + external-job + mempalace); got ${point.contributions.length}`);
// #2285: claude-orchestration's dispatch-backend-selector contribution moved
// from execute:wave:post to execute:wave:pre — wave:post fires AFTER the
// wave already dispatched inline, too late to select a dispatch backend.
assert.strictEqual(point.contributions.length, 2,
`execute:wave:post must have 2 contributions (external-job + mempalace); got ${point.contributions.length}`);
const capIds = point.contributions.map(c => c.capId).sort();
assert.deepStrictEqual(capIds, ['claude-orchestration', 'external-job', 'mempalace'],
`execute:wave:post contributions must be claude-orchestration + external-job + mempalace; got ${capIds.join(',')}`);
assert.deepStrictEqual(capIds, ['external-job', 'mempalace'],
`execute:wave:post contributions must be external-job + mempalace; got ${capIds.join(',')}`);
});
test('[happy] real registry: execute:wave:pre has 1 contribution (claude-orchestration dispatch-backend selector, #2285)', () => {
const point = realRegistry.byLoopPoint['execute:wave:pre'];
assert.strictEqual(point.steps.length, 0,
`execute:wave:pre steps must be empty; got ${point.steps.length}`);
assert.strictEqual(point.contributions.length, 1,
`execute:wave:pre must have 1 contribution (claude-orchestration); got ${point.contributions.length}`);
const capIds = point.contributions.map(c => c.capId).sort();
assert.deepStrictEqual(capIds, ['claude-orchestration'],
`execute:wave:pre contributions must be claude-orchestration; got ${capIds.join(',')}`);
});
});

View File

@@ -0,0 +1,649 @@
'use strict';
// allow-test-rule: source-text-is-the-product, see #2285 — reads gsd-core/workflows/execute-phase.md
// prose to verify the render-hooks call site + ordering. The workflow markdown IS the runtime
// contract executed by the orchestrator; there is no behavioral seam to drive this assertion
// through other than the rendered prose itself.
/**
* fix-2285-claude-orchestration-wiring.test.cjs
*
* #2285 — the `claude-orchestration` capability (Workflow backend, #1143) was
* registered `active` but fully INERT: `detectWorkflowBackend`/`emitWorkflowScript`
* had zero callers outside their own CLI router, and execute-phase.md declared
* `execute:wave:pre` as a hook point in its frontmatter but never rendered it —
* the wave loop only ever dispatched `execute:pre`, `execute:wave:post`, and
* `execute:post`. `claude_orchestration.enabled:true` therefore had no effect on
* a real execute-phase run.
*
* Fix (Approach B):
* 1. execute-phase.md now renders `execute:wave:pre` immediately before each
* wave's agents are dispatched (step 2.75, before step 3's Agent() loop).
* 2. The claude-orchestration contribution moved from `execute:wave:post`
* (fires too late — after the wave already dispatched inline) to
* `execute:wave:pre` (fires before dispatch, where a backend selector
* actually has to run to matter).
* 3. `resolveWaveDispatch` in src/claude-orchestration.cts composes
* `detectWorkflowBackend` + `emitWorkflowScript` into ONE decision seam,
* giving both functions a real caller outside their CLI router and outside
* tests. It is also exposed via `gsd-tools claude-orchestration
* resolve-wave-dispatch`.
*
* This file drives the real seam (no source-grep on implementation files) and
* asserts the fail-closed contract: disabled or any gate miss => inline,
* byte-identical to today's dispatch shape.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const fc = require('fast-check');
const {
detectWorkflowBackend,
emitWorkflowScript,
resolveWaveDispatch,
WORKFLOW_TOOL_FLOOR_VERSION,
} = require('../gsd-core/bin/lib/claude-orchestration.cjs');
const { runGsdTools, createTempDir, cleanup } = require('./helpers.cjs');
const ROOT = path.resolve(__dirname, '..');
const WORKFLOW_PATH = path.join(ROOT, 'gsd-core', 'workflows', 'execute-phase.md');
const CAP_PATH = path.join(ROOT, 'capabilities', 'claude-orchestration', 'capability.json');
// ─── Fixtures ───────────────────────────────────────────────────────────────
/** A host-integration descriptor whose dispatch axis signals Workflow-tool capability. */
const CAPABLE_HOST = { dispatch: { nested: true, background: true } };
/** A descriptor that fails the nested/background dispatch gate. */
const INCAPABLE_HOST = { dispatch: { nested: false, background: true } };
const ABOVE_FLOOR_SDK = '0.3.150';
const AT_FLOOR_SDK = WORKFLOW_TOOL_FLOOR_VERSION; // '0.3.149'
const BELOW_FLOOR_SDK = '0.3.148';
function enabledConfig(overrides = {}) {
return {
'claude_orchestration.enabled': true,
'claude_orchestration.execution_backend': 'auto',
...overrides,
};
}
function singleWave() {
return {
phaseDir: '.planning/phases/01-foo',
runId: 'run-2285-1',
waves: [
{
id: 'w1',
plans: [
{ id: 'p1', brief: 'Implement the foo module', files_modified: ['src/foo.cts'] },
],
},
],
};
}
function baseInput(overrides = {}) {
return {
runtimeId: 'claude',
hostIntegration: CAPABLE_HOST,
agentSdkVersion: ABOVE_FLOOR_SDK,
config: enabledConfig(),
...singleWave(),
...overrides,
};
}
// ─── Section A: happy path — every gate satisfied → workflow backend ────────
describe('A. resolveWaveDispatch — enabled + all gates satisfied → workflow backend with emitted script', () => {
test('[happy] enabled, claude runtime, capable host, SDK above floor, auto backend → backend:"workflow"', () => {
const result = resolveWaveDispatch(baseInput());
assert.strictEqual(result.backend, 'workflow');
assert.strictEqual(result.reason, 'workflow_backend_active');
assert.ok(typeof result.script === 'string' && result.script.length > 0, 'script must be a non-empty string');
assert.match(result.script, /resumeFromRunId\("run-2285-1"\)/);
assert.match(result.script, /agentType: "gsd-executor", isolation: "worktree"/);
assert.ok(result.summary && result.summary.plans === 1, 'summary.plans must reflect the manifest');
});
test('[happy] execution_backend explicitly "workflow" (not just "auto") also activates', () => {
const result = resolveWaveDispatch(baseInput({
config: enabledConfig({ 'claude_orchestration.execution_backend': 'workflow' }),
}));
assert.strictEqual(result.backend, 'workflow');
});
test('[bva] SDK version boundary: floor-1 → inline, floor exact → workflow, floor+1 → workflow', () => {
const below = resolveWaveDispatch(baseInput({ agentSdkVersion: BELOW_FLOOR_SDK }));
assert.strictEqual(below.backend, 'inline', 'below floor must be inline');
assert.strictEqual(below.reason, 'agent_sdk_version_below_floor');
const at = resolveWaveDispatch(baseInput({ agentSdkVersion: AT_FLOOR_SDK }));
assert.strictEqual(at.backend, 'workflow', 'exactly at floor must activate workflow');
const above = resolveWaveDispatch(baseInput({ agentSdkVersion: ABOVE_FLOOR_SDK }));
assert.strictEqual(above.backend, 'workflow', 'above floor must activate workflow');
});
});
// ─── Section B: fail-closed contract — disabled / each gate individually failing → inline ──
describe('B. resolveWaveDispatch — fail-closed contract: disabled or any gate miss → inline, matches detectWorkflowBackend 1:1', () => {
const GATE_MISS_CASES = [
{
label: 'capability disabled',
overrides: { config: {} },
expectedReason: 'capability_disabled',
},
{
label: 'capability explicitly disabled',
overrides: { config: enabledConfig({ 'claude_orchestration.enabled': false }) },
expectedReason: 'capability_disabled',
},
{
label: 'runtime is not claude',
overrides: { runtimeId: 'codex' },
expectedReason: 'runtime_not_claude',
},
{
label: 'execution_backend explicitly "inline"',
overrides: { config: enabledConfig({ 'claude_orchestration.execution_backend': 'inline' }) },
expectedReason: 'backend_inline',
},
{
label: 'host descriptor incapable (nested:false)',
overrides: { hostIntegration: INCAPABLE_HOST },
expectedReason: 'workflow_tool_unavailable',
},
{
label: 'host descriptor missing entirely',
overrides: { hostIntegration: null },
expectedReason: 'workflow_tool_unavailable',
},
{
label: 'agent SDK version missing',
overrides: { agentSdkVersion: undefined },
expectedReason: 'agent_sdk_version_unknown',
},
{
label: 'agent SDK version malformed (not semver)',
overrides: { agentSdkVersion: 'not-a-version' },
expectedReason: 'agent_sdk_version_unknown',
},
{
label: 'agent SDK version below floor',
overrides: { agentSdkVersion: BELOW_FLOOR_SDK },
expectedReason: 'agent_sdk_version_below_floor',
},
];
for (const { label, overrides, expectedReason } of GATE_MISS_CASES) {
test(`[negative] ${label} → backend:"inline", reason:"${expectedReason}"`, () => {
const input = baseInput(overrides);
const result = resolveWaveDispatch(input);
assert.strictEqual(result.backend, 'inline', `${label}: must resolve to inline`);
assert.strictEqual(result.reason, expectedReason, `${label}: reason mismatch`);
// Fail-closed CONTRACT: today's (byte-identical) inline dispatch carries no
// script/summary. Verify the shape never leaks emitter fields on a gate miss.
assert.deepStrictEqual(
Object.keys(result).sort(),
['backend', 'reason'],
`${label}: inline result must be exactly {backend, reason}, got keys: ${Object.keys(result).join(',')}`,
);
// Parity: resolveWaveDispatch must not reimplement the gate ladder — its
// reason for a detect-side miss must be IDENTICAL to calling
// detectWorkflowBackend directly with the same gate-relevant fields.
const direct = detectWorkflowBackend({
runtimeId: input.runtimeId,
hostIntegration: input.hostIntegration,
config: input.config,
agentSdkVersion: input.agentSdkVersion,
});
assert.strictEqual(direct.backend, 'inline', `${label}: detectWorkflowBackend parity check must also be inline`);
assert.strictEqual(result.reason, direct.reason, `${label}: resolveWaveDispatch must surface detectWorkflowBackend's own reason verbatim`);
});
}
test('[negative] null/undefined/non-object input → inline, reason:"invalid_input" (never throws)', () => {
assert.deepStrictEqual(resolveWaveDispatch(null), { backend: 'inline', reason: 'invalid_input' });
assert.deepStrictEqual(resolveWaveDispatch(undefined), { backend: 'inline', reason: 'invalid_input' });
assert.deepStrictEqual(resolveWaveDispatch('not-an-object'), { backend: 'inline', reason: 'invalid_input' });
});
test('[happy] a valid, dispatch-ready waves manifest never flips a gate-missed decision to workflow', () => {
// Prove the gate ladder short-circuits BEFORE emitWorkflowScript ever runs:
// even with a perfectly valid wave manifest, a disabled capability stays inline.
const result = resolveWaveDispatch(baseInput({ config: {}, ...singleWave() }));
assert.strictEqual(result.backend, 'inline');
assert.strictEqual(result.reason, 'capability_disabled');
});
});
// ─── Section C: composition correctness — detect + emit have a real, non-CLI, non-test caller ──
describe('C. resolveWaveDispatch composes detectWorkflowBackend + emitWorkflowScript (the seam itself)', () => {
test('[happy] resolveWaveDispatch is exported as a function from the core module', () => {
assert.strictEqual(typeof resolveWaveDispatch, 'function');
});
test('[happy] on a workflow-hit, the emitted script/summary are IDENTICAL to calling emitWorkflowScript directly with the same wave data', () => {
const input = baseInput();
const composed = resolveWaveDispatch(input);
assert.strictEqual(composed.backend, 'workflow');
const directEmit = emitWorkflowScript({
phaseDir: input.phaseDir,
waves: input.waves,
runId: input.runId,
});
assert.strictEqual(directEmit.ok, true);
assert.strictEqual(composed.script, directEmit.script, 'resolveWaveDispatch must not re-implement emission — script must match emitWorkflowScript byte-for-byte');
assert.deepStrictEqual(composed.summary, directEmit.summary);
});
test('[negative] detect-hit but a malformed wave manifest (emit failure) → inline, carrying emitWorkflowScript\'s own failure reason', () => {
const input = baseInput({ waves: [] }); // emitWorkflowScript rejects empty waves
const result = resolveWaveDispatch(input);
assert.strictEqual(result.backend, 'inline');
const directEmit = emitWorkflowScript({ phaseDir: input.phaseDir, waves: input.waves, runId: input.runId });
assert.strictEqual(directEmit.ok, false);
assert.strictEqual(result.reason, 'emit_failed: ' + directEmit.reason, 'the emit failure reason must be surfaced verbatim, prefixed');
// Still byte-identical inline shape — no partial/broken script ever leaks.
assert.deepStrictEqual(Object.keys(result).sort(), ['backend', 'reason']);
});
test('[happy] the CLI subcommand `claude-orchestration resolve-wave-dispatch` is ALSO a caller and matches the pure function output', () => {
const tmp = createTempDir('fix-2285-');
try {
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(tmp, '.planning', 'config.json'),
JSON.stringify({ claude_orchestration: { enabled: true, execution_backend: 'auto' } }),
);
const wavesPath = path.join(tmp, 'waves.json');
fs.writeFileSync(wavesPath, JSON.stringify({ waves: singleWave().waves }));
const res = runGsdTools([
'claude-orchestration', 'resolve-wave-dispatch',
'--waves', wavesPath,
'--run-id', 'run-2285-1',
'--phase-dir', '.planning/phases/01-foo',
'--runtime', 'claude',
'--agent-sdk-version', ABOVE_FLOOR_SDK,
'--raw',
], tmp);
assert.strictEqual(res.success, true, 'CLI command must succeed; stderr: ' + (res.error || ''));
const parsed = JSON.parse(res.output);
const direct = resolveWaveDispatch(baseInput());
assert.strictEqual(parsed.backend, direct.backend);
assert.strictEqual(parsed.script, direct.script);
assert.deepStrictEqual(parsed.summary, direct.summary);
} finally {
cleanup(tmp);
}
});
test('[negative] CLI subcommand fails closed to inline exactly like the pure function when disabled', () => {
const tmp = createTempDir('fix-2285-off-');
try {
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
fs.writeFileSync(path.join(tmp, '.planning', 'config.json'), '{}');
const wavesPath = path.join(tmp, 'waves.json');
fs.writeFileSync(wavesPath, JSON.stringify({ waves: singleWave().waves }));
const res = runGsdTools([
'claude-orchestration', 'resolve-wave-dispatch',
'--waves', wavesPath, '--run-id', 'run-x', '--raw',
], tmp);
assert.strictEqual(res.success, true, 'CLI command must succeed (fail-closed, not error); stderr: ' + (res.error || ''));
const parsed = JSON.parse(res.output);
assert.strictEqual(parsed.backend, 'inline');
assert.strictEqual(parsed.reason, 'capability_disabled');
assert.deepStrictEqual(Object.keys(parsed).sort(), ['backend', 'reason']);
} finally {
cleanup(tmp);
}
});
test('property: for ANY input, resolveWaveDispatch never throws, backend is always "inline"|"workflow", and "inline" results carry exactly {backend, reason}', () => {
fc.assert(fc.property(
fc.record({
runtimeId: fc.oneof(fc.constant('claude'), fc.constant('codex'), fc.constant(undefined), fc.string()),
agentSdkVersion: fc.oneof(fc.constant(ABOVE_FLOOR_SDK), fc.constant(BELOW_FLOOR_SDK), fc.constant(undefined), fc.string()),
enabled: fc.boolean(),
capableHost: fc.boolean(),
backendPref: fc.constantFrom('auto', 'workflow', 'inline'),
}),
({ runtimeId, agentSdkVersion, enabled, capableHost, backendPref }) => {
const input = {
runtimeId,
hostIntegration: capableHost ? CAPABLE_HOST : INCAPABLE_HOST,
agentSdkVersion,
config: {
'claude_orchestration.enabled': enabled,
'claude_orchestration.execution_backend': backendPref,
},
...singleWave(),
};
const result = resolveWaveDispatch(input);
assert.ok(result.backend === 'inline' || result.backend === 'workflow');
if (result.backend === 'inline') {
assert.deepStrictEqual(Object.keys(result).sort(), ['backend', 'reason']);
} else {
assert.ok(typeof result.script === 'string' && result.script.length > 0);
}
},
));
});
});
// ─── Section D: capability declaration now targets execute:wave:pre ─────────
describe('D. capability.json declares the contribution at execute:wave:pre (#2285)', () => {
test('[happy] contribution point is execute:wave:pre, not execute:wave:post', () => {
const cap = JSON.parse(fs.readFileSync(CAP_PATH, 'utf8'));
const wavePreContrib = cap.contributions.find((c) => c.point === 'execute:wave:pre');
assert.ok(wavePreContrib, 'capability.json must declare a contribution at execute:wave:pre');
assert.strictEqual(wavePreContrib.into, 'executor');
assert.strictEqual(wavePreContrib.when, 'claude_orchestration.enabled');
assert.strictEqual(wavePreContrib.onError, 'skip');
assert.strictEqual(wavePreContrib.fragment.path, 'fragments/execute-wave-pre.md');
const wavePostContrib = cap.contributions.find((c) => c.point === 'execute:wave:post');
assert.strictEqual(wavePostContrib, undefined, 'the capability must no longer contribute at execute:wave:post');
});
test('[happy] the declared fragment file exists on disk', () => {
const fragPath = path.join(ROOT, 'capabilities', 'claude-orchestration', 'fragments', 'execute-wave-pre.md');
assert.ok(fs.existsSync(fragPath), 'fragments/execute-wave-pre.md must exist');
const content = fs.readFileSync(fragPath, 'utf8');
assert.match(content, /execute:wave:pre/);
assert.match(content, /resolve-wave-dispatch/);
});
});
// ─── Section E: source-contract guard — execute-phase.md renders execute:wave:pre BEFORE dispatch ──
describe('E. execute-phase.md actually renders execute:wave:pre (the dead hook is now live)', () => {
test('[happy] execute-phase.md invokes `loop render-hooks execute:wave:pre`', () => {
const doc = fs.readFileSync(WORKFLOW_PATH, 'utf8');
assert.ok(
/loop render-hooks execute:wave:pre/.test(doc),
'execute-phase.md must dispatch execute:wave:pre hooks (was declared in frontmatter but never rendered — #2285)',
);
});
test('[happy] the execute:wave:pre render-hooks call site appears BEFORE the wave\'s Agent() dispatch (pre-wave, not post)', () => {
const doc = fs.readFileSync(WORKFLOW_PATH, 'utf8');
const preHooksIdx = doc.indexOf('loop render-hooks execute:wave:pre');
// Anchor on the actual per-wave dispatch call (step 3), not the generic
// `subagent_type="gsd-executor"` mention in <runtime_compatibility> near the
// top of the file — that mention predates the wave loop entirely and would
// give a false "before" reading.
const agentDispatchIdx = doc.indexOf('description="Execute plan {plan_number}');
assert.ok(preHooksIdx !== -1, 'execute:wave:pre render-hooks call site must exist');
assert.ok(agentDispatchIdx !== -1, 'the gsd-executor Agent() dispatch call site (step 3) must exist');
assert.ok(
preHooksIdx < agentDispatchIdx,
`execute:wave:pre render-hooks (idx ${preHooksIdx}) must appear BEFORE the wave's Agent() dispatch (idx ${agentDispatchIdx}) — it is a pre-wave hook`,
);
});
test('[happy] the frontmatter still declares all four execute:* points (regression guard)', () => {
const doc = fs.readFileSync(WORKFLOW_PATH, 'utf8');
const frontmatterMatch = doc.match(/points:\s*(.+)/);
assert.ok(frontmatterMatch, 'frontmatter must declare a points: line');
for (const point of ['execute:pre', 'execute:wave:pre', 'execute:wave:post', 'execute:post']) {
assert.ok(frontmatterMatch[1].includes(point), `frontmatter points: line must include ${point}`);
}
});
test('[happy] execute:wave:post is still rendered too (regression guard — did not accidentally remove the post-wave gate dispatch)', () => {
const doc = fs.readFileSync(WORKFLOW_PATH, 'utf8');
assert.ok(
/loop render-hooks execute:wave:post/.test(doc),
'execute-phase.md must still dispatch execute:wave:post hooks (drift/ui gates unaffected by #2285)',
);
});
});
// ─── Section F: orthogonal-review finding 1 — submodule plans never forced into worktree isolation ──
//
// #2772 / #2285 finding 1: emitWorkflowScript previously hardcoded
// `isolation: "worktree"` for EVERY plan. execute-phase.md step 2.5 computes
// USE_WORKTREES_FOR_PLAN per plan specifically to keep submodule-touching
// plans OUT of worktree isolation (the executor commit protocol cannot
// correctly handle submodule commits inside an isolated worktree). The
// Workflow backend must honor the SAME per-plan decision via `use_worktree`.
function waveWithSubmodulePlan() {
return {
phaseDir: '.planning/phases/01-foo',
runId: 'run-2285-submodule',
waves: [
{
id: 'w1',
plans: [
{ id: 'p1', brief: 'normal plan', files_modified: ['src/a.ts'] },
{ id: 'p2', brief: 'submodule plan', files_modified: ['vendor/lib.c'], use_worktree: false },
],
},
],
};
}
describe('F. Workflow backend never forces worktree isolation on a submodule / use_worktree:false plan', () => {
test('[happy] resolveWaveDispatch (pure seam): the submodule plan\'s agent() call carries NO isolation, the normal plan\'s does', () => {
const result = resolveWaveDispatch(baseInput({ ...waveWithSubmodulePlan() }));
assert.strictEqual(result.backend, 'workflow');
assert.match(result.script, /agent\("normal plan", \{ agentType: "gsd-executor", isolation: "worktree" \}\)/);
assert.match(result.script, /agent\("submodule plan", \{ agentType: "gsd-executor" \}\)/);
assert.ok(
!/agent\("submodule plan"[^)]*isolation/.test(result.script),
'the submodule-touching plan must NEVER be emitted with forced worktree isolation',
);
});
test('[happy] CLI `resolve-wave-dispatch`: same per-plan guarantee end-to-end through the subprocess', () => {
const tmp = createTempDir('fix-2285-submodule-');
try {
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(tmp, '.planning', 'config.json'),
JSON.stringify({ claude_orchestration: { enabled: true, execution_backend: 'auto' } }),
);
const wavesPath = path.join(tmp, 'waves.json');
fs.writeFileSync(wavesPath, JSON.stringify({ waves: waveWithSubmodulePlan().waves }));
const res = runGsdTools([
'claude-orchestration', 'resolve-wave-dispatch',
'--waves', wavesPath,
'--run-id', 'run-2285-submodule',
'--phase-dir', '.planning/phases/01-foo',
'--runtime', 'claude',
'--agent-sdk-version', ABOVE_FLOOR_SDK,
'--raw',
], tmp);
assert.strictEqual(res.success, true, 'CLI command must succeed; stderr: ' + (res.error || ''));
const parsed = JSON.parse(res.output);
assert.strictEqual(parsed.backend, 'workflow');
assert.match(parsed.script, /agent\("submodule plan", \{ agentType: "gsd-executor" \}\)/);
assert.ok(!/agent\("submodule plan"[^)]*isolation/.test(parsed.script));
} finally {
cleanup(tmp);
}
});
test('[negative] use_worktree defaults to true when omitted — a manifest with NO submodule info stays backward-compatible', () => {
const result = resolveWaveDispatch(baseInput());
assert.strictEqual(result.backend, 'workflow');
assert.match(result.script, /isolation: "worktree"/, 'default (no use_worktree field) must still isolate — backward compatible');
});
});
// ─── Section G: orthogonal-review finding 2 — missing top-level `waves` key must never silently exit 0 ──
//
// readWavesManifest previously collapsed "read/parse threw" and "parsed OK but
// no top-level `waves` key" into the same `undefined` sentinel. The call sites'
// `if (waves === undefined) return;` made the missing-key case exit 0 with ZERO
// output — fail-silent, breaking the "exit 0 => parseable JSON verdict" contract.
// A missing key must now flow through to emitWorkflowScript's own validation,
// exactly like an explicit `{"waves": null}` manifest already does.
describe('G. missing top-level `waves` key never silently exits 0 with no output', () => {
function projectWithEnabledCapability(prefix) {
const tmp = createTempDir(prefix);
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(tmp, '.planning', 'config.json'),
JSON.stringify({ claude_orchestration: { enabled: true, execution_backend: 'auto' } }),
);
return tmp;
}
test('[negative] resolve-wave-dispatch with a {"notwaves":[]} manifest → non-empty JSON verdict (NOT silent exit 0)', () => {
const tmp = projectWithEnabledCapability('fix-2285-missingkey-resolve-');
try {
const wavesPath = path.join(tmp, 'waves.json');
fs.writeFileSync(wavesPath, JSON.stringify({ notwaves: [] }));
const res = runGsdTools([
'claude-orchestration', 'resolve-wave-dispatch',
'--waves', wavesPath, '--run-id', 'run-x',
'--runtime', 'claude', '--agent-sdk-version', ABOVE_FLOOR_SDK,
'--raw',
], tmp);
assert.strictEqual(res.success, true, 'command must exit 0 (fail-closed to inline, not error); stderr: ' + (res.error || ''));
assert.ok(res.output.length > 0, 'FAIL-SILENT REGRESSION: missing waves key must NOT produce empty stdout on exit 0');
const parsed = JSON.parse(res.output);
assert.strictEqual(parsed.backend, 'inline');
assert.match(parsed.reason, /waves must be a non-empty array/, 'reason must surface emitWorkflowScript\'s own validation message');
} finally {
cleanup(tmp);
}
});
test('[negative] resolve-wave-dispatch: {"notwaves":[]} and {"waves": null} produce the IDENTICAL verdict (parity)', () => {
const tmp = projectWithEnabledCapability('fix-2285-missingkey-parity-');
try {
const missingKeyPath = path.join(tmp, 'missing.json');
fs.writeFileSync(missingKeyPath, JSON.stringify({ notwaves: [] }));
const nullWavesPath = path.join(tmp, 'null.json');
fs.writeFileSync(nullWavesPath, JSON.stringify({ waves: null }));
const argsFor = (p) => [
'claude-orchestration', 'resolve-wave-dispatch',
'--waves', p, '--run-id', 'run-x',
'--runtime', 'claude', '--agent-sdk-version', ABOVE_FLOOR_SDK,
'--raw',
];
const missingRes = runGsdTools(argsFor(missingKeyPath), tmp);
const nullRes = runGsdTools(argsFor(nullWavesPath), tmp);
assert.strictEqual(missingRes.success, true);
assert.strictEqual(nullRes.success, true);
assert.deepStrictEqual(JSON.parse(missingRes.output), JSON.parse(nullRes.output), 'a missing `waves` key must behave identically to an explicit `waves: null`');
} finally {
cleanup(tmp);
}
});
test('[negative] emit-workflow with a {"notwaves":[]} manifest → loud non-zero exit (NOT silent exit 0)', () => {
const tmp = createTempDir('fix-2285-missingkey-emit-');
try {
const wavesPath = path.join(tmp, 'waves.json');
fs.writeFileSync(wavesPath, JSON.stringify({ notwaves: [] }));
const res = runGsdTools([
'claude-orchestration', 'emit-workflow',
'--waves', wavesPath, '--run-id', 'run-x',
], tmp);
assert.strictEqual(res.success, false, 'FAIL-SILENT REGRESSION: missing waves key must produce a loud, non-zero-exit error, not a silent success');
assert.ok(res.exitCode !== 0, 'non-zero exit');
assert.match(res.error || '', /waves must be a non-empty array/);
} finally {
cleanup(tmp);
}
});
test('[happy] a genuinely malformed (unparseable) --waves file still fails loudly, unaffected by the fix', () => {
const tmp = createTempDir('fix-2285-badjson-');
try {
const wavesPath = path.join(tmp, 'waves.json');
fs.writeFileSync(wavesPath, 'not json at all');
const res = runGsdTools([
'claude-orchestration', 'resolve-wave-dispatch',
'--waves', wavesPath, '--run-id', 'run-x', '--raw',
], tmp);
assert.strictEqual(res.success, false, 'a real parse failure must still error');
assert.match(res.error || '', /could not read\/parse --waves file/);
} finally {
cleanup(tmp);
}
});
});
// ─── Section H: orthogonal-review finding 3 — manifest construction guidance is concrete ──
describe('H. the execute:wave:pre fragment documents concrete manifest construction (finding 3)', () => {
test('[happy] the fragment explains how to build WAVE_MANIFEST_PATH, PHASE_RUN_ID, and per-plan use_worktree', () => {
const fragPath = path.join(ROOT, 'capabilities', 'claude-orchestration', 'fragments', 'execute-wave-pre.md');
const content = fs.readFileSync(fragPath, 'utf8');
assert.match(content, /Manifest construction/, 'fragment must have concrete manifest-construction guidance, not just reference undefined vars');
assert.match(content, /PHASE_RUN_ID/);
assert.match(content, /WAVE_MANIFEST_PATH/);
assert.match(content, /use_worktree/);
assert.match(content, /USE_WORKTREES_FOR_PLAN/, 'must tie use_worktree back to step 2.5\'s per-plan decision');
});
test('[happy] execute-phase.md step 2.75 stays minimal — manifest/use_worktree detail lives ONLY in the fragment (#1168 byte-budget conformance)', () => {
// Per the ADR-857 Phase 6 conformance gate (tests/phase6-capstone-conformance.test.cjs),
// the host loop must stay small — optional-feature detail (manifest construction,
// per-plan use_worktree carry-through) belongs in the capability fragment, not the
// host workflow. Step 2.75 is intentionally just a render-hooks call + a one-line
// "follow the contribution or fall through to step 3" instruction.
const doc = fs.readFileSync(WORKFLOW_PATH, 'utf8');
const stepStart = doc.indexOf('2.75. **Execute:wave:pre capability dispatch:**');
const stepEnd = doc.indexOf('\n3. **Spawn executor agents:**', stepStart);
assert.ok(stepStart !== -1 && stepEnd !== -1, 'step 2.75 must exist and precede step 3');
const stepBody = doc.slice(stepStart, stepEnd);
assert.match(stepBody, /loop render-hooks execute:wave:pre/, 'step 2.75 must still render the hook point');
assert.doesNotMatch(stepBody, /use_worktree/, 'manifest-construction detail (use_worktree) must live in the fragment, not the host step');
assert.doesNotMatch(stepBody, /USE_WORKTREES_FOR_PLAN/, 'per-plan worktree gate detail must live in the fragment, not the host step');
});
test('[happy] execute-phase.md is below the ADR-857 Phase 6 pre-phase-6 byte ceiling (#1168), with margin', () => {
const { lfByteCount } = require('../scripts/workflow-size.cjs');
const bytes = lfByteCount(WORKFLOW_PATH);
assert.ok(bytes < 93600, `execute-phase.md must stay below the frozen pre-phase-6 ceiling (93600); got ${bytes}`);
assert.ok(bytes <= 93400, `execute-phase.md should carry a comfortable margin (<=93400) so minor future edits don't re-trip the gate; got ${bytes}`);
});
});
// ─── Section I: orthogonal-review finding 4 — stale doc fixed ───────────────
describe('I. docs/explanation/claude-orchestration-capability.md reflects the execute:wave:pre move (finding 4)', () => {
test('[happy] the doc no longer claims the capability registers at execute:wave:post', () => {
const docPath = path.join(ROOT, 'docs', 'explanation', 'claude-orchestration-capability.md');
const content = fs.readFileSync(docPath, 'utf8');
assert.match(content, /execute:wave:pre/, 'doc must mention execute:wave:pre as the wired point');
assert.ok(
!/execute:wave:post.*\(into the executor\)/.test(content),
'doc must not still claim the wired point is execute:wave:post',
);
});
});

View File

@@ -233,7 +233,7 @@
"gsd-core/workflows/docs-update.md": "8e986e26d0e6e1a0",
"gsd-core/workflows/edit-phase.md": "fc932e82ba1f585a",
"gsd-core/workflows/eval-review.md": "eb4040eaa5b8497f",
"gsd-core/workflows/execute-phase.md": "894d0e9efed72258",
"gsd-core/workflows/execute-phase.md": "a349bfdfcdf2c011",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "55d0706e80a2554a",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "9b7107b31b60a3b9",

View File

@@ -304,7 +304,7 @@
"gsd-core/workflows/docs-update.md": "f35922d15b7061c9",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "f898936e2cfe4130",
"gsd-core/workflows/execute-phase.md": "7d42e81188b3613a",
"gsd-core/workflows/execute-phase.md": "42bb004d479f7706",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581",

View File

@@ -303,7 +303,7 @@
"gsd-core/workflows/docs-update.md": "cd753783ab95da00",
"gsd-core/workflows/edit-phase.md": "dbbb6191f5a8b65e",
"gsd-core/workflows/eval-review.md": "086a1f2b3c11462c",
"gsd-core/workflows/execute-phase.md": "e5bc721629beaa01",
"gsd-core/workflows/execute-phase.md": "a0f26f223218812d",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "6d38bfd540030da4",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "611b2be3bd133eb1",

View File

@@ -232,7 +232,7 @@
"gsd-core/workflows/docs-update.md": "63082608d3ae92be",
"gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a",
"gsd-core/workflows/eval-review.md": "f59e8329dae1e528",
"gsd-core/workflows/execute-phase.md": "5231186012da87a2",
"gsd-core/workflows/execute-phase.md": "a5ca7da9e551bf4c",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "facb0e816d87a0c7",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581",

View File

@@ -236,7 +236,7 @@
"gsd-core/workflows/docs-update.md": "39f288623a8f6f32",
"gsd-core/workflows/edit-phase.md": "9c9fadc047c61d74",
"gsd-core/workflows/eval-review.md": "3e1d7829ed2ed494",
"gsd-core/workflows/execute-phase.md": "6213f0d6e64dc524",
"gsd-core/workflows/execute-phase.md": "7cafa358f645936a",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "67ebc93f51968cb6",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "82e6cfe1e1b1ec0e",

View File

@@ -304,7 +304,7 @@
"gsd-core/workflows/docs-update.md": "f35922d15b7061c9",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "f898936e2cfe4130",
"gsd-core/workflows/execute-phase.md": "f17719f6b780cd2b",
"gsd-core/workflows/execute-phase.md": "4d6f9e033792619a",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581",

View File

@@ -339,7 +339,7 @@
"gsd-core/workflows/docs-update.md": "e255317df939e302",
"gsd-core/workflows/edit-phase.md": "e592a4d85ce5380f",
"gsd-core/workflows/eval-review.md": "63d0d0670b54c244",
"gsd-core/workflows/execute-phase.md": "b15bbe2f8a8d8d02",
"gsd-core/workflows/execute-phase.md": "8176e734cd927891",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f4cacd27d37bac65",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581",

View File

@@ -234,7 +234,7 @@
"gsd-core/workflows/docs-update.md": "9292cfa3c52c434e",
"gsd-core/workflows/edit-phase.md": "8667c28b22b1599f",
"gsd-core/workflows/eval-review.md": "81c8e72ba3862856",
"gsd-core/workflows/execute-phase.md": "80a1e0722f72dbb1",
"gsd-core/workflows/execute-phase.md": "75bc8795af213c29",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "63b712920f21a40f",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "1b73ab2fc47c9dbf",

View File

@@ -304,7 +304,7 @@
"gsd-core/workflows/docs-update.md": "e86d7d7e2e3dac6d",
"gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a",
"gsd-core/workflows/eval-review.md": "a86279dd98dd5c03",
"gsd-core/workflows/execute-phase.md": "a761ee37b32a6fe2",
"gsd-core/workflows/execute-phase.md": "f67b5d55c3e7dc4b",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "facb0e816d87a0c7",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581",

View File

@@ -233,7 +233,7 @@
"gsd-core/workflows/docs-update.md": "ba4cf926fc463cbe",
"gsd-core/workflows/edit-phase.md": "7f27003f20e88fb8",
"gsd-core/workflows/eval-review.md": "f510e5762212dc6f",
"gsd-core/workflows/execute-phase.md": "e99263e6cfe3bdb2",
"gsd-core/workflows/execute-phase.md": "3205cca9fe364951",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "14a27cc0828f59d3",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "26ee34c543926402",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "c9ad17d6cc6dfe45",

View File

@@ -304,7 +304,7 @@
"gsd-core/workflows/docs-update.md": "79afaaf19fd527cc",
"gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a",
"gsd-core/workflows/eval-review.md": "926eda8bbee28b23",
"gsd-core/workflows/execute-phase.md": "43c8c218f8933cc8",
"gsd-core/workflows/execute-phase.md": "4babf365129ad14b",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "facb0e816d87a0c7",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581",

View File

@@ -297,7 +297,7 @@
"gsd-core/workflows/docs-update.md": "f35922d15b7061c9",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "f898936e2cfe4130",
"gsd-core/workflows/execute-phase.md": "39def0423fb4eb45",
"gsd-core/workflows/execute-phase.md": "2629630eb4a94a6b",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581",

View File

@@ -304,7 +304,7 @@
"gsd-core/workflows/docs-update.md": "850366c2ef8fb780",
"gsd-core/workflows/edit-phase.md": "1876c855fb0a0a39",
"gsd-core/workflows/eval-review.md": "5394694d29ad7543",
"gsd-core/workflows/execute-phase.md": "2a1c33e0bda26211",
"gsd-core/workflows/execute-phase.md": "895f8e910b32ebd4",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "1804215577f1ad35",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "baa2c401af10a80a",

View File

@@ -200,7 +200,7 @@
"gsd-core/workflows/docs-update.md": "f35922d15b7061c9",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "f898936e2cfe4130",
"gsd-core/workflows/execute-phase.md": "8f6dc95cc8030259",
"gsd-core/workflows/execute-phase.md": "353711104c33d19f",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581",

View File

@@ -233,7 +233,7 @@
"gsd-core/workflows/docs-update.md": "45d2f0d173c84e07",
"gsd-core/workflows/edit-phase.md": "0fb5e0123cfc6f36",
"gsd-core/workflows/eval-review.md": "6dee8a1e40ececd4",
"gsd-core/workflows/execute-phase.md": "38370aa7653a1c5b",
"gsd-core/workflows/execute-phase.md": "840ce6d4ff582aed",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "36af8d91e4ae8b9c",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "98db1ba4c39cd784",

View File

@@ -233,7 +233,7 @@
"gsd-core/workflows/docs-update.md": "f13571f08e083356",
"gsd-core/workflows/edit-phase.md": "7facd0faa33c8cad",
"gsd-core/workflows/eval-review.md": "37d545d4f0db4927",
"gsd-core/workflows/execute-phase.md": "8d197109e4d60522",
"gsd-core/workflows/execute-phase.md": "cadbd19b2a828111",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "c985a30317a1aa6b",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "0a9e915170c7121c",

View File

@@ -233,7 +233,7 @@
"gsd-core/workflows/docs-update.md": "70f73cc8c27e0ca0",
"gsd-core/workflows/edit-phase.md": "c0ae7d0063f3e789",
"gsd-core/workflows/eval-review.md": "b28be79ef29f16fd",
"gsd-core/workflows/execute-phase.md": "7d88881ded1b575e",
"gsd-core/workflows/execute-phase.md": "46a363f3f33cc596",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "47ae5482f8e64100",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "15bca39a75c664be",

View File

@@ -304,7 +304,7 @@
"gsd-core/workflows/docs-update.md": "f35922d15b7061c9",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "f898936e2cfe4130",
"gsd-core/workflows/execute-phase.md": "55c0e6f659e27ff2",
"gsd-core/workflows/execute-phase.md": "4bd16cf990e3ee63",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581",

View File

@@ -2,9 +2,12 @@
process.env.GSD_TEST_MODE = '1';
// Refinements for issue #1164 (PR #1998 follow-up):
// - A: document the execute:wave:post choice (wave:pre is declared but not
// dispatched by execute-phase.md; wiring it is a core-loop change #1164
// puts out of scope).
// - A: document the execute:wave:post choice (at the time of #1164, wave:pre
// was declared but not dispatched by execute-phase.md; wiring it was a
// core-loop change #1164 put out of scope. #2285 later wired wave:pre —
// see gsd-core/workflows/execute-phase.md step 2.75 and the
// claude-orchestration capability, which moved to wave:pre for that
// reason).
// - B: external_job.artifact_dir is now consumed by the adapter (was declared
// but unused).
// - C: external_job.submit_timeout_ms / poll_timeout_ms are now read from

View File

@@ -24,7 +24,7 @@
"docs-update.md": 55706,
"edit-phase.md": 12927,
"eval-review.md": 9967,
"execute-phase.md": 93583,
"execute-phase.md": 93363,
"execute-plan.md": 33113,
"explore.md": 10541,
"extract-learnings.md": 12893,