feat(#2584): add dispatch.isolation sub-field, descriptors, validator + negotiation (#2604)

This commit is contained in:
Tom Boucher
2026-07-24 12:51:42 -04:00
committed by GitHub
parent bd18a593a9
commit ec7978c0b4
25 changed files with 413 additions and 63 deletions

View File

@@ -128,7 +128,7 @@ Module owning bounded, never-throw git repository introspection — the single s
Module owning runtime identity normalization at runtime-selection seams. Canonicalizes alias signals from env/config (`GSD_RUNTIME`, `.planning/config.json:runtime`) to supported runtime IDs so output emitters and query runtime gates stay consistent across naming variants (for example `codex-app`/`codex-cli` -> `codex`). Sources: `gsd-core/bin/lib/runtime-name-policy.cjs`, alias manifest `gsd-core/bin/shared/runtime-aliases.manifest.json`.
### Host-Integration Interface
Pure, additive, no-I/O Module owning the versioned, negotiated contract over the six host-integration interface points (command, dispatch, model, hooks, state, artifact) — ADR-1239 Phase A. Extends the ADR-1016 runtime descriptor with nine closed-vocabulary axes carried under `capability.json` `runtime.hostIntegration`: `embeddingMode` (`imperative|declarative`), `commandSurface` (`slash-file|slash-programmatic|slash-toml|palette|prose-only`), `dispatch` (`{namedDispatch,nested,maxDepth,background,backgroundDispatch,subagentToolkit}`), `modelMode` (`active|passive`), `hookBus` (`host|engine|none`), `stateIO` (`filesystem|sandboxed-storage|session-log-append`), `transport` (`mcp|native-extension`), `runtime` (`node|bun|sandboxed-web|python|go|rust|electron|other`), `effortSurface` (`argv|none` — how reasoning effort reaches the host; ADR-1239 amendment #2481, the first axis whose consumer is an invocation-time argument rather than an install-time artifact). Interface: `negotiateHostCapabilities(host, engine?) → { protocolVersion, effective, points, warnings }` enforcing the trust-boundary invariant `effective ⊆ host-declared ∩ engine-known` (never augment with an undeclared or unknown/future-`protocolVersion` value — fail-closed via the most-restrictive-known `SAFE_DEFAULTS`); `degradationFor(point, axes) → { level, fallback }` (a pure Full/Degraded/Absent ladder table, never throws); `profileOf(axes) → 'programmatic-cli'|'declarative-cli'|'ide'|null`; plus `PROTOCOL_VERSION` (integer, starts at 1 — distinct from the package `version`/`engines.gsd` semver), `HOST_INTEGRATION_AXES` (the frozen closed vocabulary, single source of truth), `PROFILE_BASELINES`, and `shouldFlattenDispatch(dispatch) → boolean` (ADR-1239 Phase B / #1708 — graduates the #853 rule: returns `true` = run the orchestrator inline UNLESS the host is documented to background a nesting-capable orchestrator (`background === true && backgroundDispatch === true`); fail-closed to inline; exposed to the plan/execute workflows via the `gsd_run query dispatch-should-flatten --raw` CLI, which replaced the former scattered `RUNTIME === 'codex'` prose check). The runtime-descriptor validator (`gsd-core/bin/lib/capability-validator.cjs` `validateRuntimeBody`) mirrors the closed vocabulary inline (exported as `_HOST_INTEGRATION_VOCAB`) and is kept in lock-step by the parity guard `tests/host-integration-validator-parity.test.cjs`. Orthogonal axes (resolved explicitly per ADR-1239 Phase A): `commandStyle` (GSD emission style, retained) vs `commandSurface` (host surface type); `hookEvents` dialect vs `hookBus` ownership (a host with `hooksSurface:none` may still be `hookBus:host` — e.g. opencode); `runtimeCompat` (feature→host) vs these negotiated runtime→engine axes. Phase A defined the interface; Phase B (#1679) wires it incrementally — `destSubpath` write-confinement (#1704) and the typed documentation-sourced #853 dispatch-flatten (#1708, the first consumer of a negotiated `dispatch` axis); adapters/MCP/host-bindings remain Phases C–E. Source of truth: `gsd-core/bin/lib/host-integration.cjs` (generated from `src/host-integration.cts`). See ADR-1239 and ADR-1016.
Pure, additive, no-I/O Module owning the versioned, negotiated contract over the six host-integration interface points (command, dispatch, model, hooks, state, artifact) — ADR-1239 Phase A. Extends the ADR-1016 runtime descriptor with nine closed-vocabulary axes carried under `capability.json` `runtime.hostIntegration`: `embeddingMode` (`imperative|declarative`), `commandSurface` (`slash-file|slash-programmatic|slash-toml|palette|prose-only`), `dispatch` (`{namedDispatch,nested,maxDepth,background,backgroundDispatch,subagentToolkit,isolation}`), `modelMode` (`active|passive`), `hookBus` (`host|engine|none`), `stateIO` (`filesystem|sandboxed-storage|session-log-append`), `transport` (`mcp|native-extension`), `runtime` (`node|bun|sandboxed-web|python|go|rust|electron|other`), `effortSurface` (`argv|none` — how reasoning effort reaches the host; ADR-1239 amendment #2481, the first axis whose consumer is an invocation-time argument rather than an install-time artifact). `dispatch.isolation` (`harness-worktree|orchestrator-worktree|none` — how a host isolates concurrent same-wave executors; ADR-1239 Codex-binding amendment #2584; declared and negotiated but not yet consumed by any scheduler — Phase 1 of #2584). Interface: `negotiateHostCapabilities(host, engine?) → { protocolVersion, effective, points, warnings }` enforcing the trust-boundary invariant `effective ⊆ host-declared ∩ engine-known` (never augment with an undeclared or unknown/future-`protocolVersion` value — fail-closed via the most-restrictive-known `SAFE_DEFAULTS`); `degradationFor(point, axes) → { level, fallback }` (a pure Full/Degraded/Absent ladder table, never throws); `profileOf(axes) → 'programmatic-cli'|'declarative-cli'|'ide'|null`; plus `PROTOCOL_VERSION` (integer, starts at 1 — distinct from the package `version`/`engines.gsd` semver), `HOST_INTEGRATION_AXES` (the frozen closed vocabulary, single source of truth), `PROFILE_BASELINES`, and `shouldFlattenDispatch(dispatch) → boolean` (ADR-1239 Phase B / #1708 — graduates the #853 rule: returns `true` = run the orchestrator inline UNLESS the host is documented to background a nesting-capable orchestrator (`background === true && backgroundDispatch === true`); fail-closed to inline; exposed to the plan/execute workflows via the `gsd_run query dispatch-should-flatten --raw` CLI, which replaced the former scattered `RUNTIME === 'codex'` prose check). The runtime-descriptor validator (`gsd-core/bin/lib/capability-validator.cjs` `validateRuntimeBody`) mirrors the closed vocabulary inline (exported as `_HOST_INTEGRATION_VOCAB`) and is kept in lock-step by the parity guard `tests/host-integration-validator-parity.test.cjs`. Orthogonal axes (resolved explicitly per ADR-1239 Phase A): `commandStyle` (GSD emission style, retained) vs `commandSurface` (host surface type); `hookEvents` dialect vs `hookBus` ownership (a host with `hooksSurface:none` may still be `hookBus:host` — e.g. opencode); `runtimeCompat` (feature→host) vs these negotiated runtime→engine axes. Phase A defined the interface; Phase B (#1679) wires it incrementally — `destSubpath` write-confinement (#1704) and the typed documentation-sourced #853 dispatch-flatten (#1708, the first consumer of a negotiated `dispatch` axis); adapters/MCP/host-bindings remain Phases C–E. Source of truth: `gsd-core/bin/lib/host-integration.cjs` (generated from `src/host-integration.cts`). See ADR-1239 and ADR-1016.
### Statusline
Host-integration hook (`hooks/gsd-statusline.js`) that renders the session status line: model name, context-window meter, workspace directory, and the GSD-state segment (`formatGsdState()` projecting `.planning/` STATE.md). Opt-in segments are gated by `.planning/config.json` keys (`statusline.show_last_command`, `statusline.context_position`, plus the approved `statusline.show_context_tokens` and `statusline.state_format`), each registered across `gsd-core/bin/shared/config-schema.manifest.json` + `src/config.cts` + the `loadConfig` whitelist + `docs/CONFIGURATION.md`. The compact GSD-state format consumes the canonical status vocabulary from `normalizeStateStatus()` (STATE.md Document Module) rather than a parallel keyword list. **Data-source boundary (ADR-2164):** the statusline sources only local, read-only data — it refines the stdin payload Claude Code already sends and may add a new *local* source (e.g. `git`), but does not read credentials or call external/network APIs for data; account/usage/platform-level state is out of scope.

View File

@@ -82,7 +82,8 @@
"maxDepth": "undocumented",
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": "undocumented"
"backgroundDispatch": "undocumented",
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "host",

View File

@@ -95,7 +95,8 @@
"maxDepth": "undocumented",
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": "undocumented"
"backgroundDispatch": "undocumented",
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "host",

View File

@@ -72,7 +72,8 @@
"maxDepth": 5,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "harness-worktree"
},
"modelMode": "passive",
"hookBus": "host",

View File

@@ -49,7 +49,8 @@
"maxDepth": 1,
"background": true,
"subagentToolkit": "read-only",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "active",
"hookBus": "host",

View File

@@ -96,7 +96,8 @@
"maxDepth": 1,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "host",

View File

@@ -65,7 +65,8 @@
"maxDepth": 1,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": true
"backgroundDispatch": true,
"isolation": "orchestrator-worktree"
},
"modelMode": "passive",
"hookBus": "host",

View File

@@ -75,7 +75,8 @@
"maxDepth": 1,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "host",

View File

@@ -91,7 +91,8 @@
"maxDepth": 2,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": true
"backgroundDispatch": true,
"isolation": "harness-worktree"
},
"modelMode": "passive",
"hookBus": "host",

