* test(#3673): add failing tests for dispatch.maxConcurrency axis and dispatch-capacity CLI route Extends tests/host-integration.test.cjs with negotiateHostCapabilities maxConcurrency negotiation coverage (test matrix rows 1-15, including a fast-check property test) and a new #3673 dispatch-capacity CLI route describe block spawning the real gsd-tools.cjs (rows 16-25). Extends tests/host-integration-validator-parity.test.cjs with an all-19-descriptor maxConcurrency presence/validity sweep (row 26) and adds a hostile-input validator test to host-integration.test.cjs (row 27). The dispatch.maxConcurrency field does not exist yet, so these tests fail. * feat(#3673): add dispatch.maxConcurrency axis, negotiation, validator parity, and the dispatch-capacity query Adds a numeric dispatch.maxConcurrency sub-field to the Host-Integration Interface (ADR-1239 Phase 1), following the existing dispatch.isolation sub-field pattern: DispatchCapability interface, SAFE_DEFAULTS/PROFILE_BASELINES floors, and a negotiateHostCapabilities branch that passes through a positive safe integer and fails closed to 1 otherwise (no engine-side reduction, per the design doc's explicit rejection of a min(host,engine) rule). capability-validator.cjs gains parity validation for the new field (optional, positive safe integer or the "undocumented" sentinel — mirroring isolation's "added after existing descriptors" treatment). gsd-tools.cjs gains a new `query dispatch-capacity` route, a pure-read sibling of `dispatch-isolation` with no side effects: live env (GSD_DISPATCH_MAX_CONCURRENCY) > descriptor > fallback-to-1 precedence. All 19 capabilities/*/capability.json descriptors now declare dispatch.maxConcurrency: claude carries the one cited value (20, per code.claude.com/docs/en/sub-agents); the other 18 carry "undocumented" (not yet researched for this axis). * docs(#3673): document dispatch.maxConcurrency and add its citation row to the capability matrix Updates docs/reference/host-integration-interface.md's dispatch struct entry (also backfilling the previously-undocumented isolation/backgroundDispatch sub-fields found stale in the same table) and adds fail-closed/live-transport precedence prose for the new maxConcurrency field. Adds a dispatch.maxConcurrency row (with citation) to all 19 host sections in docs/reference/host-integration-capability-matrix.md — required for tests/host-integration-descriptors.test.cjs's kimi-code matrix-parity check, which asserts every declared dispatch sub-axis is documented there. Updates docs/how-to/add-or-update-a-host-integration.md's dispatch checklist and example descriptor block to mention maxConcurrency (and, likewise backfilling a stale gap, isolation/backgroundDispatch). * fix(#3673): extract shared maxConcurrency validator, drop dead reserved-name check Exports isPositiveSafeInteger from src/host-integration.cts as the single source of truth for the dispatch.maxConcurrency positive-safe-integer contract; negotiateHostCapabilities and gsd-tools.cjs's routeDispatchCapacity now both call it instead of independently reimplementing the same predicate. Also removes the __proto__/constructor/prototype reserved-name branch from capability-validator.cjs's maxConcurrency check — copy-pasted from the string-enum fields above it, but unreachable for a numeric field (the generic positive-safe-integer branch already rejects any string) and absent from maxDepth, the field the code's own comment claims to mirror. --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -89,7 +89,8 @@
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": "undocumented",
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -100,7 +100,8 @@
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": "undocumented",
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -85,7 +85,8 @@
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "harness-worktree"
|
||||
"isolation": "harness-worktree",
|
||||
"maxConcurrency": 20
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
"background": true,
|
||||
"subagentToolkit": "read-only",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -85,7 +85,8 @@
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": true,
|
||||
"isolation": "orchestrator-worktree"
|
||||
"isolation": "orchestrator-worktree",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -80,7 +80,8 @@
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -80,7 +80,8 @@
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": true,
|
||||
"isolation": "harness-worktree"
|
||||
"isolation": "harness-worktree",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -98,7 +98,8 @@
|
||||
"background": true,
|
||||
"subagentToolkit": "read-only",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -103,7 +103,8 @@
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -86,7 +86,8 @@
|
||||
"explore",
|
||||
"plan"
|
||||
],
|
||||
"isolation": "orchestrator-worktree"
|
||||
"isolation": "orchestrator-worktree",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": true,
|
||||
"isolation": "orchestrator-worktree"
|
||||
"isolation": "orchestrator-worktree",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -98,7 +98,8 @@
|
||||
"background": false,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "orchestrator-worktree"
|
||||
"isolation": "orchestrator-worktree",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -47,7 +47,8 @@
|
||||
"background": false,
|
||||
"backgroundDispatch": false,
|
||||
"subagentToolkit": "undocumented",
|
||||
"isolation": "none"
|
||||
"isolation": "none",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -85,7 +85,8 @@
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -79,7 +79,8 @@
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": "undocumented",
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "engine",
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": "undocumented",
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "engine",
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
"background": "undocumented",
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": "undocumented",
|
||||
"isolation": "none"
|
||||
"isolation": "none",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -95,7 +95,8 @@
|
||||
"background": false,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "none"
|
||||
"isolation": "none",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -30,7 +30,7 @@ Read the docs and map them to the closed vocabulary. Do not pick a value unless
|
||||
|---|---|
|
||||
| `embeddingMode` | An in-process programmatic plugin/extension API (`imperative`) vs. configuration files only (`declarative`). |
|
||||
| `commandSurface` | How custom commands are authored/invoked: `slash-file` (.md), `slash-toml`, `slash-programmatic`, `palette`, `prose-only`. |
|
||||
| `dispatch` | Sub-agent delegation: `namedDispatch`, `nested`, `maxDepth` (int; `-1` = documented-unbounded), `background`, `subagentToolkit` (`full`/`read-only`). |
|
||||
| `dispatch` | Sub-agent delegation: `namedDispatch`, `nested`, `maxDepth` (int; `-1` = documented-unbounded), `background`, `subagentToolkit` (`full`/`read-only`/`built-in-only`), `backgroundDispatch`, `isolation` (`harness-worktree`/`orchestrator-worktree`/`none`, #2584), `maxConcurrency` (positive integer — how many same-wave executors this host can run concurrently; no engine-side ceiling, unlike `maxDepth`; #3673). |
|
||||
| `modelMode` | A programmatic model request/provider API (`active`) vs. instruction/per-agent-field only (`passive`). |
|
||||
| `hookBus` | The host fires lifecycle events a plugin subscribes to (`host`), an extension host owns the bus (`engine`), or no bus (`none`). **Independent of `hooksSurface`** — e.g. opencode has `hooksSurface: none` but `hookBus: host`. |
|
||||
| `stateIO` | `filesystem`, `sandboxed-storage` (web IDE, no arbitrary FS), or `session-log-append`. |
|
||||
@@ -48,7 +48,7 @@ not state:
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": false, "subagentToolkit": "undocumented" },
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": false, "subagentToolkit": "undocumented", "backgroundDispatch": false, "isolation": "undocumented", "maxConcurrency": "undocumented" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
@@ -60,7 +60,7 @@ not state:
|
||||
**When to use `undocumented`:** only when you searched and the host's docs genuinely do not state the
|
||||
axis. It validates, but `negotiateHostCapabilities` **fail-closes** on it (degrades to the most
|
||||
restrictive known value) — so it is always safe and never a silent capability claim. A dispatch
|
||||
boolean or `maxDepth` may also be `"undocumented"`.
|
||||
boolean, `maxDepth`, `isolation`, or `maxConcurrency` may also be `"undocumented"`.
|
||||
|
||||
**Do not conflate the orthogonal axes:** `commandStyle` (GSD's emission style) is *not*
|
||||
`commandSurface` (the host's surface type); the `hookEvents` dialect is *not* `hookBus` (bus
|
||||
|
||||
@@ -86,6 +86,7 @@ Module entry and `.gsd/phase/feat-2871-trigger-resolution/40-design.md`.
|
||||
| dispatch.subagentToolkit | full | https://code.claude.com/docs/en/sub-agents | "If all tools remain selected, the subagent inherits all tools available to the main conversation." |
|
||||
| dispatch.backgroundDispatch | false | https://code.claude.com/docs/en/sub-agents | "Background subagents are limited to a depth of five and cannot spawn further, " |
|
||||
| dispatch.isolation | harness-worktree | https://code.claude.com/docs/en/sub-agents ; Claude Code Agent tool (`Agent(isolation="worktree")`) | The Claude Code Agent tool accepts an `isolation="worktree"` harness primitive — the host's own harness creates + binds a git worktree per executor; GSD passes the flag and calls no git itself (#2584) |
|
||||
| dispatch.maxConcurrency | 20 | https://code.claude.com/docs/en/sub-agents | The subagent documentation states a default concurrent-subagent limit of 20, configurable via the `CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS` environment variable (#3673). |
|
||||
|
||||
Sources consulted:
|
||||
- https://code.claude.com/docs/en/sub-agents
|
||||
@@ -124,6 +125,7 @@ Sources consulted:
|
||||
| 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 |
|
||||
| dispatch.isolation | orchestrator-worktree | https://learn.chatgpt.com/docs/environments/git-worktrees ; https://github.com/openai/codex/blob/main/codex-rs/utils/cli/src/shared_options.rs | "Worktrees are available only in Codex in the ChatGPT desktop app." (no native CLI worktree) + `codex exec --cd <dir>` sets an explicit working root, so GSD creates+manages the worktree and points the executor at it (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | https://developers.openai.com/codex/config-reference | `agents.max_concurrent_threads_per_session` is a documented config key, but the reference states no numeric default — "Codex chooses the default" — so there is no single value to cite (#3673). |
|
||||
|
||||
**GSD integration status — Phase D dogfood complete (#2088, ADR-1239).** Codex installs through the `declarative` embedding adapter (`createDeclarativeAdapter` → `installRuntimeArtifacts`); the hardcoded `runtime === 'codex'`/`isCodex` projection is folded into descriptor-driven `runtime.hostBehaviors`, and install/uninstall output is byte-parity-gated at the time (`tests/fixtures/golden-install-parity/codex.json`; superseded by the differential attribution check, #2724). Three capability upgrades land, each with a test driving the user-reachable surface:
|
||||
|
||||
@@ -169,6 +171,7 @@ Documentation gaps:
|
||||
| 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 | false | https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/effect/runtime-flags.ts ; https://github.com/anomalyco/opencode/issues/29638 ; https://github.com/anomalyco/opencode/issues/14195 | "Concurrent dispatch requires the opt-in `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` flag (default `false`), so it cannot be relied on. #14195: \"the session loop does `tasks.pop()` to grab a single subtask, `await`s it, then `continue`s the loop — so even 3 simultaneous Task calls run sequentially.\" Declaring `true` would overstate the capability, against the fail-closed posture negotiation is built for. (#2598 — corrects #2087)" |
|
||||
| dispatch.isolation | orchestrator-worktree | https://opencode.ai/docs/cli ; opencode.ai/docs/plugins ; opencode issues #14195/#29638/#5887 | "`opencode run --dir <path>` sets an explicit working root at the process level" — native subagent dispatch is synchronous-only, so GSD creates + manages the worktree and process-spawns the executor into it via `--dir` (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://opencode.ai/docs/plugins
|
||||
@@ -204,6 +207,7 @@ Documentation gaps:
|
||||
| dispatch.subagentToolkit | full | https://cursor.com/docs/subagents | "Subagents can utilize MCP tools, inheriting all tools available to their parent agent, including those from configured s" |
|
||||
| dispatch.backgroundDispatch | true | https://cursor.com/docs/subagents (FAQ: Can subagents launch other subagents?) and https://cursor.com/docs/sdk/typescript (Subagents > Nested subagents) | FAQ: "As of Cursor 2.5, subagents have the capability to launch child subagents, enabling the creation of a hierarchical structure for coordinated tasks. This nested launching functionality requires T |
|
||||
| dispatch.isolation | harness-worktree | https://cursor.com/docs/cli/reference/parameters ; cursor.com/docs/cli/using ; cursor.com/docs/cli/changelog | "`-w, --worktree [name]` — cursor-agent creates/binds a git worktree per agent (`~/.cursor/worktrees/…`); native parallel-agent dispatch" (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://cursor.com/docs/subagents
|
||||
@@ -241,6 +245,7 @@ Sources consulted:
|
||||
| dispatch.subagentToolkit | read-only | /cline/cline (Context7) — https://github.com/cline/cline/blob/main/docs/features/subagents.mdx | "Subagents are equipped with tools for read-only operations, including reading file contents (read_file), listing directo" |
|
||||
| dispatch.backgroundDispatch | false | https://docs.cline.bot/features/subagents (mirrored at https://github.com/cline/cline/blob/main/docs/features/subagents.mdx) | "They cannot edit files, use the browser, or spawn nested subagents" — and from the GitHub source: "subagents are restricted from editing files, using the browser, accessing MCP servers, or creating n |
|
||||
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://github.com/cline/cline/blob/main/docs/sdk/plugins.mdx
|
||||
@@ -279,6 +284,7 @@ Sources consulted:
|
||||
| dispatch.subagentToolkit | read-only | https://hermes-agent.nousresearch.com/docs/guides/delegation-patterns | "Nested delegation is opt-in; by default, leaf subagents cannot call delegate_task, clarify, memory, send_message, or exe" |
|
||||
| dispatch.backgroundDispatch | false | https://github.com/nousresearch/hermes-agent/blob/main/website/docs/user-guide/features/delegation.md (via Context7 query of /nousresearch/hermes-agent) | "Nested delegation is an opt-in feature, requiring role=\"orchestrator\" for children and an increased max_spawn_depth from its default of 1. It can also be globally disabled with orchestrator_enabled |
|
||||
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://hermes-agent.nousresearch.com/docs/user-guide/features/delegation
|
||||
@@ -317,6 +323,7 @@ Documentation gaps:
|
||||
| dispatch.subagentToolkit | full | https://antigravity.google/docs/cli/features | "Capabilities: Subagents have full access to tools such as code search, file editing, terminal commands, and web searches to complete their assigned tasks." (#2096 EoS migration — the page is JS-rendered/blank on a static fetch; confirmed via headless-browser render) |
|
||||
| dispatch.backgroundDispatch | undocumented | no authoritative doc — Multiple sources consulted: antigravity.google/docs/cli-subagents (returned blank/JS-rendered), antigravity.google/docs/agent (blank), github.com/google-antigravity/antigravity-cli README, Context7 /google-antigravity/antigravity-cli | All documentation consulted describes a two-level orchestrator→subagent architecture. Background subagents run asynchronously while the main agent continues accepting prompts. The DataCamp tutorial st |
|
||||
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://github.com/alphaperseii3000/google-antigravity-docs/blob/master/google-antigravity-docs.md
|
||||
@@ -356,6 +363,7 @@ Documentation gaps:
|
||||
| dispatch.subagentToolkit | full | https://docs.augmentcode.com/cli/subagents | "If neither [tools nor disabled_tools] is specified, the subagent has access to all tools (default behavior)." |
|
||||
| dispatch.backgroundDispatch | undocumented | no authoritative doc — https://docs.augmentcode.com/cosmos/automations | The Augment Code (Cosmos) docs describe workers as 'sub-agents launched mid-session by a manager Expert using the worker-launch command. Each worker is its own session with its own messages and permis |
|
||||
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://docs.augmentcode.com/cli/plugins
|
||||
@@ -420,6 +428,7 @@ upgrade coverage is in `tests/augment-upgrades.test.cjs`.
|
||||
| dispatch.subagentToolkit | full | https://qwenlm.github.io/qwen-code-docs/en/users/features/sub-agents/ | "When omitted, the subagent inherits all available tools from the parent session." |
|
||||
| dispatch.backgroundDispatch | false | https://qwenlm.github.io/qwen-code-docs/en/users/features/sub-agents/ (official Qwen Code documentation, 'Subagents' user guide page) and https://qwenlm.github.io/qwen-code-docs/en/design/fork-subagent/fork-subagent-design (Qwen Code fork-subagent design document, section '4. Recursive Fork Prevention') | The official user-facing Qwen Code docs state verbatim: "Fork children cannot create further forks. If a fork attempts spawning another fork, it receives an error instructing direct task execution ins |
|
||||
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://qwenlm.github.io/qwen-code-docs/en/developers/channel-plugins
|
||||
@@ -457,6 +466,7 @@ Documentation gaps:
|
||||
| dispatch.subagentToolkit | full | https://www.codebuddy.ai/docs/cli/sub-agents | "By default, sub-agents inherit all tools when the tools field is omitted ... Sub-agents can access MCP tools from config" |
|
||||
| dispatch.backgroundDispatch | false | https://www.codebuddy.ai/docs/cli/sub-agents | "This prevents infinite nesting of agents (sub-agents cannot spawn other sub-agents)" — the restriction is stated as universal in the Sub-Agents documentation page. The daemon/background docs (https:/ |
|
||||
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://www.codebuddy.ai/docs/cli/plugins
|
||||
@@ -490,6 +500,7 @@ Sources consulted:
|
||||
| dispatch.subagentToolkit | full | https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/create-custom-agents-for-cli | "By default, custom agents have access to all tools. If you restrict an agent's access, a tools specification is added" |
|
||||
| dispatch.backgroundDispatch | false | https://code.visualstudio.com/docs/copilot/agents/subagents | "By default, subagents cannot spawn further subagents. This prevents infinite recursion when agents accidentally call themselves in a loop." The setting `chat.subagents.allowInvocationsFromSubagents` |
|
||||
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://github.com/github/copilot-cli/blob/main/README.md (via Context7 /github/copilot-cli)
|
||||
@@ -526,6 +537,7 @@ Documentation gaps:
|
||||
| dispatch.subagentToolkit | undocumented | no authoritative doc — searched: https://kilo.ai/docs/customize/custom-subagents | — |
|
||||
| dispatch.backgroundDispatch | false | https://kilo.ai/docs/automate/tools/new-task | "Importantly, subagents cannot spawn further subagents; only primary agents can use the `new_task` tool." |
|
||||
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://kilo.ai/docs/automate/extending/plugins
|
||||
@@ -564,6 +576,7 @@ Documentation gaps:
|
||||
| dispatch.subagentToolkit | undocumented | no authoritative doc — searched: https://docs.devin.ai/cli/subagents.md | — |
|
||||
| dispatch.backgroundDispatch | undocumented | no authoritative doc — https://docs.devin.ai/desktop/cascade/cascade and https://docs.devin.ai/desktop/devin-local (official Windsurf/Devin docs, via docs.windsurf.com redirects) | The Windsurf/Cascade docs describe a background planning agent only in these terms: "In the background, a specialized planning agent continuously refines the long-term plan while your selected model f |
|
||||
| dispatch.isolation | none | shipped descriptor (`dispatch.backgroundDispatch: undocumented`) | no documented background/concurrent-dispatch primitive — isolation is moot; same-wave plans run inline (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://docs.devin.ai/desktop/cascade/workflows
|
||||
@@ -606,6 +619,7 @@ Documentation gaps:
|
||||
| dispatch.subagentToolkit | undocumented | no authoritative doc — searched: https://docs.trae.ai/ide/agent | — |
|
||||
| dispatch.backgroundDispatch | undocumented | no authoritative doc — https://docs.trae.ai/ide/agent; https://github.com/bytedance/trae-agent/blob/main/docs/roadmap.md | Trae's official documentation (docs.trae.ai) and the trae-agent GitHub roadmap do not document background/async agent dispatch or whether a background-spawned agent can itself spawn further sub-agents |
|
||||
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://docs.trae.ai/ide/model-context-protocol
|
||||
@@ -648,6 +662,7 @@ Documentation gaps:
|
||||
| dispatch.subagentToolkit | undocumented | no authoritative doc — searched: https://moonshotai.github.io/kimi-cli/en/customization/agents.html | — |
|
||||
| dispatch.backgroundDispatch | true (#2095 Upgrade 2; was `false`) | https://moonshotai.github.io/kimi-cli/en/customization/agents.html | "Subagents support foreground and background modes. The `run_in_background` parameter allows tasks to execute asynchronously" (same evidence as dispatch.background above — the root agent's `Agent` tool call itself takes the `run_in_background` param) |
|
||||
| dispatch.isolation | orchestrator-worktree | https://github.com/moonshotai/kimi-cli/blob/main/docs/en/faq.md ; /docs/en/customization/agents.md | "`--work-dir` flag sets an explicit working directory"; concurrent "explore" subagents documented — GSD creates + manages the worktree and points the executor at it via `--work-dir` (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://moonshotai.github.io/kimi-cli/en/customization/hooks.html
|
||||
@@ -688,6 +703,7 @@ Documentation gaps:
|
||||
| dispatch.subagentToolkit | built-in-only | https://github.com/moonshotai/kimi-code/blob/main/docs/en/customization/agents.md | The three built-ins carry deliberately different tool surfaces — coder "Shares most of the main Agent's toolset; can run shell commands, maintain todo lists, enter Plan mode, and invoke Agent Skills"; explore "Performs read-only operations only and does not modify any files"; plan "Even shell commands are not available". No single `full`/`read-only` value covers the set, so the `built-in-only` member applies (degrades closed to `read-only` when negotiated). |
|
||||
| dispatch.backgroundDispatch | true | https://github.com/moonshotai/kimi-code/blob/main/docs/en/reference/tools.md ; /docs/en/customization/agents.md | The `coder` built-in "can dispatch its own nested sub-agents" and the `Agent` tool's `run_in_background` is a call-time parameter available to it, so a background-dispatched sub-agent may itself dispatch further. `AgentSwarm` additionally fans out concurrently: "By default the tool ramps up concurrency without an upper limit (5 subagents start immediately, then 1 more every 700 ms); set `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` to a positive integer to cap how many subagents run at the same time during that ramp, or leave it unset for no cap." |
|
||||
| dispatch.isolation | orchestrator-worktree | https://github.com/moonshotai/kimi-code/blob/main/docs/en/reference/tools.md ; ADR-1239 §Codex-binding amendment | Neither `Agent` nor `AgentSwarm` exposes a working-directory/cwd parameter — sub-agents inherit the CLI's process cwd — and the CLI has no `--work-dir`-style flag (unlike `kimi`). GSD therefore creates, validates and merges the worktree itself and spawns the executor with that cwd (#2584). |
|
||||
| dispatch.maxConcurrency | undocumented | https://github.com/moonshotai/kimi-code/blob/main/docs/en/reference/tools.md | `AgentSwarm` concurrency is configurable ("set `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` to a positive integer to cap how many subagents run at the same time … or leave it unset for no cap") but the CLI states no bound as its OWN default — "By default the tool ramps up concurrency without an upper limit" — so there is no single default integer to cite, only a user-configurable env var (#3673). |
|
||||
|
||||
**Negotiation note.** Because `dispatch.namedDispatch` is `false`, `negotiateHostCapabilities` caps `nested`, `maxDepth`, `background` and `backgroundDispatch` to `false`/`0` in the **effective** axes for structural consistency (`src/host-integration.cts`). The declared values above are still the host-capability record the matrix exists to hold; they are what a future named-dispatch upgrade would negotiate against.
|
||||
|
||||
@@ -732,6 +748,7 @@ Documentation gaps:
|
||||
| dispatch.subagentToolkit | full | https://zcode.z.ai/en/docs/subagents | "**general-purpose** is the default built-in subagent ... It has access to all tools"; custom subagents default to "All permissions by default" (inherits every tool). |
|
||||
| dispatch.backgroundDispatch | false | https://zcode.z.ai/en/docs/subagents | "Background execution is not enabled yet" — background dispatch is therefore impossible. |
|
||||
| dispatch.isolation | none | https://zcode.z.ai/en/docs/subagents (shipped descriptor: `dispatch.backgroundDispatch: false`) | "Background execution is not enabled yet" — no concurrent fan-out primitive, so same-wave plans run inline/sequentially (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://zcode.z.ai/en/docs/skill
|
||||
@@ -774,6 +791,7 @@ EoS migration status (#2101, ADR-1239): ZCode's install is fully dogfooded throu
|
||||
| dispatch.subagentToolkit | undocumented | no authoritative doc — searched: https://pi.dev/docs/latest/extensions | pi has no named-dispatch primitive (see `dispatch.namedDispatch`), so there is no subagent tool-surface to classify as `full`/`read-only`. |
|
||||
| dispatch.backgroundDispatch | false | no authoritative doc — searched: https://pi.dev/docs/latest/extensions | Same gap as `dispatch.background` — no background-dispatch primitive is documented, so a background-dispatched agent spawning further named sub-agents is not possible. |
|
||||
| dispatch.isolation | none | shipped descriptor (`dispatch.background: false`, `dispatch.backgroundDispatch: false`) | pi has no named-dispatch/background-dispatch primitive documented — cannot fan out concurrently, so isolation is moot (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://pi.dev
|
||||
@@ -824,6 +842,7 @@ EoS migration status (#2102 Stage 2, ADR-1239): Stage 1's "in-process `gsd-core`
|
||||
| dispatch.subagentToolkit | undocumented | no authoritative doc found at authoring time | VS Code's subagent documentation does not state whether a subagent's tool surface is restricted to read-only tools or the full set an extension registers; recorded `undocumented` (fails closed to `read-only` in negotiation) rather than guessed. |
|
||||
| dispatch.backgroundDispatch | undocumented | no authoritative doc found at authoring time | Whether a background-dispatched subagent can itself spawn further NAMED subagents (the #853 discriminator) is not stated in the sources reviewed; recorded `undocumented` (fails closed to `false`) rather than guessed. |
|
||||
| dispatch.isolation | undocumented | not researched / no concurrent fan-out documented for this axis | no authoritative source consulted for concurrent-executor isolation on this host — fails closed to `none` (sequential) in negotiation (#2584) |
|
||||
| dispatch.maxConcurrency | undocumented | not researched / no documented concurrent-subagent capacity for this axis | no authoritative source consulted for concurrent-executor capacity on this host — fails closed to `1` in negotiation (#3673) |
|
||||
|
||||
Sources consulted:
|
||||
- https://code.visualstudio.com/api/references/vscode-api
|
||||
|
||||
@@ -27,7 +27,7 @@ value (or the `undocumented` sentinel):
|
||||
|---|---|
|
||||
| `embeddingMode` | `imperative` \| `declarative` |
|
||||
| `commandSurface` | `slash-file` \| `slash-programmatic` \| `slash-toml` \| `palette` \| `prose-only` |
|
||||
| `dispatch` | struct: `{ namedDispatch, nested, maxDepth, background, subagentToolkit, backgroundDispatch }` |
|
||||
| `dispatch` | struct: `{ namedDispatch, nested, maxDepth, background, subagentToolkit, backgroundDispatch, isolation, maxConcurrency }` |
|
||||
| `modelMode` | `active` \| `passive` |
|
||||
| `hookBus` | `host` \| `engine` \| `none` |
|
||||
| `stateIO` | `filesystem` \| `sandboxed-storage` \| `session-log-append` |
|
||||
@@ -35,6 +35,19 @@ value (or the `undocumented` sentinel):
|
||||
| `runtime` | `node` \| `bun` \| `sandboxed-web` \| `python` \| `go` \| `rust` \| `electron` \| `other` |
|
||||
| `effortSurface` | `argv` \| `none` |
|
||||
|
||||
`dispatch.isolation` (`harness-worktree` \| `orchestrator-worktree` \| `none`, added #2584)
|
||||
and `dispatch.maxConcurrency` (a positive integer, added #3673) are two dispatch
|
||||
sub-fields with their own per-field fail-closed floors — unlike the other five
|
||||
dispatch fields, they are not gated on `namedDispatch`. `dispatch.maxConcurrency`
|
||||
degrades to `1` (strictly sequential) on anything other than a positive safe
|
||||
integer (missing, the `undocumented` sentinel, zero, negative, fractional, an
|
||||
unsafe integer, or a non-numeric type); there is no negotiated host/engine
|
||||
reduction the way `maxDepth` has one — the resolved value passes through
|
||||
unchanged. Its live transport (`GSD_DISPATCH_MAX_CONCURRENCY`) is read only at
|
||||
the CLI query layer (`gsd-tools query dispatch-capacity`), never inside this
|
||||
pure negotiation module, and takes precedence over the descriptor value, which
|
||||
in turn takes precedence over the `1` fallback.
|
||||
|
||||
## Classification + negotiation
|
||||
|
||||
- `profileOf(axes)` → `'programmatic-cli'` \| `'declarative-cli'` \| `'ide'` \| `null`.
|
||||
|
||||
@@ -1886,6 +1886,112 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* #3673 (ADR-1239 Phase 1) — typed, PURE-READ query exposing the negotiated
|
||||
* `dispatch.maxConcurrency` numeric sub-field. Sibling of `dispatch-isolation`
|
||||
* above, but — per the #3673 design doc's explicit rejection of a write path
|
||||
* for this query — writes NOTHING: no sentinel file, no side effect at all.
|
||||
* A read-only capacity lookup for a later (Phase 4) scheduler to consult.
|
||||
*
|
||||
* Precedence:
|
||||
* 1. GSD_DISPATCH_MAX_CONCURRENCY, if it parses as a positive safe
|
||||
* integer → source: "live". A malformed value (non-numeric, zero,
|
||||
* negative, fractional) is treated exactly as if the variable were
|
||||
* unset — it falls through to tier 2, never errors.
|
||||
* 2. registry.runtimes[id].runtime.hostIntegration.dispatch.maxConcurrency,
|
||||
* if it is a positive safe integer → source: "descriptor". The
|
||||
* "undocumented" sentinel and any other malformed shape fall through
|
||||
* to tier 3.
|
||||
* 3. 1 (the fail-closed floor) → source: "fallback".
|
||||
*
|
||||
* `isPositiveSafeInteger`, required below from `./lib/host-integration.cjs`,
|
||||
* is the SAME exported predicate src/host-integration.cts's
|
||||
* negotiateHostCapabilities applies to the descriptor value — applied
|
||||
* identically to both the env value and the descriptor value here so the
|
||||
* two tiers can never silently disagree on what counts as valid.
|
||||
*
|
||||
* `reason` names why the WINNING tier (or the fallback) was chosen: "ok" on
|
||||
* the happy path (live or descriptor honored), else "missing" (descriptor
|
||||
* omitted the field), "undocumented" (descriptor sentinel), "non_integer"
|
||||
* (fractional or NaN), "unsafe_integer" (integer but outside
|
||||
* Number.isSafeInteger's range), "non_positive" (zero or negative),
|
||||
* "non_numeric" (string/object/array/null/boolean), or "unknown_runtime"
|
||||
* (no registry entry for the resolved runtime id — resolution itself never
|
||||
* throws; failure fails closed to the same fallback tier).
|
||||
*
|
||||
* Output:
|
||||
* --raw → prints exactly one positive integer, nothing else
|
||||
* --json → prints { runtime, capacity, declared, source, reason }
|
||||
* default → same as --raw
|
||||
*/
|
||||
function routeDispatchCapacity({ args, cwd, raw }) {
|
||||
const { UNDOCUMENTED, isPositiveSafeInteger } = require('./lib/host-integration.cjs');
|
||||
|
||||
let runtimeId = null;
|
||||
let runtimeEntry = null;
|
||||
try {
|
||||
const { resolveRuntime } = require('./lib/runtime-slash.cjs');
|
||||
runtimeId = resolveRuntime(cwd);
|
||||
const registry = require('./lib/capability-registry.cjs');
|
||||
runtimeEntry = registry.runtimes != null ? registry.runtimes[runtimeId] : null;
|
||||
} catch {
|
||||
runtimeEntry = null;
|
||||
}
|
||||
const declared = runtimeEntry?.runtime?.hostIntegration?.dispatch?.maxConcurrency ?? null;
|
||||
|
||||
let liveValue = null;
|
||||
const rawEnv = process.env.GSD_DISPATCH_MAX_CONCURRENCY;
|
||||
if (typeof rawEnv === 'string' && rawEnv.trim().length > 0) {
|
||||
const parsed = Number(rawEnv.trim());
|
||||
if (isPositiveSafeInteger(parsed)) {
|
||||
liveValue = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
let capacity;
|
||||
let source;
|
||||
let reason;
|
||||
if (liveValue !== null) {
|
||||
capacity = liveValue;
|
||||
source = 'live';
|
||||
reason = 'ok';
|
||||
} else if (runtimeEntry == null) {
|
||||
capacity = 1;
|
||||
source = 'fallback';
|
||||
reason = 'unknown_runtime';
|
||||
} else if (declared === UNDOCUMENTED) {
|
||||
capacity = 1;
|
||||
source = 'fallback';
|
||||
reason = 'undocumented';
|
||||
} else if (isPositiveSafeInteger(declared)) {
|
||||
capacity = declared;
|
||||
source = 'descriptor';
|
||||
reason = 'ok';
|
||||
} else if (declared === null || declared === undefined) {
|
||||
capacity = 1;
|
||||
source = 'fallback';
|
||||
reason = 'missing';
|
||||
} else if (typeof declared !== 'number') {
|
||||
capacity = 1;
|
||||
source = 'fallback';
|
||||
reason = 'non_numeric';
|
||||
} else if (!Number.isSafeInteger(declared)) {
|
||||
capacity = 1;
|
||||
source = 'fallback';
|
||||
reason = Number.isInteger(declared) ? 'unsafe_integer' : 'non_integer';
|
||||
} else {
|
||||
capacity = 1;
|
||||
source = 'fallback';
|
||||
reason = 'non_positive';
|
||||
}
|
||||
|
||||
if (args.indexOf('--json') !== -1) {
|
||||
output({ runtime: runtimeId, capacity, declared, source, reason }, raw);
|
||||
} else {
|
||||
process.stdout.write(String(capacity));
|
||||
}
|
||||
}
|
||||
|
||||
// #3714 follow-up — the dispatch seam gated only on PRESENCE of an explicit
|
||||
// pin, never on its VALUE, so an Anthropic-flavored global default
|
||||
// (~/.gsd/defaults.json model_overrides["gsd-executor"] = "sonnet"/"opus"/
|
||||
@@ -4079,6 +4185,7 @@ const HOST_COMMAND_ROUTERS = {
|
||||
'normalize-test-command': routeNormalizeTestCommand,
|
||||
'dispatch-should-flatten': routeDispatchShouldFlatten,
|
||||
'dispatch-isolation': routeDispatchIsolation,
|
||||
'dispatch-capacity': routeDispatchCapacity,
|
||||
'inspect-dispatch-isolation': routeInspectDispatchIsolation,
|
||||
'record-dispatch-isolation': routeRecordDispatchIsolation,
|
||||
'resolve-dispatch-type': routeResolveDispatchType,
|
||||
@@ -4339,7 +4446,7 @@ const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <fiel
|
||||
'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
|
||||
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, planning, profile-questionnaire, ' +
|
||||
'profile-sample, progress, project-instruction-file, prompt-budget, quick-tasks-append, requirements, research-plan, research-store, resolve-granularity, resolve-model, restore-custom-files, roadmap, runtime-identity, scaffold, smart-entry, state, ' +
|
||||
'config-set-model-profile, dispatch-isolation, dispatch-should-flatten, inspect-dispatch-isolation, record-dispatch-isolation, estimate-calibrate, estimate-calibration, estimate-check, resolve-agent, resolve-dispatch-type, ' +
|
||||
'config-set-model-profile, dispatch-capacity, dispatch-isolation, dispatch-should-flatten, inspect-dispatch-isolation, record-dispatch-isolation, estimate-calibrate, estimate-calibration, estimate-check, resolve-agent, resolve-dispatch-type, ' +
|
||||
'resolve-execution, review-lane, skill-manifest, skills-root, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
|
||||
'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
|
||||
'Global flags:\n' +
|
||||
|
||||
@@ -183,7 +183,8 @@ const capabilities = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": "undocumented",
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -437,7 +438,8 @@ const capabilities = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": "undocumented",
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -581,7 +583,8 @@ const capabilities = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "harness-worktree"
|
||||
"isolation": "harness-worktree",
|
||||
"maxConcurrency": 20
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -828,7 +831,8 @@ const capabilities = {
|
||||
"background": true,
|
||||
"subagentToolkit": "read-only",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -1011,7 +1015,8 @@ const capabilities = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -1162,7 +1167,8 @@ const capabilities = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": true,
|
||||
"isolation": "orchestrator-worktree"
|
||||
"isolation": "orchestrator-worktree",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -1325,7 +1331,8 @@ const capabilities = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -1424,7 +1431,8 @@ const capabilities = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": true,
|
||||
"isolation": "harness-worktree"
|
||||
"isolation": "harness-worktree",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -1906,7 +1914,8 @@ const capabilities = {
|
||||
"background": true,
|
||||
"subagentToolkit": "read-only",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -2074,7 +2083,8 @@ const capabilities = {
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -2173,7 +2183,8 @@ const capabilities = {
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": true,
|
||||
"isolation": "orchestrator-worktree"
|
||||
"isolation": "orchestrator-worktree",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -2288,7 +2299,8 @@ const capabilities = {
|
||||
"explore",
|
||||
"plan"
|
||||
],
|
||||
"isolation": "orchestrator-worktree"
|
||||
"isolation": "orchestrator-worktree",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -2933,7 +2945,8 @@ const capabilities = {
|
||||
"background": false,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "orchestrator-worktree"
|
||||
"isolation": "orchestrator-worktree",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -3123,7 +3136,8 @@ const capabilities = {
|
||||
"background": false,
|
||||
"backgroundDispatch": false,
|
||||
"subagentToolkit": "undocumented",
|
||||
"isolation": "none"
|
||||
"isolation": "none",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -3307,7 +3321,8 @@ const capabilities = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -3765,7 +3780,8 @@ const capabilities = {
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": "undocumented",
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "engine",
|
||||
@@ -3922,7 +3938,8 @@ const capabilities = {
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": "undocumented",
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "engine",
|
||||
@@ -4007,7 +4024,8 @@ const capabilities = {
|
||||
"background": "undocumented",
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": "undocumented",
|
||||
"isolation": "none"
|
||||
"isolation": "none",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -4121,7 +4139,8 @@ const capabilities = {
|
||||
"background": false,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "none"
|
||||
"isolation": "none",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -5455,7 +5474,8 @@ const runtimes = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": "undocumented",
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -5626,7 +5646,8 @@ const runtimes = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": "undocumented",
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -5724,7 +5745,8 @@ const runtimes = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "harness-worktree"
|
||||
"isolation": "harness-worktree",
|
||||
"maxConcurrency": 20
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -5883,7 +5905,8 @@ const runtimes = {
|
||||
"background": true,
|
||||
"subagentToolkit": "read-only",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -6005,7 +6028,8 @@ const runtimes = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -6106,7 +6130,8 @@ const runtimes = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": true,
|
||||
"isolation": "orchestrator-worktree"
|
||||
"isolation": "orchestrator-worktree",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -6269,7 +6294,8 @@ const runtimes = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -6368,7 +6394,8 @@ const runtimes = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": true,
|
||||
"isolation": "harness-worktree"
|
||||
"isolation": "harness-worktree",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -6546,7 +6573,8 @@ const runtimes = {
|
||||
"background": true,
|
||||
"subagentToolkit": "read-only",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -6662,7 +6690,8 @@ const runtimes = {
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -6761,7 +6790,8 @@ const runtimes = {
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": true,
|
||||
"isolation": "orchestrator-worktree"
|
||||
"isolation": "orchestrator-worktree",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -6876,7 +6906,8 @@ const runtimes = {
|
||||
"explore",
|
||||
"plan"
|
||||
],
|
||||
"isolation": "orchestrator-worktree"
|
||||
"isolation": "orchestrator-worktree",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -7052,7 +7083,8 @@ const runtimes = {
|
||||
"background": false,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "orchestrator-worktree"
|
||||
"isolation": "orchestrator-worktree",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -7188,7 +7220,8 @@ const runtimes = {
|
||||
"background": false,
|
||||
"backgroundDispatch": false,
|
||||
"subagentToolkit": "undocumented",
|
||||
"isolation": "none"
|
||||
"isolation": "none",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
@@ -7295,7 +7328,8 @@ const runtimes = {
|
||||
"background": true,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -7436,7 +7470,8 @@ const runtimes = {
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": "undocumented",
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "engine",
|
||||
@@ -7498,7 +7533,8 @@ const runtimes = {
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": "undocumented",
|
||||
"isolation": "undocumented"
|
||||
"isolation": "undocumented",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "engine",
|
||||
@@ -7583,7 +7619,8 @@ const runtimes = {
|
||||
"background": "undocumented",
|
||||
"subagentToolkit": "undocumented",
|
||||
"backgroundDispatch": "undocumented",
|
||||
"isolation": "none"
|
||||
"isolation": "none",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
@@ -7697,7 +7734,8 @@ const runtimes = {
|
||||
"background": false,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "none"
|
||||
"isolation": "none",
|
||||
"maxConcurrency": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
|
||||
@@ -1643,6 +1643,28 @@ function validateRuntimeBody(cap) {
|
||||
' (or "undocumented") (got: ' + JSON.stringify(d.isolation) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// maxConcurrency — ADR-1239 Phase 1 (#3673). A numeric dispatch
|
||||
// sub-field (not a closed-vocabulary enum member — same numeric
|
||||
// dispatch sub-field treatment as maxDepth above; unlike maxDepth,
|
||||
// 0 and negative values are invalid — 1 is the fail-closed floor, not
|
||||
// a legitimate "no concurrency" declaration).
|
||||
// OPTIONAL, like isolation: added after existing descriptors, so an
|
||||
// omitted maxConcurrency is legitimate — negotiateHostCapabilities
|
||||
// degrades it to 1 (the safe floor) and warns, exactly as for any
|
||||
// other undeclared dispatch sub-field. Only a PRESENT value is
|
||||
// checked against the positive-safe-integer contract.
|
||||
if (d.maxConcurrency === undefined) {
|
||||
// absent — nothing to validate; negotiateHostCapabilities fails it closed.
|
||||
} else if (
|
||||
d.maxConcurrency !== 'undocumented' &&
|
||||
(!Number.isSafeInteger(d.maxConcurrency) || d.maxConcurrency < 1)
|
||||
) {
|
||||
errors.push(
|
||||
'runtime.hostIntegration.dispatch.maxConcurrency must be a positive safe integer or "undocumented" ' +
|
||||
'(got: ' + JSON.stringify(d.maxConcurrency) + ')',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,13 @@ interface DispatchCapability {
|
||||
subagentToolkit: SubagentToolkit;
|
||||
backgroundDispatch: boolean;
|
||||
isolation: DispatchIsolation;
|
||||
// ADR-1239 Phase 1 (#3673): a numeric dispatch sub-field — not an enum axis
|
||||
// member (same "numeric dispatch sub-field, excluded from
|
||||
// HOST_INTEGRATION_AXES" precedent as maxDepth above). How many same-wave
|
||||
// executors this host can run concurrently. Fail-closed floor is 1
|
||||
// (strictly sequential); there is no engine-side ceiling to reduce
|
||||
// against (unlike maxDepth) — see negotiateHostCapabilities below.
|
||||
maxConcurrency: number;
|
||||
}
|
||||
|
||||
interface HostIntegrationAxes {
|
||||
@@ -127,7 +134,7 @@ interface DegradationResult {
|
||||
const SAFE_DEFAULTS: HostIntegrationAxes = {
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'prose-only',
|
||||
dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'read-only', backgroundDispatch: false, isolation: 'none' },
|
||||
dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'read-only', backgroundDispatch: false, isolation: 'none', maxConcurrency: 1 },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'none',
|
||||
stateIO: 'session-log-append',
|
||||
@@ -141,7 +148,7 @@ const PROFILE_BASELINES: Readonly<Record<'programmatic-cli' | 'declarative-cli'
|
||||
'programmatic-cli': Object.freeze({
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none' }),
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none', maxConcurrency: 1 }),
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
@@ -152,7 +159,7 @@ const PROFILE_BASELINES: Readonly<Record<'programmatic-cli' | 'declarative-cli'
|
||||
'declarative-cli': Object.freeze({
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full', backgroundDispatch: false, isolation: 'none' }),
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full', backgroundDispatch: false, isolation: 'none', maxConcurrency: 1 }),
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
@@ -163,7 +170,7 @@ const PROFILE_BASELINES: Readonly<Record<'programmatic-cli' | 'declarative-cli'
|
||||
'ide': Object.freeze({
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'palette',
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: 5, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none' }),
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: 5, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none', maxConcurrency: 1 }),
|
||||
modelMode: 'active',
|
||||
hookBus: 'engine',
|
||||
stateIO: 'sandboxed-storage',
|
||||
@@ -296,7 +303,7 @@ const DEFAULT_ENGINE: EngineCapabilities = {
|
||||
axes: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none' },
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none', maxConcurrency: 1 },
|
||||
modelMode: 'active',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
@@ -318,6 +325,21 @@ interface NegotiationResult {
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isPositiveSafeInteger — shared predicate (#3673)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* True iff `v` is a positive (>0) JS safe integer. Single source of truth for
|
||||
* the dispatch.maxConcurrency contract: used by negotiateHostCapabilities
|
||||
* below, and by gsd-core/bin/gsd-tools.cjs's routeDispatchCapacity (which
|
||||
* requires this module's compiled output rather than reimplementing the
|
||||
* predicate), so the two consumers cannot silently diverge.
|
||||
*/
|
||||
function isPositiveSafeInteger(v: unknown): boolean {
|
||||
return typeof v === 'number' && Number.isSafeInteger(v) && v > 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// negotiateHostCapabilities
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -423,6 +445,7 @@ function negotiateHostCapabilities(
|
||||
let effectiveSubagentToolkit: SubagentToolkit;
|
||||
let effectiveMaxDepth: number;
|
||||
let effectiveIsolation: DispatchIsolation;
|
||||
let effectiveMaxConcurrency: number;
|
||||
|
||||
if (hostDispatch === null) {
|
||||
// Host didn't declare dispatch at all — fail-closed to most-restrictive values
|
||||
@@ -434,6 +457,7 @@ function negotiateHostCapabilities(
|
||||
effectiveSubagentToolkit = 'read-only';
|
||||
effectiveMaxDepth = 0;
|
||||
effectiveIsolation = 'none';
|
||||
effectiveMaxConcurrency = 1;
|
||||
} else {
|
||||
// N1: observability warnings for 'undocumented' sentinel on dispatch fields
|
||||
if (hostDispatch.namedDispatch === 'undocumented') {
|
||||
@@ -501,6 +525,36 @@ function negotiateHostCapabilities(
|
||||
const minDepth = Math.min(hDepthNum, eDepthNum);
|
||||
effectiveMaxDepth = minDepth === Infinity ? -1 : minDepth;
|
||||
|
||||
// maxConcurrency (#3673, ADR-1239 Phase 1): a positive-safe-integer
|
||||
// passthrough with a fail-closed floor of 1. UNLIKE maxDepth, there is no
|
||||
// min(host, engine) reduction here — the design doc explicitly rejects an
|
||||
// engine-side ceiling for this field (no competing intrinsic worker
|
||||
// ceiling to cap against at the negotiation layer); a `--jobs N` cap is
|
||||
// applied later, at the Phase 4 scheduler.
|
||||
const hostMaxConcurrency = hostDispatch.maxConcurrency;
|
||||
if (hostMaxConcurrency === UNDOCUMENTED) {
|
||||
warnings.push(`dispatch.maxConcurrency is undocumented — degraded closed (1)`);
|
||||
effectiveMaxConcurrency = 1;
|
||||
} else if (isPositiveSafeInteger(hostMaxConcurrency)) {
|
||||
effectiveMaxConcurrency = hostMaxConcurrency as number;
|
||||
} else if (hostMaxConcurrency === undefined) {
|
||||
warnings.push(`host did not declare 'dispatch.maxConcurrency' — treating as 1`);
|
||||
effectiveMaxConcurrency = 1;
|
||||
} else if (typeof hostMaxConcurrency !== 'number') {
|
||||
warnings.push(`host dispatch.maxConcurrency is not a number — treating as 1`);
|
||||
effectiveMaxConcurrency = 1;
|
||||
} else if (!Number.isSafeInteger(hostMaxConcurrency)) {
|
||||
if (Number.isInteger(hostMaxConcurrency)) {
|
||||
warnings.push(`host dispatch.maxConcurrency is an unsafe integer — treating as 1`);
|
||||
} else {
|
||||
warnings.push(`host dispatch.maxConcurrency is not an integer — treating as 1`);
|
||||
}
|
||||
effectiveMaxConcurrency = 1;
|
||||
} else {
|
||||
warnings.push(`host dispatch.maxConcurrency is non-positive — treating as 1`);
|
||||
effectiveMaxConcurrency = 1;
|
||||
}
|
||||
|
||||
// If namedDispatch is false, cap maxDepth/nested/background/backgroundDispatch to 0/false/false/false (struct consistency)
|
||||
if (!effectiveNamedDispatch) {
|
||||
effectiveMaxDepth = 0;
|
||||
@@ -518,6 +572,7 @@ function negotiateHostCapabilities(
|
||||
subagentToolkit: effectiveSubagentToolkit,
|
||||
backgroundDispatch: effectiveBackgroundDispatch,
|
||||
isolation: effectiveIsolation,
|
||||
maxConcurrency: effectiveMaxConcurrency,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -938,6 +993,7 @@ export = {
|
||||
EXTENSION_EVENT_SURFACES,
|
||||
degradationFor,
|
||||
profileOf,
|
||||
isPositiveSafeInteger,
|
||||
negotiateHostCapabilities,
|
||||
shouldFlattenDispatch,
|
||||
resolveDispatchType,
|
||||
|
||||
@@ -430,3 +430,50 @@ describe('Fix 3: reserved-key guard on hostIntegration and hostIntegration.dispa
|
||||
'Must produce an error for "prototype" reserved key on dispatch; got: ' + errors.join(', '));
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #3673 — ADR-1239 Phase 1: all 19 shipped descriptors carry
|
||||
// dispatch.maxConcurrency (a real sourced integer or the "undocumented"
|
||||
// sentinel — never silently absent), and every one validates clean.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('#3673 dispatch.maxConcurrency — all 19 shipped descriptors', () => {
|
||||
const registry = require(path.join(__dirname, '../gsd-core/bin/lib/capability-registry.cjs'));
|
||||
|
||||
test('registry carries exactly 19 runtime descriptors', () => {
|
||||
assert.strictEqual(Object.keys(registry.runtimes).length, 19,
|
||||
`expected exactly 19 shipped runtimes; got: ${Object.keys(registry.runtimes).sort().join(', ')}`);
|
||||
});
|
||||
|
||||
test('every shipped descriptor declares dispatch.maxConcurrency as a number or "undocumented" — never absent', () => {
|
||||
for (const [id, cap] of Object.entries(registry.runtimes)) {
|
||||
const mc = cap && cap.runtime && cap.runtime.hostIntegration && cap.runtime.hostIntegration.dispatch
|
||||
&& cap.runtime.hostIntegration.dispatch.maxConcurrency;
|
||||
assert.ok(mc !== undefined,
|
||||
`${id}: runtime.hostIntegration.dispatch.maxConcurrency must be present (number or "undocumented")`);
|
||||
assert.ok(typeof mc === 'number' || mc === 'undocumented',
|
||||
`${id}: runtime.hostIntegration.dispatch.maxConcurrency must be a number or "undocumented"; got: ${JSON.stringify(mc)}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('claude declares the one real sourced value (20)', () => {
|
||||
assert.strictEqual(registry.runtimes.claude.runtime.hostIntegration.dispatch.maxConcurrency, 20);
|
||||
});
|
||||
|
||||
test('every other shipped descriptor declares "undocumented" (not researched for this axis)', () => {
|
||||
for (const [id, cap] of Object.entries(registry.runtimes)) {
|
||||
if (id === 'claude') continue;
|
||||
assert.strictEqual(cap.runtime.hostIntegration.dispatch.maxConcurrency, 'undocumented',
|
||||
`${id}: expected "undocumented" (only claude has a cited value)`);
|
||||
}
|
||||
});
|
||||
|
||||
test('validateCapability accepts all 19 shipped descriptors\' dispatch.maxConcurrency with zero errors', () => {
|
||||
for (const [id, cap] of Object.entries(registry.runtimes)) {
|
||||
const errors = validateCapability(cap, id);
|
||||
const mcErrors = errors.filter((e) => e.includes('maxConcurrency'));
|
||||
assert.strictEqual(mcErrors.length, 0,
|
||||
`${id}: shipped dispatch.maxConcurrency must validate clean; got: ${JSON.stringify(mcErrors)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1277,6 +1277,242 @@ describe('#2584 dispatch.isolation — negotiation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #3673 — ADR-1239 Phase 1: dispatch.maxConcurrency (numeric sibling of
|
||||
// dispatch.isolation). NOT an enum member of HOST_INTEGRATION_AXES (same
|
||||
// "numeric dispatch sub-field" precedent as maxDepth). No engine-side
|
||||
// reduction (design doc's explicit rejection of a min(host,engine) rule) —
|
||||
// the resolved value passes through verbatim when it is a positive safe
|
||||
// integer; every other shape degrades to the fail-closed floor of 1.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('#3673 dispatch.maxConcurrency — negotiation', () => {
|
||||
const BASE_DISPATCH = {
|
||||
namedDispatch: true, nested: false, maxDepth: 1, background: false,
|
||||
subagentToolkit: 'full', backgroundDispatch: false, isolation: 'none',
|
||||
};
|
||||
|
||||
test('descriptor maxConcurrency:8 (valid positive integer) is honored', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { ...BASE_DISPATCH, maxConcurrency: 8 },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, 8);
|
||||
});
|
||||
|
||||
test('missing dispatch.maxConcurrency degrades to 1', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { ...BASE_DISPATCH },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, 1);
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('maxConcurrency'), `Expected a warning naming maxConcurrency; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('undocumented maxConcurrency degrades to 1 with sentinel-specific warning', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { ...BASE_DISPATCH, maxConcurrency: 'undocumented' },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, 1);
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('dispatch.maxConcurrency') && warnText.includes('undocumented'),
|
||||
`Expected a sentinel-specific warning naming dispatch.maxConcurrency as undocumented; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('zero maxConcurrency degrades to 1 (non-positive)', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { ...BASE_DISPATCH, maxConcurrency: 0 },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, 1);
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('non-positive'), `Expected a non-positive warning; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('maxConcurrency of exactly 1 is honored as valid, not degraded', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { ...BASE_DISPATCH, maxConcurrency: 1 },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, 1);
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(!warnText.includes('maxConcurrency'), `A valid maxConcurrency:1 must not produce a maxConcurrency warning; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('maxConcurrency of 2 is honored', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { ...BASE_DISPATCH, maxConcurrency: 2 },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, 2);
|
||||
});
|
||||
|
||||
test('negative maxConcurrency degrades to 1', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { ...BASE_DISPATCH, maxConcurrency: -3 },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, 1);
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('non-positive'), `Expected a non-positive warning; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('fractional maxConcurrency degrades to 1 (non-integer)', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { ...BASE_DISPATCH, maxConcurrency: 4.5 },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, 1);
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('not an integer'), `Expected a non-integer warning; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('NaN maxConcurrency degrades to 1 (fails Number.isSafeInteger)', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { ...BASE_DISPATCH, maxConcurrency: NaN },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, 1);
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('not an integer'), `Expected a non-integer warning for NaN; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('unsafe-integer maxConcurrency (MAX_SAFE_INTEGER + 1) degrades to 1', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { ...BASE_DISPATCH, maxConcurrency: Number.MAX_SAFE_INTEGER + 1 },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, 1);
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('unsafe integer'), `Expected an unsafe-integer warning; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('MAX_SAFE_INTEGER maxConcurrency is honored (largest valid value)', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { ...BASE_DISPATCH, maxConcurrency: Number.MAX_SAFE_INTEGER },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, Number.MAX_SAFE_INTEGER);
|
||||
});
|
||||
|
||||
test('string-typed maxConcurrency ("8") degrades to 1 (non-numeric)', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { ...BASE_DISPATCH, maxConcurrency: '8' },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, 1);
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('not a number'), `Expected a non-numeric warning; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('null maxConcurrency degrades to 1', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { ...BASE_DISPATCH, maxConcurrency: null },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, 1);
|
||||
});
|
||||
|
||||
test('object/array-typed maxConcurrency degrades to 1', () => {
|
||||
for (const bogus of [[8], {}]) {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { ...BASE_DISPATCH, maxConcurrency: bogus },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, 1,
|
||||
`maxConcurrency=${JSON.stringify(bogus)} must degrade to 1`);
|
||||
}
|
||||
});
|
||||
|
||||
test('adding maxConcurrency does not change isolation/effortSurface/modelMode negotiation results', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
modelMode: 'active',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
effortSurface: 'argv',
|
||||
dispatch: { ...BASE_DISPATCH, isolation: 'harness-worktree', maxConcurrency: 8 },
|
||||
});
|
||||
assert.strictEqual(result.effective.dispatch.isolation, 'harness-worktree');
|
||||
assert.strictEqual(result.effective.effortSurface, 'argv');
|
||||
assert.strictEqual(result.effective.modelMode, 'active');
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, 8);
|
||||
});
|
||||
|
||||
test('host omits dispatch entirely → effective.dispatch.maxConcurrency === 1', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.strictEqual(result.effective.dispatch.maxConcurrency, 1);
|
||||
});
|
||||
|
||||
test('property: effective.dispatch.maxConcurrency equals the declared value iff it is a positive safe integer, else 1', () => {
|
||||
const declaredArb = fc.oneof(
|
||||
fc.integer(),
|
||||
fc.double(),
|
||||
fc.string(),
|
||||
fc.constant('undocumented'),
|
||||
fc.constant(null),
|
||||
fc.constant(undefined),
|
||||
fc.boolean(),
|
||||
fc.array(fc.integer()),
|
||||
fc.object(),
|
||||
);
|
||||
fc.assert(
|
||||
fc.property(declaredArb, (declared) => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { ...BASE_DISPATCH, maxConcurrency: declared },
|
||||
});
|
||||
const eff = result.effective.dispatch.maxConcurrency;
|
||||
const isValid = typeof declared === 'number' && Number.isSafeInteger(declared) && declared > 0;
|
||||
if (isValid) {
|
||||
assert.strictEqual(eff, declared, `a valid declared value (${declared}) must pass through unchanged`);
|
||||
} else {
|
||||
assert.strictEqual(eff, 1, `an invalid declared value (${JSON.stringify(declared)}) must degrade to 1`);
|
||||
}
|
||||
}),
|
||||
{ numRuns: 200, seed: 3673 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#3673 dispatch.maxConcurrency — validator', () => {
|
||||
test('a descriptor that omits dispatch.maxConcurrency entirely still validates clean (added after existing descriptors)', () => {
|
||||
const cap = shippedClaudeCapabilityWithoutIsolation();
|
||||
delete cap.runtime.hostIntegration.dispatch.maxConcurrency;
|
||||
const errors = validateCapability(cap, 'claude');
|
||||
const mcErrors = errors.filter((e) => e.includes('maxConcurrency'));
|
||||
assert.deepEqual(mcErrors, [], `omitted maxConcurrency must validate clean, got: ${JSON.stringify(mcErrors)}`);
|
||||
});
|
||||
|
||||
for (const value of [1, 2, 20, Number.MAX_SAFE_INTEGER, 'undocumented']) {
|
||||
test(`dispatch.maxConcurrency:${JSON.stringify(value)} → ZERO validator errors`, () => {
|
||||
const cap = shippedClaudeCapabilityWithoutIsolation();
|
||||
cap.runtime.hostIntegration.dispatch.maxConcurrency = value;
|
||||
const errors = validateCapability(cap, 'claude');
|
||||
const mcErrors = errors.filter((e) => e.includes('maxConcurrency'));
|
||||
assert.strictEqual(mcErrors.length, 0,
|
||||
`${JSON.stringify(value)} must produce no validator errors; got: ${JSON.stringify(mcErrors)}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Hostile: an out-of-vocabulary shape (boolean) — same treatment the
|
||||
// validator already gives a malformed isolation/maxDepth value.
|
||||
for (const bogus of [true, false, 0, -1, 4.5, '8', null, [], {}]) {
|
||||
test(`dispatch.maxConcurrency:${JSON.stringify(bogus)} is rejected`, () => {
|
||||
const cap = shippedClaudeCapabilityWithoutIsolation();
|
||||
cap.runtime.hostIntegration.dispatch.maxConcurrency = bogus;
|
||||
const errors = validateCapability(cap, 'claude');
|
||||
assert.ok(
|
||||
errors.some((e) => e.includes('maxConcurrency')),
|
||||
`${JSON.stringify(bogus)} must produce a validator error; got: ${JSON.stringify(errors)}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('every shipped runtime descriptor with a maxConcurrency value passes validateCapability', () => {
|
||||
const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
||||
for (const [id, cap] of Object.entries(registry.runtimes)) {
|
||||
const mc = cap && cap.runtime && cap.runtime.hostIntegration && cap.runtime.hostIntegration.dispatch
|
||||
&& cap.runtime.hostIntegration.dispatch.maxConcurrency;
|
||||
if (mc === undefined) continue;
|
||||
const errors = validateCapability(cap, id);
|
||||
const mcErrors = errors.filter((e) => e.includes('maxConcurrency'));
|
||||
assert.strictEqual(mcErrors.length, 0,
|
||||
`${id}: shipped dispatch.maxConcurrency:${JSON.stringify(mc)} must validate clean; got: ${JSON.stringify(mcErrors)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('#2584 dispatch.isolation — validator', () => {
|
||||
test('_HOST_INTEGRATION_VOCAB.isolation matches HOST_INTEGRATION_AXES.isolation (parity guard)', () => {
|
||||
assert.deepEqual(
|
||||
@@ -2759,6 +2995,96 @@ describe('#2627 dispatch-isolation CLI route', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#3673 dispatch-capacity CLI route', () => {
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
const GSD_TOOLS = path.join(REPO_ROOT, 'gsd-core', 'bin', 'gsd-tools.cjs');
|
||||
|
||||
// `maxConcurrencyEnv === undefined` means "unset" — GSD_DISPATCH_MAX_CONCURRENCY
|
||||
// is deliberately deleted from the child's env rather than passed as the
|
||||
// literal string "undefined", so the "env absent" test cases are genuinely
|
||||
// absent, not present-with-a-bogus-value.
|
||||
function query(runtimeId, extraArgs = [], maxConcurrencyEnv) {
|
||||
const env = { ...process.env, GSD_RUNTIME: runtimeId };
|
||||
delete env.GSD_DISPATCH_MAX_CONCURRENCY;
|
||||
if (maxConcurrencyEnv !== undefined) {
|
||||
env.GSD_DISPATCH_MAX_CONCURRENCY = maxConcurrencyEnv;
|
||||
}
|
||||
const r = runNode(
|
||||
[GSD_TOOLS, 'query', 'dispatch-capacity', ...extraArgs],
|
||||
{ cwd: REPO_ROOT, env, timeoutMs: PROBE_TIMEOUT_MS },
|
||||
);
|
||||
throwIfFailed(r, `gsd-tools query dispatch-capacity ${extraArgs.join(' ')}`);
|
||||
return r.stdout;
|
||||
}
|
||||
const queryJson = (runtimeId, extraArgs = [], maxConcurrencyEnv) =>
|
||||
JSON.parse(query(runtimeId, ['--json', ...extraArgs], maxConcurrencyEnv));
|
||||
|
||||
test('live env wins over descriptor', () => {
|
||||
// claude's shipped descriptor declares maxConcurrency:20.
|
||||
assert.equal(query('claude', [], '4').trim(), '4');
|
||||
assert.equal(queryJson('claude', [], '4').source, 'live');
|
||||
});
|
||||
|
||||
test('descriptor used when live is absent', () => {
|
||||
const result = queryJson('claude');
|
||||
assert.equal(result.capacity, 20);
|
||||
assert.equal(result.source, 'descriptor');
|
||||
});
|
||||
|
||||
test('fallback to 1 when both live and a real descriptor value are absent', () => {
|
||||
// codex's shipped descriptor carries the "undocumented" sentinel.
|
||||
const result = queryJson('codex');
|
||||
assert.equal(result.capacity, 1);
|
||||
assert.equal(result.source, 'fallback');
|
||||
});
|
||||
|
||||
test('malformed live env falls through to descriptor tier', () => {
|
||||
assert.equal(query('claude', [], 'abc').trim(), '20');
|
||||
assert.equal(queryJson('claude', [], 'abc').source, 'descriptor');
|
||||
});
|
||||
|
||||
test('invalid live env values (0, negative, fractional) fall through to descriptor tier', () => {
|
||||
for (const bogus of ['0', '-1', '4.5']) {
|
||||
assert.equal(query('claude', [], bogus).trim(), '20', `env=${bogus} must fall through to descriptor`);
|
||||
}
|
||||
});
|
||||
|
||||
test('JSON output has the documented shape', () => {
|
||||
const result = queryJson('claude');
|
||||
assert.deepEqual(Object.keys(result).sort(), ['capacity', 'declared', 'reason', 'runtime', 'source'].sort());
|
||||
assert.equal(result.runtime, 'claude');
|
||||
assert.equal(result.capacity, 20);
|
||||
assert.equal(result.declared, 20);
|
||||
assert.equal(result.source, 'descriptor');
|
||||
assert.equal(result.reason, 'ok');
|
||||
});
|
||||
|
||||
test('unknown runtime degrades to 1 without erroring', () => {
|
||||
const result = queryJson('no-such-runtime-xyz');
|
||||
assert.equal(result.capacity, 1);
|
||||
assert.equal(result.source, 'fallback');
|
||||
assert.equal(result.reason, 'unknown_runtime');
|
||||
});
|
||||
|
||||
test('default output mode matches --raw', () => {
|
||||
assert.equal(query('claude').trim(), query('claude', ['--raw']).trim());
|
||||
});
|
||||
|
||||
test('claude descriptor value (20) is honored', () => {
|
||||
assert.equal(query('claude').trim(), '20');
|
||||
});
|
||||
|
||||
test('undocumented descriptor value degrades to fallback', () => {
|
||||
// codex declares dispatch.maxConcurrency:"undocumented".
|
||||
const result = queryJson('codex');
|
||||
assert.equal(result.capacity, 1);
|
||||
assert.equal(result.source, 'fallback');
|
||||
assert.equal(result.reason, 'undocumented');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #2652 — dispatch-site parity: isolation is decided by the negotiated
|
||||
// dispatch.isolation capability, never by a runtime id.
|
||||
|
||||
Reference in New Issue
Block a user