feat(#1708): typed documentation-sourced #853 dispatch-flatten (ADR-1239 Phase B) (#1719)

* 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:
Tom Boucher
2026-06-25 15:37:33 -04:00
committed by GitHub
parent 481d121dd4
commit cf2e66b39e
33 changed files with 744 additions and 149 deletions

View File

@@ -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);
}
});
});

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',
);
});

View File

@@ -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',

View File

@@ -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',

View File

@@ -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'
// ---------------------------------------------------------------------------

View File

@@ -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,