From ec7978c0b4f7f9601fde8bd9b9f54694c138c0ae Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 24 Jul 2026 12:51:42 -0400 Subject: [PATCH] feat(#2584): add dispatch.isolation sub-field, descriptors, validator + negotiation (#2604) --- CONTEXT.md | 2 +- capabilities/antigravity/capability.json | 3 +- capabilities/augment/capability.json | 3 +- capabilities/claude/capability.json | 3 +- capabilities/cline/capability.json | 3 +- capabilities/codebuddy/capability.json | 3 +- capabilities/codex/capability.json | 3 +- capabilities/copilot/capability.json | 3 +- capabilities/cursor/capability.json | 3 +- capabilities/hermes/capability.json | 3 +- capabilities/kilo/capability.json | 3 +- capabilities/kimi-code/capability.json | 3 +- capabilities/kimi/capability.json | 3 +- capabilities/opencode/capability.json | 3 +- capabilities/pi/capability.json | 3 +- capabilities/qwen/capability.json | 3 +- capabilities/trae/capability.json | 3 +- capabilities/vscode/capability.json | 3 +- capabilities/windsurf/capability.json | 3 +- capabilities/zcode/capability.json | 3 +- .../host-integration-capability-matrix.md | 17 ++ gsd-core/bin/lib/capability-registry.cjs | 114 ++++++--- gsd-core/bin/lib/capability-validator.cjs | 24 ++ src/host-integration.cts | 43 +++- tests/host-integration.test.cjs | 219 ++++++++++++++++++ 25 files changed, 413 insertions(+), 63 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index a2c0c4697..4edfd313b 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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. diff --git a/capabilities/antigravity/capability.json b/capabilities/antigravity/capability.json index c3748916b..d83ef2316 100644 --- a/capabilities/antigravity/capability.json +++ b/capabilities/antigravity/capability.json @@ -82,7 +82,8 @@ "maxDepth": "undocumented", "background": true, "subagentToolkit": "full", - "backgroundDispatch": "undocumented" + "backgroundDispatch": "undocumented", + "isolation": "undocumented" }, "modelMode": "passive", "hookBus": "host", diff --git a/capabilities/augment/capability.json b/capabilities/augment/capability.json index 7df4b21a1..03db67371 100644 --- a/capabilities/augment/capability.json +++ b/capabilities/augment/capability.json @@ -95,7 +95,8 @@ "maxDepth": "undocumented", "background": true, "subagentToolkit": "full", - "backgroundDispatch": "undocumented" + "backgroundDispatch": "undocumented", + "isolation": "undocumented" }, "modelMode": "passive", "hookBus": "host", diff --git a/capabilities/claude/capability.json b/capabilities/claude/capability.json index eef7279f4..b686fa399 100644 --- a/capabilities/claude/capability.json +++ b/capabilities/claude/capability.json @@ -72,7 +72,8 @@ "maxDepth": 5, "background": true, "subagentToolkit": "full", - "backgroundDispatch": false + "backgroundDispatch": false, + "isolation": "harness-worktree" }, "modelMode": "passive", "hookBus": "host", diff --git a/capabilities/cline/capability.json b/capabilities/cline/capability.json index be1d61ae7..9321c8046 100644 --- a/capabilities/cline/capability.json +++ b/capabilities/cline/capability.json @@ -49,7 +49,8 @@ "maxDepth": 1, "background": true, "subagentToolkit": "read-only", - "backgroundDispatch": false + "backgroundDispatch": false, + "isolation": "undocumented" }, "modelMode": "active", "hookBus": "host", diff --git a/capabilities/codebuddy/capability.json b/capabilities/codebuddy/capability.json index cc258a779..bb7efcadd 100644 --- a/capabilities/codebuddy/capability.json +++ b/capabilities/codebuddy/capability.json @@ -96,7 +96,8 @@ "maxDepth": 1, "background": true, "subagentToolkit": "full", - "backgroundDispatch": false + "backgroundDispatch": false, + "isolation": "undocumented" }, "modelMode": "passive", "hookBus": "host", diff --git a/capabilities/codex/capability.json b/capabilities/codex/capability.json index 9a3aee28d..55df05aa9 100644 --- a/capabilities/codex/capability.json +++ b/capabilities/codex/capability.json @@ -65,7 +65,8 @@ "maxDepth": 1, "background": true, "subagentToolkit": "full", - "backgroundDispatch": true + "backgroundDispatch": true, + "isolation": "orchestrator-worktree" }, "modelMode": "passive", "hookBus": "host", diff --git a/capabilities/copilot/capability.json b/capabilities/copilot/capability.json index 2decbf999..cf63c1442 100644 --- a/capabilities/copilot/capability.json +++ b/capabilities/copilot/capability.json @@ -75,7 +75,8 @@ "maxDepth": 1, "background": true, "subagentToolkit": "full", - "backgroundDispatch": false + "backgroundDispatch": false, + "isolation": "undocumented" }, "modelMode": "passive", "hookBus": "host", diff --git a/capabilities/cursor/capability.json b/capabilities/cursor/capability.json index d494d6657..0ff01134d 100644 --- a/capabilities/cursor/capability.json +++ b/capabilities/cursor/capability.json @@ -91,7 +91,8 @@ "maxDepth": 2, "background": true, "subagentToolkit": "full", - "backgroundDispatch": true + "backgroundDispatch": true, + "isolation": "harness-worktree" }, "modelMode": "passive", "hookBus": "host", diff --git a/capabilities/hermes/capability.json b/capabilities/hermes/capability.json index 3698744bc..38c0cc18e 100644 --- a/capabilities/hermes/capability.json +++ b/capabilities/hermes/capability.json @@ -77,7 +77,8 @@ "maxDepth": 1, "background": true, "subagentToolkit": "read-only", - "backgroundDispatch": false + "backgroundDispatch": false, + "isolation": "undocumented" }, "modelMode": "active", "hookBus": "host", diff --git a/capabilities/kilo/capability.json b/capabilities/kilo/capability.json index f37e953bc..a498d9887 100644 --- a/capabilities/kilo/capability.json +++ b/capabilities/kilo/capability.json @@ -82,7 +82,8 @@ "maxDepth": -1, "background": true, "subagentToolkit": "undocumented", - "backgroundDispatch": false + "backgroundDispatch": false, + "isolation": "undocumented" }, "modelMode": "active", "hookBus": "host", diff --git a/capabilities/kimi-code/capability.json b/capabilities/kimi-code/capability.json index f11cb887f..0f2bcc237 100644 --- a/capabilities/kimi-code/capability.json +++ b/capabilities/kimi-code/capability.json @@ -64,7 +64,8 @@ "coder", "explore", "plan" - ] + ], + "isolation": "orchestrator-worktree" }, "modelMode": "passive", "hookBus": "host", diff --git a/capabilities/kimi/capability.json b/capabilities/kimi/capability.json index 1b1a4228f..12a0a472c 100644 --- a/capabilities/kimi/capability.json +++ b/capabilities/kimi/capability.json @@ -68,7 +68,8 @@ "maxDepth": 1, "background": true, "subagentToolkit": "undocumented", - "backgroundDispatch": true + "backgroundDispatch": true, + "isolation": "orchestrator-worktree" }, "modelMode": "passive", "hookBus": "host", diff --git a/capabilities/opencode/capability.json b/capabilities/opencode/capability.json index 8caf1724b..ef8f15c46 100644 --- a/capabilities/opencode/capability.json +++ b/capabilities/opencode/capability.json @@ -77,7 +77,8 @@ "maxDepth": "undocumented", "background": true, "subagentToolkit": "full", - "backgroundDispatch": true + "backgroundDispatch": true, + "isolation": "orchestrator-worktree" }, "modelMode": "active", "hookBus": "host", diff --git a/capabilities/pi/capability.json b/capabilities/pi/capability.json index bec2c1927..f38a8dc55 100644 --- a/capabilities/pi/capability.json +++ b/capabilities/pi/capability.json @@ -40,7 +40,8 @@ "maxDepth": 0, "background": false, "backgroundDispatch": false, - "subagentToolkit": "undocumented" + "subagentToolkit": "undocumented", + "isolation": "none" }, "modelMode": "active", "hookBus": "host", diff --git a/capabilities/qwen/capability.json b/capabilities/qwen/capability.json index 5cd7970e7..bd0dfa8ad 100644 --- a/capabilities/qwen/capability.json +++ b/capabilities/qwen/capability.json @@ -80,7 +80,8 @@ "maxDepth": 1, "background": true, "subagentToolkit": "full", - "backgroundDispatch": false + "backgroundDispatch": false, + "isolation": "undocumented" }, "modelMode": "passive", "hookBus": "host", diff --git a/capabilities/trae/capability.json b/capabilities/trae/capability.json index 3476c4914..46182d8b9 100644 --- a/capabilities/trae/capability.json +++ b/capabilities/trae/capability.json @@ -74,7 +74,8 @@ "maxDepth": "undocumented", "background": true, "subagentToolkit": "undocumented", - "backgroundDispatch": "undocumented" + "backgroundDispatch": "undocumented", + "isolation": "undocumented" }, "modelMode": "passive", "hookBus": "engine", diff --git a/capabilities/vscode/capability.json b/capabilities/vscode/capability.json index 9e1dd1c39..2c5ab654f 100644 --- a/capabilities/vscode/capability.json +++ b/capabilities/vscode/capability.json @@ -39,7 +39,8 @@ "maxDepth": 5, "background": true, "subagentToolkit": "undocumented", - "backgroundDispatch": "undocumented" + "backgroundDispatch": "undocumented", + "isolation": "undocumented" }, "modelMode": "active", "hookBus": "engine", diff --git a/capabilities/windsurf/capability.json b/capabilities/windsurf/capability.json index a44a52cd5..a3af18a43 100644 --- a/capabilities/windsurf/capability.json +++ b/capabilities/windsurf/capability.json @@ -67,7 +67,8 @@ "maxDepth": "undocumented", "background": "undocumented", "subagentToolkit": "undocumented", - "backgroundDispatch": "undocumented" + "backgroundDispatch": "undocumented", + "isolation": "none" }, "modelMode": "passive", "hookBus": "host", diff --git a/capabilities/zcode/capability.json b/capabilities/zcode/capability.json index 3f38542dc..44f3d91d8 100644 --- a/capabilities/zcode/capability.json +++ b/capabilities/zcode/capability.json @@ -90,7 +90,8 @@ "maxDepth": "undocumented", "background": false, "subagentToolkit": "full", - "backgroundDispatch": false + "backgroundDispatch": false, + "isolation": "none" }, "modelMode": "passive", "hookBus": "host", diff --git a/docs/reference/host-integration-capability-matrix.md b/docs/reference/host-integration-capability-matrix.md index dc28a9963..0fd35e98f 100644 --- a/docs/reference/host-integration-capability-matrix.md +++ b/docs/reference/host-integration-capability-matrix.md @@ -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 ` 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 diff --git a/gsd-core/bin/lib/capability-registry.cjs b/gsd-core/bin/lib/capability-registry.cjs index 6faac5bb5..2a5cdf7bc 100644 --- a/gsd-core/bin/lib/capability-registry.cjs +++ b/gsd-core/bin/lib/capability-registry.cjs @@ -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", diff --git a/gsd-core/bin/lib/capability-validator.cjs b/gsd-core/bin/lib/capability-validator.cjs index 53fd32ec9..46eeb538f 100644 --- a/gsd-core/bin/lib/capability-validator.cjs +++ b/gsd-core/bin/lib/capability-validator.cjs @@ -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, diff --git a/src/host-integration.cts b/src/host-integration.cts index a6feddf3c..da2704fbf 100644 --- a/src/host-integration.cts +++ b/src/host-integration.cts @@ -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 { 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)}`); + } + }); +});