View File

@@ -77,7 +77,8 @@
"maxDepth": 1,
"background": true,
"subagentToolkit": "read-only",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "active",
"hookBus": "host",

View File

@@ -82,7 +82,8 @@
"maxDepth": -1,
"background": true,
"subagentToolkit": "undocumented",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "active",
"hookBus": "host",

View File

@@ -64,7 +64,8 @@
"coder",
"explore",
"plan"
]
],
"isolation": "orchestrator-worktree"
},
"modelMode": "passive",
"hookBus": "host",

View File

@@ -68,7 +68,8 @@
"maxDepth": 1,
"background": true,
"subagentToolkit": "undocumented",
"backgroundDispatch": true
"backgroundDispatch": true,
"isolation": "orchestrator-worktree"
},
"modelMode": "passive",
"hookBus": "host",

View File

@@ -77,7 +77,8 @@
"maxDepth": "undocumented",
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": true
"backgroundDispatch": true,
"isolation": "orchestrator-worktree"
},
"modelMode": "active",
"hookBus": "host",

View File

@@ -40,7 +40,8 @@
"maxDepth": 0,
"background": false,
"backgroundDispatch": false,
"subagentToolkit": "undocumented"
"subagentToolkit": "undocumented",
"isolation": "none"
},
"modelMode": "active",
"hookBus": "host",

View File

@@ -80,7 +80,8 @@
"maxDepth": 1,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "host",

View File

@@ -74,7 +74,8 @@
"maxDepth": "undocumented",
"background": true,
"subagentToolkit": "undocumented",
"backgroundDispatch": "undocumented"
"backgroundDispatch": "undocumented",
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "engine",

View File

@@ -39,7 +39,8 @@
"maxDepth": 5,
"background": true,
"subagentToolkit": "undocumented",
"backgroundDispatch": "undocumented"
"backgroundDispatch": "undocumented",
"isolation": "undocumented"
},
"modelMode": "active",
"hookBus": "engine",

View File

@@ -67,7 +67,8 @@
"maxDepth": "undocumented",
"background": "undocumented",
"subagentToolkit": "undocumented",
"backgroundDispatch": "undocumented"
"backgroundDispatch": "undocumented",
"isolation": "none"
},
"modelMode": "passive",
"hookBus": "host",

View File

@@ -90,7 +90,8 @@
"maxDepth": "undocumented",
"background": false,
"subagentToolkit": "full",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "none"
},
"modelMode": "passive",
"hookBus": "host",

View File

