* feat(#1708): typed documentation-sourced #853 dispatch-flatten Graduate the #853 orchestrator-backgrounding decision from a scattered RUNTIME==='codex' prose check to a typed, documentation-sourced engine decision. Adds a backgroundDispatch dispatch sub-axis (sourced per host: codex+cursor documented true, 9 documented false, 5 undocumented), shouldFlattenDispatch(dispatch) (inline UNLESS background && backgroundDispatch, fail-closed), and a gsd_run query dispatch-should-flatten the plan/execute workflows call. Cursor is newly background-eligible per its docs (inline->background) — a documentation-justified behavior change. No RUNTIME-name residue for this decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1708): backgroundDispatch citations in matrix + CONTEXT note Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1708): address review findings on typed dispatch-flatten Code/adversarial review: convert the manager.md/autonomous.md Compound Action preamble from hardcoded 'On Codex' to FLATTEN-based branching (the handlers already use the query; the preamble contradicted them and was wrong for cursor); make shouldFlattenDispatch null-safe + type-honest (accepts raw 'undocumented' registry values); make backgroundDispatch a required descriptor field (matching its siblings, all 16 carry it); strengthen the config.runtime behavioral test; update the bug-853 prose-pin test + comment. Security review clean; Codex confirmed no fail-open. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1708): backfill backgroundDispatch in role:runtime test fixtures Making backgroundDispatch a required descriptor field broke role:runtime fixtures in capability-manifest-version/capability-registry/host-integration-descriptors tests that build a dispatch object without it (caught by full gsd-test, not scoped npm test). Backfill backgroundDispatch:false into the well-formed fixtures; the deliberately-malformed 'required-field' test fixture is left malformed by design. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1708): update fix-1521 dispatch-gating assertion to the FLATTEN gate fix-1521 pinned the codex-specific run_in_background prose that #1708 graduated to the typed dispatch-should-flatten/FLATTEN gate. Update its assertions to verify FLATTEN=false gating (not a runtime name) + that the old RUNTIME===codex gate is gone. Caught by full gsd-test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1708): add changeset for typed dispatch-flatten Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1708): remove stray temp PR-body file Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1708): add issue ref to bug-853 allow-test-rule annotations ADR-456 requires every allow-test-rule exemption to carry a see #NNN reference; the source-text-is-the-product annotations added when migrating the prose assertions lacked it (lint-tests CI gate). Add (see #1708). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/graceful-badgers-dance.md
Normal file
5
.changeset/graceful-badgers-dance.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 1719
|
||||
---
|
||||
**#853 dispatch-flatten is now data-driven (ADR-1239 Phase B)** — whether GSD backgrounds the plan/execute orchestrator is decided from a documentation-sourced `backgroundDispatch` capability per host (via `gsd_run query dispatch-should-flatten`) instead of a hardcoded `runtime === 'codex'` check. **Cursor now backgrounds the orchestrator** (its docs document backgrounded subagent nesting); codex unchanged; all other hosts run inline. Fail-closed to inline on any uncertainty.
|
||||
@@ -119,7 +119,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 eight 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,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`). 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), and `PROFILE_BASELINES`. 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 is interface-definition only — no adapter/MCP/host-binding consumers yet (Phases B–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 eight 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`). 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.
|
||||
|
||||
### Installer Migration Authoring Guard Module
|
||||
Module owning validation for Installer Migration Module records and planned actions. It enforces migration metadata, explicit install scopes, ownership evidence for destructive/config actions, and runtime contract citations for runtime config rewrites before a migration can enter planning or apply.
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": "undocumented", "nested": "undocumented", "maxDepth": "undocumented", "background": true, "subagentToolkit": "undocumented" },
|
||||
"dispatch": { "namedDispatch": "undocumented", "nested": "undocumented", "maxDepth": "undocumented", "background": true, "subagentToolkit": "undocumented", "backgroundDispatch": "undocumented" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": "undocumented", "maxDepth": "undocumented", "background": true, "subagentToolkit": "full" },
|
||||
"dispatch": { "namedDispatch": true, "nested": "undocumented", "maxDepth": "undocumented", "background": true, "subagentToolkit": "full", "backgroundDispatch": "undocumented" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": true, "maxDepth": 5, "background": true, "subagentToolkit": "full" },
|
||||
"dispatch": { "namedDispatch": true, "nested": true, "maxDepth": 5, "background": true, "subagentToolkit": "full", "backgroundDispatch": false },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "read-only" },
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "read-only", "backgroundDispatch": false },
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "full" },
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "full", "backgroundDispatch": false },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": true, "maxDepth": 1, "background": true, "subagentToolkit": "full" },
|
||||
"dispatch": { "namedDispatch": true, "nested": true, "maxDepth": 1, "background": true, "subagentToolkit": "full", "backgroundDispatch": true },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "full" },
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "full", "backgroundDispatch": false },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": true, "maxDepth": 2, "background": true, "subagentToolkit": "full" },
|
||||
"dispatch": { "namedDispatch": true, "nested": true, "maxDepth": 2, "background": true, "subagentToolkit": "full", "backgroundDispatch": true },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-toml",
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": "undocumented", "subagentToolkit": "undocumented" },
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": "undocumented", "subagentToolkit": "undocumented", "backgroundDispatch": false },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-programmatic",
|
||||
"dispatch": { "namedDispatch": false, "nested": true, "maxDepth": 1, "background": true, "subagentToolkit": "read-only" },
|
||||
"dispatch": { "namedDispatch": false, "nested": true, "maxDepth": 1, "background": true, "subagentToolkit": "read-only", "backgroundDispatch": false },
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": true, "maxDepth": -1, "background": true, "subagentToolkit": "undocumented" },
|
||||
"dispatch": { "namedDispatch": true, "nested": true, "maxDepth": -1, "background": true, "subagentToolkit": "undocumented", "backgroundDispatch": false },
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "undocumented" },
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "undocumented", "backgroundDispatch": false },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": "undocumented", "maxDepth": "undocumented", "background": false, "subagentToolkit": "full" },
|
||||
"dispatch": { "namedDispatch": true, "nested": "undocumented", "maxDepth": "undocumented", "background": false, "subagentToolkit": "full", "backgroundDispatch": "undocumented" },
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "full" },
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "full", "backgroundDispatch": false },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": "undocumented", "maxDepth": "undocumented", "background": true, "subagentToolkit": "undocumented" },
|
||||
"dispatch": { "namedDispatch": true, "nested": "undocumented", "maxDepth": "undocumented", "background": true, "subagentToolkit": "undocumented", "backgroundDispatch": "undocumented" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "engine",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": "undocumented", "nested": "undocumented", "maxDepth": "undocumented", "background": "undocumented", "subagentToolkit": "undocumented" },
|
||||
"dispatch": { "namedDispatch": "undocumented", "nested": "undocumented", "maxDepth": "undocumented", "background": "undocumented", "subagentToolkit": "undocumented", "backgroundDispatch": "undocumented" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
|
||||
@@ -34,6 +34,7 @@ consumed verbatim by `gen:capability-registry` and validated by `capability-vali
|
||||
| `maxDepth` | Maximum nesting depth (integer; -1 = unbounded; `undocumented`). |
|
||||
| `background` | Whether subagents can run asynchronously in the background (true/false/`undocumented`). |
|
||||
| `subagentToolkit` | Tool surface available to subagents: `full`, `read-only`, or `undocumented`. |
|
||||
| `backgroundDispatch` | Whether a BACKGROUND-dispatched sub-agent can itself spawn further named sub-agents — the #853 discriminator (true/false/`undocumented`). |
|
||||
|
||||
### Interface points
|
||||
|
||||
@@ -64,6 +65,7 @@ consumed verbatim by `gen:capability-registry` and validated by `capability-vali
|
||||
| dispatch.maxDepth | 5 | https://code.claude.com/docs/en/sub-agents | "foreground subagents can spawn at any depth, blocking their parent until completion. Background subagents are limited to" |
|
||||
| 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, " |
|
||||
|
||||
Sources consulted:
|
||||
- https://code.claude.com/docs/en/sub-agents
|
||||
@@ -97,6 +99,7 @@ Sources consulted:
|
||||
| dispatch.maxDepth | 1 | https://developers.openai.com/codex/config-reference | "agents.max_depth: Maximum nesting depth allowed for spawned agent threads (root sessions start at depth 0; default: 1)" |
|
||||
| dispatch.background | true | https://github.com/openai/codex/blob/main/codex-rs/core/src/tools/handlers/multi_agents_spec.rs | "spawn_agent returns the spawned agent id immediately; a separate wait_agent tool polls for final status." |
|
||||
| dispatch.subagentToolkit | full | https://developers.openai.com/codex/multi-agent | "Subagents inherit the sandbox policy and tool surface from the parent session." |
|
||||
| dispatch.backgroundDispatch | true | https://github.com/openai/codex/blob/main/codex-rs/core/templates/collab/experimental_prompt.md | "Sub-agents have access to the same set of tools as you do so you must tell them if they are allowed to spawn sub-agents themselves or not." The config (codex-rs/config/src/config_toml.rs) exposes an |
|
||||
|
||||
Sources consulted:
|
||||
- https://github.com/openai/codex (repo via gh CLI)
|
||||
@@ -132,6 +135,7 @@ Documentation gaps:
|
||||
| dispatch.maxDepth | 1 | https://github.com/google-gemini/gemini-cli/blob/main/docs/core/subagents.md | "To prevent infinite loops and excessive token usage, subagents cannot call other subagents. … The architecture enforces" |
|
||||
| dispatch.background | undocumented | no authoritative doc — searched: https://github.com/google-gemini/gemini-cli/blob/main/docs/core/subagents.md, /google-gemini/gemini-cli (Context7 library) | — |
|
||||
| dispatch.subagentToolkit | undocumented | no authoritative doc — searched: https://github.com/google-gemini/gemini-cli/blob/main/docs/core/subagents.md, /google-gemini/gemini-cli (Context7 library) | — |
|
||||
| dispatch.backgroundDispatch | false | https://raw.githubusercontent.com/google-gemini/gemini-cli/main/docs/core/subagents.md | "To prevent infinite loops and excessive token usage, subagents cannot call other subagents." Additionally: "If a subagent is granted the `*` tool wildcard, it will still be unable to see or invoke ot |
|
||||
|
||||
Sources consulted:
|
||||
- https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/custom-commands.md
|
||||
@@ -164,6 +168,7 @@ Documentation gaps:
|
||||
| dispatch.maxDepth | undocumented | no authoritative doc — searched: https://opencode.ai/docs/agents | — |
|
||||
| dispatch.background | false | https://github.com/sst/opencode/issues/5887 | "\"Currently, sub-agent delegation in `opencode` appears to be synchronous or modal... There is no native 'fire-and-forget'" |
|
||||
| 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 | undocumented | no authoritative doc — https://github.com/anomalyco/opencode/issues/18100 and https://github.com/anomalyco/opencode/blob/dev/opencode/packages/opencode/src/tool/task.ts | Opencode supports background task dispatch via the Task tool's `background: true` parameter but whether a background-spawned agent can itself spawn further sub-agents is not documented. |
|
||||
|
||||
Sources consulted:
|
||||
- https://opencode.ai/docs/plugins
|
||||
@@ -196,6 +201,7 @@ Documentation gaps:
|
||||
| dispatch.maxDepth | 2 | https://cursor.com/docs/sdk/typescript | "The top-level agent and its direct subagents can launch subagents, but a subagent launched by another subagent can't lau" |
|
||||
| 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 |
|
||||
|
||||
Sources consulted:
|
||||
- https://cursor.com/docs/subagents
|
||||
@@ -225,6 +231,7 @@ Sources consulted:
|
||||
| dispatch.maxDepth | 1 | /cline/cline (Context7) — https://github.com/cline/cline/blob/main/docs/features/subagents.mdx | "They are explicitly prohibited from ... spawning other subagents." |
|
||||
| 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 |
|
||||
|
||||
Sources consulted:
|
||||
- https://github.com/cline/cline/blob/main/docs/sdk/plugins.mdx
|
||||
@@ -254,6 +261,7 @@ Sources consulted:
|
||||
| dispatch.maxDepth | 1 | /nousresearch/hermes-agent (Context7) — configuration.md | "max_spawn_depth: 1 # Delegation tree depth cap (1-3, clamped). 1 = flat (default): parent spawns leaves that cannot dele" |
|
||||
| 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 |
|
||||
|
||||
Sources consulted:
|
||||
- https://hermes-agent.nousresearch.com/docs/user-guide/features/delegation
|
||||
@@ -287,6 +295,7 @@ Documentation gaps:
|
||||
| dispatch.maxDepth | undocumented | no authoritative doc — searched: https://antigravity.google/docs/agents | — |
|
||||
| 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 | undocumented | no authoritative doc — searched: https://www.explainx.ai/blog/antigravity-cli-features-sandbox-plugins-subagents-2026 | — |
|
||||
| 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 |
|
||||
|
||||
Sources consulted:
|
||||
- https://github.com/alphaperseii3000/google-antigravity-docs/blob/master/google-antigravity-docs.md
|
||||
@@ -321,6 +330,7 @@ Documentation gaps:
|
||||
| dispatch.maxDepth | undocumented | no authoritative doc — searched: https://docs.augmentcode.com/cli/subagents | — |
|
||||
| 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 |
|
||||
|
||||
Sources consulted:
|
||||
- https://docs.augmentcode.com/cli/plugins
|
||||
@@ -353,6 +363,7 @@ Documentation gaps:
|
||||
| dispatch.maxDepth | 1 | https://qwenlm.github.io/qwen-code-docs/en/users/features/sub-agents/ | "Fork children cannot create further forks. This is enforced at runtime" |
|
||||
| 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 |
|
||||
|
||||
Sources consulted:
|
||||
- https://qwenlm.github.io/qwen-code-docs/en/developers/channel-plugins
|
||||
@@ -385,6 +396,7 @@ Documentation gaps:
|
||||
| dispatch.maxDepth | 1 | https://www.codebuddy.ai/docs/cli/sub-agents | "The architecture enforces exactly one level of nesting — only the main CodeBuddy Code instance can invoke sub-agents." |
|
||||
| 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:/ |
|
||||
|
||||
Sources consulted:
|
||||
- https://www.codebuddy.ai/docs/cli/plugins
|
||||
@@ -413,6 +425,7 @@ Sources consulted:
|
||||
| dispatch.maxDepth | 1 | https://awesome-copilot.github.com/learning-hub/agents-and-subagents/ | "Depth counts how many agents are nested within one another. When the depth limit is reached, the innermost agent cannot" |
|
||||
| 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` |
|
||||
|
||||
Sources consulted:
|
||||
- https://github.com/github/copilot-cli/blob/main/README.md (via Context7 /github/copilot-cli)
|
||||
@@ -444,6 +457,7 @@ Documentation gaps:
|
||||
| dispatch.maxDepth | -1 | https://github.com/Kilo-Org/kilocode/issues/8637 | "there is no maximum nesting depth and the system relies entirely on permission gating" |
|
||||
| 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." |
|
||||
|
||||
Sources consulted:
|
||||
- https://kilo.ai/docs/automate/extending/plugins
|
||||
@@ -477,6 +491,7 @@ Documentation gaps:
|
||||
| dispatch.maxDepth | undocumented | no authoritative doc — searched: https://docs.devin.ai/cli/subagents.md | — |
|
||||
| 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 |
|
||||
|
||||
Sources consulted:
|
||||
- https://docs.devin.ai/desktop/cascade/workflows
|
||||
@@ -514,6 +529,7 @@ Documentation gaps:
|
||||
| dispatch.maxDepth | undocumented | no authoritative doc — searched: https://docs.trae.ai/ide/solo-mode | — |
|
||||
| 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 |
|
||||
|
||||
Sources consulted:
|
||||
- https://docs.trae.ai/ide/model-context-protocol
|
||||
@@ -549,6 +565,7 @@ Documentation gaps:
|
||||
| dispatch.maxDepth | 1 | https://moonshotai.github.io/kimi-cli/en/customization/agents.html | "All subagent types are prohibited from nesting the `Agent` tool (subagents cannot create their own subagents). Only root" |
|
||||
| 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 | false | https://github.com/moonshotai/kimi-cli/blob/main/docs/en/customization/agents.md (also mirrored at https://moonshotai.github.io/kimi-cli/en/customization/agents.html) | "All subagent types are prohibited from nesting the `Agent` tool, meaning subagents cannot create their own subagents. Only the root agent has access to the `Agent` tool for launching further subagent |
|
||||
|
||||
Sources consulted:
|
||||
- https://moonshotai.github.io/kimi-cli/en/customization/hooks.html
|
||||
|
||||
@@ -1238,6 +1238,56 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
||||
break;
|
||||
}
|
||||
|
||||
case 'dispatch-should-flatten': {
|
||||
// #1708 / #853: typed query replacing the `RUNTIME === 'codex'` prose rule.
|
||||
//
|
||||
// Resolves the current runtime (GSD_RUNTIME > config.runtime > 'claude'),
|
||||
// looks up registry.runtimes[id].runtime.hostIntegration.dispatch, and
|
||||
// calls shouldFlattenDispatch(dispatch) from host-integration.cjs.
|
||||
//
|
||||
// Fail-closed: any unknown runtime, missing dispatch, or thrown error
|
||||
// yields `true` (inline — the always-safe default).
|
||||
//
|
||||
// Output:
|
||||
// --raw → prints exactly `true` or `false`
|
||||
// --json → prints { runtime, shouldFlatten, dispatch }
|
||||
// default → same as --raw
|
||||
try {
|
||||
// Resolve runtime using the same precedence as `config-get runtime`.
|
||||
const { resolveRuntime } = require('./lib/runtime-slash.cjs');
|
||||
const runtimeId = resolveRuntime(cwd);
|
||||
|
||||
// Look up dispatch from the capability registry.
|
||||
const registry = require('./lib/capability-registry.cjs');
|
||||
const runtimeEntry = registry.runtimes != null
|
||||
? registry.runtimes[runtimeId]
|
||||
: null;
|
||||
const dispatch = runtimeEntry?.runtime?.hostIntegration?.dispatch ?? null;
|
||||
|
||||
// Call shouldFlattenDispatch from host-integration.cjs.
|
||||
const hostIntegration = require('./lib/host-integration.cjs');
|
||||
const shouldFlat = dispatch !== null
|
||||
? hostIntegration.shouldFlattenDispatch(dispatch)
|
||||
: true; // fail-closed: unknown runtime → inline
|
||||
|
||||
const jsonIdx = args.indexOf('--json');
|
||||
if (jsonIdx !== -1) {
|
||||
output({
|
||||
runtime: runtimeId,
|
||||
shouldFlatten: shouldFlat,
|
||||
dispatch: dispatch,
|
||||
}, raw);
|
||||
} else {
|
||||
// --raw or default: print exactly true or false
|
||||
process.stdout.write(shouldFlat ? 'true' : 'false');
|
||||
}
|
||||
} catch {
|
||||
// Fail-closed on any error: inline is always safe.
|
||||
process.stdout.write('true');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'config-new-project': {
|
||||
// Phase 6 (#3575): dispatch via SDK executeForCjs when available.
|
||||
const handled = _dispatchNonFamily({
|
||||
|
||||
@@ -126,7 +126,8 @@ const capabilities = {
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -248,7 +249,8 @@ const capabilities = {
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -330,7 +332,8 @@ const capabilities = {
|
||||
"nested": true,
|
||||
"maxDepth": 5,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -389,7 +392,8 @@ const capabilities = {
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "read-only"
|
||||
"subagentToolkit": "read-only",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -535,7 +539,8 @@ const capabilities = {
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -604,7 +609,8 @@ const capabilities = {
|
||||
"nested": true,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": true
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -673,7 +679,8 @@ const capabilities = {
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -758,7 +765,8 @@ const capabilities = {
|
||||
"nested": true,
|
||||
"maxDepth": 2,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": true
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -950,7 +958,8 @@ const capabilities = {
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": "undocumented",
|
||||
"subagentToolkit": "undocumented"
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -1060,7 +1069,8 @@ const capabilities = {
|
||||
"nested": true,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "read-only"
|
||||
"subagentToolkit": "read-only",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -1203,7 +1213,8 @@ const capabilities = {
|
||||
"nested": true,
|
||||
"maxDepth": -1,
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -1275,7 +1286,8 @@ const capabilities = {
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -1585,7 +1597,8 @@ const capabilities = {
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": false,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -1789,7 +1802,8 @@ const capabilities = {
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -2107,7 +2121,8 @@ const capabilities = {
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "engine",
|
||||
@@ -2262,7 +2277,8 @@ const capabilities = {
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": "undocumented",
|
||||
"subagentToolkit": "undocumented"
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -3062,7 +3078,8 @@ const runtimes = {
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -3147,7 +3164,8 @@ const runtimes = {
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -3229,7 +3247,8 @@ const runtimes = {
|
||||
"nested": true,
|
||||
"maxDepth": 5,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -3288,7 +3307,8 @@ const runtimes = {
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "read-only"
|
||||
"subagentToolkit": "read-only",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -3373,7 +3393,8 @@ const runtimes = {
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -3442,7 +3463,8 @@ const runtimes = {
|
||||
"nested": true,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": true
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -3511,7 +3533,8 @@ const runtimes = {
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -3596,7 +3619,8 @@ const runtimes = {
|
||||
"nested": true,
|
||||
"maxDepth": 2,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": true
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -3669,7 +3693,8 @@ const runtimes = {
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": "undocumented",
|
||||
"subagentToolkit": "undocumented"
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -3738,7 +3763,8 @@ const runtimes = {
|
||||
"nested": true,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "read-only"
|
||||
"subagentToolkit": "read-only",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -3829,7 +3855,8 @@ const runtimes = {
|
||||
"nested": true,
|
||||
"maxDepth": -1,
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -3901,7 +3928,8 @@ const runtimes = {
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -3987,7 +4015,8 @@ const runtimes = {
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": false,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -4060,7 +4089,8 @@ const runtimes = {
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -4128,7 +4158,8 @@ const runtimes = {
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "engine",
|
||||
@@ -4188,7 +4219,8 @@ const runtimes = {
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": "undocumented",
|
||||
"subagentToolkit": "undocumented"
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -1188,6 +1188,18 @@ function validateRuntimeBody(cap) {
|
||||
'runtime.hostIntegration.dispatch.maxDepth must be an integer >= -1 or "undocumented" (got: ' + JSON.stringify(d.maxDepth) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// backgroundDispatch — REQUIRED (all 16 runtime descriptors carry it, matching the sibling fields
|
||||
// namedDispatch/nested/background/subagentToolkit/maxDepth which are all required).
|
||||
if (!Object.prototype.hasOwnProperty.call(d, 'backgroundDispatch')) {
|
||||
errors.push(
|
||||
'runtime.hostIntegration.dispatch.backgroundDispatch is required (must be a boolean or "undocumented")',
|
||||
);
|
||||
} else if (typeof d.backgroundDispatch !== 'boolean' && d.backgroundDispatch !== 'undocumented') {
|
||||
errors.push(
|
||||
'runtime.hostIntegration.dispatch.backgroundDispatch must be a boolean or "undocumented" (got: ' + JSON.stringify(d.backgroundDispatch) + ')',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ fi
|
||||
|
||||
When `--only` is set, also set `FROM_PHASE` to the same value so existing filter logic applies.
|
||||
|
||||
When `--interactive` is set, discuss runs inline with questions. On Codex, where a backgrounded agent can still spawn subagents, plan and execute are dispatched as background agents — keeping the main context lean (only discuss conversations accumulate) and enabling overlap. On every other runtime (Claude Code and all other non-Codex runtimes), backgrounded agents cannot reliably nest subagents, so plan and execute run inline to preserve worktree isolation and independent verification, and phases run sequentially with their work accumulating in the main context. Either way, user input is preserved on all design decisions.
|
||||
When `--interactive` is set, discuss runs inline with questions. When `dispatch-should-flatten` returns `false` (e.g. codex, cursor — runtimes where a backgrounded agent can still spawn subagents), plan and execute are dispatched as background agents — keeping the main context lean (only discuss conversations accumulate) and enabling overlap. When `dispatch-should-flatten` returns `true` (e.g. claude and other runtimes where backgrounded agents cannot reliably nest subagents), plan and execute run inline to preserve worktree isolation and independent verification, and phases run sequentially with their work accumulating in the main context. Either way, user input is preserved on all design decisions.
|
||||
|
||||
When `PLAN_STRATEGY=converge`, the planning step MUST invoke the plan-review convergence workflow instead of `gsd-plan-phase`. `--cross-ai` is an alias for `--converge`. Forward `CONVERGENCE_ARGS` exactly as parsed so reviewer flags and `--max-cycles N` retain the same meaning as they have on `/gsd:plan-review-convergence`.
|
||||
|
||||
@@ -358,13 +358,13 @@ UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
|
||||
|
||||
**3b. Plan**
|
||||
|
||||
**If `INTERACTIVE` is set:** Background dispatch is only safe on a runtime where a backgrounded agent can still nest the pipeline's subagents (plan-checker / worktree executors / verifier). Among supported runtimes only **Codex** (`spawn_agent`) can do this; Claude Code's backgrounded agents have no `Agent`/`Task` tool, and every other runtime either prohibits nested subagents or disables them by default. So run **inline** everywhere except Codex, which is dispatched in the background. Resolve the runtime first:
|
||||
**If `INTERACTIVE` is set:** Background dispatch is only safe on a runtime where a backgrounded agent can still nest the pipeline's subagents (plan-checker / worktree executors / verifier). This is determined from the documentation-sourced dispatch capability in the registry (#1708); Claude Code's backgrounded agents have no `Agent`/`Task` tool, and every other runtime either prohibits nested subagents or disables them by default. So run **inline** everywhere except where `dispatch-should-flatten` returns `false`. Resolve first:
|
||||
|
||||
```bash
|
||||
RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")
|
||||
FLATTEN=$(gsd_run query dispatch-should-flatten --raw 2>/dev/null || echo "true")
|
||||
```
|
||||
|
||||
- **If `RUNTIME` is `codex`:** Dispatch plan as a background agent to keep the main context lean. While plan runs, the workflow can immediately start discussing the next phase (see step 4).
|
||||
- **If `FLATTEN` is `false`:** Dispatch plan as a background agent to keep the main context lean. While plan runs, the workflow can immediately start discussing the next phase (see step 4).
|
||||
|
||||
- If `PLAN_STRATEGY=converge`, print: `◆ Spawning background plan-convergence loop for phase ${PHASE_NUM}... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)`
|
||||
|
||||
@@ -388,7 +388,7 @@ RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null ||
|
||||
|
||||
Store the agent task_id. After discuss for the next phase completes (or if no next phase), wait for the plan agent to finish before proceeding to execute.
|
||||
|
||||
- **Otherwise (Claude Code or any other non-Codex runtime):** Run plan **inline** (do NOT background) so the plan-checker runs. The next phase's discuss does not overlap planning here — correctness over overlap.
|
||||
- **Otherwise (`FLATTEN` is `true` — run inline):** Run plan **inline** (do NOT background) so the plan-checker runs. The next phase's discuss does not overlap planning here — correctness over overlap.
|
||||
|
||||
- If `PLAN_STRATEGY=converge`:
|
||||
|
||||
@@ -420,13 +420,13 @@ Verify plan produced output — re-run `init phase-op` and check `has_plans`. If
|
||||
|
||||
**3c. Execute**
|
||||
|
||||
**If `INTERACTIVE` is set:** Wait for the plan agent to complete (if not already) and verify plans exist. Background dispatch is only safe on a runtime where a backgrounded agent can still nest the pipeline's subagents (plan-checker / worktree executors / verifier). Among supported runtimes only **Codex** (`spawn_agent`) can do this; Claude Code's backgrounded agents have no `Agent`/`Task` tool, and every other runtime either prohibits nested subagents or disables them by default. So run **inline** everywhere except Codex, which is dispatched in the background. Resolve the runtime first:
|
||||
**If `INTERACTIVE` is set:** Wait for the plan agent to complete (if not already) and verify plans exist. Background dispatch is only safe on a runtime where a backgrounded agent can still nest the pipeline's subagents (plan-checker / worktree executors / verifier). This is determined from the documentation-sourced dispatch capability in the registry (#1708); Claude Code's backgrounded agents have no `Agent`/`Task` tool, and every other runtime either prohibits nested subagents or disables them by default. So run **inline** everywhere except where `dispatch-should-flatten` returns `false`. Resolve first:
|
||||
|
||||
```bash
|
||||
RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")
|
||||
FLATTEN=$(gsd_run query dispatch-should-flatten --raw 2>/dev/null || echo "true")
|
||||
```
|
||||
|
||||
- **If `RUNTIME` is `codex`:** Dispatch execute as a background agent:
|
||||
- **If `FLATTEN` is `false`:** Dispatch execute as a background agent:
|
||||
|
||||
```
|
||||
Agent(
|
||||
@@ -438,7 +438,7 @@ Agent(
|
||||
|
||||
Store the agent task_id. The workflow can now start discussing the next phase while this phase executes in the background. Before starting post-execution routing for this phase, wait for the execute agent to complete.
|
||||
|
||||
- **Otherwise (Claude Code or any other non-Codex runtime):** Run execute **inline** (do NOT background) so worktree isolation and verification run:
|
||||
- **Otherwise (`FLATTEN` is `true` — run inline):** Run execute **inline** (do NOT background) so worktree isolation and verification run:
|
||||
|
||||
```
|
||||
Skill(skill="gsd-execute-phase", args="${PHASE_NUM} --no-transition")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<purpose>
|
||||
|
||||
Interactive command center for managing a milestone from a single terminal. Shows a dashboard of all phases with visual status, dispatches discuss inline and runs plan/execute inline (backgrounded only on Codex), and loops back to the dashboard after each action. Enables parallel phase work from one terminal.
|
||||
Interactive command center for managing a milestone from a single terminal. Shows a dashboard of all phases with visual status, dispatches discuss inline and runs plan/execute inline (backgrounded when dispatch-should-flatten returns false), and loops back to the dashboard after each action. Enables parallel phase work from one terminal.
|
||||
|
||||
</purpose>
|
||||
|
||||
@@ -45,7 +45,7 @@ Display startup banner:
|
||||
{milestone_version} — {milestone_name}
|
||||
{phase_count} phases · {completed_count} complete
|
||||
|
||||
✓ Discuss → inline ◆ Plan/Execute → inline (background on Codex)
|
||||
✓ Discuss → inline ◆ Plan/Execute → inline (background when FLATTEN=false)
|
||||
Dashboard auto-refreshes when background work is active.
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
@@ -222,10 +222,10 @@ Go to exit step.
|
||||
|
||||
### Compound Action (background + inline)
|
||||
|
||||
When the user selects a compound option, behavior depends on the runtime — the Plan Phase N / Execute Phase N handlers below resolve it via `gsd_run query config-get runtime`:
|
||||
When the user selects a compound option, behavior depends on whether the runtime supports background dispatch of nesting-capable orchestrators — the Plan Phase N / Execute Phase N handlers below resolve it via `gsd_run query dispatch-should-flatten` (#1708):
|
||||
|
||||
- **On Codex:** **Spawn all background agents first** (plan/execute) — dispatch them in parallel using the Plan Phase N / Execute Phase N handlers below — then run verification actions, then run the inline discuss; the background agents continue while you verify/discuss.
|
||||
- **On Claude Code or any other non-Codex runtime:** run the chosen plan/execute step(s) **inline** via their handlers below (in order), then run verification actions, then run the inline discuss. There is no overlap.
|
||||
- **If `FLATTEN` is `false` (the host can background a nesting-capable orchestrator — e.g. codex, cursor):** **Spawn all background agents first** (plan/execute) — dispatch them in parallel using the Plan Phase N / Execute Phase N handlers below — then run verification actions, then run the inline discuss; the background agents continue while you verify/discuss.
|
||||
- **Otherwise (`FLATTEN` is `true` — run inline):** run the chosen plan/execute step(s) **inline** via their handlers below (in order), then run verification actions, then run the inline discuss. There is no overlap.
|
||||
|
||||
Inline verification:
|
||||
|
||||
@@ -254,13 +254,13 @@ After discuss completes, loop back to dashboard step.
|
||||
|
||||
### Plan Phase N
|
||||
|
||||
Planning runs autonomously. **First resolve the runtime.** Background dispatch is only safe on a runtime where a backgrounded agent can still nest the pipeline's subagents (plan-checker / worktree executors / verifier). Among supported runtimes only **Codex** (`spawn_agent`) can do this; Claude Code's backgrounded agents have no `Agent`/`Task` tool, and every other runtime either prohibits nested subagents or disables them by default. So run **inline** everywhere except Codex, which is dispatched in the background.
|
||||
Planning runs autonomously. **First resolve whether background dispatch is safe.** Background dispatch is only safe on a runtime where a backgrounded agent can still nest the pipeline's subagents (plan-checker / worktree executors / verifier). This is determined from the documentation-sourced dispatch capability in the registry (#1708); Claude Code's backgrounded agents have no `Agent`/`Task` tool, and every other runtime either prohibits nested subagents or disables them by default. So run **inline** everywhere except where `dispatch-should-flatten` returns `false`.
|
||||
|
||||
```bash
|
||||
RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")
|
||||
FLATTEN=$(gsd_run query dispatch-should-flatten --raw 2>/dev/null || echo "true")
|
||||
```
|
||||
|
||||
**If `RUNTIME` is `codex`:** Spawn a background agent that delegates to the Skill pipeline with any configured flags:
|
||||
**If `FLATTEN` is `false`:** Spawn a background agent that delegates to the Skill pipeline with any configured flags:
|
||||
|
||||
```
|
||||
Agent(
|
||||
@@ -282,7 +282,7 @@ Important: You are running in the background. Do NOT use AskUserQuestion — mak
|
||||
)
|
||||
```
|
||||
|
||||
> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above with `run_in_background=true`, do NOT do any planning work for this phase independently. Return to the dashboard immediately and wait for the background agent to report back. Only resume planning-related work when the subagent result is available.
|
||||
> **ORCHESTRATOR RULE — BACKGROUND DISPATCH**: After calling Agent() above with `run_in_background=true`, do NOT do any planning work for this phase independently. Return to the dashboard immediately and wait for the background agent to report back. Only resume planning-related work when the subagent result is available.
|
||||
|
||||
Display:
|
||||
|
||||
@@ -292,7 +292,7 @@ Display:
|
||||
|
||||
Loop back to dashboard step.
|
||||
|
||||
**Otherwise (Claude Code or any other non-Codex runtime):** Run plan inline so the plan-checker and quality gates actually run — do NOT wrap it in `Agent(run_in_background=true, …)`:
|
||||
**Otherwise (`FLATTEN` is `true` — run inline):** Run plan inline so the plan-checker and quality gates actually run — do NOT wrap it in `Agent(run_in_background=true, …)`:
|
||||
|
||||
```
|
||||
Skill(skill="gsd-plan-phase", args="{N} --auto {manager_flags.plan}")
|
||||
@@ -308,13 +308,13 @@ Then loop back to dashboard step.
|
||||
|
||||
### Execute Phase N
|
||||
|
||||
Execution runs autonomously. **First resolve the runtime.** Background dispatch is only safe on a runtime where a backgrounded agent can still nest the pipeline's subagents (plan-checker / worktree executors / verifier). Among supported runtimes only **Codex** (`spawn_agent`) can do this; Claude Code's backgrounded agents have no `Agent`/`Task` tool, and every other runtime either prohibits nested subagents or disables them by default. So run **inline** everywhere except Codex, which is dispatched in the background.
|
||||
Execution runs autonomously. **First resolve whether background dispatch is safe.** Background dispatch is only safe on a runtime where a backgrounded agent can still nest the pipeline's subagents (plan-checker / worktree executors / verifier). This is determined from the documentation-sourced dispatch capability in the registry (#1708); Claude Code's backgrounded agents have no `Agent`/`Task` tool, and every other runtime either prohibits nested subagents or disables them by default. So run **inline** everywhere except where `dispatch-should-flatten` returns `false`.
|
||||
|
||||
```bash
|
||||
RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")
|
||||
FLATTEN=$(gsd_run query dispatch-should-flatten --raw 2>/dev/null || echo "true")
|
||||
```
|
||||
|
||||
**If `RUNTIME` is `codex`:** Spawn a background agent that delegates to the Skill pipeline with any configured flags:
|
||||
**If `FLATTEN` is `false`:** Spawn a background agent that delegates to the Skill pipeline with any configured flags:
|
||||
|
||||
```
|
||||
Agent(
|
||||
@@ -336,7 +336,7 @@ Important: You are running in the background. Do NOT use AskUserQuestion — mak
|
||||
)
|
||||
```
|
||||
|
||||
> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above with `run_in_background=true`, do NOT do any execution work for this phase independently. Return to the dashboard immediately and wait for the background agent to report back. Only resume execution-related work when the subagent result is available.
|
||||
> **ORCHESTRATOR RULE — BACKGROUND DISPATCH**: After calling Agent() above with `run_in_background=true`, do NOT do any execution work for this phase independently. Return to the dashboard immediately and wait for the background agent to report back. Only resume execution-related work when the subagent result is available.
|
||||
|
||||
Display:
|
||||
|
||||
@@ -346,7 +346,7 @@ Display:
|
||||
|
||||
Loop back to dashboard step.
|
||||
|
||||
**Otherwise (Claude Code or any other non-Codex runtime):** Run execute inline so worktree isolation and the verifier actually run — do NOT wrap it in `Agent(run_in_background=true, …)`:
|
||||
**Otherwise (`FLATTEN` is `true` — run inline):** Run execute inline so worktree isolation and the verifier actually run — do NOT wrap it in `Agent(run_in_background=true, …)`:
|
||||
|
||||
```
|
||||
Skill(skill="gsd-execute-phase", args="{N} {manager_flags.execute}")
|
||||
|
||||
@@ -70,6 +70,7 @@ interface DispatchCapability {
|
||||
maxDepth: number;
|
||||
background: boolean;
|
||||
subagentToolkit: SubagentToolkit;
|
||||
backgroundDispatch: boolean;
|
||||
}
|
||||
|
||||
interface HostIntegrationAxes {
|
||||
@@ -97,7 +98,7 @@ interface DegradationResult {
|
||||
const SAFE_DEFAULTS: HostIntegrationAxes = {
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'prose-only',
|
||||
dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'read-only' },
|
||||
dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'read-only', backgroundDispatch: false },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'none',
|
||||
stateIO: 'session-log-append',
|
||||
@@ -110,7 +111,7 @@ const PROFILE_BASELINES: Readonly<Record<'programmatic-cli' | 'declarative-cli'
|
||||
'programmatic-cli': Object.freeze({
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' }),
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true }),
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
@@ -120,7 +121,7 @@ const PROFILE_BASELINES: Readonly<Record<'programmatic-cli' | 'declarative-cli'
|
||||
'declarative-cli': Object.freeze({
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full' }),
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full', backgroundDispatch: false }),
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
@@ -130,7 +131,7 @@ const PROFILE_BASELINES: Readonly<Record<'programmatic-cli' | 'declarative-cli'
|
||||
'ide': Object.freeze({
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'palette',
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: 5, background: true, subagentToolkit: 'full' }),
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: 5, background: true, subagentToolkit: 'full', backgroundDispatch: true }),
|
||||
modelMode: 'active',
|
||||
hookBus: 'engine',
|
||||
stateIO: 'sandboxed-storage',
|
||||
@@ -255,7 +256,7 @@ const DEFAULT_ENGINE: EngineCapabilities = {
|
||||
axes: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true },
|
||||
modelMode: 'active',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
@@ -368,17 +369,19 @@ function negotiateHostCapabilities(
|
||||
let effectiveNamedDispatch: boolean;
|
||||
let effectiveNested: boolean;
|
||||
let effectiveBackground: boolean;
|
||||
let effectiveBackgroundDispatch: boolean;
|
||||
let effectiveSubagentToolkit: SubagentToolkit;
|
||||
let effectiveMaxDepth: number;
|
||||
|
||||
if (hostDispatch === null) {
|
||||
// Host didn't declare dispatch at all — fail-closed to most-restrictive values
|
||||
warnings.push(`host did not declare 'dispatch'`);
|
||||
effectiveNamedDispatch = false;
|
||||
effectiveNested = false;
|
||||
effectiveBackground = false;
|
||||
effectiveSubagentToolkit = 'read-only';
|
||||
effectiveMaxDepth = 0;
|
||||
effectiveNamedDispatch = false;
|
||||
effectiveNested = false;
|
||||
effectiveBackground = false;
|
||||
effectiveBackgroundDispatch = false;
|
||||
effectiveSubagentToolkit = 'read-only';
|
||||
effectiveMaxDepth = 0;
|
||||
} else {
|
||||
// N1: observability warnings for 'undocumented' sentinel on dispatch fields
|
||||
if (hostDispatch.namedDispatch === 'undocumented') {
|
||||
@@ -393,10 +396,14 @@ function negotiateHostCapabilities(
|
||||
if (hostDispatch.subagentToolkit === 'undocumented') {
|
||||
warnings.push(`dispatch.subagentToolkit is undocumented — degraded closed (read-only)`);
|
||||
}
|
||||
if (hostDispatch.backgroundDispatch === 'undocumented') {
|
||||
warnings.push(`dispatch.backgroundDispatch is undocumented — degraded closed`);
|
||||
}
|
||||
|
||||
effectiveNamedDispatch = (hostDispatch.namedDispatch === true) && engineDispatch.namedDispatch;
|
||||
effectiveNested = (hostDispatch.nested === true) && engineDispatch.nested;
|
||||
effectiveBackground = (hostDispatch.background === true) && engineDispatch.background;
|
||||
effectiveNamedDispatch = (hostDispatch.namedDispatch === true) && engineDispatch.namedDispatch;
|
||||
effectiveNested = (hostDispatch.nested === true) && engineDispatch.nested;
|
||||
effectiveBackground = (hostDispatch.background === true) && engineDispatch.background;
|
||||
effectiveBackgroundDispatch = (hostDispatch.backgroundDispatch === true) && engineDispatch.backgroundDispatch;
|
||||
|
||||
// subagentToolkit: fail closed to read-only unless explicitly 'full'
|
||||
// (an 'undocumented' or 'read-only' value → read-only)
|
||||
@@ -419,20 +426,22 @@ function negotiateHostCapabilities(
|
||||
const minDepth = Math.min(hDepthNum, eDepthNum);
|
||||
effectiveMaxDepth = minDepth === Infinity ? -1 : minDepth;
|
||||
|
||||
// If namedDispatch is false, cap maxDepth/nested/background to 0/false/false (struct consistency)
|
||||
// If namedDispatch is false, cap maxDepth/nested/background/backgroundDispatch to 0/false/false/false (struct consistency)
|
||||
if (!effectiveNamedDispatch) {
|
||||
effectiveMaxDepth = 0;
|
||||
effectiveNested = false;
|
||||
effectiveBackground = false;
|
||||
effectiveMaxDepth = 0;
|
||||
effectiveNested = false;
|
||||
effectiveBackground = false;
|
||||
effectiveBackgroundDispatch = false;
|
||||
}
|
||||
}
|
||||
|
||||
const effectiveDispatch: DispatchCapability = {
|
||||
namedDispatch: effectiveNamedDispatch,
|
||||
nested: effectiveNested,
|
||||
maxDepth: effectiveMaxDepth,
|
||||
background: effectiveBackground,
|
||||
subagentToolkit: effectiveSubagentToolkit,
|
||||
namedDispatch: effectiveNamedDispatch,
|
||||
nested: effectiveNested,
|
||||
maxDepth: effectiveMaxDepth,
|
||||
background: effectiveBackground,
|
||||
subagentToolkit: effectiveSubagentToolkit,
|
||||
backgroundDispatch: effectiveBackgroundDispatch,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -474,6 +483,35 @@ function negotiateHostCapabilities(
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// shouldFlattenDispatch — ADR-1239 Phase B / #1708
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns true when the orchestrator MUST run inline (flatten); false when it
|
||||
* may be backgrounded.
|
||||
*
|
||||
* A host may background only if it can reliably background a nesting-capable
|
||||
* orchestrator — i.e. both `background` AND `backgroundDispatch` are
|
||||
* explicitly `true`. Any other value (false, missing, 'undocumented') fails
|
||||
* closed to inline (the always-safe path).
|
||||
*
|
||||
* This graduates the #853 prose rule (originally `RUNTIME === 'codex'`, then
|
||||
* extended to cursor) to a typed, documentation-sourced decision; codex AND
|
||||
* cursor are both background-eligible in the registry. See
|
||||
* docs/reference/host-integration-capability-matrix.md.
|
||||
*
|
||||
* Null-safety: if dispatch is null, undefined, or not an object, returns true
|
||||
* (inline, fail-closed) instead of throwing.
|
||||
*/
|
||||
type UnvalidatedDispatch = (Partial<DispatchCapability> & { background?: unknown; backgroundDispatch?: unknown }) | null | undefined;
|
||||
|
||||
function shouldFlattenDispatch(dispatch: UnvalidatedDispatch): boolean {
|
||||
if (!dispatch || typeof dispatch !== 'object') return true;
|
||||
const canBackground = dispatch.background === true && dispatch.backgroundDispatch === true;
|
||||
return !canBackground;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module export (CommonJS — matches existing src/*.cts pattern)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -488,4 +526,5 @@ export = {
|
||||
degradationFor,
|
||||
profileOf,
|
||||
negotiateHostCapabilities,
|
||||
shouldFlattenDispatch,
|
||||
};
|
||||
|
||||
@@ -5,87 +5,225 @@
|
||||
* dispatched Plan/Execute via Agent(run_in_background=true). On Claude Code a
|
||||
* backgrounded agent has no Agent/Task tool, so it cannot spawn the nested
|
||||
* subagents (worktree executors, plan-checker, verifier). The workflows must
|
||||
* now resolve the runtime and run inline everywhere except Codex, which is the
|
||||
* only supported runtime where a backgrounded agent can still nest subagents.
|
||||
* now resolve dispatch capability from the registry (#1708) and run inline
|
||||
* everywhere except runtimes where dispatch.background && dispatch.backgroundDispatch
|
||||
* are both true (currently: codex, cursor).
|
||||
*
|
||||
* Phase B (#1708): the prose `RUNTIME === 'codex'` rule is graduated to a typed
|
||||
* `gsd_run query dispatch-should-flatten` query backed by shouldFlattenDispatch()
|
||||
* from host-integration.cjs and the documentation-sourced capability registry.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { createTempProject, cleanup: cleanupDir, runGsdTools } = require('./helpers.cjs');
|
||||
|
||||
const WORKFLOWS_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows');
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
const MANAGER = fs.readFileSync(path.join(WORKFLOWS_DIR, 'manager.md'), 'utf8');
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
const AUTONOMOUS = fs.readFileSync(path.join(WORKFLOWS_DIR, 'autonomous.md'), 'utf8');
|
||||
|
||||
describe('bug-853 — manager/autonomous gate background dispatch by runtime', () => {
|
||||
test('manager.md resolves the runtime before dispatching plan/execute', () => {
|
||||
// Two dispatch sites (plan + execute), each must resolve the runtime.
|
||||
const matches = MANAGER.match(/config-get runtime/g) || [];
|
||||
assert.ok(matches.length >= 2, 'manager.md must resolve runtime for both plan and execute dispatch');
|
||||
test('manager.md resolves dispatch-should-flatten before dispatching plan/execute', () => {
|
||||
// Two dispatch sites (plan + execute), each must use dispatch-should-flatten.
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
const matches = MANAGER.match(/dispatch-should-flatten/g) || [];
|
||||
assert.ok(matches.length >= 2, 'manager.md must use dispatch-should-flatten for both plan and execute dispatch');
|
||||
});
|
||||
|
||||
test('manager.md documents why most runtimes cannot background-dispatch', () => {
|
||||
// Accept both old singular form (backgrounded agent has no) and new plural form (backgrounded agents have no)
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
assert.match(MANAGER, /backgrounded agents? ha(?:s|ve) no `Agent`\/`Task` tool/);
|
||||
});
|
||||
|
||||
test('manager.md gates background dispatch on codex and runs plan/execute inline otherwise', () => {
|
||||
// Codex takes the background path
|
||||
assert.match(MANAGER, /If `RUNTIME` is `codex`[\s\S]{0,400}?run_in_background=true/);
|
||||
// Inline is the default/else branch for plan — anchored on the explicit non-Codex label
|
||||
test('manager.md gates background dispatch on FLATTEN=false and runs plan/execute inline otherwise', () => {
|
||||
// Background path uses FLATTEN is false
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
assert.match(MANAGER, /If `FLATTEN` is `false`[\s\S]{0,400}?run_in_background=true/);
|
||||
// Inline is the default/else branch for plan — anchored on FLATTEN=true language (not runtime name)
|
||||
assert.match(
|
||||
MANAGER,
|
||||
/Otherwise \(Claude Code or any other non-Codex runtime\)[\s\S]{0,400}?Skill\(skill="gsd-plan-phase"/,
|
||||
/Otherwise[\s\S]{0,100}?`FLATTEN`[\s\S]{0,400}?Skill\(skill="gsd-plan-phase"/,
|
||||
);
|
||||
// Inline is the default/else branch for execute — anchored on the explicit non-Codex label
|
||||
// Inline is the default/else branch for execute — anchored on FLATTEN=true language (not runtime name)
|
||||
assert.match(
|
||||
MANAGER,
|
||||
/Otherwise \(Claude Code or any other non-Codex runtime\)[\s\S]{0,400}?Skill\(skill="gsd-execute-phase"/,
|
||||
/Otherwise[\s\S]{0,100}?`FLATTEN`[\s\S]{0,400}?Skill\(skill="gsd-execute-phase"/,
|
||||
);
|
||||
});
|
||||
|
||||
test('manager.md compound actions only background plan/execute on Codex', () => {
|
||||
test('manager.md compound action preamble uses FLATTEN language (not hardcoded runtime names)', () => {
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
const compoundActionSection = MANAGER.match(
|
||||
/### Compound Action \(background \+ inline\)[\s\S]*?Inline verification:/,
|
||||
);
|
||||
assert.ok(compoundActionSection, 'manager.md must document compound action runtime dispatch');
|
||||
|
||||
// Must gate on FLATTEN being false (not runtime name)
|
||||
assert.match(
|
||||
compoundActionSection[0],
|
||||
/On Codex:[\s\S]{0,260}?Spawn all background agents first[\s\S]{0,220}?plan\/execute/,
|
||||
/If `FLATTEN` is `false`[\s\S]{0,400}?Spawn all background agents first[\s\S]{0,300}?plan\/execute/,
|
||||
);
|
||||
// Otherwise / inline branch must reference FLATTEN being true
|
||||
assert.match(
|
||||
compoundActionSection[0],
|
||||
/On Claude Code or any other non-Codex runtime:[\s\S]{0,260}?inline/,
|
||||
/Otherwise[\s\S]{0,260}?`FLATTEN`[\s\S]{0,260}?`true`[\s\S]{0,260}?inline/,
|
||||
);
|
||||
// Must NOT still hardcode "On Codex:" in this section
|
||||
assert.doesNotMatch(
|
||||
compoundActionSection[0],
|
||||
/On other runtimes:[\s\S]{0,260}?Spawn all background agents first/,
|
||||
/\*\*On Codex:\*\*/,
|
||||
);
|
||||
// Must NOT still hardcode "On Claude Code or any other non-Codex runtime:"
|
||||
assert.doesNotMatch(
|
||||
compoundActionSection[0],
|
||||
/On Claude Code or any other non-Codex runtime:/,
|
||||
);
|
||||
});
|
||||
|
||||
test('autonomous.md gates interactive background dispatch by runtime', () => {
|
||||
const autoRuntimeMatches = AUTONOMOUS.match(/config-get runtime/g) || [];
|
||||
assert.ok(autoRuntimeMatches.length >= 2, 'autonomous.md must resolve runtime in both 3b (plan) and 3c (execute) interactive branches');
|
||||
test('autonomous.md gates interactive background dispatch using dispatch-should-flatten', () => {
|
||||
// Two dispatch sites (3b plan + 3c execute), each must use dispatch-should-flatten.
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
const autoFlattenMatches = AUTONOMOUS.match(/dispatch-should-flatten/g) || [];
|
||||
assert.ok(autoFlattenMatches.length >= 2, 'autonomous.md must use dispatch-should-flatten in both 3b (plan) and 3c (execute) interactive branches');
|
||||
// Accept both old singular form (backgrounded agent has no) and new plural form (backgrounded agents have no)
|
||||
assert.match(AUTONOMOUS, /backgrounded agents? ha(?:s|ve) no `Agent`\/`Task` tool/);
|
||||
});
|
||||
|
||||
test('autonomous.md gates interactive background dispatch on codex; runs plan/execute inline otherwise', () => {
|
||||
// Codex block: run_in_background=true appears within the codex branch and gsd-plan-phase is nearby
|
||||
assert.match(AUTONOMOUS, /If `RUNTIME` is `codex`[\s\S]{0,1200}?run_in_background=true[\s\S]{0,600}?gsd-plan-phase/);
|
||||
// Codex block: run_in_background=true appears within the codex branch and gsd-execute-phase is nearby
|
||||
assert.match(AUTONOMOUS, /If `RUNTIME` is `codex`[\s\S]{0,3000}?run_in_background=true[\s\S]{0,200}?gsd-execute-phase/);
|
||||
// Inline is the otherwise/else branch for plan — anchored on the explicit non-Codex label
|
||||
test('autonomous.md gates interactive background dispatch on FLATTEN=false; runs plan/execute inline otherwise', () => {
|
||||
// Background block: run_in_background=true appears within the FLATTEN=false branch and gsd-plan-phase is nearby
|
||||
// allow-test-rule: source-text-is-the-product (see #1708)
|
||||
assert.match(AUTONOMOUS, /If `FLATTEN` is `false`[\s\S]{0,1200}?run_in_background=true[\s\S]{0,600}?gsd-plan-phase/);
|
||||
// Background block: run_in_background=true appears within the FLATTEN=false branch and gsd-execute-phase is nearby
|
||||
assert.match(AUTONOMOUS, /If `FLATTEN` is `false`[\s\S]{0,3000}?run_in_background=true[\s\S]{0,200}?gsd-execute-phase/);
|
||||
// Inline is the otherwise/else branch for plan — anchored on FLATTEN=true language (not runtime name)
|
||||
assert.match(
|
||||
AUTONOMOUS,
|
||||
/Otherwise \(Claude Code or any other non-Codex runtime\)[\s\S]{0,400}?Skill\(skill="gsd-plan-phase"/,
|
||||
/Otherwise[\s\S]{0,100}?`FLATTEN`[\s\S]{0,400}?Skill\(skill="gsd-plan-phase"/,
|
||||
);
|
||||
// Inline is the otherwise/else branch for execute — anchored on the explicit non-Codex label
|
||||
// Inline is the otherwise/else branch for execute — anchored on FLATTEN=true language (not runtime name)
|
||||
assert.match(
|
||||
AUTONOMOUS,
|
||||
/Otherwise \(Claude Code or any other non-Codex runtime\)[\s\S]{0,400}?Skill\(skill="gsd-execute-phase"/,
|
||||
/Otherwise[\s\S]{0,100}?`FLATTEN`[\s\S]{0,400}?Skill\(skill="gsd-execute-phase"/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dispatch-should-flatten query — behavioral', () => {
|
||||
// #853 / #1708: The typed query replaces prose-level RUNTIME===codex checks.
|
||||
// shouldFlattenDispatch returns false only when both dispatch.background AND
|
||||
// dispatch.backgroundDispatch are true in the capability registry.
|
||||
//
|
||||
// Registry values (from host-integration-capability-matrix.md):
|
||||
// codex: background=true, backgroundDispatch=true → shouldFlatten=false (may background)
|
||||
// claude: background=true, backgroundDispatch=false → shouldFlatten=true (must inline)
|
||||
// cursor: background=true, backgroundDispatch=true → shouldFlatten=false (may background)
|
||||
// unknown: no entry → fail-closed → shouldFlatten=true (must inline)
|
||||
|
||||
test('runtime=codex → shouldFlatten=false (background dispatch safe)', () => {
|
||||
const tmpDir = createTempProject();
|
||||
try {
|
||||
const result = runGsdTools(['query', 'dispatch-should-flatten', '--raw'], tmpDir, {
|
||||
GSD_RUNTIME: 'codex',
|
||||
});
|
||||
assert.ok(result.success, `Expected success, got error: ${result.error}`);
|
||||
assert.strictEqual(result.output, 'false', `codex should return false (may background), got: ${result.output}`);
|
||||
} finally {
|
||||
cleanupDir(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('runtime=claude → shouldFlatten=true (must inline)', () => {
|
||||
const tmpDir = createTempProject();
|
||||
try {
|
||||
const result = runGsdTools(['query', 'dispatch-should-flatten', '--raw'], tmpDir, {
|
||||
GSD_RUNTIME: 'claude',
|
||||
});
|
||||
assert.ok(result.success, `Expected success, got error: ${result.error}`);
|
||||
assert.strictEqual(result.output, 'true', `claude should return true (must inline), got: ${result.output}`);
|
||||
} finally {
|
||||
cleanupDir(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('runtime=cursor → shouldFlatten=false (background dispatch safe)', () => {
|
||||
const tmpDir = createTempProject();
|
||||
try {
|
||||
const result = runGsdTools(['query', 'dispatch-should-flatten', '--raw'], tmpDir, {
|
||||
GSD_RUNTIME: 'cursor',
|
||||
});
|
||||
assert.ok(result.success, `Expected success, got error: ${result.error}`);
|
||||
assert.strictEqual(result.output, 'false', `cursor should return false (may background), got: ${result.output}`);
|
||||
} finally {
|
||||
cleanupDir(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('unknown runtime → shouldFlatten=true (fail-closed → must inline)', () => {
|
||||
// An unknown runtime has no registry entry → dispatch is null → fail-closed to true.
|
||||
const tmpDir = createTempProject();
|
||||
try {
|
||||
const result = runGsdTools(['query', 'dispatch-should-flatten', '--raw'], tmpDir, {
|
||||
GSD_RUNTIME: 'unknown-runtime-xyz',
|
||||
});
|
||||
// The query must succeed (exit 0) even for unknown runtimes — fail-closed not crash-closed.
|
||||
assert.ok(result.success, `Expected success (fail-closed), got error: ${result.error}`);
|
||||
assert.strictEqual(result.output, 'true', `unknown runtime should return true (fail-closed), got: ${result.output}`);
|
||||
} finally {
|
||||
cleanupDir(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('--json flag returns structured { runtime, shouldFlatten, dispatch }', () => {
|
||||
const tmpDir = createTempProject();
|
||||
try {
|
||||
const result = runGsdTools(['query', 'dispatch-should-flatten', '--json'], tmpDir, {
|
||||
GSD_RUNTIME: 'codex',
|
||||
});
|
||||
assert.ok(result.success, `Expected success, got error: ${result.error}`);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(result.output);
|
||||
} catch {
|
||||
assert.fail(`Expected valid JSON output, got: ${result.output}`);
|
||||
}
|
||||
assert.strictEqual(parsed.runtime, 'codex');
|
||||
assert.strictEqual(parsed.shouldFlatten, false);
|
||||
assert.ok(parsed.dispatch !== null && typeof parsed.dispatch === 'object', 'dispatch should be an object');
|
||||
assert.strictEqual(parsed.dispatch.backgroundDispatch, true);
|
||||
} finally {
|
||||
cleanupDir(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('config.runtime takes precedence when GSD_RUNTIME not set', () => {
|
||||
// GSD_RUNTIME > config.runtime > 'claude'
|
||||
// Write config.json with runtime=codex; no GSD_RUNTIME override.
|
||||
const tmpDir = createTempProject();
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'config.json'),
|
||||
JSON.stringify({ runtime: 'codex' }),
|
||||
'utf-8',
|
||||
);
|
||||
// Override GSD_RUNTIME to '' (empty string) so any ambient value is cleared.
|
||||
// resolveRuntimeNameFromCandidates treats empty string as absent (normalizes
|
||||
// to '' which is falsy → skipped → falls through to config.runtime=codex).
|
||||
// This is the only way to suppress an ambient GSD_RUNTIME since runGsdTools
|
||||
// merges { ...process.env, ...TEST_ENV_BASE, ...env } — passing '' as the
|
||||
// override overwrites the ambient value at the correct merge position.
|
||||
const result = runGsdTools(['query', 'dispatch-should-flatten', '--raw'], tmpDir, {
|
||||
GSD_RUNTIME: '',
|
||||
});
|
||||
// config.runtime=codex with GSD_RUNTIME cleared → codex backgrounds → shouldFlatten=false
|
||||
assert.ok(result.success, `Expected success, got error: ${result.error}`);
|
||||
assert.strictEqual(result.output, 'false', `config.runtime=codex (GSD_RUNTIME cleared) should return false (may background), got: ${result.output}`);
|
||||
} finally {
|
||||
cleanupDir(tmpDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,7 +89,7 @@ function runtimeCap(overrides) {
|
||||
hostIntegration: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: false },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
|
||||
@@ -1778,7 +1778,7 @@ describe('C3: role:runtime body validation', () => {
|
||||
hostIntegration: {
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 2, background: false, subagentToolkit: 'full' },
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 2, background: false, subagentToolkit: 'full', backgroundDispatch: false },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
@@ -3231,7 +3231,7 @@ function makeRuntimeCap(overrides) {
|
||||
hostIntegration: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: false },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
@@ -4306,7 +4306,7 @@ describe('ADR-857 phase 5f: cross-field consistency gate rejection tests (DEFECT
|
||||
hostIntegration: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: false },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
@@ -5168,7 +5168,7 @@ describe('activationKey validation', () => {
|
||||
hostIntegration: {
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 2, background: false, subagentToolkit: 'full' },
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 2, background: false, subagentToolkit: 'full', backgroundDispatch: false },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
|
||||
@@ -169,12 +169,14 @@ test('execute-phase.md, quick.md, and diagnose-issues.md guards are generalized
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Orchestration gating: manager.md + autonomous.md now gate on codex for
|
||||
// background dispatch, not on "not claude". (#1521 Stage 2)
|
||||
// Orchestration gating: manager.md + autonomous.md gate background dispatch on
|
||||
// the typed FLATTEN query. #1708 (ADR-1239 Phase B) graduated #1521's
|
||||
// codex-specific check to a documentation-sourced shouldFlattenDispatch — the
|
||||
// prose now branches on `FLATTEN` (false = background), not a runtime name.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('manager.md and autonomous.md gate run_in_background on codex specifically (#1521)', () => {
|
||||
// allow-test-rule: orchestration dispatch gating in manager/autonomous .md is the runtime contract surface (#1521)
|
||||
test('manager.md and autonomous.md gate run_in_background on FLATTEN=false, not a runtime name (#1521, graduated by #1708)', () => {
|
||||
// allow-test-rule: orchestration dispatch gating in manager/autonomous .md is the runtime contract surface (#1521/#1708)
|
||||
const manager = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'gsd-core', 'workflows', 'manager.md'),
|
||||
'utf8',
|
||||
@@ -184,24 +186,29 @@ test('manager.md and autonomous.md gate run_in_background on codex specifically
|
||||
'utf8',
|
||||
);
|
||||
|
||||
// Both files must gate run_in_background on codex (not on a generic "not claude" condition)
|
||||
// Both files must gate run_in_background on the typed FLATTEN decision (not a runtime name)
|
||||
assert.ok(
|
||||
/`RUNTIME` is `codex`[\s\S]{0,500}?run_in_background=true/.test(manager),
|
||||
'manager.md: expected run_in_background dispatch gated on RUNTIME=codex specifically',
|
||||
/If `FLATTEN` is `false`[\s\S]{0,500}?run_in_background=true/.test(manager),
|
||||
'manager.md: expected run_in_background dispatch gated on FLATTEN=false (typed dispatch-should-flatten query)',
|
||||
);
|
||||
assert.ok(
|
||||
/`RUNTIME` is `codex`[\s\S]{0,700}?run_in_background=true/.test(autonomous),
|
||||
'autonomous.md: expected run_in_background dispatch gated on RUNTIME=codex specifically',
|
||||
/If `FLATTEN` is `false`[\s\S]{0,1200}?run_in_background=true/.test(autonomous),
|
||||
'autonomous.md: expected run_in_background dispatch gated on FLATTEN=false (typed dispatch-should-flatten query)',
|
||||
);
|
||||
|
||||
// Inline is the default/else branch (not just claude)
|
||||
// Inline is the else branch, keyed on FLATTEN — never a runtime name
|
||||
assert.ok(
|
||||
/Otherwise[\s\S]{0,200}?Claude Code or any other non-Codex runtime/.test(manager),
|
||||
'manager.md: expected "Otherwise (Claude Code or any other non-Codex runtime)" inline branch',
|
||||
/Otherwise[\s\S]{0,250}?inline/i.test(manager),
|
||||
'manager.md: expected "Otherwise ... inline" branch keyed on FLATTEN',
|
||||
);
|
||||
assert.ok(
|
||||
/Otherwise[\s\S]{0,200}?Claude Code or any other non-Codex runtime/.test(autonomous),
|
||||
'autonomous.md: expected "Otherwise (Claude Code or any other non-Codex runtime)" inline branch',
|
||||
/Otherwise[\s\S]{0,250}?inline/i.test(autonomous),
|
||||
'autonomous.md: expected "Otherwise ... inline" branch keyed on FLATTEN',
|
||||
);
|
||||
// And the old runtime-name gating must be gone (no `RUNTIME` is `codex` dispatch gate)
|
||||
assert.ok(
|
||||
!/`RUNTIME` is `codex`[\s\S]{0,500}?run_in_background=true/.test(manager),
|
||||
'manager.md: must no longer gate run_in_background on the runtime name',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const path = require('node:path');
|
||||
const {
|
||||
negotiateHostCapabilities,
|
||||
profileOf,
|
||||
shouldFlattenDispatch,
|
||||
} = require(path.join(__dirname, '../gsd-core/bin/lib/host-integration.cjs'));
|
||||
|
||||
const registry = require(path.join(__dirname, '../gsd-core/bin/lib/capability-registry.cjs'));
|
||||
@@ -27,8 +28,8 @@ const {
|
||||
|
||||
// All 8 scalar hostIntegration axis keys
|
||||
const SCALAR_AXES = ['embeddingMode', 'commandSurface', 'modelMode', 'hookBus', 'stateIO', 'transport', 'runtime'];
|
||||
// All 5 dispatch sub-keys
|
||||
const DISPATCH_KEYS = ['namedDispatch', 'nested', 'maxDepth', 'background', 'subagentToolkit'];
|
||||
// All 6 dispatch sub-keys (includes backgroundDispatch added in feat/1679-dispatch-flatten)
|
||||
const DISPATCH_KEYS = ['namedDispatch', 'nested', 'maxDepth', 'background', 'subagentToolkit', 'backgroundDispatch'];
|
||||
|
||||
// All 16 runtime IDs (ordered alphabetically)
|
||||
const RUNTIME_IDS = [
|
||||
@@ -95,7 +96,7 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
});
|
||||
|
||||
// (ii) hostIntegration object is present with all required keys
|
||||
test('(ii) cap.runtime.hostIntegration is present with all 8 axis keys and 5 dispatch sub-keys', () => {
|
||||
test('(ii) cap.runtime.hostIntegration is present with all 8 axis keys and 6 dispatch sub-keys', () => {
|
||||
assert.ok(
|
||||
hi !== undefined && hi !== null && typeof hi === 'object',
|
||||
id + ': cap.runtime.hostIntegration must be a non-null object',
|
||||
@@ -225,6 +226,77 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
assert.strictEqual(counts['ide'], 0, 'Must have exactly 0 ide runtimes');
|
||||
});
|
||||
|
||||
// ─── backgroundDispatch presence ─────────────────────────────────────────────
|
||||
|
||||
test('every runtime descriptor has dispatch.backgroundDispatch (boolean or "undocumented")', () => {
|
||||
for (const id of RUNTIME_IDS) {
|
||||
const cap = registry.runtimes[id];
|
||||
const dispatch = cap && cap.runtime && cap.runtime.hostIntegration && cap.runtime.hostIntegration.dispatch;
|
||||
assert.ok(
|
||||
dispatch !== null && typeof dispatch === 'object',
|
||||
id + ': hostIntegration.dispatch must be an object',
|
||||
);
|
||||
assert.ok(
|
||||
Object.prototype.hasOwnProperty.call(dispatch, 'backgroundDispatch'),
|
||||
id + ': dispatch must have a backgroundDispatch key',
|
||||
);
|
||||
const v = dispatch.backgroundDispatch;
|
||||
assert.ok(
|
||||
v === true || v === false || v === 'undocumented',
|
||||
id + ': dispatch.backgroundDispatch must be true, false, or "undocumented", got: ' + JSON.stringify(v),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── shouldFlattenDispatch per-host (#853 discriminator) ─────────────────────
|
||||
|
||||
// Expected: false (may background) for codex and cursor ONLY; true (must inline) for the other 14.
|
||||
const EXPECTED_FLATTEN = {
|
||||
antigravity: true,
|
||||
augment: true,
|
||||
claude: true,
|
||||
cline: true,
|
||||
codebuddy: true,
|
||||
codex: false,
|
||||
copilot: true,
|
||||
cursor: false,
|
||||
gemini: true,
|
||||
hermes: true,
|
||||
kilo: true,
|
||||
kimi: true,
|
||||
opencode: true,
|
||||
qwen: true,
|
||||
trae: true,
|
||||
windsurf: true,
|
||||
};
|
||||
|
||||
for (const id of RUNTIME_IDS) {
|
||||
test('shouldFlattenDispatch(' + id + ') === ' + EXPECTED_FLATTEN[id], () => {
|
||||
const cap = registry.runtimes[id];
|
||||
const dispatch = cap && cap.runtime && cap.runtime.hostIntegration && cap.runtime.hostIntegration.dispatch;
|
||||
assert.ok(dispatch, id + ': dispatch must exist');
|
||||
const result = shouldFlattenDispatch(dispatch);
|
||||
assert.strictEqual(
|
||||
result,
|
||||
EXPECTED_FLATTEN[id],
|
||||
id + ': shouldFlattenDispatch must return ' + EXPECTED_FLATTEN[id] + ' (got: ' + result + ')',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('contract-pin: exactly 2 hosts are background-eligible (shouldFlattenDispatch === false): codex and cursor', () => {
|
||||
const eligible = RUNTIME_IDS.filter((id) => {
|
||||
const cap = registry.runtimes[id];
|
||||
const dispatch = cap && cap.runtime && cap.runtime.hostIntegration && cap.runtime.hostIntegration.dispatch;
|
||||
return dispatch && shouldFlattenDispatch(dispatch) === false;
|
||||
});
|
||||
assert.deepEqual(
|
||||
eligible.slice().sort(),
|
||||
['codex', 'cursor'],
|
||||
'Exactly codex and cursor must be background-eligible, got: ' + JSON.stringify(eligible.sort()),
|
||||
);
|
||||
});
|
||||
|
||||
test('contract-pin: spot-check claude→programmatic-cli, codex→declarative-cli, opencode→programmatic-cli, gemini→declarative-cli', () => {
|
||||
const checks = [
|
||||
['claude', 'programmatic-cli'],
|
||||
@@ -273,7 +345,7 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
hostIntegration: {
|
||||
embeddingMode: 'bogus',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full' },
|
||||
dispatch: { namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full', backgroundDispatch: false },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
|
||||
@@ -68,6 +68,7 @@ function makeMinimalRuntimeCap(overrides = {}) {
|
||||
maxDepth: 1,
|
||||
background: false,
|
||||
subagentToolkit: 'full',
|
||||
backgroundDispatch: false,
|
||||
},
|
||||
},
|
||||
...overrides,
|
||||
@@ -213,6 +214,7 @@ describe('ADR-1239 validator behavioral: undocumented sentinel passes, bogus fai
|
||||
maxDepth: 'undocumented',
|
||||
background: 'undocumented',
|
||||
subagentToolkit: 'undocumented',
|
||||
backgroundDispatch: 'undocumented',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -229,6 +231,77 @@ describe('ADR-1239 validator behavioral: undocumented sentinel passes, bogus fai
|
||||
assert.strictEqual(dispatchErrors.length, 0,
|
||||
`Valid boolean dispatch fields must produce no errors; got: ${dispatchErrors.join(', ')}`);
|
||||
});
|
||||
|
||||
// Phase B: backgroundDispatch field validation
|
||||
test('dispatch.backgroundDispatch:true → ZERO validator errors', () => {
|
||||
const cap = makeMinimalRuntimeCap({
|
||||
hostIntegration: {
|
||||
...makeMinimalRuntimeCap().runtime.hostIntegration,
|
||||
dispatch: { ...makeMinimalRuntimeCap().runtime.hostIntegration.dispatch, backgroundDispatch: true },
|
||||
},
|
||||
});
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const bdErrors = errors.filter((e) => e.includes('backgroundDispatch'));
|
||||
assert.strictEqual(bdErrors.length, 0,
|
||||
`backgroundDispatch:true must produce no errors; got: ${bdErrors.join(', ')}`);
|
||||
});
|
||||
|
||||
test('dispatch.backgroundDispatch:false → ZERO validator errors', () => {
|
||||
const cap = makeMinimalRuntimeCap({
|
||||
hostIntegration: {
|
||||
...makeMinimalRuntimeCap().runtime.hostIntegration,
|
||||
dispatch: { ...makeMinimalRuntimeCap().runtime.hostIntegration.dispatch, backgroundDispatch: false },
|
||||
},
|
||||
});
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const bdErrors = errors.filter((e) => e.includes('backgroundDispatch'));
|
||||
assert.strictEqual(bdErrors.length, 0,
|
||||
`backgroundDispatch:false must produce no errors; got: ${bdErrors.join(', ')}`);
|
||||
});
|
||||
|
||||
test('dispatch.backgroundDispatch:"undocumented" → ZERO validator errors', () => {
|
||||
const cap = makeMinimalRuntimeCap({
|
||||
hostIntegration: {
|
||||
...makeMinimalRuntimeCap().runtime.hostIntegration,
|
||||
dispatch: { ...makeMinimalRuntimeCap().runtime.hostIntegration.dispatch, backgroundDispatch: 'undocumented' },
|
||||
},
|
||||
});
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const bdErrors = errors.filter((e) => e.includes('backgroundDispatch'));
|
||||
assert.strictEqual(bdErrors.length, 0,
|
||||
`backgroundDispatch:"undocumented" must produce no errors; got: ${bdErrors.join(', ')}`);
|
||||
});
|
||||
|
||||
test('dispatch.backgroundDispatch:"zzz" → produces a validator error', () => {
|
||||
const cap = makeMinimalRuntimeCap({
|
||||
hostIntegration: {
|
||||
...makeMinimalRuntimeCap().runtime.hostIntegration,
|
||||
dispatch: { ...makeMinimalRuntimeCap().runtime.hostIntegration.dispatch, backgroundDispatch: 'zzz' },
|
||||
},
|
||||
});
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const bdErrors = errors.filter((e) => e.includes('backgroundDispatch'));
|
||||
assert.ok(bdErrors.length > 0,
|
||||
`bogus value "zzz" for backgroundDispatch must produce a validator error`);
|
||||
});
|
||||
|
||||
test('dispatch without backgroundDispatch key → validator error (required field — all 16 descriptors carry it)', () => {
|
||||
// backgroundDispatch is now REQUIRED (matches sibling fields namedDispatch/nested/background/subagentToolkit/maxDepth).
|
||||
const cap = makeMinimalRuntimeCap({
|
||||
hostIntegration: {
|
||||
...makeMinimalRuntimeCap().runtime.hostIntegration,
|
||||
dispatch: (() => {
|
||||
const d = { ...makeMinimalRuntimeCap().runtime.hostIntegration.dispatch };
|
||||
delete d.backgroundDispatch;
|
||||
return d;
|
||||
})(),
|
||||
},
|
||||
});
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const bdErrors = errors.filter((e) => e.includes('backgroundDispatch'));
|
||||
assert.ok(bdErrors.length > 0,
|
||||
`Missing backgroundDispatch (required field) must produce a validator error`);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -270,6 +343,7 @@ describe('Fix 3: reserved-key guard on hostIntegration and hostIntegration.dispa
|
||||
maxDepth: 5,
|
||||
background: true,
|
||||
subagentToolkit: 'full',
|
||||
backgroundDispatch: true,
|
||||
},
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
|
||||
@@ -844,6 +844,155 @@ describe('Fix: degradationFor unknown point → {level:"absent", unknown:true}',
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: shouldFlattenDispatch — ADR-1239 Phase B / #1708
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Phase B: shouldFlattenDispatch — contract pin', () => {
|
||||
const { shouldFlattenDispatch } = hi;
|
||||
|
||||
test('shouldFlattenDispatch is exported as a function', () => {
|
||||
assert.strictEqual(typeof shouldFlattenDispatch, 'function',
|
||||
'shouldFlattenDispatch must be exported from host-integration module');
|
||||
});
|
||||
|
||||
test('{background:true, backgroundDispatch:true} → false (background OK)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch({ background: true, backgroundDispatch: true }), false,
|
||||
'canBackground=true when both background===true AND backgroundDispatch===true → flatten=false');
|
||||
});
|
||||
|
||||
test('{background:true, backgroundDispatch:false} → true (must flatten)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch({ background: true, backgroundDispatch: false }), true,
|
||||
'backgroundDispatch===false → canBackground=false → flatten=true');
|
||||
});
|
||||
|
||||
test('{background:true, backgroundDispatch:"undocumented"} → true (undocumented is not === true)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch({ background: true, backgroundDispatch: 'undocumented' }), true,
|
||||
'"undocumented" is not === true → canBackground=false → flatten=true');
|
||||
});
|
||||
|
||||
test('{background:false, backgroundDispatch:true} → true (background is false)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch({ background: false, backgroundDispatch: true }), true,
|
||||
'background===false → canBackground=false → flatten=true');
|
||||
});
|
||||
|
||||
test('{} (empty) → true (missing fields → fail-closed)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch({}), true,
|
||||
'empty dispatch → canBackground=false → flatten=true');
|
||||
});
|
||||
|
||||
test('missing fields individually', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch({ background: true }), true,
|
||||
'backgroundDispatch missing → not === true → flatten=true');
|
||||
assert.strictEqual(shouldFlattenDispatch({ backgroundDispatch: true }), true,
|
||||
'background missing → not === true → flatten=true');
|
||||
});
|
||||
|
||||
// M1: null-safety — null/undefined/non-object dispatch must fail-closed (not throw)
|
||||
test('null dispatch → true (fail-closed, no throw)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch(null), true,
|
||||
'null dispatch must fail-closed to true');
|
||||
});
|
||||
|
||||
test('undefined dispatch → true (fail-closed, no throw)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch(undefined), true,
|
||||
'undefined dispatch must fail-closed to true');
|
||||
});
|
||||
|
||||
test('string dispatch → true (fail-closed, no throw)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch('x'), true,
|
||||
'non-object dispatch (string) must fail-closed to true');
|
||||
});
|
||||
|
||||
// #853 codex-like profile: full dispatch including backgroundDispatch:true → background OK
|
||||
test('#853 codex-like: {namedDispatch:true,nested:true,maxDepth:1,background:true,subagentToolkit:"full",backgroundDispatch:true} → false (background OK)', () => {
|
||||
assert.strictEqual(
|
||||
shouldFlattenDispatch({ namedDispatch: true, nested: true, maxDepth: 1, background: true, subagentToolkit: 'full', backgroundDispatch: true }),
|
||||
false,
|
||||
'codex-like dispatch with backgroundDispatch:true must be background-OK (flatten=false)',
|
||||
);
|
||||
});
|
||||
|
||||
// #853 claude-like profile: backgroundDispatch:false → must flatten
|
||||
test('#853 claude-like: {...,background:true,backgroundDispatch:false} → true (inline)', () => {
|
||||
assert.strictEqual(
|
||||
shouldFlattenDispatch({ namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: false }),
|
||||
true,
|
||||
'claude-like dispatch with backgroundDispatch:false must flatten inline',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: negotiateHostCapabilities — backgroundDispatch field
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Phase B: negotiateHostCapabilities — backgroundDispatch', () => {
|
||||
test('host dispatch.backgroundDispatch:true against DEFAULT_ENGINE → effective.dispatch.backgroundDispatch===true', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.backgroundDispatch, true,
|
||||
'backgroundDispatch:true on host AND engine must yield effective backgroundDispatch===true');
|
||||
});
|
||||
|
||||
test('host dispatch.backgroundDispatch:"undocumented" → effective.dispatch.backgroundDispatch===false + warning', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: 'undocumented' },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.backgroundDispatch, false,
|
||||
'"undocumented" must fail-closed to false');
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('backgroundDispatch') && warnText.includes('undocumented'),
|
||||
`Expected warning about backgroundDispatch being undocumented; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('host dispatch.backgroundDispatch:false → effective.dispatch.backgroundDispatch===false', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: false },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.backgroundDispatch, false);
|
||||
});
|
||||
|
||||
test('host dispatch without backgroundDispatch key → effective.dispatch.backgroundDispatch===false (fail-closed)', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.backgroundDispatch, false,
|
||||
'Missing backgroundDispatch key must fail-closed to false');
|
||||
});
|
||||
|
||||
test('negotiateHostCapabilities({}) → effective.dispatch.backgroundDispatch===false', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.strictEqual(result.effective.dispatch.backgroundDispatch, false,
|
||||
'Empty host must produce backgroundDispatch===false (SAFE_DEFAULTS)');
|
||||
});
|
||||
|
||||
test('SAFE_DEFAULTS.dispatch.backgroundDispatch is false', () => {
|
||||
// Verified via negotiation with empty host
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.strictEqual(result.effective.dispatch.backgroundDispatch, false);
|
||||
});
|
||||
|
||||
test('DEFAULT_ENGINE.axes.dispatch.backgroundDispatch is true', () => {
|
||||
assert.strictEqual(DEFAULT_ENGINE.axes.dispatch.backgroundDispatch, true,
|
||||
'DEFAULT_ENGINE (full engine) must declare backgroundDispatch:true');
|
||||
});
|
||||
|
||||
test('existing dispatch tests still pass — effective.dispatch.namedDispatch present alongside backgroundDispatch', () => {
|
||||
const result = negotiateHostCapabilities(PROFILE_BASELINES['programmatic-cli']);
|
||||
const d = result.effective.dispatch;
|
||||
assert.ok('namedDispatch' in d, 'namedDispatch must still be present');
|
||||
assert.ok('backgroundDispatch' in d, 'backgroundDispatch must be present');
|
||||
assert.ok('nested' in d && 'maxDepth' in d && 'background' in d && 'subagentToolkit' in d,
|
||||
'all original dispatch fields must still be present');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fix 2: negotiateHostCapabilities — host omitting 'dispatch' → subagentToolkit 'read-only'
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"audit-fix.md": 10988,
|
||||
"audit-milestone.md": 17637,
|
||||
"audit-uat.md": 7425,
|
||||
"autonomous.md": 42675,
|
||||
"autonomous.md": 42747,
|
||||
"check-todos.md": 9431,
|
||||
"cleanup.md": 9897,
|
||||
"code-review-fix.md": 23890,
|
||||
@@ -40,7 +40,7 @@
|
||||
"list-phase-assumptions.md": 4305,
|
||||
"list-seeds.md": 6943,
|
||||
"list-workspaces.md": 5655,
|
||||
"manager.md": 26966,
|
||||
"manager.md": 27258,
|
||||
"map-codebase.md": 20789,
|
||||
"milestone-summary.md": 11774,
|
||||
"mvp-phase.md": 13582,
|
||||
|
||||
Reference in New Issue
Block a user