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

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