* feat(#1684): add negotiated host-integration interface module ADR-1239 Phase A: a pure, additive, no-I/O module exposing PROTOCOL_VERSION, the 8-axis HOST_INTEGRATION_AXES closed vocabulary, the UNDOCUMENTED fail-closed sentinel, negotiateHostCapabilities (effective subset of host-declared and engine-known), a typed degradation ladder, and host-capability profiles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#1684): validate and document host-integration axes (16 runtimes) Extend validateRuntimeBody to validate the 8 hostIntegration axes (closed enums + undocumented sentinel + dispatch struct + reserved-key guards) and the widened runtime vocabulary; author a documentation-sourced hostIntegration block in all 16 runtime descriptors; regenerate the registry. Every per-CLI value is documented (cited) or the explicit undocumented sentinel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1684): add host-integration capability matrix and adr amendment New per-CLI, per-axis citation reference (value/source/evidence for all 16 CLIs); ADR-1239 Phase-A-implemented amendment; CONTEXT.md glossary seam entry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1684): harden dispatch negotiation edge cases Code-review hardening: treat NaN/Infinity maxDepth as missing (fail-closed, +warning); reset nested/background when namedDispatch collapses to false (struct consistency); SAFE_DEFAULTS dispatch floor to read-only; warn on non-finite protocolVersion; symmetric undocumented warnings for dispatch fields. Pure module — no consumers; behaviour fail-closed throughout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1684): register host-integration.cjs in lint-ignore and inventory New tsc-generated bin/lib artifact: add to the eslint ignore list (ADR-457 — lint the .cts source), regenerate docs/INVENTORY-MANIFEST.json, and add the docs/INVENTORY.md CLI-modules row. Fixes the 3 gsd-test failures (551-eslint-bin-lib-coverage x2 + inventory-manifest-sync). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1684): add changeset fragment for host-integration interface Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1684): add how-to for sourcing a host's integration axes Diataxis how-to guide for adding/updating a host's runtime.hostIntegration axes from authoritative docs, the undocumented-sentinel rule, validation, and extending the closed vocabulary. Completes the Step-5 doc quadrants (reference + explanation + how-to). Indexed in docs/README.md. 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/vivid-seals-purr.md
Normal file
5
.changeset/vivid-seals-purr.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Added
|
||||
pr: 1690
|
||||
---
|
||||
**Host-Integration Interface (ADR-1239 Phase A)** — a versioned, negotiated capability contract (`runtime.hostIntegration`) over the six host-integration points (command, dispatch, model, hooks, state, artifact). Adds an in-process `negotiateHostCapabilities` handshake that fail-closes on undeclared/unknown/`undocumented` values (`effective ⊆ host-declared ∩ engine-known`), a typed degradation ladder, host-capability profiles, and a documentation-sourced per-CLI capability matrix for all 16 runtimes. Interface-definition only — no change to install behaviour.
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -67,6 +67,7 @@ build/
|
||||
# by `npm run build:lib`). Source of truth is src/; these are emitted, never edited.
|
||||
# Published via prepublishOnly; built before test via pretest. Grows as modules migrate.
|
||||
/tsconfig.build.tsbuildinfo
|
||||
/gsd-core/bin/lib/host-integration.cjs
|
||||
/gsd-core/bin/lib/capability-loader.cjs
|
||||
/gsd-core/bin/lib/capability-source.cjs
|
||||
/gsd-core/bin/lib/capability-ledger.cjs
|
||||
|
||||
@@ -118,6 +118,9 @@ Module owning bounded, never-throw git repository introspection — the single s
|
||||
### Runtime Name Policy Module
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
|
||||
@@ -55,6 +55,16 @@
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": true,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": "undocumented", "nested": "undocumented", "maxDepth": "undocumented", "background": true, "subagentToolkit": "undocumented" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "go"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,16 @@
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": true,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": "undocumented", "maxDepth": "undocumented", "background": true, "subagentToolkit": "full" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,16 @@
|
||||
"Stop",
|
||||
"PreCompact",
|
||||
"FileChanged"
|
||||
]
|
||||
],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": true, "maxDepth": 5, "background": true, "subagentToolkit": "full" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,16 @@
|
||||
"installSurface": "cline-rules",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "read-only" },
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,16 @@
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": true,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "full" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,16 @@
|
||||
"installSurface": "codex-toml",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": true, "maxDepth": 1, "background": true, "subagentToolkit": "full" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,16 @@
|
||||
"installSurface": "copilot-instructions",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "full" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "undocumented"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,16 @@
|
||||
"installSurface": "cursor-hooks-json",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": true, "maxDepth": 2, "background": true, "subagentToolkit": "full" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,16 @@
|
||||
"BeforeAgent",
|
||||
"AfterAgent",
|
||||
"BeforeModel"
|
||||
]
|
||||
],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-toml",
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": "undocumented", "subagentToolkit": "undocumented" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,16 @@
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": true,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-programmatic",
|
||||
"dispatch": { "namedDispatch": false, "nested": true, "maxDepth": 1, "background": true, "subagentToolkit": "read-only" },
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "python"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,16 @@
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": "kilo",
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": true, "maxDepth": -1, "background": true, "subagentToolkit": "undocumented" },
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "bun"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,16 @@
|
||||
"installSurface": "profile-marker-only",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "undocumented" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "python"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,16 @@
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": true,
|
||||
"permissionWriter": "opencode",
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": "undocumented", "maxDepth": "undocumented", "background": false, "subagentToolkit": "full" },
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "bun"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,16 @@
|
||||
"SubagentStop",
|
||||
"Stop",
|
||||
"PreCompact"
|
||||
]
|
||||
],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "full" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,16 @@
|
||||
"installSurface": "profile-marker-only",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": "undocumented", "maxDepth": "undocumented", "background": true, "subagentToolkit": "undocumented" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "engine",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,16 @@
|
||||
"installSurface": "profile-marker-only",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": "undocumented", "nested": "undocumented", "maxDepth": "undocumented", "background": "undocumented", "subagentToolkit": "undocumented" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "undocumented"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,6 +329,7 @@
|
||||
"graphify-command-router.cjs",
|
||||
"graphify.cjs",
|
||||
"gsd2-import.cjs",
|
||||
"host-integration.cjs",
|
||||
"init-command-router.cjs",
|
||||
"init.cjs",
|
||||
"install-profiles.cjs",
|
||||
|
||||
@@ -439,6 +439,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
||||
| `graphify.cjs` | Knowledge-graph build/query/status/diff for `/gsd-graphify` |
|
||||
| `graphify-command-router.cjs` | ADR-959 capability command router for `gsd-tools graphify` — dispatches build/query/status/diff subcommands; first real capability command cutover (phase 4d-impl-2) |
|
||||
| `gsd2-import.cjs` | External-plan ingest for `/gsd-import --from-gsd2` |
|
||||
| `host-integration.cjs` | Host-Integration Interface (ADR-1239 Phase A) — negotiated capability contract over the six host-integration points; `negotiateHostCapabilities` fail-closes on undeclared/unknown/`undocumented` values, typed degradation ladder, host-capability profiles; the 8 `runtime.hostIntegration` axes are validated in `capability-validator.cjs` and sourced per-CLI in `docs/reference/host-integration-capability-matrix.md` |
|
||||
| `init-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools init` |
|
||||
| `init.cjs` | Compound context loading for each workflow type |
|
||||
| `install-profiles.cjs` | Install profile allowlist + skill staging for `--minimal` install (#2762); single source of truth for which `gsd-*` skills/agents land in runtime config dirs |
|
||||
|
||||
@@ -36,6 +36,7 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md)
|
||||
- [Spike and sketch](how-to/spike-and-sketch.md) — use `/gsd-spike` and `/gsd-sketch` for exploratory work before committing to a plan
|
||||
- [Design a UI phase](how-to/design-a-ui-phase.md) — use the UI phase loop for frontend and visual work
|
||||
- [Develop a Capability for GSD 1.5+](how-to/develop-a-capability.md) — add feature Capabilities, hook fragments, and registry entries
|
||||
- [Add or update a host's integration](how-to/add-or-update-a-host-integration.md) — set a host's documentation-sourced `runtime.hostIntegration` axes (ADR-1239 Phase A), with the `undocumented` sentinel rule
|
||||
- [Turn a capability off (and keep it off)](how-to/turn-a-capability-off.md) — disable a capability via the surface, or gate individual hooks off without removing the capability
|
||||
- [Drive GSD from a tracker issue](how-to/drive-gsd-from-a-tracker-issue.md) — start a phase from a GitHub, Linear, or Jira issue
|
||||
- [Migrate from GSD 2](how-to/migrate-from-gsd-2.md) — upgrade an existing GSD 2 project to GSD Core
|
||||
|
||||
@@ -85,6 +85,20 @@ The **primitive vocabulary stays closed and first-party** (ADR-857 Decision 8):
|
||||
|
||||
Each phase is its own `approved-*` issue + PR with equivalence/parity proof.
|
||||
|
||||
### Amendment — Phase A implemented (#1684, v1.7.0)
|
||||
|
||||
Phase A is **implemented** (the ADR itself remains `Proposed` overall until Phases B–E land). The negotiated capability schema is materialized as a pure, additive, no-I/O module — the **Host-Integration Interface** (`src/host-integration.cts` → `gsd-core/bin/lib/host-integration.cjs`):
|
||||
|
||||
- **The eight negotiated axes** are carried under `capability.json` `runtime.hostIntegration` (extending, not replacing, the ADR-1016 axes), validated by `validateRuntimeBody` (`capability-validator.cjs`) across all 16 runtime descriptors, with the closed vocabulary kept in lock-step by a parity guard.
|
||||
- **`PROTOCOL_VERSION`** is an integer starting at `1`, **distinct** from the package `version` / `engines.gsd` semver (the `version`/`protocolVersion` overlap, resolved).
|
||||
- **`negotiateHostCapabilities(host, engine?)`** performs the in-process `initialize` exchange and enforces the trust-boundary invariant `effective ⊆ host-declared ∩ engine-known`: an undeclared axis or an unknown / higher-`protocolVersion` value is **never** trusted — it degrades to the most-restrictive known value (fail-closed), never throws.
|
||||
- **`degradationFor`** is the typed Full/Degraded/Absent ladder table; **`profileOf` + `PROFILE_BASELINES`** classify each descriptor into `programmatic-cli` (9 hosts: claude, opencode, cursor, cline, hermes, qwen, kilo, trae, kimi), `declarative-cli` (7 hosts: codex, gemini, antigravity, augment, codebuddy, copilot, windsurf), or `ide` (defined as a baseline; no installed host yet — VS Code lands in Phase D).
|
||||
- **Overlap resolutions (explicit):** `commandStyle` (GSD emission style, retained) ⊥ `commandSurface` (host surface type); `hookEvents` dialect ⊥ `hookBus` ownership (a host with `hooksSurface:none` may still be `hookBus:host` — e.g. opencode); the `opencode-subset` `hookEvents` value remains reserved for the Phase D OpenCode hook-dialect consumer; `runtimeCompat` (feature→host) stays an independent override, orthogonal to these runtime→engine axes.
|
||||
|
||||
**Every per-host axis value is documentation-sourced, with citations.** Each of the 8 axes for all 16 installed CLIs was determined from that CLI's authoritative documentation (Context7 + the official dev docs/source), never inferred. The full per-CLI, per-axis matrix — value, source, and an evidence quote — is recorded in [`docs/reference/host-integration-capability-matrix.md`](reference/host-integration-capability-matrix.md), the deployment source-of-truth that Phases B–E build on. Where a CLI's docs genuinely do not state an axis, the descriptor carries the explicit `undocumented` sentinel (which `negotiateHostCapabilities` fail-closes on) rather than a guessed value — 22 such markers exist today, each with its search trail in the matrix. Two findings corrected this ADR's original appendix matrix: (1) current OpenAI **Codex** docs document slash-commands, so its `commandSurface` is `slash-file`, not `prose-only`; (2) several hosts run non-Node runtimes (opencode & kilo on **bun**; hermes & kimi on **python**; antigravity on **go**), so the `runtime` axis vocabulary was widened to `node|bun|sandboxed-web|python|go|rust|electron|other`. The documented `embeddingMode` split (9 imperative / 7 declarative, above) likewise reflects each CLI's real plugin/extension API, not a profile assumption.
|
||||
|
||||
No consumer wires the negotiated result yet — Phase A is interface-definition only; the engine↔host boundary (Phase B) and the adapters (Phase C) are where it is consumed.
|
||||
|
||||
## Host-capability profiles (negotiation baselines)
|
||||
|
||||
- **Programmatic-CLI** (Claude Code, pi, OpenCode): imperative; full dispatch; host hook bus; MCP; `slash` surface. The richest target — minimal degradation.
|
||||
|
||||
111
docs/how-to/add-or-update-a-host-integration.md
Normal file
111
docs/how-to/add-or-update-a-host-integration.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# How to add or update a host's integration capabilities
|
||||
|
||||
This guide is for GSD maintainers adding a new host CLI, or updating an existing host's
|
||||
host-integration axes (ADR-1239 Phase A). It covers the **documentation-sourcing rule**, the
|
||||
eight `runtime.hostIntegration` axes, the `undocumented` sentinel, and how to validate.
|
||||
|
||||
The governing rule for this whole process: **every axis value must come from the host's own
|
||||
authoritative documentation. Never infer, guess, or assume.** Where the docs do not state an axis,
|
||||
record the explicit `undocumented` sentinel — not a plausible default. The reference matrix
|
||||
(`docs/reference/host-integration-capability-matrix.md`) is the source of truth, and every value in
|
||||
it carries a citation and an evidence quote.
|
||||
|
||||
---
|
||||
|
||||
## 1. Find the host's authoritative documentation
|
||||
|
||||
In order of preference:
|
||||
|
||||
1. **Context7** — `resolve-library-id` for the host, then `query-docs` for "plugins / subagents / hooks / commands / MCP / model API".
|
||||
2. **Official dev docs / source repo** — the host's documentation site or GitHub repo (plugin API, agents, hooks, MCP, command authoring).
|
||||
|
||||
Capture the exact source (Context7 library id + query, or the doc URL) and a short verbatim quote
|
||||
for each value you determine. You will paste these into the matrix in step 4.
|
||||
|
||||
## 2. Determine each of the eight axes from the docs
|
||||
|
||||
Read the docs and map them to the closed vocabulary. Do not pick a value unless a source states it.
|
||||
|
||||
| Axis | What to look for in the docs |
|
||||
|---|---|
|
||||
| `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`). |
|
||||
| `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`. |
|
||||
| `transport` | `mcp` (native MCP support) vs. `native-extension` (MCP needs a community extension). |
|
||||
| `runtime` | The plugin/extension runtime: `node`, `bun`, `sandboxed-web`, `python`, `go`, `rust`, `electron`, `other`. |
|
||||
|
||||
## 3. Write the `runtime.hostIntegration` block
|
||||
|
||||
In `capabilities/<id>/capability.json`, inside the `runtime` object, add (or edit) the block. Use a
|
||||
documented closed-vocabulary value, or the literal string `"undocumented"` for any axis the docs do
|
||||
not state:
|
||||
|
||||
```json
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": false, "subagentToolkit": "undocumented" },
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
```
|
||||
|
||||
**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"`.
|
||||
|
||||
**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
|
||||
ownership); `runtimeCompat` (which features run on a host) is independent of these runtime→engine
|
||||
axes.
|
||||
|
||||
## 4. Record the citations in the reference matrix
|
||||
|
||||
Add (or update) the host's section in `docs/reference/host-integration-capability-matrix.md` with a
|
||||
row per axis: `Axis | Value | Source | Evidence`. For an `undocumented` value, put the search trail
|
||||
in the Source column. This file is the deployment source of truth — a value without a citation here
|
||||
is not allowed.
|
||||
|
||||
## 5. Validate
|
||||
|
||||
```bash
|
||||
npm run build:lib
|
||||
npm run gen:capability-registry # validateRuntimeBody runs on every descriptor
|
||||
```
|
||||
|
||||
`gen:capability-registry` must succeed with zero errors. The validator
|
||||
(`gsd-core/bin/lib/capability-validator.cjs`) rejects out-of-vocabulary values, malformed dispatch
|
||||
structs, and reserved keys (`__proto__`/`constructor`/`prototype`).
|
||||
|
||||
Then run the host-integration tests and the full cross-platform suite:
|
||||
|
||||
```bash
|
||||
node --test tests/host-integration-descriptors.test.cjs # asserts every descriptor validates + profiles
|
||||
gsd-test-both # Mac + Linux Docker (run before any PR)
|
||||
```
|
||||
|
||||
## 6. If you need a vocabulary value that does not exist yet
|
||||
|
||||
The vocabulary is intentionally **closed** (ADR-857 Decision 8): a genuinely new host shape requires
|
||||
a first-party primitive, reviewed. To add one (e.g. a new `runtime` kind):
|
||||
|
||||
1. Add the value to the relevant axis in `HOST_INTEGRATION_AXES` in `src/host-integration.cts`.
|
||||
2. Add the same value to the matching `VALID_*` set in `capability-validator.cjs`.
|
||||
|
||||
The parity guard (`tests/host-integration-validator-parity.test.cjs`) fails if these two drift, so
|
||||
they must be updated together. Document the new value's meaning in the matrix legend.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- Reference: [`docs/reference/host-integration-capability-matrix.md`](../reference/host-integration-capability-matrix.md) — the per-CLI sourced values.
|
||||
- ADR: [`docs/adr/1239-gsd-embeddable-orchestration-engine.md`](../adr/1239-gsd-embeddable-orchestration-engine.md) — why the interface exists and the Phase A amendment.
|
||||
- The closed-vocabulary runtime descriptor it extends: [ADR-1016](../adr/1016-runtime-capability-descriptor.md).
|
||||
565
docs/reference/host-integration-capability-matrix.md
Normal file
565
docs/reference/host-integration-capability-matrix.md
Normal file
@@ -0,0 +1,565 @@
|
||||
# Host Integration Capability Matrix
|
||||
|
||||
This document is the maintainer-facing source of truth for the `hostIntegration` block in every
|
||||
`capabilities/<cli>/capability.json` runtime descriptor. Every per-CLI axis value is either:
|
||||
|
||||
- **documented** — backed by a cited authoritative source and evidence quote, or
|
||||
- **`undocumented`** — the explicit fail-closed sentinel used when the CLI's public documentation
|
||||
does not state a value for that axis. `undocumented` validates in the registry but never
|
||||
propagates into effective axes: negotiation degrades closed to the safe default.
|
||||
|
||||
Values are generated from per-CLI documentation research (Context7 + official docs). They are
|
||||
consumed verbatim by `gen:capability-registry` and validated by `capability-validator.cjs`.
|
||||
|
||||
---
|
||||
|
||||
## Axes legend
|
||||
|
||||
| Axis | Meaning |
|
||||
|---|---|
|
||||
| `embeddingMode` | Whether the CLI exposes an in-process programmatic API (`imperative`) or integrates purely through configuration files (`declarative`). |
|
||||
| `commandSurface` | How slash commands are registered: `slash-file` (markdown), `slash-toml` (TOML), `slash-programmatic` (code API), `palette`, `prose-only`. |
|
||||
| `modelMode` | Whether extensions can programmatically request or supply a model (`active`) or select only by config (`passive`). |
|
||||
| `hookBus` | Who owns the hook lifecycle: `host` (the CLI fires hooks), `engine` (VS Code/Electron extension host), `none`. |
|
||||
| `stateIO` | Filesystem access model: `filesystem` (full local FS), `sandboxed-storage`, `session-log-append`. |
|
||||
| `transport` | Integration transport: `mcp` (Model Context Protocol), `native-extension`. |
|
||||
| `runtime` | Plugin/extension execution runtime: `node`, `bun`, `python`, `go`, `rust`, `electron`, `sandboxed-web`, `other`. |
|
||||
|
||||
### dispatch sub-axes
|
||||
|
||||
| Sub-axis | Meaning |
|
||||
|---|---|
|
||||
| `namedDispatch` | Whether agents can be invoked by name (true/false/`undocumented`). |
|
||||
| `nested` | Whether subagents can themselves spawn subagents (true/false/`undocumented`). |
|
||||
| `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`. |
|
||||
|
||||
### Interface points
|
||||
|
||||
| Point | Meaning |
|
||||
|---|---|
|
||||
| `command` | Slash-command routing and invocation capability. |
|
||||
| `dispatch` | Subagent/multi-agent dispatch capability. |
|
||||
| `model` | Programmatic model selection capability. |
|
||||
| `hooks` | Lifecycle hook registration capability. |
|
||||
| `state` | Filesystem/state I/O capability. |
|
||||
| `artifact` | Artifact delivery (skills, commands) surface capability. |
|
||||
|
||||
---
|
||||
|
||||
## claude
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | imperative | https://code.claude.com/docs/en/agent-sdk/overview | "The Agent SDK offers hooks to execute custom code at critical points within the agent's lifecycle. These callback functions enable developer" |
|
||||
| commandSurface | slash-file | https://code.claude.com/docs/en/agent-sdk/slash-commands | "Each custom command is a markdown file where the filename (without the `.md` extension) becomes the command name. The file content defines w" |
|
||||
| modelMode | passive | https://code.claude.com/docs/en/agent-sdk/typescript | "setModel(model?: string): Changes the model (only available in streaming input mode) ... model overrides the default model for this subagent" |
|
||||
| hookBus | host | https://code.claude.com/docs/en/agent-sdk/python | "HookEvent = Literal['PreToolUse', 'PostToolUse', 'PostToolUseFailure', 'UserPromptSubmit', 'Stop', 'SubagentStop', 'PreCompact', 'Notificati" |
|
||||
| stateIO | filesystem | https://code.claude.com/docs/en/sandboxing | "The sandboxed Bash tool restricts file system access, granting read and write access to the current working directory and session temp direc" |
|
||||
| transport | mcp | https://code.claude.com/docs/en/mcp | "Project-Scoped MCP Server Configuration in .mcp.json ... This JSON structure illustrates the format for a project-scoped MCP server configur" |
|
||||
| runtime | node | https://code.claude.com/docs/en/agent-sdk/typescript | "import { query } from \"@anthropic-ai/claude-agent-sdk\"; ... pathToClaudeCodeExecutable (string) - Specifies the path to the Claude Code CLI" |
|
||||
| dispatch.namedDispatch | true | https://code.claude.com/docs/en/agent-sdk/subagents | "agents: { 'code-reviewer': AgentDefinition({ description: 'Expert code reviewer.', ... }) } ... subagent_type: block.inp" |
|
||||
| dispatch.nested | true | https://code.claude.com/docs/en/sub-agents | "As of Claude Code v2.1.172, a subagent can spawn its own subagents, allowing delegated tasks to split into parallel subt" |
|
||||
| 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." |
|
||||
|
||||
Sources consulted:
|
||||
- https://code.claude.com/docs/en/sub-agents
|
||||
- https://code.claude.com/docs/en/agent-sdk/slash-commands
|
||||
- https://code.claude.com/docs/en/agent-sdk/subagents
|
||||
- https://code.claude.com/docs/en/agent-sdk/python
|
||||
- https://code.claude.com/docs/en/agent-sdk/typescript
|
||||
- https://code.claude.com/docs/en/agent-sdk/overview
|
||||
- https://code.claude.com/docs/en/mcp
|
||||
- https://code.claude.com/docs/en/sandboxing
|
||||
- Context7 /websites/code_claude
|
||||
- Context7 /llmstxt/code_claude_llms_txt
|
||||
|
||||
---
|
||||
|
||||
## codex
|
||||
|
||||
> **Note:** ADR-1239's host matrix lists Codex as `prose-only`; current OpenAI Codex dev docs document slash-commands, so `commandSurface` is `slash-file` here (docs are the source of truth).
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | declarative | https://developers.openai.com/codex/plugins/build | "No in-process programmatic API exists. Plugins integrate through: External command execution (hooks), MCP server processes, Configuration fi" |
|
||||
| commandSurface | slash-file | https://github.com/openai/codex/blob/main/codex-rs/core-skills/src/loader.rs | "const SKILLS_FILENAME: &str = \"SKILL.md\"; ... Each skill is a folder with a SKILL.md file containing YAML frontmatter with name and descript" |
|
||||
| modelMode | passive | https://github.com/openai/codex/blob/main/codex-rs/config/src/config_toml.rs | "pub model_provider: Option<String> ... model is selected by config field; no programmatic model request API" |
|
||||
| hookBus | host | https://github.com/openai/codex/blob/main/codex/codex-rs/hooks/src/lib.rs | "pub const HOOK_EVENT_NAMES: [&str; 10] = [\"PreToolUse\", \"PermissionRequest\", \"PostToolUse\", \"PreCompact\", \"PostCompact\", \"SessionStart\", \"Us" |
|
||||
| stateIO | filesystem | https://developers.openai.com/codex/concepts/sandboxing | "workspace-write: The default mode allowing Codex to read files, edit within the workspace, and run routine local commands inside that bounda" |
|
||||
| transport | mcp | https://github.com/openai/codex/blob/main/codex-rs/config/src/config_toml.rs | "pub mcp_servers: HashMap<String, McpServerConfig> ... Definition for MCP servers that Codex can reach out to for tool calls." |
|
||||
| runtime | node | https://github.com/openai/codex/blob/main/codex-cli/package.json | "\"engines\": {\"node\": \">=16\"} ... The npm-distributed CLI wrapper is a Node.js script (#!/usr/bin/env node)" |
|
||||
| dispatch.namedDispatch | true | https://github.com/openai/codex/blob/main/codex-rs/core/src/tools/handlers/multi_agents_spec.rs | "\"agent_type\".to_string(), JsonSchema::string(Some(agent_type_description.to_string())) ... apply_role_to_config(&mut con" |
|
||||
| dispatch.nested | true | https://developers.openai.com/codex/multi-agent | "agents.max_depth defaults to 1, which allows a direct child agent to spawn but prevents deeper nesting." |
|
||||
| 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." |
|
||||
|
||||
Sources consulted:
|
||||
- https://github.com/openai/codex (repo via gh CLI)
|
||||
- /openai/codex (Context7 library ID)
|
||||
- https://github.com/openai/codex/blob/main/codex-rs/config/src/config_toml.rs
|
||||
- https://github.com/openai/codex/blob/main/codex-rs/core/src/tools/handlers/multi_agents_spec.rs
|
||||
- https://github.com/openai/codex/blob/main/codex-rs/core-skills/src/loader.rs
|
||||
- https://developers.openai.com/codex/config-reference
|
||||
- https://developers.openai.com/codex/plugins/build
|
||||
- https://developers.openai.com/codex/multi-agent
|
||||
- https://developers.openai.com/codex/cli/slash-commands
|
||||
|
||||
Documentation gaps:
|
||||
- dispatch.maxDepth is configurable (Option<i32> with no documented upper bound); the documented default is 1 but the actual enforced maximum is not stated.
|
||||
- dispatch.subagentToolkit: docs say subagents 'inherit the tool surface' but do not enumerate whether any tools are excluded.
|
||||
- runtime: the Node.js entry point is a thin launcher shim; the actual agent execution runtime is a compiled Rust binary — axis classification is ambiguous.
|
||||
|
||||
---
|
||||
|
||||
## gemini
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | declarative | https://github.com/google-gemini/gemini-cli/blob/main/packages/sdk/SDK_DESIGN.md | "This feature is currently not implemented. (repeated for both extension and subagent SDK APIs; all actual integration is via files: TOML com" |
|
||||
| commandSurface | slash-toml | https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/custom-commands.md | "Custom commands in Gemini CLI are defined in TOML format with the .toml file extension … Commands are invoked as slash commands in the CLI." |
|
||||
| modelMode | passive | https://github.com/google-gemini/gemini-cli/blob/main/docs/hooks/reference.md | "BeforeModel Hook: Fires before sending a request to the LLM … hookSpecificOutput.llm_request (object) — An object that overrides parts of th" |
|
||||
| hookBus | host | https://github.com/google-gemini/gemini-cli/blob/main/docs/hooks/reference.md | "Hooks function as host-fired events … The CLI fires events at predetermined lifecycle points [BeforeAgent, AfterAgent, BeforeTool, AfterTool" |
|
||||
| stateIO | filesystem | https://github.com/google-gemini/gemini-cli/blob/main/docs/reference/configuration.md | "Local access by default with gitignore/geminiignore respect … Set to a boolean to enable or disable the sandbox" |
|
||||
| transport | mcp | https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md | "Configure a Node.js MCP server using stdio … { \"mcpServers\": { \"nodeServer\": { \"command\": \"node\", \"args\": [\"dist/server.js\"] } } }" |
|
||||
| runtime | node | https://github.com/google-gemini/gemini-cli/blob/main/docs/reference/configuration.md | "References to node-pty and child_process indicate JavaScript/Node.js execution environment" |
|
||||
| dispatch.namedDispatch | true | https://github.com/google-gemini/gemini-cli/blob/main/docs/core/subagents.md | "Example of a custom subagent definition file (.gemini/agents/security-auditor.md) … name: security-auditor" |
|
||||
| dispatch.nested | false | https://github.com/google-gemini/gemini-cli/blob/main/docs/core/subagents.md | "Each subagent operates in an isolated context loop … This isolation also includes recursion protection, preventing subag" |
|
||||
| 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) | — |
|
||||
|
||||
Sources consulted:
|
||||
- https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/custom-commands.md
|
||||
- https://github.com/google-gemini/gemini-cli/blob/main/docs/core/subagents.md
|
||||
- https://github.com/google-gemini/gemini-cli/blob/main/docs/hooks/reference.md
|
||||
- https://github.com/google-gemini/gemini-cli/blob/main/docs/reference/configuration.md
|
||||
- https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md
|
||||
- https://github.com/google-gemini/gemini-cli/blob/main/packages/sdk/SDK_DESIGN.md
|
||||
- /google-gemini/gemini-cli (Context7 library)
|
||||
|
||||
Documentation gaps:
|
||||
- dispatch.background — docs describe subagents as operating in isolated context loops but do not state whether they execute synchronously or asynchronously.
|
||||
- dispatch.subagentToolkit — subagents have a configurable tool grant model but no single fixed value of 'full' or 'read-only' is stated as the architecture-level constraint.
|
||||
|
||||
---
|
||||
|
||||
## opencode
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | imperative | https://opencode.ai/docs/plugins | "Plugins are JavaScript/TypeScript modules that export plugin functions; they register hooks via `import type { Plugin } from '@opencode-ai/p'" |
|
||||
| commandSurface | slash-file | https://opencode.ai/docs/commands | "\"Create markdown files in the `commands/` directory to define custom commands.\" and \"The markdown file name becomes the command name." |
|
||||
| modelMode | active | /anomalyco/opencode (Context7) — packages/plugin/src/v2/promise/README.md | "`ctx.aisdk.sdk(async (event) => { ... event.sdk = mod.createXai(event.options) })` and `ctx.aisdk.language((event) => { ... event.language =" |
|
||||
| hookBus | host | https://opencode.ai/docs/plugins | "Host fires events including: `tool.execute.before`, `tool.execute.after`, `session.created`, `session.compacted`, `session.deleted`" |
|
||||
| stateIO | filesystem | https://opencode.ai/docs/plugins | "Plugin context includes `directory` (working directory path), `worktree` (git worktree path), and `$` (\"Bun's shell API\")" |
|
||||
| transport | mcp | https://opencode.ai/docs/mcp-servers | "\"OpenCode supports both local and remote servers.\" and \"Once added, MCP tools are automatically available to the LLM\"" |
|
||||
| runtime | bun | https://opencode.ai/docs/plugins | "\"$\": Bun's shell API for executing commands\" (plugin context property); \"OpenCode runs `bun install` at startup\"" |
|
||||
| dispatch.namedDispatch | true | https://opencode.ai/docs/agents | "\"Subagents can be invoked: Automatically by primary agents for specialized tasks based on their descriptions. Manually b" |
|
||||
| dispatch.nested | undocumented | no authoritative doc — searched: https://opencode.ai/docs/agents | — |
|
||||
| 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.\"" |
|
||||
|
||||
Sources consulted:
|
||||
- https://opencode.ai/docs/plugins
|
||||
- https://opencode.ai/docs/agents
|
||||
- https://opencode.ai/docs/commands
|
||||
- https://opencode.ai/docs/mcp-servers
|
||||
- /websites/opencode_ai_plugins (Context7)
|
||||
- /anomalyco/opencode (Context7)
|
||||
- https://github.com/sst/opencode/issues/5887
|
||||
|
||||
Documentation gaps:
|
||||
- dispatch.nested
|
||||
- dispatch.maxDepth
|
||||
|
||||
---
|
||||
|
||||
## cursor
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | imperative | https://cursor.com/docs/sdk/typescript | "local.customTools where you define tool functions that execute 'in your process, so it can reach anything your code can'; Agent.create()" |
|
||||
| commandSurface | slash-file | https://cursor.com/docs/enterprise/llm-safety-and-controls | "Commands are reusable prompts invoked via slash commands (e.g., /test), while workflows enable multi-step processes" |
|
||||
| modelMode | passive | https://cursor.com/docs/sdk/python | "The model used for a run can be overridden by passing a ModelSelection object in SendOptions to agent.send()." |
|
||||
| hookBus | host | https://cursor.com/docs/hooks | "Agent hooks: sessionStart, sessionEnd, preToolUse, postToolUse, subagentStart, subagentStop, beforeShellExecution, afterShellExecution" |
|
||||
| stateIO | filesystem | https://cursor.com/docs/reference/sandbox | "Local agents run with sandbox options disabled by default." |
|
||||
| transport | mcp | https://cursor.com/docs/mcp | "The Model Context Protocol (MCP) allows Cursor to connect to external tools and data sources." |
|
||||
| runtime | node | https://cursor.com/docs/sdk/typescript | "The SDK runs on Node.js. It requires Node.js 22.13 or later and is described as a Node-first package." |
|
||||
| dispatch.namedDispatch | true | https://cursor.com/docs/subagents | "Invoke specific subagents using slash commands in your prompt. This allows for direct control over which agent performs" |
|
||||
| dispatch.nested | true | 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.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" |
|
||||
|
||||
Sources consulted:
|
||||
- https://cursor.com/docs/subagents
|
||||
- https://cursor.com/docs/hooks
|
||||
- https://cursor.com/docs/sdk/typescript
|
||||
- https://cursor.com/docs/sdk/python
|
||||
- https://cursor.com/docs/mcp
|
||||
- https://cursor.com/docs/reference/sandbox
|
||||
- https://cursor.com/docs/enterprise/llm-safety-and-controls
|
||||
- /websites/cursor (Context7)
|
||||
|
||||
---
|
||||
|
||||
## cline
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | imperative | /cline/cline (Context7) — https://github.com/cline/cline/blob/main/docs/sdk/plugins.mdx | "Implement the AgentPlugin interface to register tools, hooks, and configuration. The setup function is used for registering capabilities." |
|
||||
| commandSurface | slash-file | /cline/cline (Context7) — https://github.com/cline/cline/blob/main/cline/apps/vscode/src/test/slash-commands.test.ts | "workflow markdown files (with .md, .markdown, or .txt extensions) are invoked as slash commands using their filename." |
|
||||
| modelMode | active | /cline/cline (Context7) — https://github.com/cline/cline/blob/main/sdk/packages/llms/README.md | "The Runtime API, accessible via createLlmsRuntime(...), allows for the creation of a registry that manages configured providers and their de" |
|
||||
| hookBus | host | /cline/cline (Context7) — https://github.com/cline/cline/blob/main/sdk/README.md | "Package agent capabilities as extensions (plugins) that can register tools, observe lifecycle events, and modify agent behavior." |
|
||||
| stateIO | filesystem | /cline/cline (Context7) — https://github.com/cline/cline/blob/main/cline/sdk/packages/shared/src/storage/paths.ts | "resolveClineDir() returns ~/.cline; resolveDocumentsExtensionPath('Workflows') returns ~/Documents/Cline/Workflows." |
|
||||
| transport | mcp | /cline/cline (Context7) — https://github.com/cline/cline/blob/main/docs/mcp/mcp-overview.mdx | "MCP (Model Context Protocol) enables Cline to interact with external tools and data sources" |
|
||||
| runtime | node | /cline/cline (Context7) — https://github.com/cline/cline/blob/main/sdk/examples/plugins/typescript-lsp/README.md | "Installs a portable subagent plugin ... cp examples/plugins/agents-squad/index.ts ~/.cline/plugins/portable-subagents.ts." |
|
||||
| dispatch.namedDispatch | true | /cline/cline (Context7) — https://github.com/cline/cline/blob/main/sdk/examples/plugins/agents-squad/README.md | "parent → start_subagent(preset: \"phantom\", task: \"Map the auth module\") → phantom: save_handoff(...)" |
|
||||
| dispatch.nested | false | /cline/cline (Context7) — https://github.com/cline/cline/blob/main/docs/features/subagents.mdx | "subagents are restricted from editing files, using the browser, accessing MCP servers, or creating nested subagents." |
|
||||
| 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" |
|
||||
|
||||
Sources consulted:
|
||||
- https://github.com/cline/cline/blob/main/docs/sdk/plugins.mdx
|
||||
- https://github.com/cline/cline/blob/main/sdk/README.md
|
||||
- https://github.com/cline/cline/blob/main/sdk/packages/agents/README.md
|
||||
- https://github.com/cline/cline/blob/main/sdk/examples/plugins/agents-squad/README.md
|
||||
- https://github.com/cline/cline/blob/main/docs/features/subagents.mdx
|
||||
- https://github.com/cline/cline/blob/main/docs/mcp/mcp-overview.mdx
|
||||
- https://github.com/cline/cline/blob/main/sdk/packages/llms/README.md
|
||||
- /cline/cline (Context7)
|
||||
|
||||
---
|
||||
|
||||
## hermes
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | imperative | https://hermes-agent.nousresearch.com/docs/guides/build-a-hermes-plugin | "ctx.register_tool() puts your tool in the registry — the model sees it immediately" |
|
||||
| commandSurface | slash-programmatic | https://hermes-agent.nousresearch.com/docs/guides/build-a-hermes-plugin | "ctx.register_command('mystatus', handler=_handle_status, description='Show plugin status') — The command appears in autocomplete, /help output" |
|
||||
| modelMode | active | https://hermes-agent.nousresearch.com/docs/guides/build-a-hermes-plugin | "register_provider(ProviderProfile(name=..., aliases=(...), display_name=..., env_vars=(...), base_url=..., auth_type=..., default_aux_model=" |
|
||||
| hookBus | host | https://hermes-agent.nousresearch.com/docs/user-guide/features/hooks | "Hermes owns and manages the entire hook infrastructure. At runtime, HookRegistry.discover_and_load() scans ~/.hermes/hooks/" |
|
||||
| stateIO | filesystem | https://hermes-agent.nousresearch.com/docs/user-guide/configuration | "The agent has the same filesystem access as your user account." |
|
||||
| transport | mcp | https://hermes-agent.nousresearch.com/docs/user-guide/features/mcp | "MCP support ships with the standard install — no extra step needed." |
|
||||
| runtime | python | Context7 /nousresearch/hermes-agent | "The plugin and agent runtime is Python (confirmed by register(ctx) in __init__.py, importlib.import_module, run_agent.py, tools/registry.py)" |
|
||||
| dispatch.namedDispatch | false | https://hermes-agent.nousresearch.com/docs/user-guide/features/delegation | "The documentation contains no mention of named agents. Subagents are identified only by role ('leaf' or 'orchestrator')" |
|
||||
| dispatch.nested | true | /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.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" |
|
||||
|
||||
Sources consulted:
|
||||
- https://hermes-agent.nousresearch.com/docs/user-guide/features/delegation
|
||||
- https://hermes-agent.nousresearch.com/docs/user-guide/features/hooks
|
||||
- https://hermes-agent.nousresearch.com/docs/user-guide/features/mcp
|
||||
- https://hermes-agent.nousresearch.com/docs/user-guide/configuration
|
||||
- https://hermes-agent.nousresearch.com/docs/guides/build-a-hermes-plugin
|
||||
- https://hermes-agent.nousresearch.com/docs/guides/delegation-patterns
|
||||
- https://github.com/NousResearch/hermes-agent/releases/tag/v2026.6.19
|
||||
- /nousresearch/hermes-agent (Context7)
|
||||
|
||||
Documentation gaps:
|
||||
- runtime — Hermes plugins and agent core run in Python, but this was confirmed by code inspection rather than explicit docs statement.
|
||||
- dispatch.namedDispatch — docs explicitly confirm no named-agent dispatch in delegate_task; Kanban has named profiles but that is a separate board system not a dispatch mechanism.
|
||||
|
||||
---
|
||||
|
||||
## antigravity
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | declarative | https://github.com/alphaperseii3000/google-antigravity-docs/blob/master/google-antigravity-docs.md | "Skills require a SKILL.md file; Workflows are saved as markdown files; Rules are manually defined constraints — all configuration-file-based" |
|
||||
| commandSurface | slash-file | https://github.com/alphaperseii3000/google-antigravity-docs/blob/master/google-antigravity-docs.md | "Workflows are saved as markdown files, providing a repeatable method for executing key processes. They can be invoked in the Agent using a s" |
|
||||
| modelMode | passive | https://dev.to/arindam_1729/antigravity-cli-a-hands-on-guide-to-googles-terminal-coding-agent-5bc7 | "Selection occurs via `-m` flag or `/model` command inside the TUI. No programmatic model request API is documented for extensions/skills" |
|
||||
| hookBus | host | https://www.aibuilderclub.com/blog/antigravity-cli-guide | "The CLI fires hooks, not the engine. These are JSON lifecycle interceptors (before tool call, after file edit, on session start)." |
|
||||
| stateIO | filesystem | https://www.explainx.ai/blog/antigravity-cli-features-sandbox-plugins-subagents-2026 | "Plugin staging at ~/.gemini/antigravity-cli/plugins/<name>/; skills at ~/.gemini/antigravity-cli/skills/" |
|
||||
| transport | mcp | https://dev.to/arindam_1729/antigravity-cli-a-hands-on-guide-to-googles-terminal-coding-agent-5bc7 | "Both local (stdio) and remote (HTTP) Model Context Protocol servers are supported" |
|
||||
| runtime | go | https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/ | "Built in Go, Antigravity CLI is snappier and more responsive." |
|
||||
| dispatch.namedDispatch | undocumented | no authoritative doc — searched: https://www.aibuilderclub.com/blog/antigravity-cli-guide, https://antigravity.google/docs/agents | — |
|
||||
| dispatch.nested | undocumented | no authoritative doc — searched: https://antigravity.google/docs/agents | — |
|
||||
| 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 | — |
|
||||
|
||||
Sources consulted:
|
||||
- https://github.com/alphaperseii3000/google-antigravity-docs/blob/master/google-antigravity-docs.md
|
||||
- https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/
|
||||
- https://dev.to/arindam_1729/antigravity-cli-a-hands-on-guide-to-googles-terminal-coding-agent-5bc7
|
||||
- https://www.explainx.ai/blog/antigravity-cli-features-sandbox-plugins-subagents-2026
|
||||
- https://www.aibuilderclub.com/blog/antigravity-cli-guide
|
||||
- https://antigravity.google/docs/agents
|
||||
- https://antigravity.google/docs/hooks
|
||||
|
||||
Documentation gaps:
|
||||
- dispatch.namedDispatch — docs describe dynamic plain-English goal dispatch where agent names subagents at runtime; no pre-registered named sub-agent API documented.
|
||||
- dispatch.nested — no documentation found on whether subagents can themselves spawn further subagents.
|
||||
- dispatch.maxDepth — no documented depth limit or explicit unbounded statement found.
|
||||
- dispatch.subagentToolkit — docs describe a permissions approval model but do not explicitly state 'full' vs 'read-only' toolkit scope for subagents.
|
||||
|
||||
---
|
||||
|
||||
## augment
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | declarative | https://docs.augmentcode.com/cli/plugins | "Plugins can provide several types of components, including Custom Commands defined in Markdown files within the `commands/` directory... Hoo" |
|
||||
| commandSurface | slash-file | https://docs.augmentcode.com/cli/plugins | "Slash commands are Markdown files in the `commands/` directory. The filename becomes the command name" |
|
||||
| modelMode | passive | https://docs.augmentcode.com/cli/subagents | "| model | No | Model to use for the agent. If not specified, the CLI default model is used." |
|
||||
| hookBus | host | https://docs.augmentcode.com/cli/hooks | "Hook event types: PreToolUse (before a tool executes), PostToolUse (immediately after a tool completes), Stop (when the agent stops respondi" |
|
||||
| stateIO | filesystem | https://github.com/augmentcode/auggie | "Node.js 22+ required. Hook configurations use `${AUGMENT_PLUGIN_ROOT}`" |
|
||||
| transport | mcp | https://docs.augmentcode.com/cli/plugins | "Auggie supports a plugin system that allows you to extend its functionality with... MCP server integrations." |
|
||||
| runtime | node | https://github.com/augmentcode/auggie | "Node.js 22+ required" |
|
||||
| dispatch.namedDispatch | true | https://docs.augmentcode.com/cli/subagents | "| **name** | Yes | Name of the agent | ... you can trigger it by sending a message that references the agent name." |
|
||||
| dispatch.nested | undocumented | no authoritative doc — searched: https://docs.augmentcode.com/cli/subagents | — |
|
||||
| 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)." |
|
||||
|
||||
Sources consulted:
|
||||
- https://docs.augmentcode.com/cli/plugins
|
||||
- https://docs.augmentcode.com/cli/hooks
|
||||
- https://docs.augmentcode.com/cli/subagents
|
||||
- https://docs.augmentcode.com/cli/sdk-typescript
|
||||
- https://docs.augmentcode.com/setup-augment/mcp
|
||||
- https://github.com/augmentcode/auggie
|
||||
- /llmstxt/augmentcode_llms-full_txt (Context7)
|
||||
|
||||
Documentation gaps:
|
||||
- dispatch.nested
|
||||
- dispatch.maxDepth
|
||||
|
||||
---
|
||||
|
||||
## qwen
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | imperative | https://qwenlm.github.io/qwen-code-docs/en/developers/channel-plugins | "Your entry point exports a ChannelPlugin object... this.registerCommand('mycommand', async (envelope, args) => { ... }); ... plugins load at startup as extensions." |
|
||||
| commandSurface | slash-file | https://qwenlm.github.io/qwen-code-docs/en/users/extension/introduction | "Extensions can provide custom commands by placing Markdown files in a commands/ subdirectory" |
|
||||
| modelMode | passive | https://qwenlm.github.io/qwen-code-docs/en/developers/channel-plugins | "The documentation does not expose a direct API for plugins to invoke the LLM or model directly." |
|
||||
| hookBus | host | https://qwenlm.github.io/qwen-code-docs/en/users/features/hooks | "Qwen Code provides 14 distinct hook events: PreToolUse, PostToolUse, PostToolUseFailure, UserPromptSubmit, SessionStart, SessionEnd, Stop" |
|
||||
| stateIO | filesystem | https://qwenlm.github.io/qwen-code-docs/en/developers/channel-plugins | "Runtime Environment: Node.js only. The architecture uses standard Node.js APIs: import, async/await, file I/O (writeFileSync), OS utilities" |
|
||||
| transport | mcp | https://qwenlm.github.io/qwen-code-docs/en/developers/tools/mcp-server | "Qwen Code integrates with MCP servers through a sophisticated discovery and execution system" |
|
||||
| runtime | node | https://qwenlm.github.io/qwen-code-docs/en/developers/channel-plugins | "Language: Node.js (TypeScript/JavaScript). Execution model: In-process — plugins load at startup as extensions." |
|
||||
| dispatch.namedDispatch | true | https://qwenlm.github.io/qwen-code-docs/en/users/features/sub-agents/ | "Named subagents are invoked when the AI identifies tasks matching their specialization... Users can also explicitly requ" |
|
||||
| dispatch.nested | false | https://qwenlm.github.io/qwen-code-docs/en/users/features/sub-agents/ | "Fork children cannot create further forks. This is enforced at runtime — if a fork attempts to spawn another fork, it re" |
|
||||
| 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." |
|
||||
|
||||
Sources consulted:
|
||||
- https://qwenlm.github.io/qwen-code-docs/en/developers/channel-plugins
|
||||
- https://qwenlm.github.io/qwen-code-docs/en/users/features/sub-agents/
|
||||
- https://qwenlm.github.io/qwen-code-docs/en/users/features/hooks
|
||||
- https://qwenlm.github.io/qwen-code-docs/en/users/extension/introduction
|
||||
- https://qwenlm.github.io/qwen-code-docs/en/developers/tools/mcp-server
|
||||
- /websites/qwenlm_github_io_qwen-code-docs_en (Context7)
|
||||
- /qwenlm/qwen-code (Context7)
|
||||
|
||||
Documentation gaps:
|
||||
- dispatch.nested — docs only restrict fork-type sub-agents from nesting; whether named sub-agents can themselves spawn named sub-agents is not stated.
|
||||
- dispatch.maxDepth — depth=1 is documented only for fork sub-agents; depth for named sub-agent chains is undocumented.
|
||||
|
||||
---
|
||||
|
||||
## codebuddy
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | declarative | https://www.codebuddy.ai/docs/cli/plugins-reference | "Commands are 'plain Markdown file[s]' located in commands/ by default ... a skill is a directory containing a SKILL.md ... The documentation" |
|
||||
| commandSurface | slash-file | https://www.codebuddy.ai/docs/cli/plugins-reference | "Commands are 'plain Markdown file[s]' located in commands/ by default ... Skills are prefixed with this (e.g., /my-first-plugin:hello)" |
|
||||
| modelMode | passive | https://www.codebuddy.ai/docs/cli/sdk | "The SDK is not for building plugins that run inside CodeBuddy. It's an external SDK for standalone applications" |
|
||||
| hookBus | host | https://www.codebuddy.ai/docs/cli/hooks | "Full support for the hook event family (27+ events), covering tool lifecycle (PreToolUse / PostToolUse / PostToolUseFailure)" |
|
||||
| stateIO | filesystem | https://www.codebuddy.ai/docs/cli/settings | "Storage operates in non-sandboxed mode by default ... Default: Full filesystem access governed by permission rules" |
|
||||
| transport | mcp | https://www.codebuddy.ai/docs/cli/cli-reference | "MCP (Model Context Protocol) is built-in as a core feature ... codebuddy mcp command to 'Configure Model Context Protocol (MCP) servers'" |
|
||||
| runtime | node | https://www.codebuddy.ai/docs/cli/sdk | "TypeScript/JavaScript: Node.js >= 18.20 ... npm install @tencent-ai/agent-sdk" |
|
||||
| dispatch.namedDispatch | true | https://www.codebuddy.ai/docs/cli/sub-agents | "Sub-agents can be invoked explicitly by name: 'Request a specific sub-agent by mentioning it in your command'" |
|
||||
| dispatch.nested | false | https://www.codebuddy.ai/docs/cli/sub-agents | "This prevents infinite nesting of agents (sub-agents cannot spawn other sub-agents)" |
|
||||
| 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" |
|
||||
|
||||
Sources consulted:
|
||||
- https://www.codebuddy.ai/docs/cli/plugins
|
||||
- https://www.codebuddy.ai/docs/cli/plugins-reference
|
||||
- https://www.codebuddy.ai/docs/cli/sub-agents
|
||||
- https://www.codebuddy.ai/docs/cli/hooks
|
||||
- https://www.codebuddy.ai/docs/cli/sdk
|
||||
- https://www.codebuddy.ai/docs/cli/settings
|
||||
- /websites/codebuddy_cn (Context7)
|
||||
|
||||
---
|
||||
|
||||
## copilot
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | declarative | https://docs.github.com/en/copilot/concepts/agents/copilot-cli/comparing-cli-features | "Declarative elements include custom instructions, skills, custom agents, and plugin configurations—all defined through configuration files" |
|
||||
| commandSurface | slash-file | https://docs.github.com/en/copilot/concepts/agents/copilot-cli/comparing-cli-features | "Skills: Markdown files with instructions for specific contexts. Users can invoke via slash commands (e.g., /Markdown-Checker check README.md)" |
|
||||
| modelMode | passive | https://github.com/github/copilot-sdk/blob/main/docs/auth/byok.md | "Model selection via config: model: 'gpt-4.1', provider: { type: 'openai', ... }." |
|
||||
| hookBus | host | https://docs.github.com/en/copilot/reference/hooks-reference | "Hooks allow you to extend and customize the behavior of GitHub Copilot agents by executing custom shell commands at key points during agent" |
|
||||
| stateIO | filesystem | https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers | "Configuration file Location: ~/.copilot/mcp-config.json. Hook config files stored in .github/hooks/*.json" |
|
||||
| transport | mcp | https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers | "Copilot CLI comes with the GitHub MCP server already configured. STDIO is the standard transport." |
|
||||
| runtime | undocumented | no authoritative doc — searched: https://github.com/github/copilot-cli/blob/main/README.md, https://github.com/github/copilot-sdk/blob/main/nodejs/README.md | — |
|
||||
| dispatch.namedDispatch | true | https://github.com/github/copilot-sdk/blob/main/docs/features/custom-agents.md | "A custom agent is a named agent configuration that includes its own prompt and tool set. A sub-agent is a custom agent i" |
|
||||
| dispatch.nested | false | https://awesome-copilot.github.com/learning-hub/agents-and-subagents/ | "By default, subagents do not keep spawning additional subagents." |
|
||||
| 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" |
|
||||
|
||||
Sources consulted:
|
||||
- https://github.com/github/copilot-cli/blob/main/README.md (via Context7 /github/copilot-cli)
|
||||
- https://github.com/github/copilot-sdk/blob/main/docs/features/custom-agents.md (via Context7 /github/copilot-sdk)
|
||||
- https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers
|
||||
- https://docs.github.com/en/copilot/reference/hooks-reference
|
||||
- https://docs.github.com/en/copilot/concepts/agents/copilot-cli/comparing-cli-features
|
||||
- https://awesome-copilot.github.com/learning-hub/agents-and-subagents/
|
||||
|
||||
Documentation gaps:
|
||||
- runtime — docs describe the CLI binary and the SDK (Node.js/Go/Python/Rust) but do not state what runtime the CLI host itself or its plugin/extension loader executes in.
|
||||
- dispatch.nested exact authoritative source is awesome-copilot.github.com (community docs) not docs.github.com.
|
||||
|
||||
---
|
||||
|
||||
## kilo
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | imperative | https://kilo.ai/docs/automate/extending/plugins | "Plugins extend Kilo by hooking into events and adding functionality. They can: add custom tools the model can call (like read, write, bash)" |
|
||||
| commandSurface | slash-file | https://kilo.ai/docs/customize/workflows | "Workflows, also known as slash commands, allow users to automate repetitive tasks by defining step-by-step instructions" |
|
||||
| modelMode | active | https://kilo.ai/docs/automate/extending/plugins | "provider — dynamically supply model catalogs. auth — register OAuth or API-key flows for model providers. chat.params — Mutate temperature" |
|
||||
| hookBus | host | https://kilo.ai/docs/automate/extending/plugins | "event — fires for every internal bus event. Session: session.created, session.updated, session.idle, session.error, session.deleted" |
|
||||
| stateIO | filesystem | https://kilo.ai/docs/contributing/architecture | "Local execution and hosted execution are separate boundaries. Local runtime instances are Directory-keyed runtime context" |
|
||||
| transport | mcp | https://kilo.ai/docs/automate/mcp/what-is-mcp | "Kilo Code implements the Model Context Protocol to connect to both local and remote MCP servers" |
|
||||
| runtime | bun | https://kilo.ai/docs/automate/extending/plugins | "npm plugins are installed automatically at startup using Bun. Plugin context includes $ (Bun shell). Plugins are TypeScript or JavaScript mo" |
|
||||
| dispatch.namedDispatch | true | https://kilo.ai/docs/customize/custom-subagents | "Configured subagents can be invoked automatically by primary agents (like the Orchestrator) using the Task tool" |
|
||||
| dispatch.nested | true | https://github.com/Kilo-Org/kilocode/issues/7055 | "A subagent can still call the task tool if its merged permissions contain an explicit task rule, which enables nested su" |
|
||||
| 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 | — |
|
||||
|
||||
Sources consulted:
|
||||
- https://kilo.ai/docs/automate/extending/plugins
|
||||
- https://kilo.ai/docs/customize/custom-subagents
|
||||
- https://kilo.ai/docs/customize/workflows
|
||||
- https://kilo.ai/docs/automate/mcp/what-is-mcp
|
||||
- https://kilo.ai/docs/code-with-ai/agents/orchestrator-mode
|
||||
- https://kilo.ai/docs/contributing/architecture
|
||||
- https://github.com/Kilo-Org/kilocode/issues/7055
|
||||
- https://github.com/Kilo-Org/kilocode/issues/8637
|
||||
- /websites/kilo_ai (Context7)
|
||||
|
||||
Documentation gaps:
|
||||
- dispatch.subagentToolkit — docs describe per-subagent configurable permissions (allow/ask/deny) but do not document a single default toolkit level (full vs read-only) for subagents that lack explicit permission overrides.
|
||||
|
||||
---
|
||||
|
||||
## windsurf
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | declarative | https://docs.devin.ai/desktop/cascade/cascade | "Cascade operates through configuration files rather than code plugins: .codeiumignore for file filtering, Memories and Rules for customizing" |
|
||||
| commandSurface | slash-file | https://docs.devin.ai/desktop/cascade/workflows | "Workflows are authored as markdown files (.md extension) … triggered through slash commands using the format /[workflow-name]." |
|
||||
| modelMode | passive | https://docs.devin.ai/desktop/models.md | "Models are selectable via configuration/UI only (SWE-1.5, SWE-1.6, Adaptive, Arena tiers, Claude, GPT)." |
|
||||
| hookBus | host | https://docs.devin.ai/desktop/cascade/hooks.md | "Cascade supports twelve hook events covering critical workflow points … Pre-hooks (can block actions): pre_read_code, pre_write_code, pre_ru" |
|
||||
| stateIO | filesystem | https://docs.devin.ai/desktop/cascade/cascade | "Cascade can create and modify codebases directly … File access can be restricted through .codeiumignore files" |
|
||||
| transport | mcp | https://docs.devin.ai/desktop/cascade/mcp | "Cascade now natively integrates with MCP, allowing you to bring your own selection of MCP servers for Cascade to use." |
|
||||
| runtime | undocumented | no authoritative doc — searched: https://docs.devin.ai/windsurf/plugins/getting-started.md, /llmstxt/windsurf_llms-full_txt (Context7) | — |
|
||||
| dispatch.namedDispatch | undocumented | no authoritative doc — searched: https://docs.devin.ai/cli/subagents.md, https://docs.devin.ai/desktop/agent-command-center.md | — |
|
||||
| dispatch.nested | undocumented | no authoritative doc — searched: https://docs.devin.ai/cli/subagents.md | — |
|
||||
| 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 | — |
|
||||
|
||||
Sources consulted:
|
||||
- https://docs.devin.ai/desktop/cascade/workflows
|
||||
- https://docs.devin.ai/desktop/cascade/mcp
|
||||
- https://docs.devin.ai/desktop/cascade/hooks.md
|
||||
- https://docs.devin.ai/desktop/cascade/cascade
|
||||
- https://docs.devin.ai/desktop/models.md
|
||||
- https://docs.devin.ai/windsurf/plugins/getting-started.md
|
||||
- https://docs.devin.ai/cli/subagents.md
|
||||
- /llmstxt/windsurf_llms-full_txt (Context7)
|
||||
|
||||
Documentation gaps:
|
||||
- dispatch.namedDispatch — Cascade docs do not document a user-facing named sub-agent dispatch system.
|
||||
- dispatch.nested — no documentation for nested sub-agent support in Windsurf Cascade.
|
||||
- dispatch.maxDepth — no documented depth limit for Cascade sub-agents.
|
||||
- dispatch.background — Cascade has an internal background planning agent but no documented user-facing background sub-agent dispatch.
|
||||
- dispatch.subagentToolkit — no documentation for toolkit restrictions on Cascade sub-agents.
|
||||
- runtime — Windsurf IDE is Electron-based but no programmatic plugin runtime is documented to developers.
|
||||
|
||||
---
|
||||
|
||||
## trae
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | imperative | https://traeide.com/docs/how-to-manage-extensions-in-trae-ide | "Trae IDE is a VSCode fork; 'If an extension isn't available in Trae's store, you can install it from VS Code's marketplace' — inherits VSCode in-process extension model" |
|
||||
| commandSurface | slash-file | https://docs.trae.ai/ide/skills | "Skills stored as SKILL.md files in '.trae/skills/{skill_name}/' directory; 'Trae allows you to manually trigger skills if needed'" |
|
||||
| modelMode | passive | https://docs.trae.ai/ide/models | "Model selection via UI: 'click on the current model name to open the model list'; no programmatic model/LLM request API documented for plugins" |
|
||||
| hookBus | engine | https://news.ycombinator.com/item?id=44703164 | "Trae is 'ByteDance's VSCode fork' built on Electron/Monaco; inherits VSCode extension host lifecycle (activate/deactivate hooks, event subsc" |
|
||||
| stateIO | filesystem | https://traeide.com/news/6 | "Rules at '.trae/project_rules.md', skills at '.trae/skills/', MCP config at '.trae/mcp.json'; 'codebase files always remain on your local de" |
|
||||
| transport | mcp | https://docs.trae.ai/ide/model-context-protocol | "Page title from official docs: 'In TRAE IDE, MCP servers support three transport types' — MCP is built-in" |
|
||||
| runtime | node | https://news.ycombinator.com/item?id=44703164 | "Trae is a VSCode fork built on Electron; 'Electron is designed to create desktop applications… a backend using the Node.js runtime'" |
|
||||
| dispatch.namedDispatch | true | https://docs.trae.ai/ide/agent | "Agents in Trae 'can be called individually, or automatically called by SOLO Agent at the corresponding stage'" |
|
||||
| dispatch.nested | undocumented | no authoritative doc — searched: https://docs.trae.ai/ide/solo-mode, https://docs.trae.ai/ide/agent | — |
|
||||
| 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 | — |
|
||||
|
||||
Sources consulted:
|
||||
- https://docs.trae.ai/ide/model-context-protocol
|
||||
- https://docs.trae.ai/ide/agent
|
||||
- https://docs.trae.ai/ide/skills
|
||||
- https://docs.trae.ai/ide/solo-mode
|
||||
- https://docs.trae.ai/ide/solo-coder
|
||||
- https://traeide.com/news/6
|
||||
- https://traeide.com/docs/how-to-manage-extensions-in-trae-ide
|
||||
- https://news.ycombinator.com/item?id=44703164
|
||||
- https://news.aibase.com/news/22829
|
||||
|
||||
Documentation gaps:
|
||||
- dispatch.nested — docs describe two-tier orchestration (SOLO → named agents) but do not state whether a spawned sub-agent can itself spawn further sub-agents.
|
||||
- dispatch.maxDepth — no integer depth limit documented beyond one orchestrator level.
|
||||
- dispatch.subagentToolkit — docs say agents can be configured with 'callable MCP services and other capabilities' but do not state whether sub-agents receive a full vs. restricted tool set.
|
||||
|
||||
---
|
||||
|
||||
## kimi
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | imperative | https://context7.com/moonshotai/kimi-cli/llms.txt | "from kimi_cli.app import KimiCLI, enable_logging ... instance = await KimiCLI.create(session, agent_file=myagent) ... class Ls(CallableTool2)" |
|
||||
| commandSurface | slash-file | https://github.com/moonshotai/kimi-cli/blob/main/docs/en/customization/skills.md | "/skill:code-style ... /flow:code-review — Skills are SKILL.md markdown files with YAML frontmatter that become /skill:<name> and /flow:<name>" |
|
||||
| modelMode | passive | https://github.com/moonshotai/kimi-cli/blob/main/docs/en/configuration/providers.md | "Use the `/model` command to switch between available models and thinking modes ... `--model` option overrides the default model" |
|
||||
| hookBus | host | https://moonshotai.github.io/kimi-cli/en/customization/hooks.html | "Core: Add hooks system (Beta) — configure `[[hooks]]` in `config.toml` to run custom shell commands at 13 lifecycle events including `PreToo" |
|
||||
| stateIO | filesystem | https://github.com/MoonshotAI/kimi-cli | "Kimi Code CLI is an AI agent that runs in the terminal ... capable of reading and editing code, executing shell commands, searching files" |
|
||||
| transport | mcp | https://github.com/moonshotai/kimi-cli/blob/main/docs/en/reference/kimi-mcp.md | "kimi mcp add ... --transport stdio|http ... Manage MCP Servers: Use the kimi mcp sub-command group to add, list, remove, or authorize MCP se" |
|
||||
| runtime | python | https://context7.com/moonshotai/kimi-cli/llms.txt | "from kimi_cli.app import KimiCLI ... from kosong.tooling import CallableTool2 — CLI core is Python" |
|
||||
| dispatch.namedDispatch | true | https://moonshotai.github.io/kimi-cli/en/customization/agents.html | "subagents:\n coder:\n path: ./coder-sub.yaml\n description: \"Handle coding tasks\"\n reviewer:\n path: ./reviewer-sub.yaml" |
|
||||
| dispatch.nested | false | 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.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 | — |
|
||||
|
||||
Sources consulted:
|
||||
- https://moonshotai.github.io/kimi-cli/en/customization/hooks.html
|
||||
- https://moonshotai.github.io/kimi-cli/en/customization/agents.html
|
||||
- https://github.com/MoonshotAI/kimi-cli
|
||||
- https://github.com/moonshotai/kimi-cli/blob/main/docs/en/customization/skills.md
|
||||
- https://github.com/moonshotai/kimi-cli/blob/main/docs/en/customization/agents.md
|
||||
- https://github.com/moonshotai/kimi-cli/blob/main/docs/en/reference/kimi-mcp.md
|
||||
- https://context7.com/moonshotai/kimi-cli/llms.txt
|
||||
- /moonshotai/kimi-cli (Context7)
|
||||
|
||||
Documentation gaps:
|
||||
- dispatch.subagentToolkit — docs show three built-in subagent types each with different tool subsets (coder=full, explore=read-only, plan=no shell/write); no single 'full' or 'read-only' value covers all types; maintainer should clarify the intended classification.
|
||||
- runtime — CLI core is Python; a Rust Wire implementation also exists; docs do not state a canonical plugin extension runtime.
|
||||
@@ -39,6 +39,7 @@ export default tseslint.config(
|
||||
'**/*.generated.cjs',
|
||||
// ADR-457: tsc-generated runtime artifact — lint the src/*.cts source, not the emitted .cjs.
|
||||
'gsd-core/bin/lib/semver-compare.cjs',
|
||||
'gsd-core/bin/lib/host-integration.cjs',
|
||||
'gsd-core/bin/lib/capability-loader.cjs',
|
||||
'gsd-core/bin/lib/capability-source.cjs',
|
||||
'gsd-core/bin/lib/capability-ledger.cjs',
|
||||
|
||||
@@ -117,7 +117,23 @@ const capabilities = {
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": true,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": "undocumented",
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "go"
|
||||
}
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
@@ -223,7 +239,23 @@ const capabilities = {
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": true,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"claude": {
|
||||
@@ -289,7 +321,23 @@ const capabilities = {
|
||||
"Stop",
|
||||
"PreCompact",
|
||||
"FileChanged"
|
||||
]
|
||||
],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": true,
|
||||
"maxDepth": 5,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cline": {
|
||||
@@ -332,7 +380,23 @@ const capabilities = {
|
||||
"installSurface": "cline-rules",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "read-only"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"code-review": {
|
||||
@@ -462,7 +526,23 @@ const capabilities = {
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": true,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codex": {
|
||||
@@ -515,7 +595,23 @@ const capabilities = {
|
||||
"installSurface": "codex-toml",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": true,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"copilot": {
|
||||
@@ -568,7 +664,23 @@ const capabilities = {
|
||||
"installSurface": "copilot-instructions",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "undocumented"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cursor": {
|
||||
@@ -637,7 +749,23 @@ const capabilities = {
|
||||
"installSurface": "cursor-hooks-json",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": true,
|
||||
"maxDepth": 2,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"drift": {
|
||||
@@ -813,7 +941,23 @@ const capabilities = {
|
||||
"BeforeAgent",
|
||||
"AfterAgent",
|
||||
"BeforeModel"
|
||||
]
|
||||
],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-toml",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": "undocumented",
|
||||
"subagentToolkit": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"graphify": {
|
||||
@@ -907,7 +1051,23 @@ const capabilities = {
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": true,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-programmatic",
|
||||
"dispatch": {
|
||||
"namedDispatch": false,
|
||||
"nested": true,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "read-only"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "python"
|
||||
}
|
||||
}
|
||||
},
|
||||
"intel": {
|
||||
@@ -1034,7 +1194,23 @@ const capabilities = {
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": "kilo",
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": true,
|
||||
"maxDepth": -1,
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "bun"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kimi": {
|
||||
@@ -1090,7 +1266,23 @@ const capabilities = {
|
||||
"installSurface": "profile-marker-only",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "python"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mempalace": {
|
||||
@@ -1384,7 +1576,23 @@ const capabilities = {
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": true,
|
||||
"permissionWriter": "opencode",
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": false,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "bun"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pattern-mapper": {
|
||||
@@ -1572,7 +1780,23 @@ const capabilities = {
|
||||
"SubagentStop",
|
||||
"Stop",
|
||||
"PreCompact"
|
||||
]
|
||||
],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"research": {
|
||||
@@ -1874,7 +2098,23 @@ const capabilities = {
|
||||
"installSurface": "profile-marker-only",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "engine",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ui": {
|
||||
@@ -2013,7 +2253,23 @@ const capabilities = {
|
||||
"installSurface": "profile-marker-only",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": "undocumented",
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": "undocumented",
|
||||
"subagentToolkit": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "undocumented"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2797,7 +3053,23 @@ const runtimes = {
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": true,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": "undocumented",
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "go"
|
||||
}
|
||||
}
|
||||
},
|
||||
"augment": {
|
||||
@@ -2866,7 +3138,23 @@ const runtimes = {
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": true,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"claude": {
|
||||
@@ -2932,7 +3220,23 @@ const runtimes = {
|
||||
"Stop",
|
||||
"PreCompact",
|
||||
"FileChanged"
|
||||
]
|
||||
],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": true,
|
||||
"maxDepth": 5,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cline": {
|
||||
@@ -2975,7 +3279,23 @@ const runtimes = {
|
||||
"installSurface": "cline-rules",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "read-only"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codebuddy": {
|
||||
@@ -3044,7 +3364,23 @@ const runtimes = {
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": true,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codex": {
|
||||
@@ -3097,7 +3433,23 @@ const runtimes = {
|
||||
"installSurface": "codex-toml",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": true,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"copilot": {
|
||||
@@ -3150,7 +3502,23 @@ const runtimes = {
|
||||
"installSurface": "copilot-instructions",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "undocumented"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cursor": {
|
||||
@@ -3219,7 +3587,23 @@ const runtimes = {
|
||||
"installSurface": "cursor-hooks-json",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": true,
|
||||
"maxDepth": 2,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"gemini": {
|
||||
@@ -3276,7 +3660,23 @@ const runtimes = {
|
||||
"BeforeAgent",
|
||||
"AfterAgent",
|
||||
"BeforeModel"
|
||||
]
|
||||
],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-toml",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": "undocumented",
|
||||
"subagentToolkit": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"hermes": {
|
||||
@@ -3329,7 +3729,23 @@ const runtimes = {
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": true,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-programmatic",
|
||||
"dispatch": {
|
||||
"namedDispatch": false,
|
||||
"nested": true,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "read-only"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "python"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kilo": {
|
||||
@@ -3404,7 +3820,23 @@ const runtimes = {
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": "kilo",
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": true,
|
||||
"maxDepth": -1,
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "bun"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kimi": {
|
||||
@@ -3460,7 +3892,23 @@ const runtimes = {
|
||||
"installSurface": "profile-marker-only",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "python"
|
||||
}
|
||||
}
|
||||
},
|
||||
"opencode": {
|
||||
@@ -3530,7 +3978,23 @@ const runtimes = {
|
||||
"installSurface": "settings-json",
|
||||
"writesSharedSettings": true,
|
||||
"permissionWriter": "opencode",
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": false,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "active",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "bun"
|
||||
}
|
||||
}
|
||||
},
|
||||
"qwen": {
|
||||
@@ -3587,7 +4051,23 @@ const runtimes = {
|
||||
"SubagentStop",
|
||||
"Stop",
|
||||
"PreCompact"
|
||||
]
|
||||
],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": false,
|
||||
"maxDepth": 1,
|
||||
"background": true,
|
||||
"subagentToolkit": "full"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"trae": {
|
||||
@@ -3639,7 +4119,23 @@ const runtimes = {
|
||||
"installSurface": "profile-marker-only",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "imperative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": true,
|
||||
"subagentToolkit": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "engine",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"windsurf": {
|
||||
@@ -3683,7 +4179,23 @@ const runtimes = {
|
||||
"installSurface": "profile-marker-only",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": []
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": "undocumented",
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": "undocumented",
|
||||
"subagentToolkit": "undocumented"
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "undocumented"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -713,6 +713,16 @@ const VALID_INSTALL_SURFACES = new Set(['settings-json', 'codex-toml', 'copilot-
|
||||
const VALID_PERMISSION_WRITERS = new Set(['opencode', 'kilo']);
|
||||
const VALID_EXTENDED_HOOK_EVENTS = new Set(['SubagentStop', 'Stop', 'PreCompact', 'FileChanged', 'BeforeAgent', 'AfterAgent', 'BeforeModel']);
|
||||
|
||||
// ADR-1239 Phase A: hostIntegration axes (MUST stay parity-identical to HOST_INTEGRATION_AXES in src/host-integration.cts)
|
||||
const VALID_EMBEDDING_MODES = new Set(['imperative', 'declarative']);
|
||||
const VALID_COMMAND_SURFACES = new Set(['slash-file', 'slash-programmatic', 'slash-toml', 'palette', 'prose-only']);
|
||||
const VALID_MODEL_MODES = new Set(['active', 'passive']);
|
||||
const VALID_HOOK_BUSES = new Set(['host', 'engine', 'none']);
|
||||
const VALID_STATE_IO = new Set(['filesystem', 'sandboxed-storage', 'session-log-append']);
|
||||
const VALID_TRANSPORTS = new Set(['mcp', 'native-extension']);
|
||||
const VALID_HOST_RUNTIMES = new Set(['node', 'bun', 'sandboxed-web', 'python', 'go', 'rust', 'electron', 'other']);
|
||||
const VALID_SUBAGENT_TOOLKITS = new Set(['full', 'read-only']);
|
||||
|
||||
// GATE A: installSurface → allowed hooksSurface values (DEFECT.GENERATIVE-FIX: parity invariant)
|
||||
// Derived from the actual pairings in the 16 real runtime descriptors.
|
||||
const INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES = new Map([
|
||||
@@ -1037,6 +1047,150 @@ function validateRuntimeBody(cap) {
|
||||
}
|
||||
}
|
||||
|
||||
// hostIntegration — ADR-1239 Phase A: required object with closed-enum axes
|
||||
if (typeof r.hostIntegration !== 'object' || r.hostIntegration === null || Array.isArray(r.hostIntegration)) {
|
||||
errors.push('runtime.hostIntegration is required and must be an object');
|
||||
} else {
|
||||
const hi = r.hostIntegration;
|
||||
|
||||
// S2b: reserved-OWN-KEY guard on hostIntegration (CodeQL barrier — inline literal comparisons)
|
||||
if (Object.prototype.hasOwnProperty.call(hi, '__proto__')) {
|
||||
errors.push('runtime.hostIntegration must not contain reserved key "__proto__"');
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(hi, 'constructor')) {
|
||||
errors.push('runtime.hostIntegration must not contain reserved key "constructor"');
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(hi, 'prototype')) {
|
||||
errors.push('runtime.hostIntegration must not contain reserved key "prototype"');
|
||||
}
|
||||
|
||||
// embeddingMode
|
||||
if (hi.embeddingMode === '__proto__' || hi.embeddingMode === 'constructor' || hi.embeddingMode === 'prototype') {
|
||||
errors.push('runtime.hostIntegration.embeddingMode "' + hi.embeddingMode + '" is a reserved name');
|
||||
} else if (hi.embeddingMode !== 'undocumented' && !VALID_EMBEDDING_MODES.has(hi.embeddingMode)) {
|
||||
errors.push(
|
||||
'runtime.hostIntegration.embeddingMode must be one of: ' + [...VALID_EMBEDDING_MODES].join(', ') +
|
||||
' (or "undocumented") (got: ' + JSON.stringify(hi.embeddingMode) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// commandSurface
|
||||
if (hi.commandSurface === '__proto__' || hi.commandSurface === 'constructor' || hi.commandSurface === 'prototype') {
|
||||
errors.push('runtime.hostIntegration.commandSurface "' + hi.commandSurface + '" is a reserved name');
|
||||
} else if (hi.commandSurface !== 'undocumented' && !VALID_COMMAND_SURFACES.has(hi.commandSurface)) {
|
||||
errors.push(
|
||||
'runtime.hostIntegration.commandSurface must be one of: ' + [...VALID_COMMAND_SURFACES].join(', ') +
|
||||
' (or "undocumented") (got: ' + JSON.stringify(hi.commandSurface) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// modelMode
|
||||
if (hi.modelMode === '__proto__' || hi.modelMode === 'constructor' || hi.modelMode === 'prototype') {
|
||||
errors.push('runtime.hostIntegration.modelMode "' + hi.modelMode + '" is a reserved name');
|
||||
} else if (hi.modelMode !== 'undocumented' && !VALID_MODEL_MODES.has(hi.modelMode)) {
|
||||
errors.push(
|
||||
'runtime.hostIntegration.modelMode must be one of: ' + [...VALID_MODEL_MODES].join(', ') +
|
||||
' (or "undocumented") (got: ' + JSON.stringify(hi.modelMode) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// hookBus
|
||||
if (hi.hookBus === '__proto__' || hi.hookBus === 'constructor' || hi.hookBus === 'prototype') {
|
||||
errors.push('runtime.hostIntegration.hookBus "' + hi.hookBus + '" is a reserved name');
|
||||
} else if (hi.hookBus !== 'undocumented' && !VALID_HOOK_BUSES.has(hi.hookBus)) {
|
||||
errors.push(
|
||||
'runtime.hostIntegration.hookBus must be one of: ' + [...VALID_HOOK_BUSES].join(', ') +
|
||||
' (or "undocumented") (got: ' + JSON.stringify(hi.hookBus) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// stateIO
|
||||
if (hi.stateIO === '__proto__' || hi.stateIO === 'constructor' || hi.stateIO === 'prototype') {
|
||||
errors.push('runtime.hostIntegration.stateIO "' + hi.stateIO + '" is a reserved name');
|
||||
} else if (hi.stateIO !== 'undocumented' && !VALID_STATE_IO.has(hi.stateIO)) {
|
||||
errors.push(
|
||||
'runtime.hostIntegration.stateIO must be one of: ' + [...VALID_STATE_IO].join(', ') +
|
||||
' (or "undocumented") (got: ' + JSON.stringify(hi.stateIO) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// transport
|
||||
if (hi.transport === '__proto__' || hi.transport === 'constructor' || hi.transport === 'prototype') {
|
||||
errors.push('runtime.hostIntegration.transport "' + hi.transport + '" is a reserved name');
|
||||
} else if (hi.transport !== 'undocumented' && !VALID_TRANSPORTS.has(hi.transport)) {
|
||||
errors.push(
|
||||
'runtime.hostIntegration.transport must be one of: ' + [...VALID_TRANSPORTS].join(', ') +
|
||||
' (or "undocumented") (got: ' + JSON.stringify(hi.transport) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// runtime (axis)
|
||||
if (hi.runtime === '__proto__' || hi.runtime === 'constructor' || hi.runtime === 'prototype') {
|
||||
errors.push('runtime.hostIntegration.runtime "' + hi.runtime + '" is a reserved name');
|
||||
} else if (hi.runtime !== 'undocumented' && !VALID_HOST_RUNTIMES.has(hi.runtime)) {
|
||||
errors.push(
|
||||
'runtime.hostIntegration.runtime must be one of: ' + [...VALID_HOST_RUNTIMES].join(', ') +
|
||||
' (or "undocumented") (got: ' + JSON.stringify(hi.runtime) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// dispatch — required object
|
||||
if (typeof hi.dispatch !== 'object' || hi.dispatch === null || Array.isArray(hi.dispatch)) {
|
||||
errors.push('runtime.hostIntegration.dispatch must be an object');
|
||||
} else {
|
||||
const d = hi.dispatch;
|
||||
|
||||
// S2b: reserved-OWN-KEY guard on dispatch (CodeQL barrier — inline literal comparisons)
|
||||
if (Object.prototype.hasOwnProperty.call(d, '__proto__')) {
|
||||
errors.push('runtime.hostIntegration.dispatch must not contain reserved key "__proto__"');
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(d, 'constructor')) {
|
||||
errors.push('runtime.hostIntegration.dispatch must not contain reserved key "constructor"');
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(d, 'prototype')) {
|
||||
errors.push('runtime.hostIntegration.dispatch must not contain reserved key "prototype"');
|
||||
}
|
||||
|
||||
// namedDispatch — boolean or 'undocumented'
|
||||
if (typeof d.namedDispatch !== 'boolean' && d.namedDispatch !== 'undocumented') {
|
||||
errors.push(
|
||||
'runtime.hostIntegration.dispatch.namedDispatch must be a boolean or "undocumented" (got: ' + JSON.stringify(d.namedDispatch) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// nested — boolean or 'undocumented'
|
||||
if (typeof d.nested !== 'boolean' && d.nested !== 'undocumented') {
|
||||
errors.push(
|
||||
'runtime.hostIntegration.dispatch.nested must be a boolean or "undocumented" (got: ' + JSON.stringify(d.nested) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// background — boolean or 'undocumented'
|
||||
if (typeof d.background !== 'boolean' && d.background !== 'undocumented') {
|
||||
errors.push(
|
||||
'runtime.hostIntegration.dispatch.background must be a boolean or "undocumented" (got: ' + JSON.stringify(d.background) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// subagentToolkit — closed enum or 'undocumented'
|
||||
if (d.subagentToolkit === '__proto__' || d.subagentToolkit === 'constructor' || d.subagentToolkit === 'prototype') {
|
||||
errors.push('runtime.hostIntegration.dispatch.subagentToolkit "' + d.subagentToolkit + '" is a reserved name');
|
||||
} else if (d.subagentToolkit !== 'undocumented' && !VALID_SUBAGENT_TOOLKITS.has(d.subagentToolkit)) {
|
||||
errors.push(
|
||||
'runtime.hostIntegration.dispatch.subagentToolkit must be one of: ' + [...VALID_SUBAGENT_TOOLKITS].join(', ') +
|
||||
' (or "undocumented") (got: ' + JSON.stringify(d.subagentToolkit) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// maxDepth — integer >= -1 or 'undocumented'
|
||||
if (d.maxDepth !== 'undocumented' && (!Number.isInteger(d.maxDepth) || d.maxDepth < -1)) {
|
||||
errors.push(
|
||||
'runtime.hostIntegration.dispatch.maxDepth must be an integer >= -1 or "undocumented" (got: ' + JSON.stringify(d.maxDepth) + ')',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GATE A: installSurface ↔ hooksSurface consistency (DEFECT.GENERATIVE-FIX)
|
||||
// Only check if both fields are valid strings (individual field validators above report type errors).
|
||||
if (typeof r.installSurface === 'string' && typeof r.hooksSurface === 'string') {
|
||||
@@ -2052,6 +2206,24 @@ module.exports = {
|
||||
VALID_INSTALL_SURFACES,
|
||||
VALID_PERMISSION_WRITERS,
|
||||
VALID_EXTENDED_HOOK_EVENTS,
|
||||
VALID_EMBEDDING_MODES,
|
||||
VALID_COMMAND_SURFACES,
|
||||
VALID_MODEL_MODES,
|
||||
VALID_HOOK_BUSES,
|
||||
VALID_STATE_IO,
|
||||
VALID_TRANSPORTS,
|
||||
VALID_HOST_RUNTIMES,
|
||||
VALID_SUBAGENT_TOOLKITS,
|
||||
_HOST_INTEGRATION_VOCAB: {
|
||||
embeddingMode: [...VALID_EMBEDDING_MODES],
|
||||
commandSurface: [...VALID_COMMAND_SURFACES],
|
||||
modelMode: [...VALID_MODEL_MODES],
|
||||
hookBus: [...VALID_HOOK_BUSES],
|
||||
stateIO: [...VALID_STATE_IO],
|
||||
transport: [...VALID_TRANSPORTS],
|
||||
runtime: [...VALID_HOST_RUNTIMES],
|
||||
subagentToolkit: [...VALID_SUBAGENT_TOOLKITS],
|
||||
},
|
||||
INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES,
|
||||
GEMINI_AGENT_EVENTS,
|
||||
CLAUDE_FAMILY_EVENTS,
|
||||
|
||||
@@ -184,6 +184,14 @@
|
||||
"fix-1464-docs-manifest-validation.test.cjs"
|
||||
],
|
||||
"issue": "1496"
|
||||
},
|
||||
"host-integration": {
|
||||
"files": [
|
||||
"host-integration.test.cjs",
|
||||
"host-integration-validator-parity.test.cjs",
|
||||
"host-integration-descriptors.test.cjs"
|
||||
],
|
||||
"issue": "1684"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
491
src/host-integration.cts
Normal file
491
src/host-integration.cts
Normal file
@@ -0,0 +1,491 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Host Integration module — ADR-1239 Phase A.
|
||||
*
|
||||
* Pure, additive, no-I/O module providing a closed vocabulary for host
|
||||
* integration axes, degradation ladder, profile classification, and
|
||||
* capability negotiation.
|
||||
*
|
||||
* The SINGLE source of truth for integration axes and degradation levels.
|
||||
* All functions are pure (no side effects, no I/O).
|
||||
*
|
||||
* Per-CLI sourced axis VALUES (with citations) live in docs/reference/host-integration-capability-matrix.md — every value is documented or explicitly 'undocumented'.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Protocol version
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const PROTOCOL_VERSION = 1;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Undocumented sentinel — fail-closed when a host omits CLI docs for an axis
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sentinel value used when a host descriptor's CLI docs do not state a value
|
||||
* for an axis. It VALIDATES (accepted by the validator) but NEVER propagates
|
||||
* into effective axes — it fails closed exactly like an unknown/missing value.
|
||||
*
|
||||
* Do NOT add to HOST_INTEGRATION_AXES (which is the documented vocabulary).
|
||||
*/
|
||||
const UNDOCUMENTED = 'undocumented';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Closed vocabulary — axes and interface points
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const HOST_INTEGRATION_AXES = Object.freeze({
|
||||
embeddingMode: Object.freeze(['imperative', 'declarative'] as const),
|
||||
commandSurface: Object.freeze(['slash-file', 'slash-programmatic', 'slash-toml', 'palette', 'prose-only'] as const),
|
||||
modelMode: Object.freeze(['active', 'passive'] as const),
|
||||
hookBus: Object.freeze(['host', 'engine', 'none'] as const),
|
||||
stateIO: Object.freeze(['filesystem', 'sandboxed-storage', 'session-log-append'] as const),
|
||||
transport: Object.freeze(['mcp', 'native-extension'] as const),
|
||||
runtime: Object.freeze(['node', 'bun', 'sandboxed-web', 'python', 'go', 'rust', 'electron', 'other'] as const),
|
||||
subagentToolkit: Object.freeze(['full', 'read-only'] as const),
|
||||
});
|
||||
|
||||
const INTERFACE_POINTS = Object.freeze(['command', 'dispatch', 'model', 'hooks', 'state', 'artifact'] as const);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type EmbeddingMode = 'imperative' | 'declarative';
|
||||
type CommandSurface = 'slash-file' | 'slash-programmatic' | 'slash-toml' | 'palette' | 'prose-only';
|
||||
type ModelMode = 'active' | 'passive';
|
||||
type HookBus = 'host' | 'engine' | 'none';
|
||||
type StateIO = 'filesystem' | 'sandboxed-storage' | 'session-log-append';
|
||||
type Transport = 'mcp' | 'native-extension';
|
||||
type HostRuntime = 'node' | 'bun' | 'sandboxed-web' | 'python' | 'go' | 'rust' | 'electron' | 'other';
|
||||
type SubagentToolkit = 'full' | 'read-only';
|
||||
type DegradationLevel = 'full' | 'degraded' | 'absent';
|
||||
type InterfacePoint = 'command' | 'dispatch' | 'model' | 'hooks' | 'state' | 'artifact';
|
||||
|
||||
interface DispatchCapability {
|
||||
namedDispatch: boolean;
|
||||
nested: boolean;
|
||||
maxDepth: number;
|
||||
background: boolean;
|
||||
subagentToolkit: SubagentToolkit;
|
||||
}
|
||||
|
||||
interface HostIntegrationAxes {
|
||||
embeddingMode: EmbeddingMode;
|
||||
commandSurface: CommandSurface;
|
||||
dispatch: DispatchCapability;
|
||||
modelMode: ModelMode;
|
||||
hookBus: HookBus;
|
||||
stateIO: StateIO;
|
||||
transport: Transport;
|
||||
runtime: HostRuntime;
|
||||
}
|
||||
|
||||
interface DegradationResult {
|
||||
level: DegradationLevel;
|
||||
fallback: string;
|
||||
unknown?: boolean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Profile baselines
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Fail-closed floor: the most restrictive known value per axis, injected when a host omits an axis (degrade-closed, never assume capability).
|
||||
const SAFE_DEFAULTS: HostIntegrationAxes = {
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'prose-only',
|
||||
dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'read-only' },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'none',
|
||||
stateIO: 'session-log-append',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
};
|
||||
|
||||
const PROFILE_BASELINES: Readonly<Record<'programmatic-cli' | 'declarative-cli' | 'ide', HostIntegrationAxes>> =
|
||||
Object.freeze({
|
||||
'programmatic-cli': Object.freeze({
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' }),
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
} as HostIntegrationAxes),
|
||||
'declarative-cli': Object.freeze({
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full' }),
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
} as HostIntegrationAxes),
|
||||
'ide': Object.freeze({
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'palette',
|
||||
dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: 5, background: true, subagentToolkit: 'full' }),
|
||||
modelMode: 'active',
|
||||
hookBus: 'engine',
|
||||
stateIO: 'sandboxed-storage',
|
||||
transport: 'mcp',
|
||||
runtime: 'sandboxed-web',
|
||||
} as HostIntegrationAxes),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// degradationFor — plain data-table lookup (NOT clever code)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Look up the degradation level for a given interface point and partial axes.
|
||||
*
|
||||
* NEVER throws. Returns { level:'absent', fallback:'...', unknown:true } for
|
||||
* any missing or unrecognised axis value.
|
||||
*/
|
||||
function degradationFor(point: InterfacePoint, axes: Partial<HostIntegrationAxes>): DegradationResult {
|
||||
const UNKNOWN: DegradationResult = {
|
||||
level: 'absent',
|
||||
fallback: 'unknown capability — degraded closed',
|
||||
unknown: true,
|
||||
};
|
||||
|
||||
switch (point) {
|
||||
case 'command': {
|
||||
const cs = (axes as Record<string, unknown>).commandSurface;
|
||||
if (cs === 'slash-file' || cs === 'slash-programmatic') return { level: 'full', fallback: '' };
|
||||
if (cs === 'slash-toml' || cs === 'palette') return { level: 'degraded', fallback: 'toml/palette surface — limited command routing' };
|
||||
if (cs === 'prose-only') return { level: 'absent', fallback: 'AGENTS.md prose + skills menu' };
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
case 'dispatch': {
|
||||
const d = (axes as Record<string, unknown>).dispatch;
|
||||
if (!d || typeof d !== 'object') return UNKNOWN;
|
||||
const disp = d as Record<string, unknown>;
|
||||
if (disp.namedDispatch !== true || disp.maxDepth === 0) {
|
||||
return { level: 'absent', fallback: 'single-agent inline / SDK sub-session' };
|
||||
}
|
||||
// maxDepth < 0 means unbounded
|
||||
const isUnbounded = typeof disp.maxDepth === 'number' && Number.isFinite(disp.maxDepth) && disp.maxDepth < 0;
|
||||
const depth = (typeof disp.maxDepth === 'number' && Number.isFinite(disp.maxDepth)) ? disp.maxDepth : 0;
|
||||
const isFullDepth = isUnbounded || (disp.nested === true && depth >= 2);
|
||||
if (isFullDepth) {
|
||||
// Fail-closed: return 'full' ONLY when subagentToolkit is explicitly 'full';
|
||||
// any other value (read-only, undocumented, unknown, missing) → degraded.
|
||||
if (disp.subagentToolkit === 'full') {
|
||||
return { level: 'full', fallback: '' };
|
||||
}
|
||||
return { level: 'degraded', fallback: 'restricted/undocumented subagent toolkit — limited dispatch surface' };
|
||||
}
|
||||
// flat (maxDepth===1)
|
||||
return { level: 'degraded', fallback: 'flat dispatch — waves run inline' };
|
||||
}
|
||||
|
||||
case 'model': {
|
||||
const mm = (axes as Record<string, unknown>).modelMode;
|
||||
if (mm === 'active') return { level: 'full', fallback: '' };
|
||||
if (mm === 'passive') return { level: 'degraded', fallback: 'instruction-injection / per-agent model field' };
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
case 'hooks': {
|
||||
const hb = (axes as Record<string, unknown>).hookBus;
|
||||
if (hb === 'host') return { level: 'full', fallback: '' };
|
||||
if (hb === 'engine') return { level: 'degraded', fallback: 'engine-owned bus' };
|
||||
if (hb === 'none') return { level: 'absent', fallback: 'rule-text instructions' };
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
case 'state': {
|
||||
const si = (axes as Record<string, unknown>).stateIO;
|
||||
if (si === 'filesystem') return { level: 'full', fallback: '' };
|
||||
if (si === 'sandboxed-storage') return { level: 'degraded', fallback: 'sandboxed storage' };
|
||||
if (si === 'session-log-append') return { level: 'degraded', fallback: 'append-only session log' };
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
case 'artifact': {
|
||||
const cs = (axes as Record<string, unknown>).commandSurface;
|
||||
if (cs === 'slash-file' || cs === 'slash-programmatic') return { level: 'full', fallback: '' };
|
||||
if (cs === 'slash-toml' || cs === 'prose-only') return { level: 'degraded', fallback: 'menu / @-only' };
|
||||
if (cs === 'palette') return { level: 'absent', fallback: 'palette + chat participant; skills become LM tools' };
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
default:
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// profileOf
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Classify a partial set of integration axes into a named profile.
|
||||
* Returns null when no profile can be determined.
|
||||
*/
|
||||
function profileOf(axes: Partial<HostIntegrationAxes>): 'programmatic-cli' | 'declarative-cli' | 'ide' | null {
|
||||
const a = axes as Record<string, unknown>;
|
||||
if (a.embeddingMode === 'imperative' && a.runtime === 'sandboxed-web') return 'ide';
|
||||
if (a.embeddingMode === 'imperative') return 'programmatic-cli';
|
||||
if (a.embeddingMode === 'declarative') return 'declarative-cli';
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EngineCapabilities + DEFAULT_ENGINE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface EngineCapabilities {
|
||||
protocolVersion: number;
|
||||
axes: HostIntegrationAxes;
|
||||
known: typeof HOST_INTEGRATION_AXES;
|
||||
}
|
||||
|
||||
const DEFAULT_ENGINE: EngineCapabilities = {
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
axes: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
modelMode: 'active',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
},
|
||||
known: HOST_INTEGRATION_AXES,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NegotiationResult
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface NegotiationResult {
|
||||
protocolVersion: number;
|
||||
effective: HostIntegrationAxes;
|
||||
points: Record<InterfacePoint, { hostLevel: DegradationLevel; effectiveLevel: DegradationLevel; fallback: string }>;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// negotiateHostCapabilities
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Negotiate host integration capabilities against an engine.
|
||||
*
|
||||
* POST-CONDITION: every effective scalar axis value is in engine.known[axis].
|
||||
* effective never contains a value the host didn't declare AND the engine
|
||||
* cannot drive.
|
||||
*
|
||||
* NEVER throws. Returns a fresh object each call (mutation-safe).
|
||||
*/
|
||||
function negotiateHostCapabilities(
|
||||
host: Partial<HostIntegrationAxes> & { protocolVersion?: number },
|
||||
engine: EngineCapabilities = DEFAULT_ENGINE,
|
||||
): NegotiationResult {
|
||||
const warnings: string[] = [];
|
||||
const h = host as Record<string, unknown>;
|
||||
// Warn if protocolVersion is present but not a finite number
|
||||
if (h.protocolVersion !== undefined && (typeof h.protocolVersion !== 'number' || !Number.isFinite(h.protocolVersion))) {
|
||||
warnings.push(`host protocolVersion is not a finite number — using engine version ${engine.protocolVersion}`);
|
||||
}
|
||||
const hostPV = (typeof h.protocolVersion === 'number' && Number.isFinite(h.protocolVersion)) ? h.protocolVersion : engine.protocolVersion;
|
||||
const enginePV = engine.protocolVersion;
|
||||
|
||||
// Warn if host declares a newer protocol version
|
||||
if (hostPV > enginePV) {
|
||||
warnings.push(
|
||||
`host protocolVersion ${hostPV} newer than engine ${enginePV} — capabilities beyond version ${enginePV} not trusted`,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: negotiate a single scalar axis
|
||||
// ---------------------------------------------------------------------------
|
||||
function negotiateScalar<K extends keyof typeof HOST_INTEGRATION_AXES>(
|
||||
axis: K,
|
||||
): (typeof HOST_INTEGRATION_AXES)[K][number] {
|
||||
type V = (typeof HOST_INTEGRATION_AXES)[K][number];
|
||||
const knownValues: ReadonlyArray<V> = engine.known[axis];
|
||||
const hostVal = h[axis] as V | undefined;
|
||||
const engineVal = engine.axes[axis as keyof HostIntegrationAxes] as V;
|
||||
const safeDefault = SAFE_DEFAULTS[axis as keyof HostIntegrationAxes] as V;
|
||||
|
||||
if (hostVal === undefined || hostVal === null) {
|
||||
// Host did not declare this axis
|
||||
warnings.push(`host did not declare '${axis}'`);
|
||||
return safeDefault;
|
||||
}
|
||||
if ((hostVal as unknown) === UNDOCUMENTED) {
|
||||
// Host declared the undocumented sentinel — treat as fail-closed (degrade to safe default)
|
||||
warnings.push(`host axis '${axis}' is undocumented — degraded closed`);
|
||||
return safeDefault;
|
||||
}
|
||||
if (!knownValues.includes(hostVal)) {
|
||||
// Host declared an unknown/future value — NEVER copy into effective
|
||||
warnings.push(
|
||||
`host declared unknown '${axis}' value '${String(hostVal)}' — not trusted (host protocolVersion ${hostPV} vs engine ${enginePV})`,
|
||||
);
|
||||
return safeDefault;
|
||||
}
|
||||
// Engine capability cap: if the engine can't drive the host's value,
|
||||
// use the engine's lesser capability.
|
||||
// For modelMode: 'active' > 'passive' — if host wants active but engine
|
||||
// is passive, cap to passive.
|
||||
if (axis === 'modelMode') {
|
||||
if (hostVal === 'active' && engineVal === 'passive') return 'passive';
|
||||
}
|
||||
return hostVal;
|
||||
}
|
||||
|
||||
// Negotiate all scalar axes
|
||||
const effectiveEmbeddingMode = negotiateScalar('embeddingMode');
|
||||
const effectiveCommandSurface = negotiateScalar('commandSurface');
|
||||
const effectiveModelMode = negotiateScalar('modelMode');
|
||||
const effectiveHookBus = negotiateScalar('hookBus');
|
||||
const effectiveStateIO = negotiateScalar('stateIO');
|
||||
const effectiveTransport = negotiateScalar('transport');
|
||||
const effectiveRuntime = negotiateScalar('runtime');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dispatch struct negotiation
|
||||
// ---------------------------------------------------------------------------
|
||||
const hostDispatch = (typeof h.dispatch === 'object' && h.dispatch !== null)
|
||||
? h.dispatch as Record<string, unknown>
|
||||
: null;
|
||||
const engineDispatch = engine.axes.dispatch;
|
||||
|
||||
let effectiveNamedDispatch: boolean;
|
||||
let effectiveNested: boolean;
|
||||
let effectiveBackground: 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;
|
||||
} else {
|
||||
// N1: observability warnings for 'undocumented' sentinel on dispatch fields
|
||||
if (hostDispatch.namedDispatch === 'undocumented') {
|
||||
warnings.push(`dispatch.namedDispatch is undocumented — degraded closed`);
|
||||
}
|
||||
if (hostDispatch.nested === 'undocumented') {
|
||||
warnings.push(`dispatch.nested is undocumented — degraded closed`);
|
||||
}
|
||||
if (hostDispatch.background === 'undocumented') {
|
||||
warnings.push(`dispatch.background is undocumented — degraded closed`);
|
||||
}
|
||||
if (hostDispatch.subagentToolkit === 'undocumented') {
|
||||
warnings.push(`dispatch.subagentToolkit is undocumented — degraded closed (read-only)`);
|
||||
}
|
||||
|
||||
effectiveNamedDispatch = (hostDispatch.namedDispatch === true) && engineDispatch.namedDispatch;
|
||||
effectiveNested = (hostDispatch.nested === true) && engineDispatch.nested;
|
||||
effectiveBackground = (hostDispatch.background === true) && engineDispatch.background;
|
||||
|
||||
// subagentToolkit: fail closed to read-only unless explicitly 'full'
|
||||
// (an 'undocumented' or 'read-only' value → read-only)
|
||||
const hostToolkit = hostDispatch.subagentToolkit === 'full' ? 'full' : 'read-only';
|
||||
const engineToolkit = engineDispatch.subagentToolkit === 'read-only' ? 'read-only' : 'full';
|
||||
effectiveSubagentToolkit = (hostToolkit === 'read-only' || engineToolkit === 'read-only') ? 'read-only' : 'full';
|
||||
|
||||
// maxDepth: missing/non-number/non-finite → 0 + warning
|
||||
let hostMaxDepth: number;
|
||||
if (typeof hostDispatch.maxDepth !== 'number' || !Number.isFinite(hostDispatch.maxDepth)) {
|
||||
warnings.push(`host dispatch.maxDepth is missing or not a number — treating as 0`);
|
||||
hostMaxDepth = 0;
|
||||
} else {
|
||||
hostMaxDepth = hostDispatch.maxDepth;
|
||||
}
|
||||
|
||||
// Treat negative as +Infinity for the min, then if result is +Infinity emit -1
|
||||
const hDepthNum = hostMaxDepth < 0 ? Infinity : hostMaxDepth;
|
||||
const eDepthNum = engineDispatch.maxDepth < 0 ? Infinity : engineDispatch.maxDepth;
|
||||
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 (!effectiveNamedDispatch) {
|
||||
effectiveMaxDepth = 0;
|
||||
effectiveNested = false;
|
||||
effectiveBackground = false;
|
||||
}
|
||||
}
|
||||
|
||||
const effectiveDispatch: DispatchCapability = {
|
||||
namedDispatch: effectiveNamedDispatch,
|
||||
nested: effectiveNested,
|
||||
maxDepth: effectiveMaxDepth,
|
||||
background: effectiveBackground,
|
||||
subagentToolkit: effectiveSubagentToolkit,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Assemble effective axes
|
||||
// ---------------------------------------------------------------------------
|
||||
const effective: HostIntegrationAxes = {
|
||||
embeddingMode: effectiveEmbeddingMode,
|
||||
commandSurface: effectiveCommandSurface,
|
||||
dispatch: effectiveDispatch,
|
||||
modelMode: effectiveModelMode,
|
||||
hookBus: effectiveHookBus,
|
||||
stateIO: effectiveStateIO,
|
||||
transport: effectiveTransport,
|
||||
runtime: effectiveRuntime,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compute points (fresh objects — mutation-safe)
|
||||
// ---------------------------------------------------------------------------
|
||||
const points = {} as Record<InterfacePoint, { hostLevel: DegradationLevel; effectiveLevel: DegradationLevel; fallback: string }>;
|
||||
for (const point of INTERFACE_POINTS) {
|
||||
const hostDeg = degradationFor(point, host);
|
||||
const effectiveDeg = degradationFor(point, effective);
|
||||
points[point] = {
|
||||
hostLevel: hostDeg.level,
|
||||
effectiveLevel: effectiveDeg.level,
|
||||
fallback: effectiveDeg.fallback,
|
||||
};
|
||||
}
|
||||
|
||||
// protocolVersion: min of host and engine
|
||||
const resultProtocolVersion = Math.min(hostPV, enginePV);
|
||||
|
||||
return {
|
||||
protocolVersion: resultProtocolVersion,
|
||||
effective,
|
||||
points,
|
||||
warnings: [...warnings], // fresh copy
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module export (CommonJS — matches existing src/*.cts pattern)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export = {
|
||||
PROTOCOL_VERSION,
|
||||
UNDOCUMENTED,
|
||||
HOST_INTEGRATION_AXES,
|
||||
INTERFACE_POINTS,
|
||||
PROFILE_BASELINES,
|
||||
DEFAULT_ENGINE,
|
||||
degradationFor,
|
||||
profileOf,
|
||||
negotiateHostCapabilities,
|
||||
};
|
||||
@@ -86,6 +86,16 @@ function runtimeCap(overrides) {
|
||||
writesSharedSettings: false,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
hostIntegration: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
},
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
|
||||
@@ -1775,6 +1775,16 @@ describe('C3: role:runtime body validation', () => {
|
||||
writesSharedSettings: false,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
hostIntegration: {
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 2, background: false, subagentToolkit: 'full' },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3218,6 +3228,16 @@ function makeRuntimeCap(overrides) {
|
||||
writesSharedSettings: true,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
hostIntegration: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
},
|
||||
...((overrides && overrides.runtime) ? overrides.runtime : {}),
|
||||
},
|
||||
...overrides,
|
||||
@@ -4283,6 +4303,16 @@ describe('ADR-857 phase 5f: cross-field consistency gate rejection tests (DEFECT
|
||||
writesSharedSettings: true,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
hostIntegration: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
},
|
||||
},
|
||||
};
|
||||
if (overrides && typeof overrides === 'object') {
|
||||
@@ -5135,6 +5165,16 @@ describe('activationKey validation', () => {
|
||||
writesSharedSettings: false,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
hostIntegration: {
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 2, background: false, subagentToolkit: 'full' },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
},
|
||||
},
|
||||
};
|
||||
const errors = validateCapability(cap, 'cursor');
|
||||
|
||||
328
tests/host-integration-descriptors.test.cjs
Normal file
328
tests/host-integration-descriptors.test.cjs
Normal file
@@ -0,0 +1,328 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* ADR-1239 Phase A: Descriptor tests — validate that all 16 role:runtime
|
||||
* capability descriptors have correct hostIntegration axes, pass the validator,
|
||||
* and negotiate correctly via the host-integration module.
|
||||
*
|
||||
* Expectations are derived from the generated capability registry and
|
||||
* .host-cli-final.json (source of truth). Values are verbatim; 'undocumented'
|
||||
* sentinels fail-closed in negotiation (safe documented default, never propagate).
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
|
||||
const {
|
||||
negotiateHostCapabilities,
|
||||
profileOf,
|
||||
} = require(path.join(__dirname, '../gsd-core/bin/lib/host-integration.cjs'));
|
||||
|
||||
const registry = require(path.join(__dirname, '../gsd-core/bin/lib/capability-registry.cjs'));
|
||||
|
||||
const {
|
||||
validateCapability,
|
||||
} = require(path.join(__dirname, '../gsd-core/bin/lib/capability-validator.cjs'));
|
||||
|
||||
// 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 16 runtime IDs (ordered alphabetically)
|
||||
const RUNTIME_IDS = [
|
||||
'antigravity', 'augment', 'claude', 'cline', 'codebuddy',
|
||||
'codex', 'copilot', 'cursor', 'gemini', 'hermes',
|
||||
'kilo', 'kimi', 'opencode', 'qwen', 'trae', 'windsurf',
|
||||
];
|
||||
|
||||
// Contract-pinned profile split (derived from .host-cli-final.json):
|
||||
// programmatic-cli: claude, cline, cursor, hermes, kilo, kimi, opencode, qwen, trae (9)
|
||||
// declarative-cli: antigravity, augment, codebuddy, codex, copilot, gemini, windsurf (7)
|
||||
// ide: 0
|
||||
const EXPECTED_PROFILES = {
|
||||
claude: 'programmatic-cli',
|
||||
cline: 'programmatic-cli',
|
||||
cursor: 'programmatic-cli',
|
||||
hermes: 'programmatic-cli',
|
||||
kilo: 'programmatic-cli',
|
||||
kimi: 'programmatic-cli',
|
||||
opencode: 'programmatic-cli',
|
||||
qwen: 'programmatic-cli',
|
||||
trae: 'programmatic-cli',
|
||||
antigravity: 'declarative-cli',
|
||||
augment: 'declarative-cli',
|
||||
codebuddy: 'declarative-cli',
|
||||
codex: 'declarative-cli',
|
||||
copilot: 'declarative-cli',
|
||||
gemini: 'declarative-cli',
|
||||
windsurf: 'declarative-cli',
|
||||
};
|
||||
|
||||
describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
// ─── Registry shape ──────────────────────────────────────────────────────────
|
||||
|
||||
test('registry.runtimes contains all 16 expected runtime ids', () => {
|
||||
for (const id of RUNTIME_IDS) {
|
||||
assert.ok(
|
||||
Object.prototype.hasOwnProperty.call(registry.runtimes, id),
|
||||
'registry.runtimes must contain "' + id + '"',
|
||||
);
|
||||
}
|
||||
assert.strictEqual(
|
||||
Object.keys(registry.runtimes).length,
|
||||
16,
|
||||
'registry.runtimes must have exactly 16 entries',
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Per-runtime assertions ───────────────────────────────────────────────────
|
||||
|
||||
for (const id of RUNTIME_IDS) {
|
||||
describe('runtime: ' + id, () => {
|
||||
const cap = registry.runtimes[id];
|
||||
const hi = cap && cap.runtime && cap.runtime.hostIntegration;
|
||||
|
||||
// (i) Validator passes with zero errors
|
||||
test('(i) validateCapability returns zero errors', () => {
|
||||
const errors = validateCapability(cap, id);
|
||||
assert.deepEqual(
|
||||
errors,
|
||||
[],
|
||||
id + ': validateCapability must return no errors, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
|
||||
// (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', () => {
|
||||
assert.ok(
|
||||
hi !== undefined && hi !== null && typeof hi === 'object',
|
||||
id + ': cap.runtime.hostIntegration must be a non-null object',
|
||||
);
|
||||
// All 8 scalar axes present
|
||||
for (const axis of SCALAR_AXES) {
|
||||
assert.ok(
|
||||
Object.prototype.hasOwnProperty.call(hi, axis),
|
||||
id + ': hostIntegration must have axis "' + axis + '"',
|
||||
);
|
||||
}
|
||||
// dispatch is an object
|
||||
assert.ok(
|
||||
hi.dispatch !== null && typeof hi.dispatch === 'object',
|
||||
id + ': hostIntegration.dispatch must be a non-null object',
|
||||
);
|
||||
// All 5 dispatch sub-keys present
|
||||
for (const key of DISPATCH_KEYS) {
|
||||
assert.ok(
|
||||
Object.prototype.hasOwnProperty.call(hi.dispatch, key),
|
||||
id + ': hostIntegration.dispatch must have key "' + key + '"',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// (iii) negotiateHostCapabilities does not throw and behaves correctly
|
||||
test('(iii) negotiateHostCapabilities: documented scalars pass through; undocumented scalars degrade with warning', () => {
|
||||
assert.ok(hi, id + ': hostIntegration must exist to negotiate');
|
||||
let result;
|
||||
assert.doesNotThrow(() => {
|
||||
result = negotiateHostCapabilities(hi);
|
||||
}, id + ': negotiateHostCapabilities must not throw');
|
||||
|
||||
const eff = result.effective;
|
||||
|
||||
// For each scalar axis: if declared !== 'undocumented', effective === declared
|
||||
// If declared === 'undocumented', effective !== 'undocumented' (safe default) and
|
||||
// warnings must mention that axis.
|
||||
for (const axis of SCALAR_AXES) {
|
||||
const declared = hi[axis];
|
||||
if (declared !== 'undocumented') {
|
||||
assert.strictEqual(
|
||||
eff[axis],
|
||||
declared,
|
||||
id + ': effective.' + axis + ' must equal declared (' + JSON.stringify(declared) + '), got: ' + JSON.stringify(eff[axis]),
|
||||
);
|
||||
} else {
|
||||
// fail-closed: effective must be a documented safe default, not 'undocumented'
|
||||
assert.notStrictEqual(
|
||||
eff[axis],
|
||||
'undocumented',
|
||||
id + ': effective.' + axis + ' must NOT be "undocumented" (fail-closed)',
|
||||
);
|
||||
// warnings must mention this axis
|
||||
const mentionsAxis = result.warnings.some((w) => w.includes(axis));
|
||||
assert.ok(
|
||||
mentionsAxis,
|
||||
id + ': result.warnings must mention axis "' + axis + '" when declared is undocumented, got: ' + JSON.stringify(result.warnings),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// (iii-b) dispatch negotiation for namedDispatch
|
||||
test('(iii-b) dispatch.namedDispatch negotiation', () => {
|
||||
assert.ok(hi, id + ': hostIntegration must exist to negotiate');
|
||||
const result = negotiateHostCapabilities(hi);
|
||||
|
||||
const declaredND = hi.dispatch && hi.dispatch.namedDispatch;
|
||||
|
||||
if (declaredND === true) {
|
||||
// documented as true → effective must be true
|
||||
assert.strictEqual(
|
||||
result.effective.dispatch.namedDispatch,
|
||||
true,
|
||||
id + ': effective.dispatch.namedDispatch must be true when declared is true',
|
||||
);
|
||||
} else if (declaredND === 'undocumented') {
|
||||
// undocumented → fail-closed: effective namedDispatch must be false
|
||||
assert.strictEqual(
|
||||
result.effective.dispatch.namedDispatch,
|
||||
false,
|
||||
id + ': effective.dispatch.namedDispatch must be false when declared is "undocumented" (fail-closed)',
|
||||
);
|
||||
// dispatch.effectiveLevel must be 'absent' (no named dispatch)
|
||||
assert.strictEqual(
|
||||
result.points.dispatch.effectiveLevel,
|
||||
'absent',
|
||||
id + ': points.dispatch.effectiveLevel must be "absent" when namedDispatch is undocumented',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// (iv) profileOf returns expected profile
|
||||
test('(iv) profileOf returns expected profile', () => {
|
||||
assert.ok(hi, id + ': hostIntegration must exist to profile');
|
||||
const profile = profileOf(hi);
|
||||
assert.ok(
|
||||
profile !== null,
|
||||
id + ': profileOf must return a non-null profile',
|
||||
);
|
||||
assert.strictEqual(
|
||||
profile,
|
||||
EXPECTED_PROFILES[id],
|
||||
id + ': profileOf must return "' + EXPECTED_PROFILES[id] + '" (got: "' + profile + '")',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Contract-pin profile split ───────────────────────────────────────────────
|
||||
|
||||
test('contract-pin: exactly 9 programmatic-cli, 7 declarative-cli, 0 ide', () => {
|
||||
const counts = { 'programmatic-cli': 0, 'declarative-cli': 0, 'ide': 0 };
|
||||
for (const id of RUNTIME_IDS) {
|
||||
const cap = registry.runtimes[id];
|
||||
const hi = cap && cap.runtime && cap.runtime.hostIntegration;
|
||||
assert.ok(hi, id + ': hostIntegration must exist for profile count');
|
||||
const profile = profileOf(hi);
|
||||
assert.ok(profile !== null, id + ': profileOf must be non-null');
|
||||
if (counts[profile] !== undefined) {
|
||||
counts[profile]++;
|
||||
}
|
||||
}
|
||||
assert.strictEqual(counts['programmatic-cli'], 9, 'Must have exactly 9 programmatic-cli runtimes');
|
||||
assert.strictEqual(counts['declarative-cli'], 7, 'Must have exactly 7 declarative-cli runtimes');
|
||||
assert.strictEqual(counts['ide'], 0, 'Must have exactly 0 ide runtimes');
|
||||
});
|
||||
|
||||
test('contract-pin: spot-check claude→programmatic-cli, codex→declarative-cli, opencode→programmatic-cli, gemini→declarative-cli', () => {
|
||||
const checks = [
|
||||
['claude', 'programmatic-cli'],
|
||||
['codex', 'declarative-cli'],
|
||||
['opencode', 'programmatic-cli'],
|
||||
['gemini', 'declarative-cli'],
|
||||
];
|
||||
for (const [id, expectedProfile] of checks) {
|
||||
const cap = registry.runtimes[id];
|
||||
const hi = cap && cap.runtime && cap.runtime.hostIntegration;
|
||||
assert.ok(hi, id + ': hostIntegration must exist');
|
||||
const profile = profileOf(hi);
|
||||
assert.strictEqual(
|
||||
profile,
|
||||
expectedProfile,
|
||||
id + ': profileOf must return "' + expectedProfile + '" (got: "' + profile + '")',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── NEGATIVE cases ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('NEGATIVE: invalid hostIntegration.embeddingMode triggers validator error', () => {
|
||||
test('embeddingMode "bogus" produces a validator error naming embeddingMode', () => {
|
||||
const cap = {
|
||||
id: 'test-neg',
|
||||
role: 'runtime',
|
||||
version: '1.0.0',
|
||||
title: 'Test Negative',
|
||||
description: 'Negative case for hostIntegration validation.',
|
||||
tier: 'core',
|
||||
requires: [],
|
||||
runtime: {
|
||||
configHome: { kind: 'dot-home', name: '.test-neg', env: [] },
|
||||
configFormat: 'settings-json',
|
||||
artifactLayout: { global: [], local: [] },
|
||||
commandStyle: 'slash-hyphen',
|
||||
hooksSurface: 'settings-json',
|
||||
hookEvents: 'claude',
|
||||
sandboxTier: 'none',
|
||||
supportTier: 1,
|
||||
installSurface: 'settings-json',
|
||||
writesSharedSettings: true,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
hostIntegration: {
|
||||
embeddingMode: 'bogus',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full' },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
},
|
||||
},
|
||||
};
|
||||
const errors = validateCapability(cap, 'test-neg');
|
||||
assert.ok(errors.length > 0, 'Expected validation errors for bogus embeddingMode');
|
||||
assert.ok(
|
||||
errors.some((e) => e.includes('embeddingMode')),
|
||||
'At least one error must mention embeddingMode, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NEGATIVE: missing hostIntegration produces required-object error', () => {
|
||||
test('runtime body without hostIntegration produces the required-object error', () => {
|
||||
const cap = {
|
||||
id: 'test-missing-hi',
|
||||
role: 'runtime',
|
||||
version: '1.0.0',
|
||||
title: 'Test Missing HI',
|
||||
description: 'Negative case for missing hostIntegration.',
|
||||
tier: 'core',
|
||||
requires: [],
|
||||
runtime: {
|
||||
configHome: { kind: 'dot-home', name: '.test-missing-hi', env: [] },
|
||||
configFormat: 'settings-json',
|
||||
artifactLayout: { global: [], local: [] },
|
||||
commandStyle: 'slash-hyphen',
|
||||
hooksSurface: 'settings-json',
|
||||
hookEvents: 'claude',
|
||||
sandboxTier: 'none',
|
||||
supportTier: 1,
|
||||
installSurface: 'settings-json',
|
||||
writesSharedSettings: true,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
// hostIntegration intentionally absent
|
||||
},
|
||||
};
|
||||
const errors = validateCapability(cap, 'test-missing-hi');
|
||||
assert.ok(errors.length > 0, 'Expected validation errors for missing hostIntegration');
|
||||
assert.ok(
|
||||
errors.some((e) => e.includes('hostIntegration') && e.includes('required')),
|
||||
'At least one error must mention hostIntegration and required, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
356
tests/host-integration-validator-parity.test.cjs
Normal file
356
tests/host-integration-validator-parity.test.cjs
Normal file
@@ -0,0 +1,356 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* ADR-1239 Phase A: Parity guard — validator VALID_* sets MUST exactly match
|
||||
* HOST_INTEGRATION_AXES arrays from host-integration.cjs.
|
||||
*
|
||||
* If either side drifts, this test fails immediately (not silently).
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
|
||||
const {
|
||||
HOST_INTEGRATION_AXES,
|
||||
} = require(path.join(__dirname, '../gsd-core/bin/lib/host-integration.cjs'));
|
||||
|
||||
const {
|
||||
_HOST_INTEGRATION_VOCAB,
|
||||
validateCapability,
|
||||
} = require(path.join(__dirname, '../gsd-core/bin/lib/capability-validator.cjs'));
|
||||
|
||||
// Sort for deterministic comparison
|
||||
function sorted(arr) {
|
||||
return [...arr].sort();
|
||||
}
|
||||
|
||||
// Minimal valid runtime capability descriptor (all documented values)
|
||||
function makeMinimalRuntimeCap(overrides = {}) {
|
||||
return {
|
||||
id: 'test-runtime',
|
||||
role: 'runtime',
|
||||
title: 'Test Runtime',
|
||||
description: 'Test runtime capability for parity tests',
|
||||
tier: 'core',
|
||||
requires: [],
|
||||
version: '1.0.0',
|
||||
runtime: {
|
||||
configHome: {
|
||||
kind: 'dot-home',
|
||||
name: '.test-runtime',
|
||||
env: [],
|
||||
},
|
||||
configFormat: 'markdown',
|
||||
artifactLayout: {
|
||||
global: [],
|
||||
local: [],
|
||||
},
|
||||
commandStyle: 'slash-hyphen',
|
||||
hooksSurface: 'none',
|
||||
sandboxTier: 'none',
|
||||
supportTier: 1,
|
||||
installSurface: 'profile-marker-only',
|
||||
writesSharedSettings: false,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
hostIntegration: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
dispatch: {
|
||||
namedDispatch: true,
|
||||
nested: false,
|
||||
maxDepth: 1,
|
||||
background: false,
|
||||
subagentToolkit: 'full',
|
||||
},
|
||||
},
|
||||
...overrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('ADR-1239 Phase A: host-integration validator parity', () => {
|
||||
test('_HOST_INTEGRATION_VOCAB is exported from capability-validator.cjs', () => {
|
||||
assert.ok(
|
||||
_HOST_INTEGRATION_VOCAB !== undefined && _HOST_INTEGRATION_VOCAB !== null,
|
||||
'_HOST_INTEGRATION_VOCAB must be exported from capability-validator.cjs',
|
||||
);
|
||||
assert.strictEqual(typeof _HOST_INTEGRATION_VOCAB, 'object');
|
||||
});
|
||||
|
||||
test('embeddingMode: validator set === HOST_INTEGRATION_AXES.embeddingMode', () => {
|
||||
assert.deepEqual(
|
||||
sorted(_HOST_INTEGRATION_VOCAB.embeddingMode),
|
||||
sorted(HOST_INTEGRATION_AXES.embeddingMode),
|
||||
'validator VALID_EMBEDDING_MODES must exactly match HOST_INTEGRATION_AXES.embeddingMode',
|
||||
);
|
||||
});
|
||||
|
||||
test('commandSurface: validator set === HOST_INTEGRATION_AXES.commandSurface', () => {
|
||||
assert.deepEqual(
|
||||
sorted(_HOST_INTEGRATION_VOCAB.commandSurface),
|
||||
sorted(HOST_INTEGRATION_AXES.commandSurface),
|
||||
'validator VALID_COMMAND_SURFACES must exactly match HOST_INTEGRATION_AXES.commandSurface',
|
||||
);
|
||||
});
|
||||
|
||||
test('modelMode: validator set === HOST_INTEGRATION_AXES.modelMode', () => {
|
||||
assert.deepEqual(
|
||||
sorted(_HOST_INTEGRATION_VOCAB.modelMode),
|
||||
sorted(HOST_INTEGRATION_AXES.modelMode),
|
||||
'validator VALID_MODEL_MODES must exactly match HOST_INTEGRATION_AXES.modelMode',
|
||||
);
|
||||
});
|
||||
|
||||
test('hookBus: validator set === HOST_INTEGRATION_AXES.hookBus', () => {
|
||||
assert.deepEqual(
|
||||
sorted(_HOST_INTEGRATION_VOCAB.hookBus),
|
||||
sorted(HOST_INTEGRATION_AXES.hookBus),
|
||||
'validator VALID_HOOK_BUSES must exactly match HOST_INTEGRATION_AXES.hookBus',
|
||||
);
|
||||
});
|
||||
|
||||
test('stateIO: validator set === HOST_INTEGRATION_AXES.stateIO', () => {
|
||||
assert.deepEqual(
|
||||
sorted(_HOST_INTEGRATION_VOCAB.stateIO),
|
||||
sorted(HOST_INTEGRATION_AXES.stateIO),
|
||||
'validator VALID_STATE_IO must exactly match HOST_INTEGRATION_AXES.stateIO',
|
||||
);
|
||||
});
|
||||
|
||||
test('transport: validator set === HOST_INTEGRATION_AXES.transport', () => {
|
||||
assert.deepEqual(
|
||||
sorted(_HOST_INTEGRATION_VOCAB.transport),
|
||||
sorted(HOST_INTEGRATION_AXES.transport),
|
||||
'validator VALID_TRANSPORTS must exactly match HOST_INTEGRATION_AXES.transport',
|
||||
);
|
||||
});
|
||||
|
||||
test('runtime (axis): validator set === HOST_INTEGRATION_AXES.runtime (8 documented values)', () => {
|
||||
assert.deepEqual(
|
||||
sorted(_HOST_INTEGRATION_VOCAB.runtime),
|
||||
sorted(HOST_INTEGRATION_AXES.runtime),
|
||||
'validator VALID_HOST_RUNTIMES must exactly match HOST_INTEGRATION_AXES.runtime',
|
||||
);
|
||||
});
|
||||
|
||||
test('subagentToolkit: validator set === HOST_INTEGRATION_AXES.subagentToolkit', () => {
|
||||
assert.deepEqual(
|
||||
sorted(_HOST_INTEGRATION_VOCAB.subagentToolkit),
|
||||
sorted(HOST_INTEGRATION_AXES.subagentToolkit),
|
||||
'validator VALID_SUBAGENT_TOOLKITS must exactly match HOST_INTEGRATION_AXES.subagentToolkit',
|
||||
);
|
||||
});
|
||||
|
||||
test('all axis keys in HOST_INTEGRATION_AXES are covered by _HOST_INTEGRATION_VOCAB', () => {
|
||||
const axisKeys = Object.keys(HOST_INTEGRATION_AXES).sort();
|
||||
const vocabKeys = Object.keys(_HOST_INTEGRATION_VOCAB).sort();
|
||||
assert.deepEqual(
|
||||
vocabKeys,
|
||||
axisKeys,
|
||||
'_HOST_INTEGRATION_VOCAB must cover exactly the same axis keys as HOST_INTEGRATION_AXES',
|
||||
);
|
||||
});
|
||||
|
||||
test('_HOST_INTEGRATION_VOCAB does NOT include "undocumented" (documented vocab only)', () => {
|
||||
for (const [axis, values] of Object.entries(_HOST_INTEGRATION_VOCAB)) {
|
||||
assert.ok(
|
||||
!values.includes('undocumented'),
|
||||
`_HOST_INTEGRATION_VOCAB.${axis} must not include "undocumented" (sentinel is NOT documented vocab)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Behavioral: "undocumented" passes validator; bogus values still fail
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('ADR-1239 validator behavioral: undocumented sentinel passes, bogus fails', () => {
|
||||
const SCALAR_AXES = ['embeddingMode', 'commandSurface', 'modelMode', 'hookBus', 'stateIO', 'transport', 'runtime'];
|
||||
|
||||
for (const axis of SCALAR_AXES) {
|
||||
test(`hostIntegration.${axis}:"undocumented" → ZERO validator errors`, () => {
|
||||
const cap = makeMinimalRuntimeCap({
|
||||
hostIntegration: {
|
||||
...makeMinimalRuntimeCap().runtime.hostIntegration,
|
||||
[axis]: 'undocumented',
|
||||
},
|
||||
});
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const hiErrors = errors.filter((e) => e.includes('hostIntegration.' + axis));
|
||||
assert.strictEqual(hiErrors.length, 0,
|
||||
`"undocumented" for axis "${axis}" must produce no validator errors; got: ${hiErrors.join(', ')}`);
|
||||
});
|
||||
|
||||
test(`hostIntegration.${axis}:"zzz" → produces a validator error`, () => {
|
||||
const cap = makeMinimalRuntimeCap({
|
||||
hostIntegration: {
|
||||
...makeMinimalRuntimeCap().runtime.hostIntegration,
|
||||
[axis]: 'zzz',
|
||||
},
|
||||
});
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const hiErrors = errors.filter((e) => e.includes('hostIntegration.' + axis));
|
||||
assert.ok(hiErrors.length > 0,
|
||||
`bogus value "zzz" for axis "${axis}" must produce a validator error`);
|
||||
});
|
||||
}
|
||||
|
||||
test('dispatch.namedDispatch:"undocumented" → ZERO validator errors for that field', () => {
|
||||
const cap = makeMinimalRuntimeCap({
|
||||
hostIntegration: {
|
||||
...makeMinimalRuntimeCap().runtime.hostIntegration,
|
||||
dispatch: {
|
||||
namedDispatch: 'undocumented',
|
||||
nested: 'undocumented',
|
||||
maxDepth: 'undocumented',
|
||||
background: 'undocumented',
|
||||
subagentToolkit: 'undocumented',
|
||||
},
|
||||
},
|
||||
});
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const dispatchErrors = errors.filter((e) => e.includes('hostIntegration.dispatch'));
|
||||
assert.strictEqual(dispatchErrors.length, 0,
|
||||
`"undocumented" for all dispatch fields must produce no validator errors; got: ${dispatchErrors.join(', ')}`);
|
||||
});
|
||||
|
||||
test('dispatch boolean fields: true/false still accepted', () => {
|
||||
const cap = makeMinimalRuntimeCap();
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const dispatchErrors = errors.filter((e) => e.includes('hostIntegration.dispatch'));
|
||||
assert.strictEqual(dispatchErrors.length, 0,
|
||||
`Valid boolean dispatch fields must produce no errors; got: ${dispatchErrors.join(', ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fix 3: validator must reject reserved keys on hostIntegration and dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Fix 3: reserved-key guard on hostIntegration and hostIntegration.dispatch', () => {
|
||||
// Base valid runtime body (all documented values, claude layout)
|
||||
// We build it via JSON.parse to produce an own "__proto__" key that would
|
||||
// normally be swallowed by a spread (JSON.parse always produces own props).
|
||||
const BASE_RUNTIME_JSON = JSON.stringify({
|
||||
id: 'test-runtime',
|
||||
role: 'runtime',
|
||||
title: 'Test',
|
||||
description: 'Test runtime',
|
||||
tier: 'core',
|
||||
requires: [],
|
||||
version: '1.6.0',
|
||||
engines: { gsd: '>=1.6.0' },
|
||||
runtime: {
|
||||
configHome: { kind: 'dot-home', name: '.test', env: [] },
|
||||
configFormat: 'settings-json',
|
||||
artifactLayout: { global: [], local: [] },
|
||||
commandStyle: 'slash-hyphen',
|
||||
hooksSurface: 'settings-json',
|
||||
hookEvents: 'claude',
|
||||
sandboxTier: 'none',
|
||||
supportTier: 1,
|
||||
installSurface: 'settings-json',
|
||||
writesSharedSettings: true,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: ['SubagentStop', 'Stop', 'PreCompact', 'FileChanged'],
|
||||
hostIntegration: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: {
|
||||
namedDispatch: true,
|
||||
nested: true,
|
||||
maxDepth: 5,
|
||||
background: true,
|
||||
subagentToolkit: 'full',
|
||||
},
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
test('baseline (no reserved keys) → ZERO errors', () => {
|
||||
const cap = JSON.parse(BASE_RUNTIME_JSON);
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
assert.strictEqual(errors.length, 0,
|
||||
'Baseline with no reserved keys must produce zero errors; got: ' + errors.join(', '));
|
||||
});
|
||||
|
||||
test('hostIntegration with own __proto__ key → error mentioning "reserved key" and "__proto__"', () => {
|
||||
// Inject a raw __proto__ key via JSON string manipulation — JSON.parse gives it
|
||||
// as an OWN property (unlike { ..., __proto__: ... } which sets prototype chain).
|
||||
const json = BASE_RUNTIME_JSON.replace(
|
||||
'"hostIntegration":{',
|
||||
'"hostIntegration":{"__proto__":{"polluted":true},',
|
||||
);
|
||||
const cap = JSON.parse(json);
|
||||
// Verify the own-key is actually present (our assumption about JSON.parse)
|
||||
assert.ok(
|
||||
Object.prototype.hasOwnProperty.call(cap.runtime.hostIntegration, '__proto__'),
|
||||
'JSON.parse must produce an own __proto__ key on hostIntegration',
|
||||
);
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const reservedErrors = errors.filter((e) => e.includes('reserved key') && e.includes('__proto__'));
|
||||
assert.ok(reservedErrors.length > 0,
|
||||
'Must produce an error mentioning "reserved key" and "__proto__" for hostIntegration; got: ' + errors.join(', '));
|
||||
});
|
||||
|
||||
test('hostIntegration with own "constructor" key → error mentioning "reserved key" and "constructor"', () => {
|
||||
const json = BASE_RUNTIME_JSON.replace(
|
||||
'"hostIntegration":{',
|
||||
'"hostIntegration":{"constructor":"polluted",',
|
||||
);
|
||||
const cap = JSON.parse(json);
|
||||
assert.ok(
|
||||
Object.prototype.hasOwnProperty.call(cap.runtime.hostIntegration, 'constructor'),
|
||||
'JSON.parse must produce an own constructor key on hostIntegration',
|
||||
);
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const reservedErrors = errors.filter((e) => e.includes('reserved key') && e.includes('constructor'));
|
||||
assert.ok(reservedErrors.length > 0,
|
||||
'Must produce an error for "constructor" reserved key on hostIntegration; got: ' + errors.join(', '));
|
||||
});
|
||||
|
||||
test('hostIntegration.dispatch with own __proto__ key → error mentioning "reserved key" and "__proto__"', () => {
|
||||
const json = BASE_RUNTIME_JSON.replace(
|
||||
'"dispatch":{',
|
||||
'"dispatch":{"__proto__":{"polluted":true},',
|
||||
);
|
||||
const cap = JSON.parse(json);
|
||||
assert.ok(
|
||||
Object.prototype.hasOwnProperty.call(cap.runtime.hostIntegration.dispatch, '__proto__'),
|
||||
'JSON.parse must produce an own __proto__ key on dispatch',
|
||||
);
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const reservedErrors = errors.filter((e) => e.includes('reserved key') && e.includes('__proto__'));
|
||||
assert.ok(reservedErrors.length > 0,
|
||||
'Must produce an error for "__proto__" reserved key on dispatch; got: ' + errors.join(', '));
|
||||
});
|
||||
|
||||
test('hostIntegration.dispatch with own "prototype" key → error mentioning "reserved key" and "prototype"', () => {
|
||||
const json = BASE_RUNTIME_JSON.replace(
|
||||
'"dispatch":{',
|
||||
'"dispatch":{"prototype":{"polluted":true},',
|
||||
);
|
||||
const cap = JSON.parse(json);
|
||||
assert.ok(
|
||||
Object.prototype.hasOwnProperty.call(cap.runtime.hostIntegration.dispatch, 'prototype'),
|
||||
'JSON.parse must produce an own prototype key on dispatch',
|
||||
);
|
||||
const errors = validateCapability(cap, 'test-runtime');
|
||||
const reservedErrors = errors.filter((e) => e.includes('reserved key') && e.includes('prototype'));
|
||||
assert.ok(reservedErrors.length > 0,
|
||||
'Must produce an error for "prototype" reserved key on dispatch; got: ' + errors.join(', '));
|
||||
});
|
||||
});
|
||||
888
tests/host-integration.test.cjs
Normal file
888
tests/host-integration.test.cjs
Normal file
@@ -0,0 +1,888 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Unit tests for host-integration.cjs (ADR-1239 Phase A).
|
||||
* Pure, additive, no-I/O module — no temp dirs needed.
|
||||
* Uses node:test + node:assert/strict.
|
||||
* Requires the COMPILED artifact: ../gsd-core/bin/lib/host-integration.cjs
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const hi = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
const {
|
||||
PROTOCOL_VERSION,
|
||||
HOST_INTEGRATION_AXES,
|
||||
INTERFACE_POINTS,
|
||||
PROFILE_BASELINES,
|
||||
DEFAULT_ENGINE,
|
||||
UNDOCUMENTED,
|
||||
degradationFor,
|
||||
profileOf,
|
||||
negotiateHostCapabilities,
|
||||
} = hi;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CONTRACT-PIN: constants and vocabulary
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('CONTRACT-PIN', () => {
|
||||
test('PROTOCOL_VERSION === 1', () => {
|
||||
assert.strictEqual(PROTOCOL_VERSION, 1);
|
||||
});
|
||||
|
||||
test('HOST_INTEGRATION_AXES is frozen', () => {
|
||||
assert.ok(Object.isFrozen(HOST_INTEGRATION_AXES), 'HOST_INTEGRATION_AXES must be frozen');
|
||||
});
|
||||
|
||||
test('each axis sub-array is frozen', () => {
|
||||
for (const [axis, arr] of Object.entries(HOST_INTEGRATION_AXES)) {
|
||||
assert.ok(Object.isFrozen(arr), `HOST_INTEGRATION_AXES.${axis} must be frozen`);
|
||||
}
|
||||
});
|
||||
|
||||
test('embeddingMode values (sorted)', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.embeddingMode].sort(),
|
||||
['declarative', 'imperative'],
|
||||
);
|
||||
});
|
||||
|
||||
test('commandSurface values (sorted)', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.commandSurface].sort(),
|
||||
['palette', 'prose-only', 'slash-file', 'slash-programmatic', 'slash-toml'],
|
||||
);
|
||||
});
|
||||
|
||||
test('modelMode values (sorted)', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.modelMode].sort(),
|
||||
['active', 'passive'],
|
||||
);
|
||||
});
|
||||
|
||||
test('hookBus values (sorted)', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.hookBus].sort(),
|
||||
['engine', 'host', 'none'],
|
||||
);
|
||||
});
|
||||
|
||||
test('stateIO values (sorted)', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.stateIO].sort(),
|
||||
['filesystem', 'sandboxed-storage', 'session-log-append'],
|
||||
);
|
||||
});
|
||||
|
||||
test('transport values (sorted)', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.transport].sort(),
|
||||
['mcp', 'native-extension'],
|
||||
);
|
||||
});
|
||||
|
||||
test('runtime values (sorted) — 8 documented values', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.runtime].sort(),
|
||||
['bun', 'electron', 'go', 'node', 'other', 'python', 'rust', 'sandboxed-web'],
|
||||
);
|
||||
});
|
||||
|
||||
test('UNDOCUMENTED === "undocumented"', () => {
|
||||
assert.equal(UNDOCUMENTED, 'undocumented');
|
||||
});
|
||||
|
||||
test('subagentToolkit values (sorted)', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.subagentToolkit].sort(),
|
||||
['full', 'read-only'],
|
||||
);
|
||||
});
|
||||
|
||||
test('INTERFACE_POINTS frozen and contains expected values', () => {
|
||||
assert.ok(Object.isFrozen(INTERFACE_POINTS), 'INTERFACE_POINTS must be frozen');
|
||||
const expected = ['command', 'dispatch', 'model', 'hooks', 'state', 'artifact'].sort();
|
||||
assert.deepStrictEqual([...INTERFACE_POINTS].sort(), expected);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// degradationFor — happy path per enum value
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('degradationFor — happy path', () => {
|
||||
test('command: slash-file → full', () => {
|
||||
const r = degradationFor('command', { commandSurface: 'slash-file' });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
assert.strictEqual(typeof r.fallback, 'string');
|
||||
});
|
||||
|
||||
test('command: slash-programmatic → full', () => {
|
||||
const r = degradationFor('command', { commandSurface: 'slash-programmatic' });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('command: slash-toml → degraded', () => {
|
||||
const r = degradationFor('command', { commandSurface: 'slash-toml' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('command: palette → degraded', () => {
|
||||
const r = degradationFor('command', { commandSurface: 'palette' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('command: prose-only → absent', () => {
|
||||
const r = degradationFor('command', { commandSurface: 'prose-only' });
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
assert.ok(r.fallback.length > 0, 'fallback must be non-empty for prose-only');
|
||||
});
|
||||
|
||||
test('model: active → full', () => {
|
||||
const r = degradationFor('model', { modelMode: 'active' });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('model: passive → degraded', () => {
|
||||
const r = degradationFor('model', { modelMode: 'passive' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('hooks: host → full', () => {
|
||||
const r = degradationFor('hooks', { hookBus: 'host' });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('hooks: engine → degraded', () => {
|
||||
const r = degradationFor('hooks', { hookBus: 'engine' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('hooks: none → absent', () => {
|
||||
const r = degradationFor('hooks', { hookBus: 'none' });
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
});
|
||||
|
||||
test('state: filesystem → full', () => {
|
||||
const r = degradationFor('state', { stateIO: 'filesystem' });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('state: sandboxed-storage → degraded', () => {
|
||||
const r = degradationFor('state', { stateIO: 'sandboxed-storage' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('state: session-log-append → degraded', () => {
|
||||
const r = degradationFor('state', { stateIO: 'session-log-append' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('artifact: slash-file → full', () => {
|
||||
const r = degradationFor('artifact', { commandSurface: 'slash-file' });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('artifact: slash-programmatic → full', () => {
|
||||
const r = degradationFor('artifact', { commandSurface: 'slash-programmatic' });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('artifact: slash-toml → degraded', () => {
|
||||
const r = degradationFor('artifact', { commandSurface: 'slash-toml' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('artifact: prose-only → degraded', () => {
|
||||
const r = degradationFor('artifact', { commandSurface: 'prose-only' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('artifact: palette → absent', () => {
|
||||
const r = degradationFor('artifact', { commandSurface: 'palette' });
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
});
|
||||
|
||||
// dispatch variants
|
||||
test('dispatch: no namedDispatch → absent', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'full' } });
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
});
|
||||
|
||||
test('dispatch: maxDepth===0 → absent', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { namedDispatch: true, nested: true, maxDepth: 0, background: true, subagentToolkit: 'full' } });
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
});
|
||||
|
||||
test('dispatch: unbounded (-1) nested → full', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' } });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('dispatch: nested maxDepth>=2 → full', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { namedDispatch: true, nested: true, maxDepth: 2, background: true, subagentToolkit: 'full' } });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('dispatch: full but subagentToolkit read-only → degraded', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'read-only' } });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('dispatch: flat (maxDepth===1) → degraded', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full' } });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// degradationFor — EVERY enum value returns a defined result with valid level
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('degradationFor — all enum values return valid level', () => {
|
||||
const VALID_LEVELS = new Set(['full', 'degraded', 'absent']);
|
||||
|
||||
test('command — all commandSurface values', () => {
|
||||
for (const v of HOST_INTEGRATION_AXES.commandSurface) {
|
||||
const r = degradationFor('command', { commandSurface: v });
|
||||
assert.ok(VALID_LEVELS.has(r.level), `command/${v}: level '${r.level}' invalid`);
|
||||
assert.strictEqual(typeof r.fallback, 'string');
|
||||
}
|
||||
});
|
||||
|
||||
test('model — all modelMode values', () => {
|
||||
for (const v of HOST_INTEGRATION_AXES.modelMode) {
|
||||
const r = degradationFor('model', { modelMode: v });
|
||||
assert.ok(VALID_LEVELS.has(r.level), `model/${v}: level '${r.level}' invalid`);
|
||||
}
|
||||
});
|
||||
|
||||
test('hooks — all hookBus values', () => {
|
||||
for (const v of HOST_INTEGRATION_AXES.hookBus) {
|
||||
const r = degradationFor('hooks', { hookBus: v });
|
||||
assert.ok(VALID_LEVELS.has(r.level), `hooks/${v}: level '${r.level}' invalid`);
|
||||
}
|
||||
});
|
||||
|
||||
test('state — all stateIO values', () => {
|
||||
for (const v of HOST_INTEGRATION_AXES.stateIO) {
|
||||
const r = degradationFor('state', { stateIO: v });
|
||||
assert.ok(VALID_LEVELS.has(r.level), `state/${v}: level '${r.level}' invalid`);
|
||||
}
|
||||
});
|
||||
|
||||
test('artifact — all commandSurface values', () => {
|
||||
for (const v of HOST_INTEGRATION_AXES.commandSurface) {
|
||||
const r = degradationFor('artifact', { commandSurface: v });
|
||||
assert.ok(VALID_LEVELS.has(r.level), `artifact/${v}: level '${r.level}' invalid`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// degradationFor — unknown / missing axis → absent + unknown:true, never throws
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('degradationFor — unknown / missing axis', () => {
|
||||
test('unknown commandSurface value for command → absent + unknown:true', () => {
|
||||
const r = degradationFor('command', { commandSurface: 'zzz' });
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
assert.strictEqual(r.unknown, true);
|
||||
});
|
||||
|
||||
test('missing commandSurface for command → absent + unknown:true', () => {
|
||||
const r = degradationFor('command', {});
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
assert.strictEqual(r.unknown, true);
|
||||
});
|
||||
|
||||
test('unknown modelMode → absent + unknown:true', () => {
|
||||
const r = degradationFor('model', { modelMode: 'zzz' });
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
assert.strictEqual(r.unknown, true);
|
||||
});
|
||||
|
||||
test('missing hookBus for hooks → absent + unknown:true', () => {
|
||||
const r = degradationFor('hooks', {});
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
assert.strictEqual(r.unknown, true);
|
||||
});
|
||||
|
||||
test('no throw on unknown axis value', () => {
|
||||
assert.doesNotThrow(() => degradationFor('dispatch', { dispatch: 'not-an-object' }));
|
||||
});
|
||||
|
||||
test('no throw on completely empty axes', () => {
|
||||
for (const point of INTERFACE_POINTS) {
|
||||
assert.doesNotThrow(() => degradationFor(point, {}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// profileOf
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('profileOf', () => {
|
||||
test('profileOf(PROFILE_BASELINES["programmatic-cli"]) === "programmatic-cli"', () => {
|
||||
assert.strictEqual(profileOf(PROFILE_BASELINES['programmatic-cli']), 'programmatic-cli');
|
||||
});
|
||||
|
||||
test('profileOf(PROFILE_BASELINES["declarative-cli"]) === "declarative-cli"', () => {
|
||||
assert.strictEqual(profileOf(PROFILE_BASELINES['declarative-cli']), 'declarative-cli');
|
||||
});
|
||||
|
||||
test('profileOf(PROFILE_BASELINES["ide"]) === "ide"', () => {
|
||||
assert.strictEqual(profileOf(PROFILE_BASELINES['ide']), 'ide');
|
||||
});
|
||||
|
||||
test('imperative + sandboxed-web → ide', () => {
|
||||
assert.strictEqual(
|
||||
profileOf({ embeddingMode: 'imperative', runtime: 'sandboxed-web' }),
|
||||
'ide',
|
||||
);
|
||||
});
|
||||
|
||||
test('imperative + node → programmatic-cli', () => {
|
||||
assert.strictEqual(
|
||||
profileOf({ embeddingMode: 'imperative', runtime: 'node' }),
|
||||
'programmatic-cli',
|
||||
);
|
||||
});
|
||||
|
||||
test('declarative → declarative-cli', () => {
|
||||
assert.strictEqual(
|
||||
profileOf({ embeddingMode: 'declarative' }),
|
||||
'declarative-cli',
|
||||
);
|
||||
});
|
||||
|
||||
test('empty axes → null', () => {
|
||||
assert.strictEqual(profileOf({}), null);
|
||||
});
|
||||
|
||||
test('PROFILE_BASELINES are frozen', () => {
|
||||
assert.ok(Object.isFrozen(PROFILE_BASELINES), 'PROFILE_BASELINES must be frozen');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// negotiateHostCapabilities — HAPPY PATH
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('negotiateHostCapabilities — happy path', () => {
|
||||
test('declarative-cli baseline → effective matches, no warnings, points.command.effectiveLevel===full', () => {
|
||||
const baseline = PROFILE_BASELINES['declarative-cli'];
|
||||
const result = negotiateHostCapabilities(baseline);
|
||||
|
||||
// No warnings
|
||||
assert.deepStrictEqual(result.warnings, [], 'Expected no warnings for full declarative-cli baseline');
|
||||
|
||||
// Key points
|
||||
assert.strictEqual(result.points.command.effectiveLevel, 'full');
|
||||
assert.strictEqual(result.points.hooks.effectiveLevel, 'full');
|
||||
assert.strictEqual(result.points.state.effectiveLevel, 'full');
|
||||
|
||||
// protocolVersion
|
||||
assert.strictEqual(result.protocolVersion, PROTOCOL_VERSION);
|
||||
|
||||
// effective axes match baseline (scalar)
|
||||
assert.strictEqual(result.effective.embeddingMode, baseline.embeddingMode);
|
||||
assert.strictEqual(result.effective.commandSurface, baseline.commandSurface);
|
||||
assert.strictEqual(result.effective.modelMode, baseline.modelMode);
|
||||
assert.strictEqual(result.effective.hookBus, baseline.hookBus);
|
||||
assert.strictEqual(result.effective.stateIO, baseline.stateIO);
|
||||
|
||||
// effective dispatch has maxDepth resolved (declarative has maxDepth:1)
|
||||
assert.strictEqual(result.effective.dispatch.maxDepth, 1);
|
||||
assert.strictEqual(result.effective.dispatch.namedDispatch, true);
|
||||
});
|
||||
|
||||
test('all INTERFACE_POINTS are present in result.points', () => {
|
||||
const result = negotiateHostCapabilities(PROFILE_BASELINES['programmatic-cli']);
|
||||
for (const point of INTERFACE_POINTS) {
|
||||
assert.ok(point in result.points, `Missing point: ${point}`);
|
||||
assert.ok(['full', 'degraded', 'absent'].includes(result.points[point].effectiveLevel),
|
||||
`Invalid effectiveLevel for ${point}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// negotiateHostCapabilities — SECURITY / HOSTILE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('negotiateHostCapabilities — security / hostile', () => {
|
||||
test('(1) host declares future commandSurface at protocolVersion 99 → effective is KNOWN value, NOT the unknown one', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
commandSurface: 'future-surface',
|
||||
protocolVersion: 99,
|
||||
});
|
||||
// effective.commandSurface must be a KNOWN value
|
||||
assert.ok(
|
||||
HOST_INTEGRATION_AXES.commandSurface.includes(result.effective.commandSurface),
|
||||
`effective.commandSurface '${result.effective.commandSurface}' is not in known vocabulary`,
|
||||
);
|
||||
assert.notStrictEqual(result.effective.commandSurface, 'future-surface',
|
||||
'future-surface must NOT appear in effective');
|
||||
// A warning mentioning protocolVersion
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('protocolVersion') || warnText.includes('unknown'),
|
||||
`Expected a warning about protocolVersion or unknown value; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('(2) host modelMode active but engine passive → effective.modelMode === passive', () => {
|
||||
const restrictedEngine = {
|
||||
...DEFAULT_ENGINE,
|
||||
axes: { ...DEFAULT_ENGINE.axes, modelMode: 'passive' },
|
||||
};
|
||||
const result = negotiateHostCapabilities(
|
||||
{ ...PROFILE_BASELINES['programmatic-cli'], modelMode: 'active' },
|
||||
restrictedEngine,
|
||||
);
|
||||
assert.strictEqual(result.effective.modelMode, 'passive');
|
||||
});
|
||||
|
||||
test('(3) host dispatch maxDepth:5 nested:true but engine dispatch maxDepth:1 → effective.dispatch.maxDepth===1', () => {
|
||||
const restrictedEngine = {
|
||||
...DEFAULT_ENGINE,
|
||||
axes: {
|
||||
...DEFAULT_ENGINE.axes,
|
||||
dispatch: { ...DEFAULT_ENGINE.axes.dispatch, maxDepth: 1, nested: false },
|
||||
},
|
||||
};
|
||||
const result = negotiateHostCapabilities(
|
||||
{
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 5, background: true, subagentToolkit: 'full' },
|
||||
},
|
||||
restrictedEngine,
|
||||
);
|
||||
assert.strictEqual(result.effective.dispatch.maxDepth, 1);
|
||||
});
|
||||
|
||||
test('(4) host omits hookBus → effective.hookBus is safe default + warning present', () => {
|
||||
const hostWithoutHookBus = { ...PROFILE_BASELINES['declarative-cli'] };
|
||||
delete hostWithoutHookBus.hookBus;
|
||||
|
||||
const result = negotiateHostCapabilities(hostWithoutHookBus);
|
||||
// effective hookBus must be a known value
|
||||
assert.ok(
|
||||
HOST_INTEGRATION_AXES.hookBus.includes(result.effective.hookBus),
|
||||
`effective.hookBus '${result.effective.hookBus}' is not known`,
|
||||
);
|
||||
// points.hooks must be present
|
||||
assert.ok('hooks' in result.points, 'points.hooks must be present');
|
||||
// a warning mentioning hookBus
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('hookBus'), `Expected warning about hookBus; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('(5) INVARIANT: every effective scalar ∈ engine.known[axis] for hostile hosts', () => {
|
||||
const hostileHosts = [
|
||||
// All unknown values
|
||||
{
|
||||
embeddingMode: 'future-mode',
|
||||
commandSurface: 'future-surface',
|
||||
modelMode: 'quantum',
|
||||
hookBus: 'blockchain',
|
||||
stateIO: 'cloud-magic',
|
||||
transport: 'telepathy',
|
||||
runtime: 'wasm',
|
||||
protocolVersion: 999,
|
||||
},
|
||||
// Mix of known and unknown
|
||||
{
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'palette',
|
||||
modelMode: 'active',
|
||||
hookBus: 'none',
|
||||
stateIO: 'unknown-future',
|
||||
transport: 'mcp',
|
||||
runtime: 'sandboxed-web',
|
||||
},
|
||||
// Empty host
|
||||
{},
|
||||
// Only dispatch with extreme values
|
||||
{
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 9999, background: true, subagentToolkit: 'full' },
|
||||
},
|
||||
];
|
||||
|
||||
const scalarAxes = ['embeddingMode', 'commandSurface', 'modelMode', 'hookBus', 'stateIO', 'transport', 'runtime'];
|
||||
|
||||
for (const host of hostileHosts) {
|
||||
const result = negotiateHostCapabilities(host);
|
||||
for (const axis of scalarAxes) {
|
||||
const effectiveVal = result.effective[axis];
|
||||
assert.ok(
|
||||
HOST_INTEGRATION_AXES[axis].includes(effectiveVal),
|
||||
`INVARIANT VIOLATION: effective.${axis}='${effectiveVal}' is NOT in known vocabulary for host=${JSON.stringify(host)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('host protocolVersion > engine → warning mentioning protocolVersion', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
...PROFILE_BASELINES['declarative-cli'],
|
||||
protocolVersion: 99,
|
||||
});
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('protocolVersion'), `Expected protocolVersion warning; got: ${warnText}`);
|
||||
assert.strictEqual(result.protocolVersion, PROTOCOL_VERSION);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// INDEPENDENCE: mutation safety
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('independence / mutation safety', () => {
|
||||
test('mutating returned result does not affect second call', () => {
|
||||
const host = PROFILE_BASELINES['declarative-cli'];
|
||||
const r1 = negotiateHostCapabilities(host);
|
||||
// Mutate r1
|
||||
r1.warnings.push('injected');
|
||||
r1.effective.modelMode = 'active';
|
||||
r1.points.command.effectiveLevel = 'absent';
|
||||
|
||||
const r2 = negotiateHostCapabilities(host);
|
||||
// r2 must not be affected
|
||||
assert.deepStrictEqual(r2.warnings, [], 'r2.warnings must not include injected warning');
|
||||
assert.strictEqual(r2.effective.modelMode, host.modelMode, 'r2.effective.modelMode must be original value');
|
||||
assert.strictEqual(r2.points.command.effectiveLevel, 'full', 'r2.points.command.effectiveLevel must be full');
|
||||
});
|
||||
|
||||
test('all exports are present on the module', () => {
|
||||
const expectedExports = [
|
||||
'PROTOCOL_VERSION', 'HOST_INTEGRATION_AXES', 'INTERFACE_POINTS',
|
||||
'PROFILE_BASELINES', 'DEFAULT_ENGINE', 'UNDOCUMENTED',
|
||||
'degradationFor', 'profileOf', 'negotiateHostCapabilities',
|
||||
];
|
||||
for (const exp of expectedExports) {
|
||||
assert.ok(exp in hi, `Missing export: ${exp}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Decision 1: undocumented sentinel — fail-closed in negotiation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Decision 1: UNDOCUMENTED sentinel — fail-closed negotiation', () => {
|
||||
test('negotiate with embeddingMode:"undocumented" → effective is safe default (documented value), NOT "undocumented"', () => {
|
||||
const host = {
|
||||
...PROFILE_BASELINES['declarative-cli'],
|
||||
embeddingMode: 'undocumented',
|
||||
};
|
||||
const result = negotiateHostCapabilities(host);
|
||||
// effective.embeddingMode must be a documented value, NOT 'undocumented'
|
||||
assert.ok(
|
||||
HOST_INTEGRATION_AXES.embeddingMode.includes(result.effective.embeddingMode),
|
||||
`effective.embeddingMode must be a documented value; got '${result.effective.embeddingMode}'`,
|
||||
);
|
||||
assert.notStrictEqual(result.effective.embeddingMode, 'undocumented',
|
||||
'effective.embeddingMode must not be "undocumented"');
|
||||
// A warning mentioning "undocumented"
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('undocumented'),
|
||||
`Expected a warning mentioning "undocumented"; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('negotiate with dispatch fields all "undocumented" → fail-closed dispatch', () => {
|
||||
const host = {
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
dispatch: {
|
||||
namedDispatch: 'undocumented',
|
||||
nested: 'undocumented',
|
||||
maxDepth: 'undocumented',
|
||||
background: 'undocumented',
|
||||
subagentToolkit: 'undocumented',
|
||||
},
|
||||
};
|
||||
const result = negotiateHostCapabilities(host);
|
||||
const d = result.effective.dispatch;
|
||||
assert.strictEqual(d.namedDispatch, false, 'namedDispatch must be false when "undocumented"');
|
||||
assert.strictEqual(d.nested, false, 'nested must be false when "undocumented"');
|
||||
assert.strictEqual(d.background, false, 'background must be false when "undocumented"');
|
||||
assert.strictEqual(d.subagentToolkit, 'read-only', 'subagentToolkit must be "read-only" when "undocumented"');
|
||||
assert.strictEqual(d.maxDepth, 0, 'maxDepth must be 0 when "undocumented"');
|
||||
// points.dispatch must be absent
|
||||
assert.strictEqual(result.points.dispatch.effectiveLevel, 'absent',
|
||||
'points.dispatch.effectiveLevel must be "absent" when dispatch is all undocumented');
|
||||
});
|
||||
|
||||
test('degradationFor dispatch with namedDispatch:"undocumented" → level "absent"', () => {
|
||||
const r = degradationFor('dispatch', {
|
||||
dispatch: {
|
||||
namedDispatch: 'undocumented',
|
||||
nested: false,
|
||||
maxDepth: 0,
|
||||
background: false,
|
||||
subagentToolkit: 'full',
|
||||
},
|
||||
});
|
||||
assert.strictEqual(r.level, 'absent',
|
||||
`degradationFor with namedDispatch:"undocumented" must return absent; got "${r.level}"`);
|
||||
});
|
||||
|
||||
test('subagentToolkit "undocumented" (truthy string) fails closed to read-only', () => {
|
||||
const host = {
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
dispatch: {
|
||||
namedDispatch: true,
|
||||
nested: true,
|
||||
maxDepth: -1,
|
||||
background: true,
|
||||
subagentToolkit: 'undocumented',
|
||||
},
|
||||
};
|
||||
const result = negotiateHostCapabilities(host);
|
||||
assert.strictEqual(result.effective.dispatch.subagentToolkit, 'read-only',
|
||||
'subagentToolkit "undocumented" must degrade to "read-only"');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Decision 2: expanded runtime vocabulary (8 documented values)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Decision 2: expanded runtime vocabulary', () => {
|
||||
const newRuntimes = ['python', 'go', 'rust', 'electron', 'other'];
|
||||
|
||||
for (const rt of newRuntimes) {
|
||||
test(`negotiate with runtime:"${rt}" → effective.runtime === "${rt}" (no warn about unknown)`, () => {
|
||||
const host = {
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
runtime: rt,
|
||||
};
|
||||
const result = negotiateHostCapabilities(host);
|
||||
assert.strictEqual(result.effective.runtime, rt,
|
||||
`effective.runtime must be "${rt}"; got "${result.effective.runtime}"`);
|
||||
// Must NOT have an unknown-value warning for this runtime
|
||||
const runtimeWarnings = result.warnings.filter((w) => w.includes('runtime') && w.includes('not trusted'));
|
||||
assert.strictEqual(runtimeWarnings.length, 0,
|
||||
`Must not warn about unknown runtime "${rt}"; warnings: ${result.warnings.join(', ')}`);
|
||||
});
|
||||
}
|
||||
|
||||
test('runtime "undocumented" (sentinel) → fail-closed to safe default', () => {
|
||||
const host = {
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
runtime: 'undocumented',
|
||||
};
|
||||
const result = negotiateHostCapabilities(host);
|
||||
// Must be a documented value, not "undocumented"
|
||||
assert.ok(
|
||||
HOST_INTEGRATION_AXES.runtime.includes(result.effective.runtime),
|
||||
`effective.runtime must be documented; got "${result.effective.runtime}"`,
|
||||
);
|
||||
assert.notStrictEqual(result.effective.runtime, 'undocumented');
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('undocumented'), `Expected undocumented warning; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('"wasm" (genuinely unknown, not sentinel) → still fails closed with "not trusted" warning', () => {
|
||||
const host = {
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
runtime: 'wasm',
|
||||
};
|
||||
const result = negotiateHostCapabilities(host);
|
||||
assert.ok(HOST_INTEGRATION_AXES.runtime.includes(result.effective.runtime),
|
||||
`effective.runtime must be documented; got "${result.effective.runtime}"`);
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('not trusted') || warnText.includes('unknown'),
|
||||
`Expected not-trusted/unknown warning; got: ${warnText}`);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fix 1: degradationFor('dispatch') fail-closed on non-'full' subagentToolkit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Fix 1: degradationFor dispatch fails closed on non-full subagentToolkit', () => {
|
||||
const FULL_DEPTH_DISPATCH = { namedDispatch: true, nested: true, maxDepth: -1, background: true };
|
||||
|
||||
test('subagentToolkit:"full" + full depth → level "full"', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { ...FULL_DEPTH_DISPATCH, subagentToolkit: 'full' } });
|
||||
assert.strictEqual(r.level, 'full',
|
||||
'subagentToolkit:"full" with full depth must return level "full"');
|
||||
});
|
||||
|
||||
test('subagentToolkit:"read-only" + full depth → level "degraded"', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { ...FULL_DEPTH_DISPATCH, subagentToolkit: 'read-only' } });
|
||||
assert.strictEqual(r.level, 'degraded',
|
||||
'subagentToolkit:"read-only" must return level "degraded"');
|
||||
assert.ok(r.fallback.length > 0, 'fallback must be non-empty');
|
||||
});
|
||||
|
||||
test('subagentToolkit:"undocumented" + full depth → level "degraded" (fail-closed)', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { ...FULL_DEPTH_DISPATCH, subagentToolkit: 'undocumented' } });
|
||||
assert.strictEqual(r.level, 'degraded',
|
||||
'subagentToolkit:"undocumented" must fail closed to level "degraded"; got "' + r.level + '"');
|
||||
assert.ok(r.fallback.length > 0, 'fallback must be non-empty');
|
||||
});
|
||||
|
||||
test('subagentToolkit:"future-xyz" + full depth → level "degraded" (fail-closed)', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { ...FULL_DEPTH_DISPATCH, subagentToolkit: 'future-xyz' } });
|
||||
assert.strictEqual(r.level, 'degraded',
|
||||
'subagentToolkit:"future-xyz" (unknown) must fail closed to level "degraded"; got "' + r.level + '"');
|
||||
assert.ok(r.fallback.length > 0, 'fallback must be non-empty');
|
||||
});
|
||||
|
||||
test('subagentToolkit:"" (empty string) + full depth → level "degraded" (fail-closed)', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { ...FULL_DEPTH_DISPATCH, subagentToolkit: '' } });
|
||||
assert.strictEqual(r.level, 'degraded',
|
||||
'subagentToolkit:"" must fail closed to level "degraded"; got "' + r.level + '"');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// New fixes: M1 maxDepth NaN, M2 struct consistency, L1 SAFE_DEFAULTS,
|
||||
// L2 protocolVersion warn, N1 undocumented dispatch warnings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Fix M1: maxDepth NaN bypasses number guard', () => {
|
||||
test('negotiate with dispatch.maxDepth NaN → effective.dispatch.maxDepth === 0 AND warning about maxDepth AND Number.isFinite', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { namedDispatch: true, nested: false, maxDepth: NaN, background: false, subagentToolkit: 'full' },
|
||||
});
|
||||
const d = result.effective.dispatch;
|
||||
assert.strictEqual(d.maxDepth, 0, 'NaN maxDepth must be normalized to 0');
|
||||
assert.ok(Number.isFinite(d.maxDepth), 'effective.dispatch.maxDepth must be finite (Number.isFinite)');
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('maxDepth'), `Expected a warning about maxDepth; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('degradationFor dispatch with maxDepth NaN → level "degraded" (not NaN-dependent, not "full")', () => {
|
||||
const r = degradationFor('dispatch', {
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: NaN, subagentToolkit: 'full' },
|
||||
});
|
||||
// After fix: depth=(NaN not finite)→0; NaN===0 is false so initial check doesn't fire;
|
||||
// isUnbounded=false; isFullDepth = false || (nested:true && 0>=2) = false → 'degraded' (flat)
|
||||
assert.strictEqual(r.level, 'degraded',
|
||||
`NaN maxDepth with nested:true must yield 'degraded' (depth=0, not full-depth); got: ${r.level}`);
|
||||
assert.notStrictEqual(r.level, 'full', 'NaN maxDepth must NOT yield "full"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fix M2: cap nested/background when namedDispatch is false', () => {
|
||||
test('negotiate with namedDispatch:"undocumented" → namedDispatch false, nested false, background false, maxDepth 0; warnings include namedDispatch undocumented note', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { namedDispatch: 'undocumented', nested: true, background: true, maxDepth: 5, subagentToolkit: 'full' },
|
||||
});
|
||||
const d = result.effective.dispatch;
|
||||
assert.strictEqual(d.namedDispatch, false, 'namedDispatch must be false');
|
||||
assert.strictEqual(d.nested, false, 'nested must be false when namedDispatch is false');
|
||||
assert.strictEqual(d.background, false, 'background must be false when namedDispatch is false');
|
||||
assert.strictEqual(d.maxDepth, 0, 'maxDepth must be 0 when namedDispatch is false');
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('namedDispatch') || warnText.includes('dispatch.namedDispatch'),
|
||||
`Expected a warning about namedDispatch being undocumented; got: ${warnText}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fix L1: SAFE_DEFAULTS.dispatch.subagentToolkit is read-only', () => {
|
||||
// CONTRACT: negotiate({}) uses SAFE_DEFAULTS for each axis; dispatch uses its floor
|
||||
test('negotiate({}) → effective axes match documented SAFE_DEFAULTS (CONTRACT)', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
const eff = result.effective;
|
||||
assert.strictEqual(eff.embeddingMode, 'declarative');
|
||||
assert.strictEqual(eff.commandSurface, 'prose-only');
|
||||
assert.strictEqual(eff.modelMode, 'passive');
|
||||
assert.strictEqual(eff.hookBus, 'none');
|
||||
assert.strictEqual(eff.stateIO, 'session-log-append');
|
||||
assert.strictEqual(eff.transport, 'mcp');
|
||||
assert.strictEqual(eff.runtime, 'node');
|
||||
assert.strictEqual(eff.dispatch.subagentToolkit, 'read-only',
|
||||
'SAFE_DEFAULTS dispatch floor must be read-only');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fix L2: warn on present-but-non-number protocolVersion', () => {
|
||||
test('negotiate with protocolVersion:"beta" → warnings include protocolVersion note; result.protocolVersion === engine default (1)', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'slash-file',
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
dispatch: { namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full' },
|
||||
protocolVersion: 'beta',
|
||||
});
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('protocolVersion'),
|
||||
`Expected a warning about protocolVersion being non-finite/non-number; got: ${warnText}`);
|
||||
assert.strictEqual(result.protocolVersion, 1,
|
||||
'result.protocolVersion must fall back to engine default (1)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fix N1: symmetric observability warnings for undocumented dispatch fields', () => {
|
||||
test('dispatch.subagentToolkit:"undocumented" → warning includes "dispatch.subagentToolkit is undocumented"', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'undocumented' },
|
||||
});
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('subagentToolkit') && warnText.includes('undocumented'),
|
||||
`Expected warning about dispatch.subagentToolkit undocumented; got: ${warnText}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fix: degradationFor unknown point → {level:"absent", unknown:true}', () => {
|
||||
test('degradationFor("totally-unknown-point", {}) → {level:"absent", unknown:true}', () => {
|
||||
const r = degradationFor('totally-unknown-point', {});
|
||||
assert.strictEqual(r.level, 'absent', 'unknown point must return absent');
|
||||
assert.strictEqual(r.unknown, true, 'unknown point must have unknown:true');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fix 2: negotiateHostCapabilities — host omitting 'dispatch' → subagentToolkit 'read-only'
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Fix 2: negotiate — host omits dispatch → subagentToolkit read-only (fail-closed)', () => {
|
||||
test('negotiateHostCapabilities({}) → effective.dispatch.subagentToolkit === "read-only"', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.strictEqual(result.effective.dispatch.subagentToolkit, 'read-only',
|
||||
'When host omits dispatch, subagentToolkit must fail-closed to "read-only"; got "' + result.effective.dispatch.subagentToolkit + '"');
|
||||
});
|
||||
|
||||
test('negotiateHostCapabilities({}) → effective.dispatch.namedDispatch===false, maxDepth===0, nested===false, background===false', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
const d = result.effective.dispatch;
|
||||
assert.strictEqual(d.namedDispatch, false);
|
||||
assert.strictEqual(d.maxDepth, 0);
|
||||
assert.strictEqual(d.nested, false);
|
||||
assert.strictEqual(d.background, false);
|
||||
});
|
||||
|
||||
test('negotiateHostCapabilities({}) → points.dispatch.effectiveLevel === "absent"', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.strictEqual(result.points.dispatch.effectiveLevel, 'absent',
|
||||
'dispatch absent when host omits it');
|
||||
});
|
||||
|
||||
test('host with all axes but no dispatch → subagentToolkit "read-only"', () => {
|
||||
const hostWithoutDispatch = {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
// no dispatch key
|
||||
};
|
||||
const result = negotiateHostCapabilities(hostWithoutDispatch);
|
||||
assert.strictEqual(result.effective.dispatch.subagentToolkit, 'read-only',
|
||||
'Host missing dispatch must produce subagentToolkit "read-only"; got "' + result.effective.dispatch.subagentToolkit + '"');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user