* 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:
@@ -70,6 +70,7 @@ interface DispatchCapability {
|
||||
maxDepth: number;
|
||||
background: boolean;
|
||||
subagentToolkit: SubagentToolkit;
|
||||
backgroundDispatch: boolean;
|
||||
}
|
||||
|
||||
interface HostIntegrationAxes {
|
||||
@@ -97,7 +98,7 @@ interface DegradationResult {
|
||||
const SAFE_DEFAULTS: HostIntegrationAxes = {
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'prose-only',
|
||||
dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'read-only' },
|
||||
dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'read-only', backgroundDispatch: false },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'none',
|
||||
stateIO: 'session-log-append',
|
||||
@@ -110,7 +111,7 @@ const PROFILE_BASELINES: Readonly<Record<'programmatic-cli' | 'declarative-cli'
|
||||
'programmatic-cli': Object.freeze({
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' }),
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true }),
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
@@ -120,7 +121,7 @@ const PROFILE_BASELINES: Readonly<Record<'programmatic-cli' | 'declarative-cli'
|
||||
'declarative-cli': Object.freeze({
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full' }),
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full', backgroundDispatch: false }),
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
@@ -130,7 +131,7 @@ const PROFILE_BASELINES: Readonly<Record<'programmatic-cli' | 'declarative-cli'
|
||||
'ide': Object.freeze({
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'palette',
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: 5, background: true, subagentToolkit: 'full' }),
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: 5, background: true, subagentToolkit: 'full', backgroundDispatch: true }),
|
||||
modelMode: 'active',
|
||||
hookBus: 'engine',
|
||||
stateIO: 'sandboxed-storage',
|
||||
@@ -255,7 +256,7 @@ const DEFAULT_ENGINE: EngineCapabilities = {
|
||||
axes: {
|
||||
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: true },
|
||||
modelMode: 'active',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
@@ -368,17 +369,19 @@ function negotiateHostCapabilities(
|
||||
let effectiveNamedDispatch: boolean;
|
||||
let effectiveNested: boolean;
|
||||
let effectiveBackground: boolean;
|
||||
let effectiveBackgroundDispatch: boolean;
|
||||
let effectiveSubagentToolkit: SubagentToolkit;
|
||||
let effectiveMaxDepth: number;
|
||||
|
||||
if (hostDispatch === null) {
|
||||
// Host didn't declare dispatch at all — fail-closed to most-restrictive values
|
||||
warnings.push(`host did not declare 'dispatch'`);
|
||||
effectiveNamedDispatch = false;
|
||||
effectiveNested = false;
|
||||
effectiveBackground = false;
|
||||
effectiveSubagentToolkit = 'read-only';
|
||||
effectiveMaxDepth = 0;
|
||||
effectiveNamedDispatch = false;
|
||||
effectiveNested = false;
|
||||
effectiveBackground = false;
|
||||
effectiveBackgroundDispatch = false;
|
||||
effectiveSubagentToolkit = 'read-only';
|
||||
effectiveMaxDepth = 0;
|
||||
} else {
|
||||
// N1: observability warnings for 'undocumented' sentinel on dispatch fields
|
||||
if (hostDispatch.namedDispatch === 'undocumented') {
|
||||
@@ -393,10 +396,14 @@ function negotiateHostCapabilities(
|
||||
if (hostDispatch.subagentToolkit === 'undocumented') {
|
||||
warnings.push(`dispatch.subagentToolkit is undocumented — degraded closed (read-only)`);
|
||||
}
|
||||
if (hostDispatch.backgroundDispatch === 'undocumented') {
|
||||
warnings.push(`dispatch.backgroundDispatch is undocumented — degraded closed`);
|
||||
}
|
||||
|
||||
effectiveNamedDispatch = (hostDispatch.namedDispatch === true) && engineDispatch.namedDispatch;
|
||||
effectiveNested = (hostDispatch.nested === true) && engineDispatch.nested;
|
||||
effectiveBackground = (hostDispatch.background === true) && engineDispatch.background;
|
||||
effectiveNamedDispatch = (hostDispatch.namedDispatch === true) && engineDispatch.namedDispatch;
|
||||
effectiveNested = (hostDispatch.nested === true) && engineDispatch.nested;
|
||||
effectiveBackground = (hostDispatch.background === true) && engineDispatch.background;
|
||||
effectiveBackgroundDispatch = (hostDispatch.backgroundDispatch === true) && engineDispatch.backgroundDispatch;
|
||||
|
||||
// subagentToolkit: fail closed to read-only unless explicitly 'full'
|
||||
// (an 'undocumented' or 'read-only' value → read-only)
|
||||
@@ -419,20 +426,22 @@ function negotiateHostCapabilities(
|
||||
const minDepth = Math.min(hDepthNum, eDepthNum);
|
||||
effectiveMaxDepth = minDepth === Infinity ? -1 : minDepth;
|
||||
|
||||
// If namedDispatch is false, cap maxDepth/nested/background to 0/false/false (struct consistency)
|
||||
// If namedDispatch is false, cap maxDepth/nested/background/backgroundDispatch to 0/false/false/false (struct consistency)
|
||||
if (!effectiveNamedDispatch) {
|
||||
effectiveMaxDepth = 0;
|
||||
effectiveNested = false;
|
||||
effectiveBackground = false;
|
||||
effectiveMaxDepth = 0;
|
||||
effectiveNested = false;
|
||||
effectiveBackground = false;
|
||||
effectiveBackgroundDispatch = false;
|
||||
}
|
||||
}
|
||||
|
||||
const effectiveDispatch: DispatchCapability = {
|
||||
namedDispatch: effectiveNamedDispatch,
|
||||
nested: effectiveNested,
|
||||
maxDepth: effectiveMaxDepth,
|
||||
background: effectiveBackground,
|
||||
subagentToolkit: effectiveSubagentToolkit,
|
||||
namedDispatch: effectiveNamedDispatch,
|
||||
nested: effectiveNested,
|
||||
maxDepth: effectiveMaxDepth,
|
||||
background: effectiveBackground,
|
||||
subagentToolkit: effectiveSubagentToolkit,
|
||||
backgroundDispatch: effectiveBackgroundDispatch,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -474,6 +483,35 @@ function negotiateHostCapabilities(
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// shouldFlattenDispatch — ADR-1239 Phase B / #1708
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns true when the orchestrator MUST run inline (flatten); false when it
|
||||
* may be backgrounded.
|
||||
*
|
||||
* A host may background only if it can reliably background a nesting-capable
|
||||
* orchestrator — i.e. both `background` AND `backgroundDispatch` are
|
||||
* explicitly `true`. Any other value (false, missing, 'undocumented') fails
|
||||
* closed to inline (the always-safe path).
|
||||
*
|
||||
* This graduates the #853 prose rule (originally `RUNTIME === 'codex'`, then
|
||||
* extended to cursor) to a typed, documentation-sourced decision; codex AND
|
||||
* cursor are both background-eligible in the registry. See
|
||||
* docs/reference/host-integration-capability-matrix.md.
|
||||
*
|
||||
* Null-safety: if dispatch is null, undefined, or not an object, returns true
|
||||
* (inline, fail-closed) instead of throwing.
|
||||
*/
|
||||
type UnvalidatedDispatch = (Partial<DispatchCapability> & { background?: unknown; backgroundDispatch?: unknown }) | null | undefined;
|
||||
|
||||
function shouldFlattenDispatch(dispatch: UnvalidatedDispatch): boolean {
|
||||
if (!dispatch || typeof dispatch !== 'object') return true;
|
||||
const canBackground = dispatch.background === true && dispatch.backgroundDispatch === true;
|
||||
return !canBackground;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module export (CommonJS — matches existing src/*.cts pattern)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -488,4 +526,5 @@ export = {
|
||||
degradationFor,
|
||||
profileOf,
|
||||
negotiateHostCapabilities,
|
||||
shouldFlattenDispatch,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user