* feat(#1708): typed documentation-sourced #853 dispatch-flatten Graduate the #853 orchestrator-backgrounding decision from a scattered RUNTIME==='codex' prose check to a typed, documentation-sourced engine decision. Adds a backgroundDispatch dispatch sub-axis (sourced per host: codex+cursor documented true, 9 documented false, 5 undocumented), shouldFlattenDispatch(dispatch) (inline UNLESS background && backgroundDispatch, fail-closed), and a gsd_run query dispatch-should-flatten the plan/execute workflows call. Cursor is newly background-eligible per its docs (inline->background) — a documentation-justified behavior change. No RUNTIME-name residue for this decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1708): backgroundDispatch citations in matrix + CONTEXT note Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1708): address review findings on typed dispatch-flatten Code/adversarial review: convert the manager.md/autonomous.md Compound Action preamble from hardcoded 'On Codex' to FLATTEN-based branching (the handlers already use the query; the preamble contradicted them and was wrong for cursor); make shouldFlattenDispatch null-safe + type-honest (accepts raw 'undocumented' registry values); make backgroundDispatch a required descriptor field (matching its siblings, all 16 carry it); strengthen the config.runtime behavioral test; update the bug-853 prose-pin test + comment. Security review clean; Codex confirmed no fail-open. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1708): backfill backgroundDispatch in role:runtime test fixtures Making backgroundDispatch a required descriptor field broke role:runtime fixtures in capability-manifest-version/capability-registry/host-integration-descriptors tests that build a dispatch object without it (caught by full gsd-test, not scoped npm test). Backfill backgroundDispatch:false into the well-formed fixtures; the deliberately-malformed 'required-field' test fixture is left malformed by design. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1708): update fix-1521 dispatch-gating assertion to the FLATTEN gate fix-1521 pinned the codex-specific run_in_background prose that #1708 graduated to the typed dispatch-should-flatten/FLATTEN gate. Update its assertions to verify FLATTEN=false gating (not a runtime name) + that the old RUNTIME===codex gate is gone. Caught by full gsd-test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1708): add changeset for typed dispatch-flatten Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1708): remove stray temp PR-body file Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1708): add issue ref to bug-853 allow-test-rule annotations ADR-456 requires every allow-test-rule exemption to carry a see #NNN reference; the source-text-is-the-product annotations added when migrating the prose assertions lacked it (lint-tests CI gate). Add (see #1708). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,87 +5,225 @@
|
||||
* dispatched Plan/Execute via Agent(run_in_background=true). On Claude Code a
|
||||
* backgrounded agent has no Agent/Task tool, so it cannot spawn the nested
|
||||
* subagents (worktree executors, plan-checker, verifier). The workflows must
|
||||
* now resolve the runtime and run inline everywhere except Codex, which is the
|
||||
* only supported runtime where a backgrounded agent can still nest subagents.
|
||||
* now resolve dispatch capability from the registry (#1708) and run inline
|
||||
* everywhere except runtimes where dispatch.background && dispatch.backgroundDispatch
|
||||
* are both true (currently: codex, cursor).
|
||||
*
|
||||
* Phase B (#1708): the prose `RUNTIME === 'codex'` rule is graduated to a typed
|
||||
* `gsd_run query dispatch-should-flatten` query backed by shouldFlattenDispatch()
|
||||
* from host-integration.cjs and the documentation-sourced capability registry.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { createTempProject, cleanup: cleanupDir, runGsdTools } = require('./helpers.cjs');
|
||||
|
||||
const WORKFLOWS_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows');
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
const MANAGER = fs.readFileSync(path.join(WORKFLOWS_DIR, 'manager.md'), 'utf8');
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
const AUTONOMOUS = fs.readFileSync(path.join(WORKFLOWS_DIR, 'autonomous.md'), 'utf8');
|
||||
|
||||
describe('bug-853 — manager/autonomous gate background dispatch by runtime', () => {
|
||||
test('manager.md resolves the runtime before dispatching plan/execute', () => {
|
||||
// Two dispatch sites (plan + execute), each must resolve the runtime.
|
||||
const matches = MANAGER.match(/config-get runtime/g) || [];
|
||||
assert.ok(matches.length >= 2, 'manager.md must resolve runtime for both plan and execute dispatch');
|
||||
test('manager.md resolves dispatch-should-flatten before dispatching plan/execute', () => {
|
||||
// Two dispatch sites (plan + execute), each must use dispatch-should-flatten.
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
const matches = MANAGER.match(/dispatch-should-flatten/g) || [];
|
||||
assert.ok(matches.length >= 2, 'manager.md must use dispatch-should-flatten for both plan and execute dispatch');
|
||||
});
|
||||
|
||||
test('manager.md documents why most runtimes cannot background-dispatch', () => {
|
||||
// Accept both old singular form (backgrounded agent has no) and new plural form (backgrounded agents have no)
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
assert.match(MANAGER, /backgrounded agents? ha(?:s|ve) no `Agent`\/`Task` tool/);
|
||||
});
|
||||
|
||||
test('manager.md gates background dispatch on codex and runs plan/execute inline otherwise', () => {
|
||||
// Codex takes the background path
|
||||
assert.match(MANAGER, /If `RUNTIME` is `codex`[\s\S]{0,400}?run_in_background=true/);
|
||||
// Inline is the default/else branch for plan — anchored on the explicit non-Codex label
|
||||
test('manager.md gates background dispatch on FLATTEN=false and runs plan/execute inline otherwise', () => {
|
||||
// Background path uses FLATTEN is false
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
assert.match(MANAGER, /If `FLATTEN` is `false`[\s\S]{0,400}?run_in_background=true/);
|
||||
// Inline is the default/else branch for plan — anchored on FLATTEN=true language (not runtime name)
|
||||
assert.match(
|
||||
MANAGER,
|
||||
/Otherwise \(Claude Code or any other non-Codex runtime\)[\s\S]{0,400}?Skill\(skill="gsd-plan-phase"/,
|
||||
/Otherwise[\s\S]{0,100}?`FLATTEN`[\s\S]{0,400}?Skill\(skill="gsd-plan-phase"/,
|
||||
);
|
||||
// Inline is the default/else branch for execute — anchored on the explicit non-Codex label
|
||||
// Inline is the default/else branch for execute — anchored on FLATTEN=true language (not runtime name)
|
||||
assert.match(
|
||||
MANAGER,
|
||||
/Otherwise \(Claude Code or any other non-Codex runtime\)[\s\S]{0,400}?Skill\(skill="gsd-execute-phase"/,
|
||||
/Otherwise[\s\S]{0,100}?`FLATTEN`[\s\S]{0,400}?Skill\(skill="gsd-execute-phase"/,
|
||||
);
|
||||
});
|
||||
|
||||
test('manager.md compound actions only background plan/execute on Codex', () => {
|
||||
test('manager.md compound action preamble uses FLATTEN language (not hardcoded runtime names)', () => {
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
const compoundActionSection = MANAGER.match(
|
||||
/### Compound Action \(background \+ inline\)[\s\S]*?Inline verification:/,
|
||||
);
|
||||
assert.ok(compoundActionSection, 'manager.md must document compound action runtime dispatch');
|
||||
|
||||
// Must gate on FLATTEN being false (not runtime name)
|
||||
assert.match(
|
||||
compoundActionSection[0],
|
||||
/On Codex:[\s\S]{0,260}?Spawn all background agents first[\s\S]{0,220}?plan\/execute/,
|
||||
/If `FLATTEN` is `false`[\s\S]{0,400}?Spawn all background agents first[\s\S]{0,300}?plan\/execute/,
|
||||
);
|
||||
// Otherwise / inline branch must reference FLATTEN being true
|
||||
assert.match(
|
||||
compoundActionSection[0],
|
||||
/On Claude Code or any other non-Codex runtime:[\s\S]{0,260}?inline/,
|
||||
/Otherwise[\s\S]{0,260}?`FLATTEN`[\s\S]{0,260}?`true`[\s\S]{0,260}?inline/,
|
||||
);
|
||||
// Must NOT still hardcode "On Codex:" in this section
|
||||
assert.doesNotMatch(
|
||||
compoundActionSection[0],
|
||||
/On other runtimes:[\s\S]{0,260}?Spawn all background agents first/,
|
||||
/\*\*On Codex:\*\*/,
|
||||
);
|
||||
// Must NOT still hardcode "On Claude Code or any other non-Codex runtime:"
|
||||
assert.doesNotMatch(
|
||||
compoundActionSection[0],
|
||||
/On Claude Code or any other non-Codex runtime:/,
|
||||
);
|
||||
});
|
||||
|
||||
test('autonomous.md gates interactive background dispatch by runtime', () => {
|
||||
const autoRuntimeMatches = AUTONOMOUS.match(/config-get runtime/g) || [];
|
||||
assert.ok(autoRuntimeMatches.length >= 2, 'autonomous.md must resolve runtime in both 3b (plan) and 3c (execute) interactive branches');
|
||||
test('autonomous.md gates interactive background dispatch using dispatch-should-flatten', () => {
|
||||
// Two dispatch sites (3b plan + 3c execute), each must use dispatch-should-flatten.
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
const autoFlattenMatches = AUTONOMOUS.match(/dispatch-should-flatten/g) || [];
|
||||
assert.ok(autoFlattenMatches.length >= 2, 'autonomous.md must use dispatch-should-flatten in both 3b (plan) and 3c (execute) interactive branches');
|
||||
// Accept both old singular form (backgrounded agent has no) and new plural form (backgrounded agents have no)
|
||||
assert.match(AUTONOMOUS, /backgrounded agents? ha(?:s|ve) no `Agent`\/`Task` tool/);
|
||||
});
|
||||
|
||||
test('autonomous.md gates interactive background dispatch on codex; runs plan/execute inline otherwise', () => {
|
||||
// Codex block: run_in_background=true appears within the codex branch and gsd-plan-phase is nearby
|
||||
assert.match(AUTONOMOUS, /If `RUNTIME` is `codex`[\s\S]{0,1200}?run_in_background=true[\s\S]{0,600}?gsd-plan-phase/);
|
||||
// Codex block: run_in_background=true appears within the codex branch and gsd-execute-phase is nearby
|
||||
assert.match(AUTONOMOUS, /If `RUNTIME` is `codex`[\s\S]{0,3000}?run_in_background=true[\s\S]{0,200}?gsd-execute-phase/);
|
||||
// Inline is the otherwise/else branch for plan — anchored on the explicit non-Codex label
|
||||
test('autonomous.md gates interactive background dispatch on FLATTEN=false; runs plan/execute inline otherwise', () => {
|
||||
// Background block: run_in_background=true appears within the FLATTEN=false branch and gsd-plan-phase is nearby
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
assert.match(AUTONOMOUS, /If `FLATTEN` is `false`[\s\S]{0,1200}?run_in_background=true[\s\S]{0,600}?gsd-plan-phase/);
|
||||
// Background block: run_in_background=true appears within the FLATTEN=false branch and gsd-execute-phase is nearby
|
||||
assert.match(AUTONOMOUS, /If `FLATTEN` is `false`[\s\S]{0,3000}?run_in_background=true[\s\S]{0,200}?gsd-execute-phase/);
|
||||
// Inline is the otherwise/else branch for plan — anchored on FLATTEN=true language (not runtime name)
|
||||
assert.match(
|
||||
AUTONOMOUS,
|
||||
/Otherwise \(Claude Code or any other non-Codex runtime\)[\s\S]{0,400}?Skill\(skill="gsd-plan-phase"/,
|
||||
/Otherwise[\s\S]{0,100}?`FLATTEN`[\s\S]{0,400}?Skill\(skill="gsd-plan-phase"/,
|
||||
);
|
||||
// Inline is the otherwise/else branch for execute — anchored on the explicit non-Codex label
|
||||
// Inline is the otherwise/else branch for execute — anchored on FLATTEN=true language (not runtime name)
|
||||
assert.match(
|
||||
AUTONOMOUS,
|
||||
/Otherwise \(Claude Code or any other non-Codex runtime\)[\s\S]{0,400}?Skill\(skill="gsd-execute-phase"/,
|
||||
/Otherwise[\s\S]{0,100}?`FLATTEN`[\s\S]{0,400}?Skill\(skill="gsd-execute-phase"/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dispatch-should-flatten query — behavioral', () => {
|
||||
// #853 / #1708: The typed query replaces prose-level RUNTIME===codex checks.
|
||||
// shouldFlattenDispatch returns false only when both dispatch.background AND
|
||||
// dispatch.backgroundDispatch are true in the capability registry.
|
||||
//
|
||||
// Registry values (from host-integration-capability-matrix.md):
|
||||
// codex: background=true, backgroundDispatch=true → shouldFlatten=false (may background)
|
||||
// claude: background=true, backgroundDispatch=false → shouldFlatten=true (must inline)
|
||||
// cursor: background=true, backgroundDispatch=true → shouldFlatten=false (may background)
|
||||
// unknown: no entry → fail-closed → shouldFlatten=true (must inline)
|
||||
|
||||
test('runtime=codex → shouldFlatten=false (background dispatch safe)', () => {
|
||||
const tmpDir = createTempProject();
|
||||
try {
|
||||
const result = runGsdTools(['query', 'dispatch-should-flatten', '--raw'], tmpDir, {
|
||||
GSD_RUNTIME: 'codex',
|
||||
});
|
||||
assert.ok(result.success, `Expected success, got error: ${result.error}`);
|
||||
assert.strictEqual(result.output, 'false', `codex should return false (may background), got: ${result.output}`);
|
||||
} finally {
|
||||
cleanupDir(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('runtime=claude → shouldFlatten=true (must inline)', () => {
|
||||
const tmpDir = createTempProject();
|
||||
try {
|
||||
const result = runGsdTools(['query', 'dispatch-should-flatten', '--raw'], tmpDir, {
|
||||
GSD_RUNTIME: 'claude',
|
||||
});
|
||||
assert.ok(result.success, `Expected success, got error: ${result.error}`);
|
||||
assert.strictEqual(result.output, 'true', `claude should return true (must inline), got: ${result.output}`);
|
||||
} finally {
|
||||
cleanupDir(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('runtime=cursor → shouldFlatten=false (background dispatch safe)', () => {
|
||||
const tmpDir = createTempProject();
|
||||
try {
|
||||
const result = runGsdTools(['query', 'dispatch-should-flatten', '--raw'], tmpDir, {
|
||||
GSD_RUNTIME: 'cursor',
|
||||
});
|
||||
assert.ok(result.success, `Expected success, got error: ${result.error}`);
|
||||
assert.strictEqual(result.output, 'false', `cursor should return false (may background), got: ${result.output}`);
|
||||
} finally {
|
||||
cleanupDir(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('unknown runtime → shouldFlatten=true (fail-closed → must inline)', () => {
|
||||
// An unknown runtime has no registry entry → dispatch is null → fail-closed to true.
|
||||
const tmpDir = createTempProject();
|
||||
try {
|
||||
const result = runGsdTools(['query', 'dispatch-should-flatten', '--raw'], tmpDir, {
|
||||
GSD_RUNTIME: 'unknown-runtime-xyz',
|
||||
});
|
||||
// The query must succeed (exit 0) even for unknown runtimes — fail-closed not crash-closed.
|
||||
assert.ok(result.success, `Expected success (fail-closed), got error: ${result.error}`);
|
||||
assert.strictEqual(result.output, 'true', `unknown runtime should return true (fail-closed), got: ${result.output}`);
|
||||
} finally {
|
||||
cleanupDir(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('--json flag returns structured { runtime, shouldFlatten, dispatch }', () => {
|
||||
const tmpDir = createTempProject();
|
||||
try {
|
||||
const result = runGsdTools(['query', 'dispatch-should-flatten', '--json'], tmpDir, {
|
||||
GSD_RUNTIME: 'codex',
|
||||
});
|
||||
assert.ok(result.success, `Expected success, got error: ${result.error}`);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(result.output);
|
||||
} catch {
|
||||
assert.fail(`Expected valid JSON output, got: ${result.output}`);
|
||||
}
|
||||
assert.strictEqual(parsed.runtime, 'codex');
|
||||
assert.strictEqual(parsed.shouldFlatten, false);
|
||||
assert.ok(parsed.dispatch !== null && typeof parsed.dispatch === 'object', 'dispatch should be an object');
|
||||
assert.strictEqual(parsed.dispatch.backgroundDispatch, true);
|
||||
} finally {
|
||||
cleanupDir(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('config.runtime takes precedence when GSD_RUNTIME not set', () => {
|
||||
// GSD_RUNTIME > config.runtime > 'claude'
|
||||
// Write config.json with runtime=codex; no GSD_RUNTIME override.
|
||||
const tmpDir = createTempProject();
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'config.json'),
|
||||
JSON.stringify({ runtime: 'codex' }),
|
||||
'utf-8',
|
||||
);
|
||||
// Override GSD_RUNTIME to '' (empty string) so any ambient value is cleared.
|
||||
// resolveRuntimeNameFromCandidates treats empty string as absent (normalizes
|
||||
// to '' which is falsy → skipped → falls through to config.runtime=codex).
|
||||
// This is the only way to suppress an ambient GSD_RUNTIME since runGsdTools
|
||||
// merges { ...process.env, ...TEST_ENV_BASE, ...env } — passing '' as the
|
||||
// override overwrites the ambient value at the correct merge position.
|
||||
const result = runGsdTools(['query', 'dispatch-should-flatten', '--raw'], tmpDir, {
|
||||
GSD_RUNTIME: '',
|
||||
});
|
||||
// config.runtime=codex with GSD_RUNTIME cleared → codex backgrounds → shouldFlatten=false
|
||||
assert.ok(result.success, `Expected success, got error: ${result.error}`);
|
||||
assert.strictEqual(result.output, 'false', `config.runtime=codex (GSD_RUNTIME cleared) should return false (may background), got: ${result.output}`);
|
||||
} finally {
|
||||
cleanupDir(tmpDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,7 +89,7 @@ function runtimeCap(overrides) {
|
||||
hostIntegration: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: false },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
|
||||
@@ -1778,7 +1778,7 @@ describe('C3: role:runtime body validation', () => {
|
||||
hostIntegration: {
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 2, background: false, subagentToolkit: 'full' },
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 2, background: false, subagentToolkit: 'full', backgroundDispatch: false },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
@@ -3231,7 +3231,7 @@ function makeRuntimeCap(overrides) {
|
||||
hostIntegration: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: false },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
@@ -4306,7 +4306,7 @@ describe('ADR-857 phase 5f: cross-field consistency gate rejection tests (DEFECT
|
||||
hostIntegration: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: false },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
@@ -5168,7 +5168,7 @@ describe('activationKey validation', () => {
|
||||
hostIntegration: {
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 2, background: false, subagentToolkit: 'full' },
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 2, background: false, subagentToolkit: 'full', backgroundDispatch: false },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
|
||||
@@ -169,12 +169,14 @@ test('execute-phase.md, quick.md, and diagnose-issues.md guards are generalized
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Orchestration gating: manager.md + autonomous.md now gate on codex for
|
||||
// background dispatch, not on "not claude". (#1521 Stage 2)
|
||||
// Orchestration gating: manager.md + autonomous.md gate background dispatch on
|
||||
// the typed FLATTEN query. #1708 (ADR-1239 Phase B) graduated #1521's
|
||||
// codex-specific check to a documentation-sourced shouldFlattenDispatch — the
|
||||
// prose now branches on `FLATTEN` (false = background), not a runtime name.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('manager.md and autonomous.md gate run_in_background on codex specifically (#1521)', () => {
|
||||
// allow-test-rule: orchestration dispatch gating in manager/autonomous .md is the runtime contract surface (#1521)
|
||||
test('manager.md and autonomous.md gate run_in_background on FLATTEN=false, not a runtime name (#1521, graduated by #1708)', () => {
|
||||
// allow-test-rule: orchestration dispatch gating in manager/autonomous .md is the runtime contract surface (#1521/#1708)
|
||||
const manager = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'gsd-core', 'workflows', 'manager.md'),
|
||||
'utf8',
|
||||
@@ -184,24 +186,29 @@ test('manager.md and autonomous.md gate run_in_background on codex specifically
|
||||
'utf8',
|
||||
);
|
||||
|
||||
// Both files must gate run_in_background on codex (not on a generic "not claude" condition)
|
||||
// Both files must gate run_in_background on the typed FLATTEN decision (not a runtime name)
|
||||
assert.ok(
|
||||
/`RUNTIME` is `codex`[\s\S]{0,500}?run_in_background=true/.test(manager),
|
||||
'manager.md: expected run_in_background dispatch gated on RUNTIME=codex specifically',
|
||||
/If `FLATTEN` is `false`[\s\S]{0,500}?run_in_background=true/.test(manager),
|
||||
'manager.md: expected run_in_background dispatch gated on FLATTEN=false (typed dispatch-should-flatten query)',
|
||||
);
|
||||
assert.ok(
|
||||
/`RUNTIME` is `codex`[\s\S]{0,700}?run_in_background=true/.test(autonomous),
|
||||
'autonomous.md: expected run_in_background dispatch gated on RUNTIME=codex specifically',
|
||||
/If `FLATTEN` is `false`[\s\S]{0,1200}?run_in_background=true/.test(autonomous),
|
||||
'autonomous.md: expected run_in_background dispatch gated on FLATTEN=false (typed dispatch-should-flatten query)',
|
||||
);
|
||||
|
||||
// Inline is the default/else branch (not just claude)
|
||||
// Inline is the else branch, keyed on FLATTEN — never a runtime name
|
||||
assert.ok(
|
||||
/Otherwise[\s\S]{0,200}?Claude Code or any other non-Codex runtime/.test(manager),
|
||||
'manager.md: expected "Otherwise (Claude Code or any other non-Codex runtime)" inline branch',
|
||||
/Otherwise[\s\S]{0,250}?inline/i.test(manager),
|
||||
'manager.md: expected "Otherwise ... inline" branch keyed on FLATTEN',
|
||||
);
|
||||
assert.ok(
|
||||
/Otherwise[\s\S]{0,200}?Claude Code or any other non-Codex runtime/.test(autonomous),
|
||||
'autonomous.md: expected "Otherwise (Claude Code or any other non-Codex runtime)" inline branch',
|
||||
/Otherwise[\s\S]{0,250}?inline/i.test(autonomous),
|
||||
'autonomous.md: expected "Otherwise ... inline" branch keyed on FLATTEN',
|
||||
);
|
||||
// And the old runtime-name gating must be gone (no `RUNTIME` is `codex` dispatch gate)
|
||||
assert.ok(
|
||||
!/`RUNTIME` is `codex`[\s\S]{0,500}?run_in_background=true/.test(manager),
|
||||
'manager.md: must no longer gate run_in_background on the runtime name',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const path = require('node:path');
|
||||
const {
|
||||
negotiateHostCapabilities,
|
||||
profileOf,
|
||||
shouldFlattenDispatch,
|
||||
} = require(path.join(__dirname, '../gsd-core/bin/lib/host-integration.cjs'));
|
||||
|
||||
const registry = require(path.join(__dirname, '../gsd-core/bin/lib/capability-registry.cjs'));
|
||||
@@ -27,8 +28,8 @@ const {
|
||||
|
||||
// All 8 scalar hostIntegration axis keys
|
||||
const SCALAR_AXES = ['embeddingMode', 'commandSurface', 'modelMode', 'hookBus', 'stateIO', 'transport', 'runtime'];
|
||||
// All 5 dispatch sub-keys
|
||||
const DISPATCH_KEYS = ['namedDispatch', 'nested', 'maxDepth', 'background', 'subagentToolkit'];
|
||||
// All 6 dispatch sub-keys (includes backgroundDispatch added in feat/1679-dispatch-flatten)
|
||||
const DISPATCH_KEYS = ['namedDispatch', 'nested', 'maxDepth', 'background', 'subagentToolkit', 'backgroundDispatch'];
|
||||
|
||||
// All 16 runtime IDs (ordered alphabetically)
|
||||
const RUNTIME_IDS = [
|
||||
@@ -95,7 +96,7 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
});
|
||||
|
||||
// (ii) hostIntegration object is present with all required keys
|
||||
test('(ii) cap.runtime.hostIntegration is present with all 8 axis keys and 5 dispatch sub-keys', () => {
|
||||
test('(ii) cap.runtime.hostIntegration is present with all 8 axis keys and 6 dispatch sub-keys', () => {
|
||||
assert.ok(
|
||||
hi !== undefined && hi !== null && typeof hi === 'object',
|
||||
id + ': cap.runtime.hostIntegration must be a non-null object',
|
||||
@@ -225,6 +226,77 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
assert.strictEqual(counts['ide'], 0, 'Must have exactly 0 ide runtimes');
|
||||
});
|
||||
|
||||
// ─── backgroundDispatch presence ─────────────────────────────────────────────
|
||||
|
||||
test('every runtime descriptor has dispatch.backgroundDispatch (boolean or "undocumented")', () => {
|
||||
for (const id of RUNTIME_IDS) {
|
||||
const cap = registry.runtimes[id];
|
||||
const dispatch = cap && cap.runtime && cap.runtime.hostIntegration && cap.runtime.hostIntegration.dispatch;
|
||||
assert.ok(
|
||||
dispatch !== null && typeof dispatch === 'object',
|
||||
id + ': hostIntegration.dispatch must be an object',
|
||||
);
|
||||
assert.ok(
|
||||
Object.prototype.hasOwnProperty.call(dispatch, 'backgroundDispatch'),
|
||||
id + ': dispatch must have a backgroundDispatch key',
|
||||
);
|
||||
const v = dispatch.backgroundDispatch;
|
||||
assert.ok(
|
||||
v === true || v === false || v === 'undocumented',
|
||||
id + ': dispatch.backgroundDispatch must be true, false, or "undocumented", got: ' + JSON.stringify(v),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── shouldFlattenDispatch per-host (#853 discriminator) ─────────────────────
|
||||
|
||||
// Expected: false (may background) for codex and cursor ONLY; true (must inline) for the other 14.
|
||||
const EXPECTED_FLATTEN = {
|
||||
antigravity: true,
|
||||
augment: true,
|
||||
claude: true,
|
||||
cline: true,
|
||||
codebuddy: true,
|
||||
codex: false,
|
||||
copilot: true,
|
||||
cursor: false,
|
||||
gemini: true,
|
||||
hermes: true,
|
||||
kilo: true,
|
||||
kimi: true,
|
||||
opencode: true,
|
||||
qwen: true,
|
||||
trae: true,
|
||||
windsurf: true,
|
||||
};
|
||||
|
||||
for (const id of RUNTIME_IDS) {
|
||||
test('shouldFlattenDispatch(' + id + ') === ' + EXPECTED_FLATTEN[id], () => {
|
||||
const cap = registry.runtimes[id];
|
||||
const dispatch = cap && cap.runtime && cap.runtime.hostIntegration && cap.runtime.hostIntegration.dispatch;
|
||||
assert.ok(dispatch, id + ': dispatch must exist');
|
||||
const result = shouldFlattenDispatch(dispatch);
|
||||
assert.strictEqual(
|
||||
result,
|
||||
EXPECTED_FLATTEN[id],
|
||||
id + ': shouldFlattenDispatch must return ' + EXPECTED_FLATTEN[id] + ' (got: ' + result + ')',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('contract-pin: exactly 2 hosts are background-eligible (shouldFlattenDispatch === false): codex and cursor', () => {
|
||||
const eligible = RUNTIME_IDS.filter((id) => {
|
||||
const cap = registry.runtimes[id];
|
||||
const dispatch = cap && cap.runtime && cap.runtime.hostIntegration && cap.runtime.hostIntegration.dispatch;
|
||||
return dispatch && shouldFlattenDispatch(dispatch) === false;
|
||||
});
|
||||
assert.deepEqual(
|
||||
eligible.slice().sort(),
|
||||
['codex', 'cursor'],
|
||||
'Exactly codex and cursor must be background-eligible, got: ' + JSON.stringify(eligible.sort()),
|
||||
);
|
||||
});
|
||||
|
||||
test('contract-pin: spot-check claude→programmatic-cli, codex→declarative-cli, opencode→programmatic-cli, gemini→declarative-cli', () => {
|
||||
const checks = [
|
||||
['claude', 'programmatic-cli'],
|
||||
@@ -273,7 +345,7 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
hostIntegration: {
|
||||
embeddingMode: 'bogus',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full' },
|
||||
dispatch: { namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full', backgroundDispatch: false },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
|
||||
@@ -68,6 +68,7 @@ function makeMinimalRuntimeCap(overrides = {}) {
|
||||
maxDepth: 1,
|
||||
background: false,
|
||||
subagentToolkit: 'full',
|
||||
backgroundDispatch: false,
|
||||
},
|
||||
},
|
||||
...overrides,
|
||||
@@ -213,6 +214,7 @@ describe('ADR-1239 validator behavioral: undocumented sentinel passes, bogus fai
|
||||
maxDepth: 'undocumented',
|
||||
background: 'undocumented',
|
||||
subagentToolkit: 'undocumented',
|
||||
backgroundDispatch: 'undocumented',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -229,6 +231,77 @@ describe('ADR-1239 validator behavioral: undocumented sentinel passes, bogus fai
|
||||
assert.strictEqual(dispatchErrors.length, 0,
|
||||
`Valid boolean dispatch fields must produce no errors; got: ${dispatchErrors.join(', ')}`);
|
||||
});
|
||||
|
||||
// Phase B: backgroundDispatch field validation
|
||||
test('dispatch.backgroundDispatch:true → ZERO validator errors', () => {
|
||||
const cap = makeMinimalRuntimeCap({
|
||||
hostIntegration: {
|
||||
...makeMinimalRuntimeCap().runtime.hostIntegration,
|
||||
dispatch: { ...makeMinimalRuntimeCap().runtime.hostIntegration.dispatch, backgroundDispatch: true },
|
||||
},
|
||||
});
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const bdErrors = errors.filter((e) => e.includes('backgroundDispatch'));
|
||||
assert.strictEqual(bdErrors.length, 0,
|
||||
`backgroundDispatch:true must produce no errors; got: ${bdErrors.join(', ')}`);
|
||||
});
|
||||
|
||||
test('dispatch.backgroundDispatch:false → ZERO validator errors', () => {
|
||||
const cap = makeMinimalRuntimeCap({
|
||||
hostIntegration: {
|
||||
...makeMinimalRuntimeCap().runtime.hostIntegration,
|
||||
dispatch: { ...makeMinimalRuntimeCap().runtime.hostIntegration.dispatch, backgroundDispatch: false },
|
||||
},
|
||||
});
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const bdErrors = errors.filter((e) => e.includes('backgroundDispatch'));
|
||||
assert.strictEqual(bdErrors.length, 0,
|
||||
`backgroundDispatch:false must produce no errors; got: ${bdErrors.join(', ')}`);
|
||||
});
|
||||
|
||||
test('dispatch.backgroundDispatch:"undocumented" → ZERO validator errors', () => {
|
||||
const cap = makeMinimalRuntimeCap({
|
||||
hostIntegration: {
|
||||
...makeMinimalRuntimeCap().runtime.hostIntegration,
|
||||
dispatch: { ...makeMinimalRuntimeCap().runtime.hostIntegration.dispatch, backgroundDispatch: 'undocumented' },
|
||||
},
|
||||
});
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const bdErrors = errors.filter((e) => e.includes('backgroundDispatch'));
|
||||
assert.strictEqual(bdErrors.length, 0,
|
||||
`backgroundDispatch:"undocumented" must produce no errors; got: ${bdErrors.join(', ')}`);
|
||||
});
|
||||
|
||||
test('dispatch.backgroundDispatch:"zzz" → produces a validator error', () => {
|
||||
const cap = makeMinimalRuntimeCap({
|
||||
hostIntegration: {
|
||||
...makeMinimalRuntimeCap().runtime.hostIntegration,
|
||||
dispatch: { ...makeMinimalRuntimeCap().runtime.hostIntegration.dispatch, backgroundDispatch: 'zzz' },
|
||||
},
|
||||
});
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const bdErrors = errors.filter((e) => e.includes('backgroundDispatch'));
|
||||
assert.ok(bdErrors.length > 0,
|
||||
`bogus value "zzz" for backgroundDispatch must produce a validator error`);
|
||||
});
|
||||
|
||||
test('dispatch without backgroundDispatch key → validator error (required field — all 16 descriptors carry it)', () => {
|
||||
// backgroundDispatch is now REQUIRED (matches sibling fields namedDispatch/nested/background/subagentToolkit/maxDepth).
|
||||
const cap = makeMinimalRuntimeCap({
|
||||
hostIntegration: {
|
||||
...makeMinimalRuntimeCap().runtime.hostIntegration,
|
||||
dispatch: (() => {
|
||||
const d = { ...makeMinimalRuntimeCap().runtime.hostIntegration.dispatch };
|
||||
delete d.backgroundDispatch;
|
||||
return d;
|
||||
})(),
|
||||
},
|
||||
});
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const bdErrors = errors.filter((e) => e.includes('backgroundDispatch'));
|
||||
assert.ok(bdErrors.length > 0,
|
||||
`Missing backgroundDispatch (required field) must produce a validator error`);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -270,6 +343,7 @@ describe('Fix 3: reserved-key guard on hostIntegration and hostIntegration.dispa
|
||||
maxDepth: 5,
|
||||
background: true,
|
||||
subagentToolkit: 'full',
|
||||
backgroundDispatch: true,
|
||||
},
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
|
||||
@@ -844,6 +844,155 @@ describe('Fix: degradationFor unknown point → {level:"absent", unknown:true}',
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: shouldFlattenDispatch — ADR-1239 Phase B / #1708
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Phase B: shouldFlattenDispatch — contract pin', () => {
|
||||
const { shouldFlattenDispatch } = hi;
|
||||
|
||||
test('shouldFlattenDispatch is exported as a function', () => {
|
||||
assert.strictEqual(typeof shouldFlattenDispatch, 'function',
|
||||
'shouldFlattenDispatch must be exported from host-integration module');
|
||||
});
|
||||
|
||||
test('{background:true, backgroundDispatch:true} → false (background OK)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch({ background: true, backgroundDispatch: true }), false,
|
||||
'canBackground=true when both background===true AND backgroundDispatch===true → flatten=false');
|
||||
});
|
||||
|
||||
test('{background:true, backgroundDispatch:false} → true (must flatten)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch({ background: true, backgroundDispatch: false }), true,
|
||||
'backgroundDispatch===false → canBackground=false → flatten=true');
|
||||
});
|
||||
|
||||
test('{background:true, backgroundDispatch:"undocumented"} → true (undocumented is not === true)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch({ background: true, backgroundDispatch: 'undocumented' }), true,
|
||||
'"undocumented" is not === true → canBackground=false → flatten=true');
|
||||
});
|
||||
|
||||
test('{background:false, backgroundDispatch:true} → true (background is false)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch({ background: false, backgroundDispatch: true }), true,
|
||||
'background===false → canBackground=false → flatten=true');
|
||||
});
|
||||
|
||||
test('{} (empty) → true (missing fields → fail-closed)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch({}), true,
|
||||
'empty dispatch → canBackground=false → flatten=true');
|
||||
});
|
||||
|
||||
test('missing fields individually', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch({ background: true }), true,
|
||||
'backgroundDispatch missing → not === true → flatten=true');
|
||||
assert.strictEqual(shouldFlattenDispatch({ backgroundDispatch: true }), true,
|
||||
'background missing → not === true → flatten=true');
|
||||
});
|
||||
|
||||
// M1: null-safety — null/undefined/non-object dispatch must fail-closed (not throw)
|
||||
test('null dispatch → true (fail-closed, no throw)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch(null), true,
|
||||
'null dispatch must fail-closed to true');
|
||||
});
|
||||
|
||||
test('undefined dispatch → true (fail-closed, no throw)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch(undefined), true,
|
||||
'undefined dispatch must fail-closed to true');
|
||||
});
|
||||
|
||||
test('string dispatch → true (fail-closed, no throw)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch('x'), true,
|
||||
'non-object dispatch (string) must fail-closed to true');
|
||||
});
|
||||
|
||||
// #853 codex-like profile: full dispatch including backgroundDispatch:true → background OK
|
||||
test('#853 codex-like: {namedDispatch:true,nested:true,maxDepth:1,background:true,subagentToolkit:"full",backgroundDispatch:true} → false (background OK)', () => {
|
||||
assert.strictEqual(
|
||||
shouldFlattenDispatch({ namedDispatch: true, nested: true, maxDepth: 1, background: true, subagentToolkit: 'full', backgroundDispatch: true }),
|
||||
false,
|
||||
'codex-like dispatch with backgroundDispatch:true must be background-OK (flatten=false)',
|
||||
);
|
||||
});
|
||||
|
||||
// #853 claude-like profile: backgroundDispatch:false → must flatten
|
||||
test('#853 claude-like: {...,background:true,backgroundDispatch:false} → true (inline)', () => {
|
||||
assert.strictEqual(
|
||||
shouldFlattenDispatch({ namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: false }),
|
||||
true,
|
||||
'claude-like dispatch with backgroundDispatch:false must flatten inline',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: negotiateHostCapabilities — backgroundDispatch field
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Phase B: negotiateHostCapabilities — backgroundDispatch', () => {
|
||||
test('host dispatch.backgroundDispatch:true against DEFAULT_ENGINE → effective.dispatch.backgroundDispatch===true', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.backgroundDispatch, true,
|
||||
'backgroundDispatch:true on host AND engine must yield effective backgroundDispatch===true');
|
||||
});
|
||||
|
||||
test('host dispatch.backgroundDispatch:"undocumented" → effective.dispatch.backgroundDispatch===false + warning', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: 'undocumented' },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.backgroundDispatch, false,
|
||||
'"undocumented" must fail-closed to false');
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('backgroundDispatch') && warnText.includes('undocumented'),
|
||||
`Expected warning about backgroundDispatch being undocumented; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('host dispatch.backgroundDispatch:false → effective.dispatch.backgroundDispatch===false', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: false },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.backgroundDispatch, false);
|
||||
});
|
||||
|
||||
test('host dispatch without backgroundDispatch key → effective.dispatch.backgroundDispatch===false (fail-closed)', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.backgroundDispatch, false,
|
||||
'Missing backgroundDispatch key must fail-closed to false');
|
||||
});
|
||||
|
||||
test('negotiateHostCapabilities({}) → effective.dispatch.backgroundDispatch===false', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.strictEqual(result.effective.dispatch.backgroundDispatch, false,
|
||||
'Empty host must produce backgroundDispatch===false (SAFE_DEFAULTS)');
|
||||
});
|
||||
|
||||
test('SAFE_DEFAULTS.dispatch.backgroundDispatch is false', () => {
|
||||
// Verified via negotiation with empty host
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.strictEqual(result.effective.dispatch.backgroundDispatch, false);
|
||||
});
|
||||
|
||||
test('DEFAULT_ENGINE.axes.dispatch.backgroundDispatch is true', () => {
|
||||
assert.strictEqual(DEFAULT_ENGINE.axes.dispatch.backgroundDispatch, true,
|
||||
'DEFAULT_ENGINE (full engine) must declare backgroundDispatch:true');
|
||||
});
|
||||
|
||||
test('existing dispatch tests still pass — effective.dispatch.namedDispatch present alongside backgroundDispatch', () => {
|
||||
const result = negotiateHostCapabilities(PROFILE_BASELINES['programmatic-cli']);
|
||||
const d = result.effective.dispatch;
|
||||
assert.ok('namedDispatch' in d, 'namedDispatch must still be present');
|
||||
assert.ok('backgroundDispatch' in d, 'backgroundDispatch must be present');
|
||||
assert.ok('nested' in d && 'maxDepth' in d && 'background' in d && 'subagentToolkit' in d,
|
||||
'all original dispatch fields must still be present');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fix 2: negotiateHostCapabilities — host omitting 'dispatch' → subagentToolkit 'read-only'
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"audit-fix.md": 10988,
|
||||
"audit-milestone.md": 17637,
|
||||
"audit-uat.md": 7425,
|
||||
"autonomous.md": 42675,
|
||||
"autonomous.md": 42747,
|
||||
"check-todos.md": 9431,
|
||||
"cleanup.md": 9897,
|
||||
"code-review-fix.md": 23890,
|
||||
@@ -40,7 +40,7 @@
|
||||
"list-phase-assumptions.md": 4305,
|
||||
"list-seeds.md": 6943,
|
||||
"list-workspaces.md": 5655,
|
||||
"manager.md": 26966,
|
||||
"manager.md": 27258,
|
||||
"map-codebase.md": 20789,
|
||||
"milestone-summary.md": 11774,
|
||||
"mvp-phase.md": 13582,
|
||||
|
||||
Reference in New Issue
Block a user