@@ -66,6 +66,7 @@ consumed verbatim by `gen:capability-registry` and validated by `capability-vali
| dispatch.background | true | https://code.claude.com/docs/en/sub-agents | "Subagents can run in the foreground, blocking the main conversation and passing permission prompts to you, or in the bac" |
| dispatch.subagentToolkit | full | https://code.claude.com/docs/en/sub-agents | "If all tools remain selected, the subagent inherits all tools available to the main conversation." |
| dispatch.backgroundDispatch | false | https://code.claude.com/docs/en/sub-agents | "Background subagents are limited to a depth of five and cannot spawn further, " |
| dispatch.isolation | harness-worktree | https://code.claude.com/docs/en/sub-agents ; Claude Code Agent tool (`Agent(isolation="worktree")`) | The Claude Code Agent tool accepts an `isolation="worktree"` harness primitive — the host's own harness creates + binds a git worktree per executor; GSD passes the flag and calls no git itself (#2584) |
Sources consulted:
- https://code.claude.com/docs/en/sub-agents
@@ -144,6 +145,7 @@ Documentation gaps:
| dispatch.background | true | https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/tool/task.ts (v1.15.0, commit 22de34c4d) + src/effect/runtime-flags.ts (v1.17, commit 81f6e0668) | "New in v1.15.0: experimental background subagents — the Task tool gains a `background` parameter (`Schema.optional(Schema.Boolean)`) that launches subagents asynchronously with completion notifications. v1.17: `BACKGROUND_SUBAGENTS_ENABLED = true` (\"feat: enable background subagents by default\") — default-on, concurrent execution in all modes. (#2087, superseding the stale sst/opencode#5887 snapshot)" |
| dispatch.subagentToolkit | full | https://opencode.ai/docs/agents | "The 'general' subagent \"Has full tool access (except todo), so it can make file changes when needed.\"" |
| dispatch.backgroundDispatch | true | https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/effect/runtime-flags.ts (v1.17, commit 81f6e0668) + src/server/routes/instance/httpapi/handlers/experimental.ts | "v1.17 `BACKGROUND_SUBAGENTS_ENABLED = true` enables background subagent execution by default in all modes; the experimental capabilities endpoint exposes `{ backgroundSubagents: true }`. Background-spawned subagents run concurrently without blocking the main interaction flow. (#2087)" |
| dispatch.isolation | orchestrator-worktree | https://opencode.ai/docs/cli ; opencode.ai/docs/plugins ; opencode issues #14195/#29638/#5887 | "`opencode run --dir <path>` sets an explicit working root at the process level" — native subagent dispatch is synchronous-only, so GSD creates + manages the worktree and process-spawns the executor into it via `--dir` (#2584) |
Sources consulted:
- https://opencode.ai/docs/plugins
@@ -177,6 +179,7 @@ Documentation gaps:
| dispatch.background | true | https://cursor.com/docs/subagents | "Background, which returns immediately while the subagent works independently, best for long-running tasks or parallel wo" |
| dispatch.subagentToolkit | full | https://cursor.com/docs/subagents | "Subagents can utilize MCP tools, inheriting all tools available to their parent agent, including those from configured s" |
| dispatch.backgroundDispatch | true | https://cursor.com/docs/subagents (FAQ: Can subagents launch other subagents?) and https://cursor.com/docs/sdk/typescript (Subagents > Nested subagents) | FAQ: "As of Cursor 2.5, subagents have the capability to launch child subagents, enabling the creation of a hierarchical structure for coordinated tasks. This nested launching functionality requires T |
| dispatch.isolation | harness-worktree | https://cursor.com/docs/cli/reference/parameters ; cursor.com/docs/cli/using ; cursor.com/docs/cli/changelog | "`-w, --worktree [name]` — cursor-agent creates/binds a git worktree per agent (`~/.cursor/worktrees/…`); native parallel-agent dispatch" (#2584) |
Sources consulted:
- https://cursor.com/docs/subagents
@@ -212,6 +215,7 @@ Sources consulted:
| dispatch.background | true | /cline/cline (Context7) — https://github.com/cline/cline/blob/main/docs/features/subagents.mdx | "Commands executed by subagents run in the background and are strictly limited to read-only operations" |
| dispatch.subagentToolkit | read-only | /cline/cline (Context7) — https://github.com/cline/cline/blob/main/docs/features/subagents.mdx | "Subagents are equipped with tools for read-only operations, including reading file contents (read_file), listing directo" |
| dispatch.backgroundDispatch | false | https://docs.cline.bot/features/subagents (mirrored at https://github.com/cline/cline/blob/main/docs/features/subagents.mdx) | "They cannot edit files, use the browser, or spawn nested subagents" — and from the GitHub source: "subagents are restricted from editing files, using the browser, accessing MCP servers, or creating n |
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
Sources consulted:
- https://github.com/cline/cline/blob/main/docs/sdk/plugins.mdx
@@ -248,6 +252,7 @@ Sources consulted:
| dispatch.background | true | https://github.com/NousResearch/hermes-agent/releases/tag/v2026.6.19 | "delegate_task(background=true) dispatches a subagent that runs in the background and returns a handle immediately" |
| dispatch.subagentToolkit | read-only | https://hermes-agent.nousresearch.com/docs/guides/delegation-patterns | "Nested delegation is opt-in; by default, leaf subagents cannot call delegate_task, clarify, memory, send_message, or exe" |
| dispatch.backgroundDispatch | false | https://github.com/nousresearch/hermes-agent/blob/main/website/docs/user-guide/features/delegation.md (via Context7 query of /nousresearch/hermes-agent) | "Nested delegation is an opt-in feature, requiring role=\"orchestrator\" for children and an increased max_spawn_depth from its default of 1. It can also be globally disabled with orchestrator_enabled |
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
Sources consulted:
- https://hermes-agent.nousresearch.com/docs/user-guide/features/delegation
@@ -284,6 +289,7 @@ Documentation gaps:
| dispatch.background | true | https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/ | "Antigravity CLI orchestrates multiple agents for complex tasks in the background" |
| dispatch.subagentToolkit | full | https://antigravity.google/docs/cli/features | "Capabilities: Subagents have full access to tools such as code search, file editing, terminal commands, and web searches to complete their assigned tasks." (#2096 EoS migration — the page is JS-rendered/blank on a static fetch; confirmed via headless-browser render) |
| dispatch.backgroundDispatch | undocumented | no authoritative doc — Multiple sources consulted: antigravity.google/docs/cli-subagents (returned blank/JS-rendered), antigravity.google/docs/agent (blank), github.com/google-antigravity/antigravity-cli README, Context7 /google-antigravity/antigravity-cli | All documentation consulted describes a two-level orchestrator→subagent architecture. Background subagents run asynchronously while the main agent continues accepting prompts. The DataCamp tutorial st |
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
Sources consulted:
- https://github.com/alphaperseii3000/google-antigravity-docs/blob/master/google-antigravity-docs.md
@@ -321,6 +327,7 @@ Documentation gaps:
| dispatch.background | true | https://docs.augmentcode.com/cli/subagents | "Subagents run in parallel with other subagents... will show a summary of their current progress in the main thread." |
| dispatch.subagentToolkit | full | https://docs.augmentcode.com/cli/subagents | "If neither [tools nor disabled_tools] is specified, the subagent has access to all tools (default behavior)." |
| dispatch.backgroundDispatch | undocumented | no authoritative doc — https://docs.augmentcode.com/cosmos/automations | The Augment Code (Cosmos) docs describe workers as 'sub-agents launched mid-session by a manager Expert using the worker-launch command. Each worker is its own session with its own messages and permis |
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
Sources consulted:
- https://docs.augmentcode.com/cli/plugins
@@ -383,6 +390,7 @@ upgrade coverage is in `tests/augment-upgrades.test.cjs`.
| dispatch.background | true | https://qwenlm.github.io/qwen-code-docs/en/users/features/sub-agents/ | "Runs in background, parent continues immediately... Forks run parallel to the parent; the main conversation continues im" |
| dispatch.subagentToolkit | full | https://qwenlm.github.io/qwen-code-docs/en/users/features/sub-agents/ | "When omitted, the subagent inherits all available tools from the parent session." |
| dispatch.backgroundDispatch | false | https://qwenlm.github.io/qwen-code-docs/en/users/features/sub-agents/ (official Qwen Code documentation, 'Subagents' user guide page) and https://qwenlm.github.io/qwen-code-docs/en/design/fork-subagent/fork-subagent-design (Qwen Code fork-subagent design document, section '4. Recursive Fork Prevention') | The official user-facing Qwen Code docs state verbatim: "Fork children cannot create further forks. If a fork attempts spawning another fork, it receives an error instructing direct task execution ins |
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
Sources consulted:
- https://qwenlm.github.io/qwen-code-docs/en/developers/channel-plugins
@@ -418,6 +426,7 @@ Documentation gaps:
| dispatch.background | true | https://www.codebuddy.ai/docs/cli/sub-agents | "Launch a background agent using the run_in_background: true parameter ... Tasks return immediately with an ID" |
| dispatch.subagentToolkit | full | https://www.codebuddy.ai/docs/cli/sub-agents | "By default, sub-agents inherit all tools when the tools field is omitted ... Sub-agents can access MCP tools from config" |
| dispatch.backgroundDispatch | false | https://www.codebuddy.ai/docs/cli/sub-agents | "This prevents infinite nesting of agents (sub-agents cannot spawn other sub-agents)" — the restriction is stated as universal in the Sub-Agents documentation page. The daemon/background docs (https:/ |
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
Sources consulted:
- https://www.codebuddy.ai/docs/cli/plugins
@@ -449,6 +458,7 @@ Sources consulted:
| dispatch.background | true | https://docs.github.com/en/copilot/how-tos/copilot-cli/speed-up-task-completion | "Allow Copilot to use subagents and work autonomously to implement the plan without any further input." |
| dispatch.subagentToolkit | full | https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/create-custom-agents-for-cli | "By default, custom agents have access to all tools. If you restrict an agent's access, a tools specification is added" |
| dispatch.backgroundDispatch | false | https://code.visualstudio.com/docs/copilot/agents/subagents | "By default, subagents cannot spawn further subagents. This prevents infinite recursion when agents accidentally call themselves in a loop." The setting `chat.subagents.allowInvocationsFromSubagents` |
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
Sources consulted:
- https://github.com/github/copilot-cli/blob/main/README.md (via Context7 /github/copilot-cli)
@@ -483,6 +493,7 @@ Documentation gaps:
| dispatch.background | true | https://kilo.ai/docs/code-with-ai/agents/orchestrator-mode | "Agents are also capable of launching multiple subagent sessions concurrently to facilitate parallel processing." |
| dispatch.subagentToolkit | undocumented | no authoritative doc — searched: https://kilo.ai/docs/customize/custom-subagents | — |
| dispatch.backgroundDispatch | false | https://kilo.ai/docs/automate/tools/new-task | "Importantly, subagents cannot spawn further subagents; only primary agents can use the `new_task` tool." |
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
Sources consulted:
- https://kilo.ai/docs/automate/extending/plugins
@@ -519,6 +530,7 @@ Documentation gaps:
| dispatch.background | undocumented | no authoritative doc — searched: https://docs.devin.ai/desktop/acp.md, https://docs.devin.ai/cli/subagents.md | — |
| dispatch.subagentToolkit | undocumented | no authoritative doc — searched: https://docs.devin.ai/cli/subagents.md | — |
| dispatch.backgroundDispatch | undocumented | no authoritative doc — https://docs.devin.ai/desktop/cascade/cascade and https://docs.devin.ai/desktop/devin-local (official Windsurf/Devin docs, via docs.windsurf.com redirects) | The Windsurf/Cascade docs describe a background planning agent only in these terms: "In the background, a specialized planning agent continuously refines the long-term plan while your selected model f |
| dispatch.isolation | none | shipped descriptor (`dispatch.backgroundDispatch: undocumented`) | no documented background/concurrent-dispatch primitive — isolation is moot; same-wave plans run inline (#2584) |
Sources consulted:
- https://docs.devin.ai/desktop/cascade/workflows
@@ -559,6 +571,7 @@ Documentation gaps:
| dispatch.background | true | https://news.aibase.com/news/22829 | "SOLO 'supports multi-tasking, allowing you to work on multiple development tasks simultaneously'; 'run multiple agents i" |
| dispatch.subagentToolkit | undocumented | no authoritative doc — searched: https://docs.trae.ai/ide/agent | — |
| dispatch.backgroundDispatch | undocumented | no authoritative doc — https://docs.trae.ai/ide/agent; https://github.com/bytedance/trae-agent/blob/main/docs/roadmap.md | Trae's official documentation (docs.trae.ai) and the trae-agent GitHub roadmap do not document background/async agent dispatch or whether a background-spawned agent can itself spawn further sub-agents |
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
Sources consulted:
- https://docs.trae.ai/ide/model-context-protocol
@@ -599,6 +612,7 @@ Documentation gaps:
| dispatch.background | true | https://moonshotai.github.io/kimi-cli/en/customization/agents.html | "Subagents support foreground and background modes. The `run_in_background` parameter allows tasks to execute asynchronou" |
| dispatch.subagentToolkit | undocumented | no authoritative doc — searched: https://moonshotai.github.io/kimi-cli/en/customization/agents.html | — |
| dispatch.backgroundDispatch | true (#2095 Upgrade 2; was `false`) | https://moonshotai.github.io/kimi-cli/en/customization/agents.html | "Subagents support foreground and background modes. The `run_in_background` parameter allows tasks to execute asynchronously" (same evidence as dispatch.background above — the root agent's `Agent` tool call itself takes the `run_in_background` param) |
| dispatch.isolation | orchestrator-worktree | https://github.com/moonshotai/kimi-cli/blob/main/docs/en/faq.md ; /docs/en/customization/agents.md | "`--work-dir` flag sets an explicit working directory"; concurrent "explore" subagents documented — GSD creates + manages the worktree and points the executor at it via `--work-dir` (#2584) |
Sources consulted:
- https://moonshotai.github.io/kimi-cli/en/customization/hooks.html
@@ -635,6 +649,7 @@ Documentation gaps:
| dispatch.background | false | https://zcode.z.ai/en/docs/subagents | "**Foreground execution.** Subagents run in the foreground ... Background execution is not enabled yet." |
| dispatch.subagentToolkit | full | https://zcode.z.ai/en/docs/subagents | "**general-purpose** is the default built-in subagent ... It has access to all tools"; custom subagents default to "All permissions by default" (inherits every tool). |
| dispatch.backgroundDispatch | false | https://zcode.z.ai/en/docs/subagents | "Background execution is not enabled yet" — background dispatch is therefore impossible. |
| dispatch.isolation | none | https://zcode.z.ai/en/docs/subagents (shipped descriptor: `dispatch.backgroundDispatch: false`) | "Background execution is not enabled yet" — no concurrent fan-out primitive, so same-wave plans run inline/sequentially (#2584) |
Sources consulted:
- https://zcode.z.ai/en/docs/skill
@@ -675,6 +690,7 @@ EoS migration status (#2101, ADR-1239): ZCode's install is fully dogfooded throu
| dispatch.background | false | no authoritative doc — searched: https://pi.dev/docs/latest/extensions | No documented background/async subagent-execution primitive. |
| dispatch.subagentToolkit | undocumented | no authoritative doc — searched: https://pi.dev/docs/latest/extensions | pi has no named-dispatch primitive (see `dispatch.namedDispatch`), so there is no subagent tool-surface to classify as `full`/`read-only`. |
| dispatch.backgroundDispatch | false | no authoritative doc — searched: https://pi.dev/docs/latest/extensions | Same gap as `dispatch.background` — no background-dispatch primitive is documented, so a background-dispatched agent spawning further named sub-agents is not possible. |
| dispatch.isolation | none | shipped descriptor (`dispatch.background: false`, `dispatch.backgroundDispatch: false`) | pi has no named-dispatch/background-dispatch primitive documented — cannot fan out concurrently, so isolation is moot (#2584) |
Sources consulted:
- https://pi.dev
@@ -721,6 +737,7 @@ EoS migration status (#2102 Stage 2, ADR-1239): Stage 1's "in-process `gsd-core`
| dispatch.background | true | https://code.visualstudio.com/api/extension-guides/ai/tools | Language Model Tools can be invoked as part of an asynchronous agent turn (the primary agent does not block synchronously on a single extension call). |
| dispatch.subagentToolkit | undocumented | no authoritative doc found at authoring time | VS Code's subagent documentation does not state whether a subagent's tool surface is restricted to read-only tools or the full set an extension registers; recorded `undocumented` (fails closed to `read-only` in negotiation) rather than guessed. |
| dispatch.backgroundDispatch | undocumented | no authoritative doc found at authoring time | Whether a background-dispatched subagent can itself spawn further NAMED subagents (the #853 discriminator) is not stated in the sources reviewed; recorded `undocumented` (fails closed to `false`) rather than guessed. |
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
Sources consulted:
- https://code.visualstudio.com/api/references/vscode-api

View File

@@ -176,7 +176,8 @@ const capabilities = {
"maxDepth": "undocumented",
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": "undocumented"
"backgroundDispatch": "undocumented",
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "host",
@@ -374,7 +375,8 @@ const capabilities = {
"maxDepth": "undocumented",
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": "undocumented"
"backgroundDispatch": "undocumented",
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "host",
@@ -505,7 +507,8 @@ const capabilities = {
"maxDepth": 5,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "harness-worktree"
},
"modelMode": "passive",
"hookBus": "host",
@@ -676,7 +679,8 @@ const capabilities = {
"maxDepth": 1,
"background": true,
"subagentToolkit": "read-only",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "active",
"hookBus": "host",
@@ -854,7 +858,8 @@ const capabilities = {
"maxDepth": 1,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "host",
@@ -935,7 +940,8 @@ const capabilities = {
"maxDepth": 1,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": true
"backgroundDispatch": true,
"isolation": "orchestrator-worktree"
},
"modelMode": "passive",
"hookBus": "host",
@@ -1031,7 +1037,8 @@ const capabilities = {
"maxDepth": 1,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "host",
@@ -1141,7 +1148,8 @@ const capabilities = {
"maxDepth": 2,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": true
"backgroundDispatch": true,
"isolation": "harness-worktree"
},
"modelMode": "passive",
"hookBus": "host",
@@ -1492,7 +1500,8 @@ const capabilities = {
"maxDepth": 1,
"background": true,
"subagentToolkit": "read-only",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "active",
"hookBus": "host",
@@ -1639,7 +1648,8 @@ const capabilities = {
"maxDepth": -1,
"background": true,
"subagentToolkit": "undocumented",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "active",
"hookBus": "host",
@@ -1733,7 +1743,8 @@ const capabilities = {
"maxDepth": 1,
"background": true,
"subagentToolkit": "undocumented",
"backgroundDispatch": true
"backgroundDispatch": true,
"isolation": "orchestrator-worktree"
},
"modelMode": "passive",
"hookBus": "host",
@@ -1818,7 +1829,8 @@ const capabilities = {
"coder",
"explore",
"plan"
]
],
"isolation": "orchestrator-worktree"
},
"modelMode": "passive",
"hookBus": "host",
@@ -2140,7 +2152,8 @@ const capabilities = {
"maxDepth": "undocumented",
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": true
"backgroundDispatch": true,
"isolation": "orchestrator-worktree"
},
"modelMode": "active",
"hookBus": "host",
@@ -2264,7 +2277,8 @@ const capabilities = {
"maxDepth": 0,
"background": false,
"backgroundDispatch": false,
"subagentToolkit": "undocumented"
"subagentToolkit": "undocumented",
"isolation": "none"
},
"modelMode": "active",
"hookBus": "host",
@@ -2442,7 +2456,8 @@ const capabilities = {
"maxDepth": 1,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "host",
@@ -2792,7 +2807,8 @@ const capabilities = {
"maxDepth": "undocumented",
"background": true,
"subagentToolkit": "undocumented",
"backgroundDispatch": "undocumented"
"backgroundDispatch": "undocumented",
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "engine",
@@ -2943,7 +2959,8 @@ const capabilities = {
"maxDepth": 5,
"background": true,
"subagentToolkit": "undocumented",
"backgroundDispatch": "undocumented"
"backgroundDispatch": "undocumented",
"isolation": "undocumented"
},
"modelMode": "active",
"hookBus": "engine",
@@ -3023,7 +3040,8 @@ const capabilities = {
"maxDepth": "undocumented",
"background": "undocumented",
"subagentToolkit": "undocumented",
"backgroundDispatch": "undocumented"
"backgroundDispatch": "undocumented",
"isolation": "none"
},
"modelMode": "passive",
"hookBus": "host",
@@ -3132,7 +3150,8 @@ const capabilities = {
"maxDepth": "undocumented",
"background": false,
"subagentToolkit": "full",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "none"
},
"modelMode": "passive",
"hookBus": "host",
@@ -4159,7 +4178,8 @@ const runtimes = {
"maxDepth": "undocumented",
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": "undocumented"
"backgroundDispatch": "undocumented",
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "host",
@@ -4274,7 +4294,8 @@ const runtimes = {
"maxDepth": "undocumented",
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": "undocumented"
"backgroundDispatch": "undocumented",
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "host",
@@ -4359,7 +4380,8 @@ const runtimes = {
"maxDepth": 5,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "harness-worktree"
},
"modelMode": "passive",
"hookBus": "host",
@@ -4442,7 +4464,8 @@ const runtimes = {
"maxDepth": 1,
"background": true,
"subagentToolkit": "read-only",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "active",
"hookBus": "host",
@@ -4559,7 +4582,8 @@ const runtimes = {
"maxDepth": 1,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "host",
@@ -4640,7 +4664,8 @@ const runtimes = {
"maxDepth": 1,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": true
"backgroundDispatch": true,
"isolation": "orchestrator-worktree"
},
"modelMode": "passive",
"hookBus": "host",
@@ -4736,7 +4761,8 @@ const runtimes = {
"maxDepth": 1,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "host",
@@ -4846,7 +4872,8 @@ const runtimes = {
"maxDepth": 2,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": true
"backgroundDispatch": true,
"isolation": "harness-worktree"
},
"modelMode": "passive",
"hookBus": "host",
@@ -4954,7 +4981,8 @@ const runtimes = {
"maxDepth": 1,
"background": true,
"subagentToolkit": "read-only",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "active",
"hookBus": "host",
@@ -5049,7 +5077,8 @@ const runtimes = {
"maxDepth": -1,
"background": true,
"subagentToolkit": "undocumented",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "active",
"hookBus": "host",
@@ -5143,7 +5172,8 @@ const runtimes = {
"maxDepth": 1,
"background": true,
"subagentToolkit": "undocumented",
"backgroundDispatch": true
"backgroundDispatch": true,
"isolation": "orchestrator-worktree"
},
"modelMode": "passive",
"hookBus": "host",
@@ -5228,7 +5258,8 @@ const runtimes = {
"coder",
"explore",
"plan"
]
],
"isolation": "orchestrator-worktree"
},
"modelMode": "passive",
"hookBus": "host",
@@ -5326,7 +5357,8 @@ const runtimes = {
"maxDepth": "undocumented",
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": true
"backgroundDispatch": true,
"isolation": "orchestrator-worktree"
},
"modelMode": "active",
"hookBus": "host",
@@ -5396,7 +5428,8 @@ const runtimes = {
"maxDepth": 0,
"background": false,
"backgroundDispatch": false,
"subagentToolkit": "undocumented"
"subagentToolkit": "undocumented",
"isolation": "none"
},
"modelMode": "active",
"hookBus": "host",
@@ -5497,7 +5530,8 @@ const runtimes = {
"maxDepth": 1,
"background": true,
"subagentToolkit": "full",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "host",
@@ -5597,7 +5631,8 @@ const runtimes = {
"maxDepth": "undocumented",
"background": true,
"subagentToolkit": "undocumented",
"backgroundDispatch": "undocumented"
"backgroundDispatch": "undocumented",
"isolation": "undocumented"
},
"modelMode": "passive",
"hookBus": "engine",
@@ -5653,7 +5688,8 @@ const runtimes = {
"maxDepth": 5,
"background": true,
"subagentToolkit": "undocumented",
"backgroundDispatch": "undocumented"
"backgroundDispatch": "undocumented",
"isolation": "undocumented"
},
"modelMode": "active",
"hookBus": "engine",
@@ -5733,7 +5769,8 @@ const runtimes = {
"maxDepth": "undocumented",
"background": "undocumented",
"subagentToolkit": "undocumented",
"backgroundDispatch": "undocumented"
"backgroundDispatch": "undocumented",
"isolation": "none"
},
"modelMode": "passive",
"hookBus": "host",
@@ -5842,7 +5879,8 @@ const runtimes = {
"maxDepth": "undocumented",
"background": false,
"subagentToolkit": "full",
"backgroundDispatch": false
"backgroundDispatch": false,
"isolation": "none"
},
"modelMode": "passive",
"hookBus": "host",

View File

@@ -739,6 +739,9 @@ const VALID_HOST_RUNTIMES = new Set(['node', 'bun', 'sandboxed-web', 'python
const VALID_SUBAGENT_TOOLKITS = new Set(['full', 'read-only', 'built-in-only']);
// ADR-1239 amendment (#2481): how reasoning effort reaches the host.
const VALID_EFFORT_SURFACES = new Set(['argv', 'none']);
// ADR-1239 Codex-binding amendment (#2584): how a host isolates concurrent
// same-wave executors — a dispatch sub-field, not a top-level axis.
const VALID_DISPATCH_ISOLATION = new Set(['harness-worktree', 'orchestrator-worktree', 'none']);
// GATE A: installSurface → allowed hooksSurface values (DEFECT.GENERATIVE-FIX: parity invariant)
// Derived from the actual pairings in the 16 real runtime descriptors.
@@ -1291,6 +1294,25 @@ function validateRuntimeBody(cap) {
'runtime.hostIntegration.dispatch.backgroundDispatch must be a boolean or "undocumented" (got: ' + JSON.stringify(d.backgroundDispatch) + ')',
);
}
// isolation — ADR-1239 Codex-binding amendment (#2584).
// OPTIONAL, like effortSurface: added after descriptors already existed,
// so requiring it would invalidate every descriptor authored before it —
// including third-party ones, breaking the "purely additive" property
// ADR-1239 promises for external descriptors. An omitted isolation is
// legitimate: negotiation degrades it to 'none' (the safe floor) and
// warns, exactly as for any other undeclared dispatch sub-field. Only a
// PRESENT value is checked against the closed vocabulary.
if (d.isolation === undefined) {
// absent — nothing to validate; negotiateHostCapabilities fails it closed.
} else if (d.isolation === '__proto__' || d.isolation === 'constructor' || d.isolation === 'prototype') {
errors.push('runtime.hostIntegration.dispatch.isolation "' + d.isolation + '" is a reserved name');
} else if (d.isolation !== 'undocumented' && !VALID_DISPATCH_ISOLATION.has(d.isolation)) {
errors.push(
'runtime.hostIntegration.dispatch.isolation must be one of: ' + [...VALID_DISPATCH_ISOLATION].join(', ') +
' (or "undocumented") (got: ' + JSON.stringify(d.isolation) + ')',
);
}
}
}
@@ -2321,6 +2343,7 @@ module.exports = {
VALID_TRANSPORTS,
VALID_HOST_RUNTIMES,
VALID_SUBAGENT_TOOLKITS,
VALID_DISPATCH_ISOLATION,
_HOST_INTEGRATION_VOCAB: {
embeddingMode: [...VALID_EMBEDDING_MODES],
commandSurface: [...VALID_COMMAND_SURFACES],
@@ -2331,6 +2354,7 @@ module.exports = {
runtime: [...VALID_HOST_RUNTIMES],
subagentToolkit: [...VALID_SUBAGENT_TOOLKITS],
effortSurface: [...VALID_EFFORT_SURFACES],
isolation: [...VALID_DISPATCH_ISOLATION],
},
INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES,
GEMINI_AGENT_EVENTS,

View File

@@ -54,6 +54,22 @@ const HOST_INTEGRATION_AXES = Object.freeze({
// runtime in #1928/#1996, and neither its successor Antigravity CLI nor ZCode
// documents a reasoning setting. Adding a member with no host would be a guess.
effortSurface: Object.freeze(['argv', 'none'] as const),
// ADR-1239 Codex-binding amendment (#2584): a `dispatch` sub-field — not a new
// axis — declaring how a host isolates concurrent same-wave executors.
// `harness-worktree` — the host's own harness creates + binds a git worktree
// per executor; GSD passes the host's own isolation flag and calls no git
// itself (host-driven fan-out).
// `orchestrator-worktree` — GSD itself process-spawns each executor with an
// explicit working directory into a worktree GSD created, validated, and
// merges (GSD-driven fan-out; concurrency is OS-level, not the host's).
// `none` — no isolation primitive; same-wave plans run inline/sequentially
// (the #853 flatten rule).
// `undocumented` is NOT a member here; it is the corpus-wide sentinel above.
// Mechanism-specific ("worktree"), not abstract — same "name only what a
// host actually has" rule that kept effortSurface from guessing a
// config-file member above. A future non-worktree isolation mechanism adds a
// `*-container` member then, evidence-backed.
isolation: Object.freeze(['harness-worktree', 'orchestrator-worktree', 'none'] as const),
});
const INTERFACE_POINTS = Object.freeze(['command', 'dispatch', 'model', 'hooks', 'state', 'artifact'] as const);
@@ -71,6 +87,7 @@ type Transport = 'mcp' | 'native-extension';
type HostRuntime = 'node' | 'bun' | 'sandboxed-web' | 'python' | 'go' | 'rust' | 'electron' | 'other';
type SubagentToolkit = 'full' | 'read-only';
type EffortSurface = 'argv' | 'none';
type DispatchIsolation = 'harness-worktree' | 'orchestrator-worktree' | 'none';
type DegradationLevel = 'full' | 'degraded' | 'absent';
type InterfacePoint = 'command' | 'dispatch' | 'model' | 'hooks' | 'state' | 'artifact';
@@ -81,6 +98,7 @@ interface DispatchCapability {
background: boolean;
subagentToolkit: SubagentToolkit;
backgroundDispatch: boolean;
isolation: DispatchIsolation;
}
interface HostIntegrationAxes {
@@ -109,7 +127,7 @@ interface DegradationResult {
const SAFE_DEFAULTS: HostIntegrationAxes = {
embeddingMode: 'declarative',
commandSurface: 'prose-only',
dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'read-only', backgroundDispatch: false },
dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'read-only', backgroundDispatch: false, isolation: 'none' },
modelMode: 'passive',
hookBus: 'none',
stateIO: 'session-log-append',
@@ -123,7 +141,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', backgroundDispatch: true }),
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none' }),
modelMode: 'passive',
hookBus: 'host',
stateIO: 'filesystem',
@@ -134,7 +152,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', backgroundDispatch: false }),
dispatch: Object.freeze({ namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full', backgroundDispatch: false, isolation: 'none' }),
modelMode: 'passive',
hookBus: 'host',
stateIO: 'filesystem',
@@ -145,7 +163,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', backgroundDispatch: true }),
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: 5, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none' }),
modelMode: 'active',
hookBus: 'engine',
stateIO: 'sandboxed-storage',
@@ -278,7 +296,7 @@ const DEFAULT_ENGINE: EngineCapabilities = {
axes: {
embeddingMode: 'imperative',
commandSurface: 'slash-file',
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true },
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none' },
modelMode: 'active',
hookBus: 'host',
stateIO: 'filesystem',
@@ -404,6 +422,7 @@ function negotiateHostCapabilities(
let effectiveBackgroundDispatch: boolean;
let effectiveSubagentToolkit: SubagentToolkit;
let effectiveMaxDepth: number;
let effectiveIsolation: DispatchIsolation;
if (hostDispatch === null) {
// Host didn't declare dispatch at all — fail-closed to most-restrictive values
@@ -414,6 +433,7 @@ function negotiateHostCapabilities(
effectiveBackgroundDispatch = false;
effectiveSubagentToolkit = 'read-only';
effectiveMaxDepth = 0;
effectiveIsolation = 'none';
} else {
// N1: observability warnings for 'undocumented' sentinel on dispatch fields
if (hostDispatch.namedDispatch === 'undocumented') {
@@ -431,6 +451,9 @@ function negotiateHostCapabilities(
if (hostDispatch.backgroundDispatch === 'undocumented') {
warnings.push(`dispatch.backgroundDispatch is undocumented — degraded closed`);
}
if (hostDispatch.isolation === 'undocumented') {
warnings.push(`dispatch.isolation is undocumented — degraded closed (none)`);
}
effectiveNamedDispatch = (hostDispatch.namedDispatch === true) && engineDispatch.namedDispatch;
effectiveNested = (hostDispatch.nested === true) && engineDispatch.nested;
@@ -443,6 +466,15 @@ function negotiateHostCapabilities(
const engineToolkit = engineDispatch.subagentToolkit === 'read-only' ? 'read-only' : 'full';
effectiveSubagentToolkit = (hostToolkit === 'read-only' || engineToolkit === 'read-only') ? 'read-only' : 'full';
// isolation: effective = the host's declared value only if it is a known
// valid vocabulary member; otherwise 'none'. NOT host && engine gated —
// GSD owns the vocabulary, so "engine-known" == "in the valid set" (this
// still satisfies effective ⊆ host-declared ∩ engine-known).
const hostIso = hostDispatch.isolation;
effectiveIsolation = (typeof hostIso === 'string' && (HOST_INTEGRATION_AXES.isolation as readonly string[]).includes(hostIso))
? hostIso as DispatchIsolation
: 'none';
// maxDepth: missing/non-number/non-finite → 0 + warning
let hostMaxDepth: number;
if (typeof hostDispatch.maxDepth !== 'number' || !Number.isFinite(hostDispatch.maxDepth)) {
@@ -474,6 +506,7 @@ function negotiateHostCapabilities(
background: effectiveBackground,
subagentToolkit: effectiveSubagentToolkit,
backgroundDispatch: effectiveBackgroundDispatch,
isolation: effectiveIsolation,
};
// ---------------------------------------------------------------------------

View File

@@ -9,6 +9,9 @@
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const fc = require('fast-check');
const hi = require('../gsd-core/bin/lib/host-integration.cjs');
const {
@@ -27,6 +30,27 @@ const {
EXTENSION_EVENT_SURFACES,
} = hi;
const {
_HOST_INTEGRATION_VOCAB,
validateCapability,
} = require('../gsd-core/bin/lib/capability-validator.cjs');
const REPO_ROOT = path.resolve(__dirname, '..');
/**
* A real shipped runtime descriptor with its `dispatch.isolation` value
* stripped, so validator behavioral tests exercise the actual dispatch shape
* shipped for a host rather than a hand-modeled fixture (fixture-provenance,
* #2371 — mirrors `shippedDescriptorWithout` in tests/effort-surface-axis.test.cjs).
*/
function shippedClaudeCapabilityWithoutIsolation() {
const cap = JSON.parse(
fs.readFileSync(path.join(REPO_ROOT, 'capabilities', 'claude', 'capability.json'), 'utf8'),
);
delete cap.runtime.hostIntegration.dispatch.isolation;
return cap;
}
describe('hookEventSurfaceFor (MANAGED-hook dialect consumer — claude/gemini only)', () => {
test('returns the full Claude managed-hook surface for "claude"', () => {
const s = hookEventSurfaceFor('claude');
@@ -165,6 +189,17 @@ describe('CONTRACT-PIN', () => {
);
});
test('isolation values (sorted) — #2584 ADR-1239 Codex-binding amendment', () => {
assert.deepStrictEqual(
[...HOST_INTEGRATION_AXES.isolation].sort(),
['harness-worktree', 'none', 'orchestrator-worktree'],
);
});
test('isolation: "undocumented" is NOT a vocabulary member — it is the corpus sentinel', () => {
assert.ok(!HOST_INTEGRATION_AXES.isolation.includes('undocumented'));
});
test('INTERFACE_POINTS frozen and contains expected values', () => {
assert.ok(Object.isFrozen(INTERFACE_POINTS), 'INTERFACE_POINTS must be frozen');
const expected = ['command', 'dispatch', 'model', 'hooks', 'state', 'artifact'].sort();
@@ -1098,3 +1133,187 @@ describe('Fix 2: negotiate — host omits dispatch → subagentToolkit read-only
'Host missing dispatch must produce subagentToolkit "read-only"; got "' + result.effective.dispatch.subagentToolkit + '"');
});
});
// ---------------------------------------------------------------------------
// #2584 — ADR-1239 Codex-binding amendment: dispatch.isolation sub-field
// (Phase 1 — declared and negotiated, but NOT consumed by any scheduler yet).
// ---------------------------------------------------------------------------
describe('#2584 dispatch.isolation — negotiation', () => {
const BASE_DISPATCH = {
namedDispatch: true, nested: false, maxDepth: 1, background: false,
subagentToolkit: 'full', backgroundDispatch: false,
};
for (const value of HOST_INTEGRATION_AXES.isolation) {
test(`host declares isolation:"${value}" → effective.dispatch.isolation === "${value}"`, () => {
const result = negotiateHostCapabilities({
dispatch: { ...BASE_DISPATCH, isolation: value },
});
assert.strictEqual(result.effective.dispatch.isolation, value);
});
}
test('isolation:"undocumented" → effective "none" + a warning naming dispatch.isolation', () => {
const result = negotiateHostCapabilities({
dispatch: { ...BASE_DISPATCH, isolation: 'undocumented' },
});
assert.strictEqual(result.effective.dispatch.isolation, 'none');
const warnText = result.warnings.join(' ');
assert.ok(warnText.includes('dispatch.isolation') && warnText.includes('undocumented'),
`Expected a warning naming dispatch.isolation as undocumented; got: ${warnText}`);
});
test('isolation: unknown/garbage value (not the sentinel) → effective "none", no throw', () => {
const result = negotiateHostCapabilities({
dispatch: { ...BASE_DISPATCH, isolation: 'quantum-worktree' },
});
assert.strictEqual(result.effective.dispatch.isolation, 'none');
});
test('isolation: non-string value (number/object/array/null) → effective "none", no throw', () => {
for (const bogus of [42, {}, [], null, true]) {
const result = negotiateHostCapabilities({
dispatch: { ...BASE_DISPATCH, isolation: bogus },
});
assert.strictEqual(result.effective.dispatch.isolation, 'none',
`isolation=${JSON.stringify(bogus)} must degrade to "none"`);
}
});
test('host declares dispatch but omits isolation entirely → effective "none"', () => {
const result = negotiateHostCapabilities({ dispatch: { ...BASE_DISPATCH } });
assert.strictEqual(result.effective.dispatch.isolation, 'none');
});
test('host omits dispatch entirely → effective.dispatch.isolation === "none"', () => {
const result = negotiateHostCapabilities({});
assert.strictEqual(result.effective.dispatch.isolation, 'none');
});
test('negotiateHostCapabilities({}) → SAFE_DEFAULTS floor carries isolation "none"', () => {
// FAIL_CLOSED_FLOOR.dispatch.isolation (src/host-integration.cts SAFE_DEFAULTS)
const result = negotiateHostCapabilities({});
assert.strictEqual(result.effective.dispatch.isolation, 'none');
});
test('isolation is NOT gated by namedDispatch:false — unlike nested/background/backgroundDispatch, it is not capped', () => {
// orchestrator-worktree fan-out is OS-level (process-spawn), independent of
// the host's native named-subagent dispatch (ADR-1239 §2584: "does not use
// the host's native subagent tool"). A host may plausibly declare
// namedDispatch:false yet still have isolation info; either way it must not
// silently flip to a DIFFERENT valid value or throw.
const result = negotiateHostCapabilities({
dispatch: { ...BASE_DISPATCH, namedDispatch: false, isolation: 'orchestrator-worktree' },
});
assert.strictEqual(result.effective.dispatch.isolation, 'orchestrator-worktree');
});
// ─── Boundary: exact valid-set membership ──────────────────────────────────
describe('boundary — a value one character off a valid member fails closed to "none"', () => {
const NEAR_MISSES = [
'harness-worktre', // missing trailing 'e' (limit-1)
'harness-worktreee', // extra trailing 'e' (limit+1)
'Harness-Worktree', // case mismatch
'orchestrator-worktre', // missing trailing 'e'
'orchestrator-worktrees', // extra trailing 's'
'non', // missing trailing 'e' of "none"
'nonee', // extra trailing 'e'
' none', // leading space
'none ', // trailing space
];
for (const nearMiss of NEAR_MISSES) {
test(`isolation:${JSON.stringify(nearMiss)} → "none"`, () => {
const result = negotiateHostCapabilities({
dispatch: { ...BASE_DISPATCH, isolation: nearMiss },
});
assert.strictEqual(result.effective.dispatch.isolation, 'none');
});
}
});
// ─── Property: valid-set-passthrough-else-none contract ─────────────────────
test('property: effective.dispatch.isolation equals the declared value iff it is a known vocabulary member, else "none"', () => {
const declaredArb = fc.oneof(
fc.constantFrom(...HOST_INTEGRATION_AXES.isolation, 'undocumented'),
fc.string(),
);
fc.assert(
fc.property(declaredArb, (declared) => {
const result = negotiateHostCapabilities({
dispatch: { ...BASE_DISPATCH, isolation: declared },
});
const eff = result.effective.dispatch.isolation;
assert.ok(HOST_INTEGRATION_AXES.isolation.includes(eff),
`effective.dispatch.isolation '${eff}' must always be a known vocabulary member`);
if (HOST_INTEGRATION_AXES.isolation.includes(declared)) {
assert.strictEqual(eff, declared, `a valid declared value ('${declared}') must pass through unchanged`);
} else {
assert.strictEqual(eff, 'none', `an invalid/sentinel declared value ('${declared}') must degrade to "none"`);
}
}),
{ numRuns: 200, seed: 2584 },
);
});
});
describe('#2584 dispatch.isolation — validator', () => {
test('_HOST_INTEGRATION_VOCAB.isolation matches HOST_INTEGRATION_AXES.isolation (parity guard)', () => {
assert.deepEqual(
[..._HOST_INTEGRATION_VOCAB.isolation].sort(),
[...HOST_INTEGRATION_AXES.isolation].sort(),
);
});
test('a descriptor that omits dispatch.isolation entirely still validates clean (added after existing descriptors)', () => {
const cap = shippedClaudeCapabilityWithoutIsolation();
const errors = validateCapability(cap, 'claude');
assert.deepEqual(errors, [], `omitted isolation must validate clean, got: ${JSON.stringify(errors)}`);
});
for (const value of ['harness-worktree', 'orchestrator-worktree', 'none', 'undocumented']) {
test(`dispatch.isolation:"${value}" → ZERO validator errors`, () => {
const cap = shippedClaudeCapabilityWithoutIsolation();
cap.runtime.hostIntegration.dispatch.isolation = value;
const errors = validateCapability(cap, 'claude');
const isoErrors = errors.filter((e) => e.includes('dispatch.isolation'));
assert.strictEqual(isoErrors.length, 0,
`"${value}" must produce no validator errors; got: ${JSON.stringify(isoErrors)}`);
});
}
test('a present invalid dispatch.isolation value is rejected', () => {
const cap = shippedClaudeCapabilityWithoutIsolation();
cap.runtime.hostIntegration.dispatch.isolation = 'quantum-worktree';
const errors = validateCapability(cap, 'claude');
assert.ok(
errors.some((e) => e.includes('dispatch.isolation')),
`an invalid dispatch.isolation must produce a validator error; got: ${JSON.stringify(errors)}`,
);
});
test('a reserved-name dispatch.isolation value ("__proto__") is rejected', () => {
const cap = shippedClaudeCapabilityWithoutIsolation();
cap.runtime.hostIntegration.dispatch.isolation = '__proto__';
const errors = validateCapability(cap, 'claude');
assert.ok(
errors.some((e) => e.includes('dispatch.isolation') && e.includes('reserved name')),
`"__proto__" must produce a reserved-name validator error; got: ${JSON.stringify(errors)}`,
);
});
test('every shipped runtime descriptor with an isolation value passes validateCapability', () => {
const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
for (const [id, cap] of Object.entries(registry.runtimes)) {
const iso = cap && cap.runtime && cap.runtime.hostIntegration && cap.runtime.hostIntegration.dispatch
&& cap.runtime.hostIntegration.dispatch.isolation;
if (iso === undefined) continue;
const errors = validateCapability(cap, id);
const isoErrors = errors.filter((e) => e.includes('dispatch.isolation'));
assert.strictEqual(isoErrors.length, 0,
`${id}: shipped dispatch.isolation:"${iso}" must validate clean; got: ${JSON.stringify(isoErrors)}`);
}
});
});