* 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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user