diff --git a/.changeset/2102-eos-pi.md b/.changeset/2102-eos-pi.md new file mode 100644 index 000000000..4cbebe59e --- /dev/null +++ b/.changeset/2102-eos-pi.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 2205 +--- +**GSD is now installable on pi** — `npx @opengsd/gsd-core --pi` installs the GSD extension to `~/.pi/agent/extensions/gsd.cjs`, and `/gsd ` now dispatches real commands through the embedded engine (the reference binding previously could only run `query help`). Drives pi through the negotiated imperative Host-Integration adapter, with active-model steering and the full pi lifecycle-event surface. (#2102) diff --git a/bin/install.js b/bin/install.js index e0aae3554..3f6c1a5b8 100755 --- a/bin/install.js +++ b/bin/install.js @@ -507,6 +507,7 @@ const { installRuntimeArtifacts, uninstallRuntimeArtifacts, installOpencodeFamilySkills, + _installNativePluginIfDeclared, _copyStaged, hasExistingSymlinkBetween, preserveUserArtifacts, @@ -556,7 +557,7 @@ if (hasMinimal && _profileArgRaw) { function selectRuntimesFromArgs(runtimeArgs) { if (runtimeArgs.includes('--all')) { - return ['claude', 'kimi', 'kilo', 'opencode', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'zcode']; + return ['claude', 'kimi', 'kilo', 'opencode', 'pi', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'zcode']; } if (runtimeArgs.includes('--both')) { return ['claude', 'opencode']; @@ -565,6 +566,7 @@ function selectRuntimesFromArgs(runtimeArgs) { const selected = []; if (runtimeArgs.includes('--claude')) selected.push('claude'); if (runtimeArgs.includes('--opencode')) selected.push('opencode'); + if (runtimeArgs.includes('--pi')) selected.push('pi'); if (runtimeArgs.includes('--kilo')) selected.push('kilo'); if (runtimeArgs.includes('--codex')) selected.push('codex'); if (runtimeArgs.includes('--copilot')) selected.push('copilot'); @@ -710,7 +712,7 @@ const banner = '\n' + ' GSD Core ' + dim + 'v' + pkg.version + reset + '\n' + ' Git. Ship. Done.\n' + ' A meta-prompting, context engineering and spec-driven\n' + - ' development workflows for Claude Code, OpenCode, Kimi CLI, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline, CodeBuddy and ZCode.\n'; + ' development workflows for Claude Code, OpenCode, Kimi CLI, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline, CodeBuddy, ZCode and pi.\n'; // Pure seam: parse --config-dir / -c from an arbitrary args array. // Returns the path string, '' for an empty equals-form value, or null when the @@ -9494,6 +9496,16 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { // Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into // hostBehaviors.localCommandsViaRules. console.log(` ${green}✓${reset} Cline: commands will be available via .clinerules`); + } else if (_hostBehaviors(runtime).pluginOnlyInstall) { + // pi (ADR-1239 / #2102 Stage 1): plugin-only install — pi's /gsd command is + // registered programmatically by the native extension (pi/gsd.cjs → + // extensions/gsd.cjs, staged separately below) and dispatches in-process + // through the embedded gsd-core command-routing hub. pi has no host-read + // markdown surface (unlike Claude/OpenCode/etc., which scan commands/ or + // command/ directories), so writing flat gsd-.md files here would be + // dead weight the extension never reads. Skip the flat-commands fallback + // entirely for pluginOnlyInstall runtimes. + console.log(` ${green}✓${reset} pi: /gsd registered via native extension (no declarative command files)`); } else { // Claude Code local: flat gsd-.md layout — Claude Code registers // commands from .claude/commands/ using the filename stem as the command @@ -9564,6 +9576,18 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { } } + // Native-extension/plugin staging for runtimes OUTSIDE the layout-driven + // _isSkillsRuntime branch above (ADR-1239 / #2102 Stage 1: pi). OpenCode/Kilo + // already get their nativePlugin file from installOpencodeFamilyArtifacts + // (called inside the _isSkillsRuntime branch, since both declare a non-empty + // artifactLayout) — guard on `!_isSkillsRuntime` so this standalone call never + // double-stages their plugin file. A runtime like pi, whose artifactLayout is + // intentionally empty for both scopes (`_isSkillsRuntime` is false), still + // needs its declared hostBehaviors.nativePlugin file copied into targetDir. + if (!_isSkillsRuntime && _hostBehaviors(runtime).nativePlugin) { + _installNativePluginIfDeclared(runtime, targetDir, _hostBehaviors(runtime), src); + } + // Copy gsd-core skill with path replacement // Preserve user-generated files before the wipe-and-copy so they survive re-install const skillSrc = path.join(src, 'gsd-core'); @@ -9689,8 +9713,10 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { // `--minimal` actually shrinks a previously-full install. // For Codex this also covers per-agent `.toml` files alongside the `.md` // sources so a full → minimal switch doesn't leave stale registrations. - // Skipped for descriptor-agent runtimes (installRuntimeArtifacts prunes). - if (!_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime) && fs.existsSync(agentsDest)) { + // Skipped for descriptor-agent runtimes (installRuntimeArtifacts prunes) and + // for pluginOnlyInstall runtimes (pi, ADR-1239 / #2102 Stage 1 — no agents/ + // dir is ever written for them, see the leading branch below). + if (!_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime) && !_hostBehaviors(runtime).pluginOnlyInstall && fs.existsSync(agentsDest)) { for (const file of fs.readdirSync(agentsDest)) { if ( file.startsWith('gsd-') && @@ -9701,7 +9727,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { } } - if (_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime)) { + if (_hostBehaviors(runtime).pluginOnlyInstall) { + // pi (ADR-1239 / #2102 Stage 1): programmatic dispatch has no named-dispatch + // subagent toolkit (dispatch.subagentToolkit: "undocumented", no Agent-tool + // equivalent) and no host-read markdown surface — skip writing agents/ entirely. + console.log(` ${green}✓${reset} pi: no subagent files (programmatic dispatch, no named-dispatch toolkit)`); + } else if (_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime)) { // installRuntimeArtifacts already wrote agents + handles stale-file cleanup // via its own prune pass. No further action needed. console.log(` ${dim}↳${reset} Agents installed via descriptor-driven layout (${runtime})`); @@ -11301,13 +11332,14 @@ const runtimeMap = { '10': 'kimi', '11': 'kilo', '12': 'opencode', - '13': 'qwen', - '14': 'trae', - '15': 'windsurf', - '16': 'zcode' + '13': 'pi', + '14': 'qwen', + '15': 'trae', + '16': 'windsurf', + '17': 'zcode' }; -const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'hermes', 'kimi', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf', 'zcode']; -const ALL_RUNTIMES_OPTION = '17'; +const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'hermes', 'kimi', 'kilo', 'opencode', 'pi', 'qwen', 'trae', 'windsurf', 'zcode']; +const ALL_RUNTIMES_OPTION = '18'; /** * Build the runtime-selection prompt text shown by the interactive installer. @@ -11327,11 +11359,12 @@ function buildRuntimePromptText() { ${cyan}10${reset}) Kimi ${dim}(~/.config/agents, then ~/.agents if existing)${reset} ${cyan}11${reset}) Kilo ${dim}(~/.config/kilo)${reset} ${cyan}12${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - ${cyan}13${reset}) Qwen Code ${dim}(~/.qwen)${reset} - ${cyan}14${reset}) Trae ${dim}(~/.trae)${reset} - ${cyan}15${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset} - ${cyan}16${reset}) ZCode ${dim}(~/.zcode)${reset} - ${cyan}17${reset}) All + ${cyan}13${reset}) pi ${dim}(~/.pi/agent)${reset} + ${cyan}14${reset}) Qwen Code ${dim}(~/.qwen)${reset} + ${cyan}15${reset}) Trae ${dim}(~/.trae)${reset} + ${cyan}16${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset} + ${cyan}17${reset}) ZCode ${dim}(~/.zcode)${reset} + ${cyan}18${reset}) All ${dim}Select multiple: 1,2,6 or 1 2 6${reset} `; diff --git a/capabilities/pi/capability.json b/capabilities/pi/capability.json new file mode 100644 index 000000000..f3b0b9094 --- /dev/null +++ b/capabilities/pi/capability.json @@ -0,0 +1,60 @@ +{ + "id": "pi", + "role": "runtime", + "version": "1.7.0-rc.5", + "title": "pi", + "description": "pi (pi.dev) — bun-runtime programmatic-CLI; TS ExtensionAPI (registerCommand/registerTool/registerProvider/pi.on); single native-extension file at ~/.pi/agent/extensions/gsd.cjs; no shared-settings hook surface; tier-2 support.", + "tier": "core", + "requires": [], + "engines": { + "gsd": ">=1.7.0" + }, + "runtime": { + "configHome": { + "kind": "dot-home-nested", + "name": "agent", + "parent": ".pi", + "env": [] + }, + "localConfigDir": ".pi", + "configFormat": "none", + "artifactLayout": { + "global": [], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "extensionEvents": "pi", + "sandboxTier": "none", + "supportTier": 2, + "installSurface": "profile-marker-only", + "writesSharedSettings": false, + "permissionWriter": null, + "extendedHookEvents": [], + "hostIntegration": { + "embeddingMode": "imperative", + "commandSurface": "slash-programmatic", + "dispatch": { + "namedDispatch": false, + "nested": false, + "maxDepth": 0, + "background": false, + "backgroundDispatch": false, + "subagentToolkit": "undocumented" + }, + "modelMode": "active", + "hookBus": "host", + "stateIO": "session-log-append", + "transport": "native-extension", + "runtime": "bun" + }, + "hostBehaviors": { + "nativePlugin": { + "dir": "extensions", + "file": "gsd.cjs", + "source": "pi/gsd.cjs" + }, + "pluginOnlyInstall": true + } + } +} diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index a50622b40..1812c9470 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1425,6 +1425,7 @@ When `runtime` is set, profile tiers (`opus`/`sonnet`/`haiku`) resolve to runtim | `copilot` | `claude-opus-4-8` | `claude-sonnet-5` | `claude-haiku-4-5` | (not used) | | `hermes` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` | (not used) | | `kilo` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` | (not used) | +| `pi` | `claude-opus-4-8` | `claude-sonnet-5` | `claude-haiku-4-5` | (not used) | | Group B (`cline`, `cursor`, `windsurf` (alias: `devin-desktop`), `augment`, `trae`, `codebuddy`, `antigravity`) | (no built-in default — your runtime handles model selection) | | | | > **How these model IDs are sourced.** The catalog (`bin/shared/model-catalog.json`) pins each runtime's tier defaults to that provider's current frontier IDs, and may intentionally carry forward-dated IDs ahead of a provider's public docs. To verify an ID is live before changing it, check the provider's own source/API — e.g. Codex: `codex debug models` or the OpenAI Codex models page; Qwen: Alibaba Model Studio model list. Only change an ID that the provider actually rejects — absence from documentation alone is not proof of invalidity. diff --git a/docs/how-to/install-on-your-runtime.md b/docs/how-to/install-on-your-runtime.md index 3adc7a61c..58e3507ec 100644 --- a/docs/how-to/install-on-your-runtime.md +++ b/docs/how-to/install-on-your-runtime.md @@ -461,6 +461,20 @@ GSD's hook-automation and native-MCP-registration integrations are not yet wired --- +### pi + +```bash +npx @opengsd/gsd-core@latest --pi --global +``` + +[pi](https://pi.dev) is a bun-runtime programmatic CLI whose extensions implement pi's own `ExtensionAPI` (`registerCommand`/`registerTool`/`registerProvider`/`pi.on`) rather than a settings-file or slash-markdown surface. GSD ships a single native-extension file: + +- **Extension** → `~/.pi/agent/extensions/gsd.cjs` (global) or `.pi/extensions/gsd.cjs` (local) + +The extension registers a `/gsd` command and a `gsd_invoke` tool that dispatch GSD commands via a bounded subprocess call to `gsd-core/bin/gsd-tools.cjs` (no fully-populated in-process command-routing hub exists — see the matrix's Stage 2 note). This is a **plugin-only install**: pi has no shared-settings hook surface (`hooksSurface: none`) and, unlike Claude/OpenCode/Kilo, no host-read markdown surface at all — pi's `/gsd` command is registered programmatically by the extension, not discovered from files, so GSD installs the extension plus its universal `gsd-core/` engine payload and the shared `hooks/`/`hooks/lib/` bundle (spawned by the extension itself, not by any config-file hook bus), and does **not** write any `commands/`, `agents/`, or `skills/` directory for pi. The extension bridges GSD's `session_start`/`before_agent_start`/`session_before_compact`/`tool_call` lifecycle events to those staged `hooks/` scripts as bounded, fail-open subprocesses, and steers pi's active model (`modelMode: active`) to a tier-resolved bare anthropic id via `pi.on('before_provider_request', ...)`. See the [`## pi`](host-integration-capability-matrix.md#pi) section of the host-integration capability matrix for the negotiated axes and citations. + +--- + ## Local vs global install All examples above use `--global`, which installs GSD once for your user account. To scope an install to a single project, replace `--global` with `--local`: diff --git a/docs/reference/capability-matrix.md b/docs/reference/capability-matrix.md index 6cb34f4a9..fcee42040 100644 --- a/docs/reference/capability-matrix.md +++ b/docs/reference/capability-matrix.md @@ -72,7 +72,7 @@ points. | `tdd` | feature | full | `>=1.6.0` | `plan:pre`, `execute:post` | contribution, gate | first-party | | `ui` | feature | full | `>=1.6.0` | `plan:pre`, `execute:wave:post`, `verify:post` | step, gate | first-party | -### Runtime capabilities (role: runtime) — 16 +### Runtime capabilities (role: runtime) — 17 Runtime capabilities adapt GSD to a specific AI runtime or IDE — emitting skills, agents, hooks configuration, and surface files for that host. They @@ -93,6 +93,7 @@ emission), so their extension-point and hook-kind cells are `—`. | `kilo` | runtime | core | `>=1.6.0` | — | — | first-party | | `kimi` | runtime | core | `>=1.6.0` | — | — | first-party | | `opencode` | runtime | core | `>=1.6.0` | — | — | first-party | +| `pi` | runtime | core | `>=1.7.0` | — | — | first-party | | `qwen` | runtime | core | `>=1.6.0` | — | — | first-party | | `trae` | runtime | core | `>=1.6.0` | — | — | first-party | | `windsurf` | runtime | core | `>=1.6.0` | — | — | first-party | diff --git a/docs/reference/host-integration-capability-matrix.md b/docs/reference/host-integration-capability-matrix.md index 0263727b9..5d700cf4c 100644 --- a/docs/reference/host-integration-capability-matrix.md +++ b/docs/reference/host-integration-capability-matrix.md @@ -653,3 +653,41 @@ EoS migration status (#2101, ADR-1239): ZCode's install is fully dogfooded throu - **Hook automation** (the plugin `Hook` component, `hookBus: host` above) — https://zcode.z.ai/en/docs/plugin documents the capability only at a high level ("Automation hooks triggered on specific events"; components are "detected from directory layout, shown as badges"). No config file format, on-disk location, event-name vocabulary, or payload schema is published, so GSD cannot faithfully wire hook events into a plugin bundle. BLOCKED (undocumented on-disk hook-config format). - **MCP registration** (`transport: mcp` above) — https://zcode.z.ai/en/docs/mcp-services confirms servers are "stored in the .zcode configuration file of the chosen scope" and accepts both a bare `{"server-name":{...}}` map and an `{"mcpServers":{...}}` wrapper shape, but does not document the exact settings filename/path or full schema (the docs describe the UI flow, not the on-disk contract) — this is the same gap already noted under `configHome` above. BLOCKED (undocumented settings-filename/schema gap). +--- + +## pi + +> pi (pi.dev) is a bun-runtime Programmatic-CLI: it exposes an in-process TypeScript `ExtensionAPI` (`registerCommand`/`registerTool`/`registerProvider`/`pi.on`) rather than a settings-file or slash-markdown surface. GSD ships a single native-extension file (`pi/gsd.cjs`) installed to `~/.pi/agent/extensions/gsd.cjs` (global) or `.pi/extensions/gsd.cjs` (local) — the programmatic-CLI peer of the OpenCode/Kilo native-plugin binding. **Sourcing note:** the citations below are the pi.dev documentation pages named in ADR-1239 Stage 1 (#2102) as the source for each axis; this environment did not have live doc-fetch access at authoring time, so the Evidence column below is a paraphrase of pi's documented extension model rather than a verbatim excerpt — a maintainer with Context7/web access should verify the exact wording before treating this section as fully cited (flagged in the #2102 PR). + +| Axis | Value | Source | Evidence | +|---|---|---|---| +| embeddingMode | imperative | https://pi.dev/docs/latest/extensions | pi extensions are loaded in-process (via jiti) and call an `ExtensionAPI` object directly (`registerCommand`/`registerTool`/`registerProvider`/`pi.on`) — an in-process programmatic API, not a config-file-only integration. | +| commandSurface | slash-programmatic | https://pi.dev/docs/latest/extensions | Commands are registered by calling `registerCommand(name, definition)` from extension code, not by dropping a markdown/TOML file — the command surface is code, not a file format. | +| modelMode | active | https://pi.dev/docs/latest/extensions | The `ExtensionAPI` exposes `registerProvider`, letting an extension supply/select model providers programmatically rather than only reading a static config value. | +| hookBus | host | https://pi.dev/docs/latest/extensions | `pi.on(event, handler)` subscribes an extension to host-fired lifecycle events (e.g. `tool_call`) — the pi host owns and fires the event bus; extensions only subscribe. | +| stateIO | session-log-append | https://pi.dev/docs/latest/session-format | pi persists conversation/tool-call state as an append-only session log/transcript format rather than exposing unrestricted local filesystem access to extensions. | +| transport | native-extension | https://pi.dev/docs/latest/extensions | Integration is a single loaded extension file (`~/.pi/agent/extensions/.cjs`), not an MCP server process — the peer mechanism to OpenCode's native `plugins/*.js` adapter. | +| runtime | bun | https://pi.dev | pi is distributed and executed as a bun-runtime CLI (its extensions are loaded via jiti under bun, not Node.js or Python). | +| dispatch.namedDispatch | undocumented | no authoritative doc — searched: https://pi.dev/docs/latest/extensions | The `ExtensionAPI` documents `registerCommand`/`registerTool`/`registerProvider`/`pi.on`; it does not document a named-subagent-invocation primitive. | +| dispatch.nested | undocumented | no authoritative doc — searched: https://pi.dev/docs/latest/extensions | No documented subagent-of-subagent nesting capability. | +| dispatch.maxDepth | 0 | no authoritative doc — searched: https://pi.dev/docs/latest/extensions | No named-dispatch primitive is documented at all (see `dispatch.namedDispatch`), so there is no nesting depth to bound; `0` records "no dispatch levels beyond the root extension," not a measured limit. | +| dispatch.background | false | no authoritative doc — searched: https://pi.dev/docs/latest/extensions | No documented background/async subagent-execution primitive. | +| dispatch.subagentToolkit | undocumented | no authoritative doc — searched: https://pi.dev/docs/latest/extensions | pi has no named-dispatch primitive (see `dispatch.namedDispatch`), so there is no subagent tool-surface to classify as `full`/`read-only`. | +| dispatch.backgroundDispatch | false | no authoritative doc — searched: https://pi.dev/docs/latest/extensions | Same gap as `dispatch.background` — no background-dispatch primitive is documented, so a background-dispatched agent spawning further named sub-agents is not possible. | + +Sources consulted: +- https://pi.dev +- https://pi.dev/docs/latest/extensions +- https://pi.dev/docs/latest/session-format + +Documentation gaps: +- dispatch.namedDispatch / dispatch.nested / dispatch.subagentToolkit — pi's `ExtensionAPI` (`registerCommand`/`registerTool`/`registerProvider`/`pi.on`) does not document a named-subagent-dispatch primitive at all, unlike Claude Code/Codex/OpenCode-style "Agent tool" surfaces; all three axes stay `undocumented` and negotiation fails closed (no named dispatch, dispatch flattened). +- dispatch.maxDepth / dispatch.background / dispatch.backgroundDispatch — recorded as `0`/`false`/`false` (not `undocumented`) because the absence of any dispatch primitive is itself the documented ceiling, matching `shouldFlattenDispatch`'s fail-closed default. +- This section's Evidence-column wording was authored without live Context7/web-fetch access (see the sourcing note above the table) — verify against the cited pi.dev pages before relying on it for a future capability upgrade. + +EoS migration status (#2102 Stage 1, ADR-1239): pi lands as a NET-NEW installable runtime — pure additive descriptor + installer wiring, no prior `runtime === 'pi'` branches existed to fold. `artifactLayout` is declared empty (`global: []`, `local: []`) — pi has no skills/commands/agents layout, and installs as **PLUGIN-ONLY**: `hostBehaviors.pluginOnlyInstall: true` explicitly skips `bin/install.js`'s generic flat-commands-and-agents fallback (the legacy path Claude Code's LOCAL layout also uses), which would otherwise write inert `commands/gsd-.md` + `agents/gsd-.md` reference files no part of pi ever reads. pi's `/gsd` command and `gsd_invoke` tool are registered **programmatically** by the native extension (`pi/gsd.cjs` → `extensions/gsd.cjs`, mirroring OpenCode/Kilo's `nativePlugin` shape) — pi has no host-read markdown surface at all (unlike Claude/OpenCode/Kilo, which scan a `commands/`/`command/` directory), so a declarative artifact surface would be dead weight, not merely unused. `dispatch.subagentToolkit: "undocumented"` and `dispatch.backgroundDispatch: false` are both required by the capability validator's dispatch schema and reflect that pi has no documented named-dispatch primitive at all. (Stage 1 originally also set `hostBehaviors.skipSharedHooksInstall:true`, reasoning the staged `hooks/*.js` bundle would be dead weight for pi the way it genuinely is for Kilo/ZCode — **corrected in Stage 2 below**: pi's native extension DOES spawn them, so they are live, not dead, and the flag was removed.) + +EoS migration status (#2102 Stage 2, ADR-1239): Stage 1's "in-process `gsd-core` command-routing hub" framing was aspirational and is corrected here — no fully-populated hub factory exists anywhere in gsd-core (every `createHub()` caller in the tree builds a single-family hub for its own narrow purpose), so `/gsd` and `gsd_invoke` instead dispatch via **SUBPROCESS REUSE**: `dispatchGsdCommand` (`src/shell-command-projection.cts`) spawns `gsd-core/bin/gsd-tools.cjs [subcommand] ... --cwd --raw --json-errors` bounded and non-throwing, mirroring the precedent already established for the OpenCode/Kilo hook bridge (`.opencode/plugins/gsd-core.js`'s "Architecture: SUBPROCESS REUSE" header). The companion MCP server's `gsd_invoke_command` tool dispatches through the SAME shared helper (it had the identical `createHub()`-with-no-args bug). `/gsd`'s command handler is `handler(args, ctx)` (pi's real ExtensionAPI shape — a raw args string, not `execute(ctx)`); `gsd_invoke`'s tool handler is the real 5-arg `execute(toolCallId, params, signal, onUpdate, ctx)`. The event surface (`EXTENSION_EVENT_SURFACES.pi`, `src/host-integration.cts`) now declares the full ~30-event pi ExtensionAPI vocabulary (was a placeholder `['tool_call']`), and `pi/gsd.cjs` binds `session_start` (→ `gsd-ensure-canonical-path.js`), `before_agent_start` (→ `gsd-workflow-guard.js`, a forward-compatible no-op today since that hook's triggers are tool-scoped), `session_before_compact` (→ `gsd-context-monitor.js`), and `tool_call`, each as a bounded fail-open `spawnSync` subprocess (mirroring `.opencode/plugins/gsd-core.js`'s `runHook`). `modelMode: active` is realized via `pi.on('before_provider_request', ...)`, which resolves a tier through the model-catalog's now-populated `runtimeTierDefaults.pi` entries (bare anthropic ids — `claude-opus-4-8`/`claude-sonnet-5`/`claude-haiku-4-5`, matching the `claude` runtime's own ids since pi talks the anthropic API) and returns a modified payload, or `undefined` (fail-open, pi's model left untouched) when resolution comes back null — **not** `registerProvider`, which would register a new model provider rather than steering pi's existing built-in anthropic models. + +**Adversarial-review correction (#2102 Stage 2, post-review):** the event bridges above and the `/gsd` tokenizer's `hooks/lib/git-cmd.js` require were DEAD in a real install — Stage 1's `hostBehaviors.skipSharedHooksInstall:true` meant pi shipped NO `hooks/` directory at all, so `runHook('gsd-ensure-canonical-path.js', ...)` etc. always hit the "hook file absent → silent no-op" branch, and the tokenizer always fell back to plain whitespace-splitting. The tests masked this because they run against the dev tree, where `hooks/` genuinely exists. **Fix:** `capabilities/pi/capability.json` no longer sets `skipSharedHooksInstall` — pi is architecturally identical to OpenCode here (`hooksSurface: "none"` + a native extension that spawns the staged hooks), not to Kilo/ZCode (`hooksSurface: "none"` with NO plugin surface, where the same hooks genuinely are dead weight). pi now installs `hooks/` + `hooks/lib/` (27 entries: the same `INSTALLED_HOOK_FILES` set OpenCode gets) alongside `extensions/gsd.cjs`, verified end-to-end via a real `node bin/install.js --pi --global`/`--local` — `resolveEngineRoot`'s walk-up from the installed extension's own directory finds `ENGINE_ROOT/hooks/{gsd-ensure-canonical-path.js,gsd-workflow-guard.js,gsd-context-monitor.js,lib/git-cmd.js}`, and each bridge/`runHook` call exits 0 against the real installed files. `hooksSurface: "none"` + `configFormat: "none"` + `writesSharedSettings: false` are unaffected — no settings/hooks.json/config.toml is written for pi; the extension spawns hooks by absolute path, not via a config-file hook bus. `tests/fixtures/golden-install-parity/pi.json` grew from 292 → 320 entries (the 28 new `hooks/`/`hooks/lib/` files); `commands/`, `agents/`, `skills/` remain absent (`pluginOnlyInstall` is untouched — it only gates the declarative-markdown surfaces, not hooks). `tests/install-minimal-hooks.test.cjs`'s #1821 suite moved pi from the Kilo/ZCode (no-hooks) group into the OpenCode (ships-hooks) group accordingly. + diff --git a/gsd-core/bin/lib/capability-registry.cjs b/gsd-core/bin/lib/capability-registry.cjs index e94456718..368d09bf1 100644 --- a/gsd-core/bin/lib/capability-registry.cjs +++ b/gsd-core/bin/lib/capability-registry.cjs @@ -2079,6 +2079,66 @@ const capabilities = { "contributions": [], "gates": [] }, + "pi": { + "id": "pi", + "role": "runtime", + "version": "1.7.0-rc.5", + "title": "pi", + "description": "pi (pi.dev) — bun-runtime programmatic-CLI; TS ExtensionAPI (registerCommand/registerTool/registerProvider/pi.on); single native-extension file at ~/.pi/agent/extensions/gsd.cjs; no shared-settings hook surface; tier-2 support.", + "tier": "core", + "requires": [], + "engines": { + "gsd": ">=1.7.0" + }, + "runtime": { + "configHome": { + "kind": "dot-home-nested", + "name": "agent", + "parent": ".pi", + "env": [] + }, + "localConfigDir": ".pi", + "configFormat": "none", + "artifactLayout": { + "global": [], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "extensionEvents": "pi", + "sandboxTier": "none", + "supportTier": 2, + "installSurface": "profile-marker-only", + "writesSharedSettings": false, + "permissionWriter": null, + "extendedHookEvents": [], + "hostIntegration": { + "embeddingMode": "imperative", + "commandSurface": "slash-programmatic", + "dispatch": { + "namedDispatch": false, + "nested": false, + "maxDepth": 0, + "background": false, + "backgroundDispatch": false, + "subagentToolkit": "undocumented" + }, + "modelMode": "active", + "hookBus": "host", + "stateIO": "session-log-append", + "transport": "native-extension", + "runtime": "bun" + }, + "hostBehaviors": { + "nativePlugin": { + "dir": "extensions", + "file": "gsd.cjs", + "source": "pi/gsd.cjs" + }, + "pluginOnlyInstall": true + } + } + }, "profile-pipeline": { "id": "profile-pipeline", "role": "feature", @@ -4975,6 +5035,66 @@ const runtimes = { } } }, + "pi": { + "id": "pi", + "role": "runtime", + "version": "1.7.0-rc.5", + "title": "pi", + "description": "pi (pi.dev) — bun-runtime programmatic-CLI; TS ExtensionAPI (registerCommand/registerTool/registerProvider/pi.on); single native-extension file at ~/.pi/agent/extensions/gsd.cjs; no shared-settings hook surface; tier-2 support.", + "tier": "core", + "requires": [], + "engines": { + "gsd": ">=1.7.0" + }, + "runtime": { + "configHome": { + "kind": "dot-home-nested", + "name": "agent", + "parent": ".pi", + "env": [] + }, + "localConfigDir": ".pi", + "configFormat": "none", + "artifactLayout": { + "global": [], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "extensionEvents": "pi", + "sandboxTier": "none", + "supportTier": 2, + "installSurface": "profile-marker-only", + "writesSharedSettings": false, + "permissionWriter": null, + "extendedHookEvents": [], + "hostIntegration": { + "embeddingMode": "imperative", + "commandSurface": "slash-programmatic", + "dispatch": { + "namedDispatch": false, + "nested": false, + "maxDepth": 0, + "background": false, + "backgroundDispatch": false, + "subagentToolkit": "undocumented" + }, + "modelMode": "active", + "hookBus": "host", + "stateIO": "session-log-append", + "transport": "native-extension", + "runtime": "bun" + }, + "hostBehaviors": { + "nativePlugin": { + "dir": "extensions", + "file": "gsd.cjs", + "source": "pi/gsd.cjs" + }, + "pluginOnlyInstall": true + } + } + }, "qwen": { "id": "qwen", "role": "runtime", @@ -5538,6 +5658,7 @@ const _requiresGraph = { "pattern-mapper": [ "research" ], + "pi": [], "profile-pipeline": [], "qwen": [], "research": [], diff --git a/gsd-core/bin/shared/model-catalog.json b/gsd-core/bin/shared/model-catalog.json index 9c24fd97c..4a2015644 100644 --- a/gsd-core/bin/shared/model-catalog.json +++ b/gsd-core/bin/shared/model-catalog.json @@ -86,6 +86,11 @@ "opus": null, "sonnet": null, "haiku": null + }, + "pi": { + "opus": { "model": "claude-opus-4-8" }, + "sonnet": { "model": "claude-sonnet-5" }, + "haiku": { "model": "claude-haiku-4-5" } } }, "providerPresets": { diff --git a/gsd-core/workflows/settings-advanced.md b/gsd-core/workflows/settings-advanced.md index 28a2c03c8..3c97f5272 100644 --- a/gsd-core/workflows/settings-advanced.md +++ b/gsd-core/workflows/settings-advanced.md @@ -361,6 +361,7 @@ Built-in tier defaults by runtime: | `copilot` | `claude-opus-4-8` | `claude-sonnet-5` | `claude-haiku-4-5` | | `hermes` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` | | `kilo` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` | +| `pi` | `claude-opus-4-8` | `claude-sonnet-5` | `claude-haiku-4-5` | | Group B (`cline`, `cursor`, `windsurf`, `augment`, `trae`, `codebuddy`, `antigravity`) | (no built-in default — your runtime handles model selection) | | | Display a table to the user showing the effective configuration: diff --git a/package.json b/package.json index 949ad1a8b..21ac230e4 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ ".opencode", "GEMINI.md", "hooks", - "scripts" + "scripts", + "pi" ], "keywords": [ "claude", diff --git a/pi/gsd.cjs b/pi/gsd.cjs index 422f0cde1..9953d35fe 100644 --- a/pi/gsd.cjs +++ b/pi/gsd.cjs @@ -1,79 +1,354 @@ 'use strict'; /** - * GSD extension for pi (pi.dev) — ADR-1239 Phase D / #1944. + * GSD extension for pi (pi.dev) — ADR-1239 Phase D / #1944, upgraded #2102 Stage 2. * * pi is a Programmatic-CLI host whose TS extensions implement the ExtensionAPI - * (`@earendil-works/pi-coding-agent`): registerTool / registerCommand / pi.on. + * (`@earendil-works/pi-coding-agent`): registerCommand({handler(args, ctx)}) / + * registerTool({execute(toolCallId, params, signal, onUpdate, ctx)}) / pi.on(event, handler). * This extension binds GSD's command surface to pi via the imperative adapter * path — the programmatic-CLI peer of the OpenCode worked binding. * * Installation: copy this file to ~/.pi/agent/extensions/gsd.cjs (pi loads * extensions via jiti from that dir). The engine is resolved from the installed - * GSD tree (walk-up like the OpenCode plugin). + * GSD tree (walk-up like the OpenCode plugin). pi's shared hooks/ bundle + * (hooks/*.js + hooks/lib/git-cmd.js) is installed alongside the extension — + * capabilities/pi/capability.json does NOT set + * `hostBehaviors.skipSharedHooksInstall` (#2102 Stage 2 fix; pi is + * architecturally identical to OpenCode here: `hooksSurface: 'none'` + a + * native extension that spawns the staged hooks — not Kilo/ZCode's + * no-plugin-surface case, where the same hooks would be genuine dead weight). + * This is what makes the event bridges below (and the tokenizer require) + * resolve for real in an installed tree, not just in this dev repo. * - * Engine entry: the /gsd handler dispatches IN-PROCESS through the GSD - * command-routing hub (createHub/dispatch) — Bun-compatible CJS require. The - * companion MCP server (gsd-mcp-server) is the alternative for out-of-process - * hosts; in-process is the first cut per the ADR's "thin plugin" ideal. + * Engine entry: dispatch is SUBPROCESS-REUSE to gsd-tools.cjs (bounded, + * no-throw — dispatchGsdCommand in shell-command-projection.cjs), NOT an + * in-process command-routing hub. No fully-populated hub factory exists + * anywhere in gsd-core — every createHub() caller in the tree builds a + * single-family hub for its own narrow purpose — so the "in-process createHub" + * framing of the original #1944 cut was aspirational and is not achievable + * without a hub factory that doesn't exist. This mirrors the precedent already + * established for the OpenCode/Kilo hook bridge (.opencode/plugins/gsd-core.js + * header: "Architecture: SUBPROCESS REUSE ... spawns existing hook scripts as + * child processes") — the same pattern, applied to command dispatch. The + * companion MCP server (gsd-mcp-server) dispatches through the SAME shared + * helper for out-of-process hosts. * * @param {object} pi pi ExtensionAPI (registerTool/registerCommand/on/…) */ + +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +// Resolve the GSD engine tree (the dir holding gsd-core/ + hooks/). +// Works across dev (/pi/gsd.cjs → ) and installed layouts. +function resolveEngineRoot(startDir) { + let dir = startDir; + for (let i = 0; i < 6; i++) { + if (fs.existsSync(path.join(dir, 'gsd-core'))) return dir; + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return path.resolve(startDir, '..'); +} + +const ENGINE_ROOT = resolveEngineRoot(__dirname); +const GSD_CORE = path.join(ENGINE_ROOT, 'gsd-core'); + +// ── curated top-level command families (gsd-tools.cjs TOP_LEVEL_USAGE) ────── +// readCmdNames() (scripts/fix-slash-commands.cjs) reads commands/, which pi +// does NOT install (it ships a single native-extension file, no shared +// commands/ dir) — it would always return []. This is a self-contained, +// hand-curated subset of the STABLE top-level families documented by +// `node gsd-core/bin/gsd-tools.cjs --help` (gsd-tools.cjs:689-705). Named + +// exported (via _internals) so a test can assert against it directly. +const PI_COMMAND_FAMILIES = Object.freeze([ + 'agent', 'capability', 'check', 'commit', 'config-get', 'config-path', + 'config-set', 'effort', 'git', 'graphify', 'init', 'intel', 'learnings', + 'list-todos', 'loop', 'milestone', 'phase', 'phases', 'progress', + 'requirements', 'research-plan', 'research-store', 'resolve-granularity', + 'resolve-model', 'roadmap', 'scaffold', 'smart-entry', 'state', 'task', + 'template', 'user-story', 'validate', 'verify', 'workstream', 'worktree', +]); + +/** + * Filter PI_COMMAND_FAMILIES by prefix (startsWith). Returns null when there + * are no matches, per pi's `AutocompleteItem[]|null` contract. + * @param {string} prefix + * @returns {{value: string, label: string}[] | null} + */ +function getArgumentCompletions(prefix) { + const p = typeof prefix === 'string' ? prefix : ''; + const matches = PI_COMMAND_FAMILIES.filter((name) => name.startsWith(p)); + if (matches.length === 0) return null; + return matches.map((value) => ({ value, label: value })); +} + +/** + * Tokenize the raw `/gsd ` string into { family, subcommand, args }. + * Reuses the quote-aware whitespace tokenizer already shipped for hooks + * (hooks/lib/git-cmd.js's `tokenize`) rather than re-implementing shell-word + * splitting a second time. #2102 Stage 2: pi's capability descriptor no + * longer sets `hostBehaviors.skipSharedHooksInstall` (adversarial-review + * finding #1/#2 — pi ships NO hooks/ with that flag set, so this require was + * dead in a real install), so the shared hooks/ bundle — including + * hooks/lib/git-cmd.js — is installed alongside the extension for real + * (mirrors OpenCode, whose native plugin also spawns the staged hooks/*.js + * bundle). The require below is therefore the PRIMARY, live path in an + * installed pi tree; the whitespace-split fallback stays as defense-in-depth + * for a corrupted/partial install (e.g. a user who deleted hooks/lib/ by + * hand) rather than the only-ever-taken path. + * @param {string} rawArgs + * @returns {{ family: string, subcommand?: string, args: string[] }} + */ +function parseGsdCommandArgs(rawArgs) { + let tokenize; + try { + ({ tokenize } = require(path.join(ENGINE_ROOT, 'hooks', 'lib', 'git-cmd.js'))); + } catch { + tokenize = (s) => String(s || '').split(/\s+/).filter(Boolean); + } + const tokens = tokenize(typeof rawArgs === 'string' ? rawArgs : ''); + return { + // Empty args → dispatch gsd-tools.cjs's own --help surface (a real, + // working, ok:true default — NOT the 'query'/'help' pairing the original + // #1944 cut used, which is not a valid gsd-tools.cjs command). + family: tokens[0] || '--help', + subcommand: tokens[1], + args: tokens.slice(2), + }; +} + +/** + * Best-effort TypeBox schema for gsd_invoke's `parameters`, falling back to a + * plain JSON-Schema object when the `typebox` package is unavailable (it is + * NOT a gsd-core dependency — pi's own ExtensionAPI contract expects TypeBox, + * but nothing in this repo installs it). TypeBox schemas ARE JSON Schema, so + * the fallback object is structurally equivalent for hosts that accept plain + * JSON Schema; this is a best-effort shim for the flat-file extension case. + * @returns {object} + */ +function buildGsdInvokeParameters() { + try { + const typebox = require('typebox'); + const Type = typebox && typebox.Type; + if (Type) { + return Type.Object({ + family: Type.String(), + subcommand: Type.Optional(Type.String()), + args: Type.Optional(Type.Array(Type.String())), + }); + } + } catch { + // typebox is not installed in this environment — fall through. + } + process.stderr.write( + 'gsd: typebox unavailable — gsd_invoke "parameters" falling back to a plain JSON-schema object.\n', + ); + return { + type: 'object', + properties: { + family: { type: 'string' }, + subcommand: { type: 'string' }, + args: { type: 'array', items: { type: 'string' } }, + }, + required: ['family'], + }; +} + +/** + * Build the `before_provider_request` handler that steers pi's model + * selection to GSD's tier-resolved id (modelMode: 'active' per + * capabilities/pi/capability.json). GSD does NOT call `pi.registerProvider` — + * that registers a NEW model provider; GSD's job here is only to pick a + * tier-appropriate id AMONG pi's EXISTING built-in anthropic models, so + * registerProvider would be the wrong primitive (it would wrongly add a fake + * provider instead of steering the real one). + * + * v1 tier policy: GSD does not yet expose a per-turn/per-agent tier signal to + * this event, so a conservative fixed default tier is used (parameterized — + * default 'sonnet' — so a future richer signal, or a test, can override it). + * + * ASSUMPTION (flagged — verify against a live pi host): the event payload's + * model field is named `model`, matching the anthropic-messages payload shape + * (Context7-confirmed for the wire protocol; pi's own before_provider_request + * event schema was not independently verifiable in this environment). If pi's + * actual field name differs, this returns the WRONG key and pi's fail-open + * default takes over only because bare-model-id mismatches degrade to + * provider-level errors, not GSD-level ones — a discrepancy here needs a + * live-host smoke test before shipping past this stage. + * + * Fail-open: any resolution failure (or a null/falsy resolved model — e.g. an + * unrecognized tier) returns `undefined`, leaving pi's model choice untouched. + * NEVER returns a payload with a missing/empty model id. + * + * @param {{ tier?: string }} [opts] + * @returns {(event: object, ctx: object) => Promise} + */ +function buildBeforeProviderRequestHandler({ tier = 'sonnet' } = {}) { + return async function onBeforeProviderRequest(event, ctx) { + try { + const effectiveCwd = (ctx && ctx.cwd) || process.cwd(); + const { resolveTierEntry } = require(path.join(GSD_CORE, 'bin', 'lib', 'model-resolver.cjs')); + const { loadConfig } = require(path.join(GSD_CORE, 'bin', 'lib', 'config-loader.cjs')); + const config = loadConfig(effectiveCwd); + const overrides = (config && config.model_profile_overrides) || undefined; + const entry = resolveTierEntry({ runtime: 'pi', tier, overrides }); + const modelId = entry && typeof entry.model === 'string' && entry.model.length > 0 ? entry.model : null; + if (!modelId) return undefined; // fail-open — leave pi's model untouched + const basePayload = (event && typeof event === 'object' && event.payload && typeof event.payload === 'object') + ? event.payload + : {}; + return { ...basePayload, model: modelId }; + } catch { + return undefined; // fail-open on any resolution error + } + }; +} + +/** + * Bounded subprocess bridge to GSD's Claude Code hook scripts. Mirrors + * .opencode/plugins/gsd-core.js's `runHook` (SUBPROCESS-REUSE): spawns + * `node ` with the payload piped to stdin, on a bounded + * timeout. NEVER throws — a missing hook file, a spawn error, or a timeout + * all degrade to a silent-allow result so a hook problem can never block pi. + * @param {string} hookFile filename under hooks/, e.g. "gsd-context-monitor.js" + * @param {object} payload + * @param {{ timeout?: number, cwd?: string }} [opts] + * @returns {{ stdout: string, exitCode: number, timedOut: boolean }} + */ +function runHook(hookFile, payload, opts = {}) { + const hookPath = path.join(ENGINE_ROOT, 'hooks', hookFile); + if (!fs.existsSync(hookPath)) return { stdout: '', exitCode: 0, timedOut: false }; + const timeout = opts.timeout || 8000; + let result; + try { + result = spawnSync(process.execPath, [hookPath], { + input: JSON.stringify(payload || {}), + encoding: 'utf8', + timeout, + cwd: opts.cwd || process.cwd(), + windowsHide: true, + }); + } catch { + return { stdout: '', exitCode: 0, timedOut: false }; + } + const stdout = (result && typeof result.stdout === 'string') ? result.stdout.trim() : ''; + const exitCode = (result && result.status != null) ? result.status : 0; + return { stdout, exitCode, timedOut: !!(result && result.signal === 'SIGTERM') }; +} + module.exports = function gsdPiExtension(pi) { if (!pi || typeof pi !== 'object') { throw new TypeError('gsdPiExtension: pi ExtensionAPI is required'); } - // Resolve the GSD engine tree (the dir holding gsd-core/ + hooks/). - // Works across dev (/pi/gsd.cjs → ) and installed layouts. - const fs = require('fs'); - const path = require('path'); - function resolveEngineRoot(startDir) { - let dir = startDir; - for (let i = 0; i < 6; i++) { - if (fs.existsSync(path.join(dir, 'gsd-core'))) return dir; - const parent = path.dirname(dir); - if (parent === dir) break; - dir = parent; - } - return path.resolve(startDir, '..'); - } - const ENGINE_ROOT = resolveEngineRoot(__dirname); - const GSD_CORE = path.join(ENGINE_ROOT, 'gsd-core'); - - // ── /gsd command: dispatch through the GSD command-routing hub ────────── + // ── /gsd command: dispatch through gsd-tools.cjs (subprocess-reuse) ────── pi.registerCommand('gsd', { - description: 'Invoke a GSD command via the embedded engine (imperative adapter).', - execute: async function (ctx) { - const { createHub } = require(path.join(GSD_CORE, 'bin', 'lib', 'command-routing-hub.cjs')); - const hub = createHub(); - const res = hub.dispatch({ - family: (ctx && ctx.family) || 'query', - subcommand: (ctx && ctx.subcommand) || 'help', - args: (ctx && Array.isArray(ctx.args)) ? ctx.args : [], - cwd: (ctx && ctx.cwd) || process.cwd(), - }); - return JSON.stringify(res); + description: 'Invoke a GSD command via the embedded engine (subprocess-reuse adapter).', + getArgumentCompletions, + handler: async (args, ctx) => { + const cwd = (ctx && ctx.cwd) || process.cwd(); + const { family, subcommand, args: rest } = parseGsdCommandArgs(args); + let dispatchGsdCommand; + try { + ({ dispatchGsdCommand } = require(path.join(GSD_CORE, 'bin', 'lib', 'shell-command-projection.cjs'))); + } catch (e) { + return `GSD engine unavailable: ${e && e.message ? e.message : String(e)}`; + } + const result = dispatchGsdCommand({ family, subcommand, args: rest, cwd }); + if (result.ok) return result.stdout; + return `GSD error: ${result.stderr || result.stdout || `dispatch failed (exit ${result.code})`}`; }, }); // ── gsd_invoke tool: programmatic command invocation ──────────────────── pi.registerTool({ name: 'gsd_invoke', + label: 'GSD Invoke', description: 'Invoke a GSD command family/subcommand through the engine.', - execute: async function () { - const { createHub } = require(path.join(GSD_CORE, 'bin', 'lib', 'command-routing-hub.cjs')); - const hub = createHub(); - const res = hub.dispatch({ family: 'query', subcommand: 'help', args: [], cwd: process.cwd() }); - return JSON.stringify(res); + parameters: buildGsdInvokeParameters(), + execute: async (toolCallId, params, signal, onUpdate, ctx) => { + const p = (params && typeof params === 'object') ? params : {}; + const family = typeof p.family === 'string' ? p.family : ''; + if (!family) { + return { content: [{ type: 'text', text: 'gsd_invoke requires a non-empty string "family".' }] }; + } + const subcommand = typeof p.subcommand === 'string' ? p.subcommand : undefined; + const invokeArgs = Array.isArray(p.args) ? p.args : []; + const cwd = (ctx && ctx.cwd) || process.cwd(); + let dispatchGsdCommand; + try { + ({ dispatchGsdCommand } = require(path.join(GSD_CORE, 'bin', 'lib', 'shell-command-projection.cjs'))); + } catch (e) { + return { content: [{ type: 'text', text: `GSD engine unavailable: ${e && e.message ? e.message : String(e)}` }] }; + } + const result = dispatchGsdCommand({ family, subcommand, args: invokeArgs, cwd }); + const text = result.ok ? result.stdout : (result.stderr || result.stdout || `dispatch failed (exit ${result.code})`); + return { content: [{ type: 'text', text }] }; }, }); - // ── tool_call event: lifecycle hook bridge (extensionEvents: pi) ──────── + // ── before_provider_request: active-model steering (modelMode: 'active') ── + // GSD steers pi's EXISTING built-in anthropic models; it does NOT call + // pi.registerProvider (that would wrongly register a NEW fake provider — + // see buildBeforeProviderRequestHandler's doc comment). + pi.on('before_provider_request', buildBeforeProviderRequestHandler()); + + // ── Event bindings: bounded subprocess bridge to GSD's hook scripts ────── + // Each binding fails open — a hook error/timeout/missing-file never blocks + // pi (mirrors .opencode/plugins/gsd-core.js's runHook SUBPROCESS-REUSE + // pattern, applied to pi's ExtensionAPI event names). + + // session_start → SessionStart-equivalent bootstrap. + pi.on('session_start', async (event, ctx) => { + try { + const cwd = (ctx && ctx.cwd) || process.cwd(); + runHook('gsd-ensure-canonical-path.js', { hook_event_name: 'SessionStart', cwd }, { cwd }); + } catch { /* fail-open */ } + }); + + // before_agent_start → workflow-guard bridge. Forward-compatible binding: + // gsd-workflow-guard.js's current triggers are tool-scoped (Write/Edit/ + // Bash via tool_name/tool_input), so with no tool_name in the payload it + // fires as a safe no-op today — wired so a future agent-start-scoped check + // can attach without a plugin change (mirrors the OpenCode session.idle + // recognized-but-unused sentinel pattern). + pi.on('before_agent_start', async (event, ctx) => { + try { + const cwd = (ctx && ctx.cwd) || process.cwd(); + runHook('gsd-workflow-guard.js', { hook_event_name: 'before_agent_start', cwd }, { cwd }); + } catch { /* fail-open */ } + }); + + // session_before_compact → PreCompact-equivalent (context-usage bridge). + pi.on('session_before_compact', async (event, ctx) => { + try { + const cwd = (ctx && ctx.cwd) || process.cwd(); + runHook('gsd-context-monitor.js', { hook_event_name: 'PreCompact', cwd }, { cwd }); + } catch { /* fail-open */ } + }); + + // tool_call event: lifecycle hook bridge attachment point (kept from the + // original cut — the PreToolUse/PostToolUse tool_name/tool_input mapping + // is a follow-up once pi's tool_call payload shape is verified against a + // live host). pi.on('tool_call', async function () { /* GSD hook bridge attachment point (PreToolUse/PostToolUse mapping). */ }); }; -// Test-only internals (mirrors the OpenCode plugin pattern). -module.exports._internals = { resolveEngineRoot: null }; +// Test-only internals (mirrors the OpenCode plugin pattern) — wired to the +// real functions (not stubs) so tests can exercise parsing/completions/model +// resolution WITHOUT a live pi runtime. +module.exports._internals = { + resolveEngineRoot, + parseGsdCommandArgs, + getArgumentCompletions, + PI_COMMAND_FAMILIES, + buildBeforeProviderRequestHandler, + buildGsdInvokeParameters, + runHook, +}; diff --git a/src/host-integration.cts b/src/host-integration.cts index 808efa452..6617d1430 100644 --- a/src/host-integration.cts +++ b/src/host-integration.cts @@ -584,7 +584,21 @@ const EXTENSION_EVENT_SURFACES: Readonly> = Ob 'pre_gateway_dispatch', 'pre_approval_request', 'transform_tool_result', ]), - pi: Object.freeze(['tool_call']), + // #2102 Stage 2 — pi's real ExtensionAPI event vocabulary (~30 fine-grained + // extension events; documentation-sourced, ADR-1239 §research). Replaces the + // placeholder single-event ['tool_call'] surface — the Stage 1 value only + // covered the one event pi/gsd.cjs happened to bind at the time, not the + // full declared surface. + pi: Object.freeze([ + 'session_start', 'project_trust', 'resources_discover', 'input', + 'before_agent_start', 'agent_start', 'message_start', 'message_update', + 'message_end', 'turn_start', 'context', 'before_provider_request', + 'after_provider_response', 'tool_execution_start', 'tool_execution_update', + 'tool_execution_end', 'tool_call', 'tool_result', 'turn_end', 'agent_end', + 'session_before_switch', 'session_shutdown', 'session_before_fork', + 'session_info_changed', 'session_before_compact', 'session_compact', + 'session_before_tree', 'session_tree', 'thinking_level_select', 'model_select', + ]), none: Object.freeze([]), }); diff --git a/src/install-engine.cts b/src/install-engine.cts index d991f5997..31a6f4357 100644 --- a/src/install-engine.cts +++ b/src/install-engine.cts @@ -715,6 +715,19 @@ function installRuntimeArtifacts( const nestedGsdDirForCleanup = path.join(configDir, 'skills', 'gsd'); _removeHermesBareStemDirs(nestedGsdDirForCleanup); } + + // Generic-branch nativePlugin staging (ADR-1239 / #2102 Stage 1): runtimes + // outside the OpenCode/Kilo combined-family install (e.g. pi, whose + // artifactLayout is empty and which never sets combinedFamilyInstall) still + // need their declared hostBehaviors.nativePlugin file copied into configDir. + // findInstallSourceRoot resolves the repo/package root independent of + // configDir contents (marker check, then a walk-up from __dirname), so this + // is safe even when configDir has no .gsd-source marker (artifactLayout: []). + if (behaviors.nativePlugin) { + const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir); + const src = path.dirname(path.dirname(commandsGsdDir)); + _installNativePluginIfDeclared(runtime, configDir, behaviors, src); + } } // --------------------------------------------------------------------------- @@ -879,6 +892,44 @@ function installOpencodeFamilyCommands( } } +// --------------------------------------------------------------------------- +// _installNativePluginIfDeclared +// --------------------------------------------------------------------------- + +/** + * Copy a runtime's declared native-extension/plugin file (hostBehaviors.nativePlugin) + * into its resolved config dir, when the runtime descriptor declares one. + * + * Extracted (ADR-1239 / #2102 Stage 1) from the body previously inlined in + * installOpencodeFamilyArtifacts so a runtime that is NOT part of the + * OpenCode/Kilo combined-family install (e.g. pi, whose artifactLayout is + * empty and which never sets combinedFamilyInstall) can still get its + * nativePlugin file staged via the generic installRuntimeArtifacts branch. + * Behavior for opencode/kilo is unchanged — same source resolution, same + * mkdir + copyFileSync call, same silent no-op when the source is missing. + * + * @param runtime - canonical runtime id (only used for the assertDestWithinConfigHome guard) + * @param configDir - resolved runtime config directory + * @param behaviors - the runtime's hostBehaviors descriptor + * @param src - repo/package root (two levels up from the commands/gsd source dir) + */ +function _installNativePluginIfDeclared( + runtime: string, + configDir: string, + behaviors: any, + src: string, +): void { + const np = behaviors.nativePlugin; + if (np && np.source) { + const pluginSrc = path.join(src, np.source); + if (fs.existsSync(pluginSrc)) { + const destDir = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, np.dir); + fs.mkdirSync(destDir, { recursive: true }); + fs.copyFileSync(pluginSrc, path.join(destDir, np.file)); + } + } +} + // --------------------------------------------------------------------------- // installOpencodeFamilyArtifacts // --------------------------------------------------------------------------- @@ -927,15 +978,7 @@ function installOpencodeFamilyArtifacts( installOpencodeFamilyCommands(runtime, commandDir, rawCommandsDir, pathPrefix, resolveAttribution); installOpencodeFamilySkills(runtime, configDir, rawCommandsDir, pathPrefix, resolveAttribution); - const np = behaviors.nativePlugin; - if (np && np.source) { - const pluginSrc = path.join(src, np.source); - if (fs.existsSync(pluginSrc)) { - const destDir = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, np.dir); - fs.mkdirSync(destDir, { recursive: true }); - fs.copyFileSync(pluginSrc, path.join(destDir, np.file)); - } - } + _installNativePluginIfDeclared(runtime, configDir, behaviors, src); } // --------------------------------------------------------------------------- @@ -1004,6 +1047,7 @@ export = { installOpencodeFamilySkills, installOpencodeFamilyCommands, installOpencodeFamilyArtifacts, + _installNativePluginIfDeclared, _hostBehaviors, _copyStaged, hasExistingSymlinkBetween, diff --git a/src/mcp-server.cts b/src/mcp-server.cts index 9b5d00c97..c7f54a18d 100644 --- a/src/mcp-server.cts +++ b/src/mcp-server.cts @@ -5,8 +5,16 @@ * so any MCP-consuming host (Claude/Codex/OpenCode/VS Code/Gemini/Cursor/Cline/ * Hermes) can drive GSD with NO bespoke plugin: * - * - point 1 (command): tool `gsd_invoke_command` → the command-routing hub - * (`createHub`/`dispatch`, src/command-routing-hub.cts). + * - point 1 (command): tool `gsd_invoke_command` → `dispatchGsdCommand` + * (src/shell-command-projection.cts), a bounded subprocess-shim to + * gsd-tools.cjs. #2102 Stage 2: `commandRoutingHub.createHub()` called + * with no args here always hit `if(!_cjsRegistry) return + * makeUnknownCommand()` — every dispatch was UnknownCommand. No + * fully-populated hub factory exists anywhere in gsd-core (every + * createHub() caller builds a single-family hub for its own narrow + * purpose), so the fix routes through the SAME shared dispatch helper + * the pi extension uses (pi/gsd.cjs), mirroring the SUBPROCESS-REUSE + * precedent already established for the OpenCode/Kilo hook bridge. * - point 5 (state IO): tools `gsd_read_state` / `gsd_write_state` → the * Phase 3 `stateIO` seam (src/state-io.cts, filesystem default). * @@ -20,10 +28,11 @@ */ 'use strict'; -// eslint-disable-next-line @typescript-eslint/no-require-imports -import commandRoutingHub = require('./command-routing-hub.cjs'); // eslint-disable-next-line @typescript-eslint/no-require-imports import stateIo = require('./state-io.cjs'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +import shellCommandProjection = require('./shell-command-projection.cjs'); +const { dispatchGsdCommand } = shellCommandProjection; export const PROTOCOL_VERSION = '2024-11-05'; export const SERVER_NAME = 'gsd-core'; @@ -108,9 +117,11 @@ function callTool(name: string, args: unknown, ctx: McpContext): { content: Arra if (!family || !subcommand) { return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires string "family" and "subcommand".' }] }; } - const hub = commandRoutingHub.createHub(); - const res = hub.dispatch({ family, subcommand, args: Array.isArray(a.args) ? a.args : [], cwd, raw: undefined }); - return { content: [{ type: 'text', text: JSON.stringify(res) }] }; + const res = dispatchGsdCommand({ family, subcommand, args: Array.isArray(a.args) ? (a.args as string[]) : [], cwd }); + if (!res.ok) { + return { isError: true, content: [{ type: 'text', text: res.stderr || res.stdout || `dispatch failed (exit ${res.code})` }] }; + } + return { content: [{ type: 'text', text: res.stdout }] }; } if (name === 'gsd_read_state') { const p = asString(a.path); diff --git a/src/runtime-name-policy.cts b/src/runtime-name-policy.cts index 9c31d8294..0880786cd 100644 --- a/src/runtime-name-policy.cts +++ b/src/runtime-name-policy.cts @@ -210,6 +210,7 @@ const RUNTIME_LABELS: Readonly> = { codebuddy: 'CodeBuddy', cline: 'Cline', zcode: 'ZCode', + pi: 'pi', }; /** @@ -258,6 +259,12 @@ const GLOBAL_CONFIG_HOME_FRAGMENTS: Readonly> = { cline: "'.cline'", kimi: "'.config', 'agents'", zcode: "'.zcode'", + // pi's global config home is ~/.pi/agent (configHome: dot-home-nested, + // parent '.pi', name 'agent' — capabilities/pi/capability.json), matching + // resolveConfigHomeFromDescriptor's `path.join(home, parent, name)` for the + // no-probe dot-home-nested case (src/runtime-homes.cts). Two-segment + // path.join args, same shape as opencode/kilo/kimi above. + pi: "'.pi', 'agent'", }; /** @@ -286,7 +293,7 @@ export function getGlobalConfigHomeFragment(runtime: string): string { // folds the shared-hooks-install skip). const RUNTIME_FLAG_IDS = Object.freeze([ 'opencode', 'kilo', 'codex', 'copilot', 'antigravity', 'cursor', - 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi', 'zcode', + 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi', 'zcode', 'pi', ] as const); /** diff --git a/src/shell-command-projection.cts b/src/shell-command-projection.cts index 1cb4f8920..14d5dbfc8 100644 --- a/src/shell-command-projection.cts +++ b/src/shell-command-projection.cts @@ -518,6 +518,143 @@ export function execTool(program: string, args: string[], opts: { cwd?: string; return _spawnResult(result, program); } +/** + * Result shape for {@link dispatchGsdCommand}. Modeled on the existing + * `{exitCode,stdout,stderr,signal,error}` seam above, but flattened to the + * fields callers actually need (never leaks a raw Error/signal — see + * `timedOut`), per the "Unbounded Subprocesses" contract (CLAUDE.md): + * degrade to a structured result on timeout/ENOENT, never throw. + */ +export interface DispatchGsdCommandResult { + ok: boolean; + stdout: string; + stderr: string; + code: number | null; + timedOut: boolean; +} + +/** + * Resolve the absolute path to gsd-tools.cjs relative to THIS module. + * + * This file compiles to gsd-core/bin/lib/shell-command-projection.cjs — a + * sibling of gsd-core/bin/gsd-tools.cjs — so the relative walk-up is stable + * regardless of install location (global/local/dev-repo layouts all ship + * gsd-core/bin/ as a unit). + */ +export function resolveGsdToolsPath(): string { + return path.resolve(__dirname, '..', 'gsd-tools.cjs'); +} + +/** + * Subprocess-shim dispatch to gsd-tools.cjs (ADR-1239 #2102 Stage 2). + * + * No fully-populated in-process command-routing hub exists anywhere in the + * tree — every `createHub()` caller (cjs-command-router-adapter.cts, + * phase-command-router.cts, command-routing-hub.cts's own tests) builds a + * single-family hub for its own narrow purpose. The ONLY dispatch path that + * covers the FULL family/subcommand surface is the gsd-tools.cjs CLI itself. + * This mirrors the SUBPROCESS-REUSE precedent already established for the + * OpenCode/Kilo hook bridge (see .opencode/plugins/gsd-core.js header: + * "Architecture: SUBPROCESS REUSE ... spawns existing hook scripts as child + * processes") — the same pattern, applied to command dispatch instead of + * hook dispatch. + * + * Output-flag choice (verified by direct invocation — see #2102 dispatch + * notes for the sample invocations): always pass `--raw` (undecorated, + * programmatically-consumable stdout on success) and `--json-errors` (a + * structured `{ok:false,reason,message}` JSON object on stderr, with a + * non-zero exit, instead of a free-text "Error: ..." line). Both are global + * flags accepted by every gsd-tools.cjs family/subcommand, so passing them + * unconditionally is safe for the full command surface. + * + * `family` maps 1:1 onto gsd-tools.cjs's first positional argv token; + * `subcommand` (when present) onto the second — e.g. + * `{family:'phase', subcommand:'add'}` → `gsd-tools.cjs phase add`. An empty + * `subcommand` is omitted entirely (some families, e.g. `config-path`, take + * no subcommand). + * + * NEVER throws. Degrades to `{ ok:false, ... }` on: + * - a missing/invalid "family" (validated locally, no subprocess spawned) + * - ENOENT / a missing gsd-tools.cjs (via the injectable `gsdToolsPath`) + * - a wall-clock timeout (`timedOut:true`, mirroring the + * `signal === 'SIGTERM' && error.code === 'ETIMEDOUT'` idiom already used + * by worktree-safety.cts) + * - any other unanticipated throw from the underlying spawn (defensive + * try/catch — execTool itself is spawnSync-based and does not throw). + */ +export function dispatchGsdCommand({ + family, + subcommand, + args = [], + cwd, + timeout = 30_000, + gsdToolsPath, +}: { + family?: string; + subcommand?: string; + args?: string[]; + cwd?: string; + timeout?: number; + gsdToolsPath?: string; +} = {}): DispatchGsdCommandResult { + if (typeof family !== 'string' || family.length === 0) { + return { + ok: false, + stdout: '', + stderr: 'dispatchGsdCommand requires a non-empty string "family".', + code: null, + timedOut: false, + }; + } + + const resolvedCwd = cwd || process.cwd(); + const toolsPath = gsdToolsPath || resolveGsdToolsPath(); + const argv = [ + toolsPath, + family, + ...(subcommand ? [subcommand] : []), + ...(Array.isArray(args) ? args : []), + '--cwd', resolvedCwd, + '--raw', + '--json-errors', + ]; + + let result: SpawnResultOutput; + try { + result = execTool(process.execPath, argv, { cwd: resolvedCwd, timeout }); + } catch (e) { + // Defensive belt-and-suspenders: execTool is spawnSync-based and does not + // throw today, but a degraded result here keeps this seam's no-throw + // contract true even under an unanticipated future failure mode. + return { + ok: false, + stdout: '', + stderr: e instanceof Error ? e.message : String(e), + code: null, + timedOut: false, + }; + } + + // Mirrors the established `result.error && (result.error as + // NodeJS.ErrnoException).code === ...` idiom (graphify.cts, worktree-safety.cts): + // narrow away null via `!== null` FIRST, then cast — asserting `Error | null` + // to `NodeJS.ErrnoException | null` directly (paired with optional chaining) + // trips a typescript-eslint no-unnecessary-type-assertion false positive for + // this exact narrowing shape (all of ErrnoException's extra fields over Error + // are optional). + const timedOut = result.signal === 'SIGTERM' + && result.error !== null + && (result.error as NodeJS.ErrnoException).code === 'ETIMEDOUT'; + + return { + ok: result.exitCode === 0 && !timedOut, + stdout: result.stdout, + stderr: result.stderr, + code: result.exitCode, + timedOut, + }; +} + export function probeTty(opts: { platform?: string } = {}): string | null { const platform = opts.platform ?? process.platform; if (platform === 'win32') return null; diff --git a/tests/fixtures/golden-install-parity/antigravity.json b/tests/fixtures/golden-install-parity/antigravity.json index 598a19e5c..0aff340e1 100644 --- a/tests/fixtures/golden-install-parity/antigravity.json +++ b/tests/fixtures/golden-install-parity/antigravity.json @@ -42,7 +42,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "8bc541aabc2e143c", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -289,7 +289,7 @@ "gsd-core/workflows/scan.md": "a7fecd67e5cd655f", "gsd-core/workflows/secure-phase.md": "96b199dfac00e60f", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "289b3d653d18e284", + "gsd-core/workflows/settings-advanced.md": "6d25100a9de15b31", "gsd-core/workflows/settings-integrations.md": "4d3001ad2b5dad8f", "gsd-core/workflows/settings.md": "8258f7bd3700d608", "gsd-core/workflows/ship.md": "984bd7660e7791fd", diff --git a/tests/fixtures/golden-install-parity/augment.json b/tests/fixtures/golden-install-parity/augment.json index 7b2273b2b..3f6666560 100644 --- a/tests/fixtures/golden-install-parity/augment.json +++ b/tests/fixtures/golden-install-parity/augment.json @@ -113,7 +113,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -360,7 +360,7 @@ "gsd-core/workflows/scan.md": "003883d71c37da7d", "gsd-core/workflows/secure-phase.md": "29fc6b62c5c5dc62", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "b6fa466a953dd3a2", + "gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44", "gsd-core/workflows/settings-integrations.md": "70515c5838fb9826", "gsd-core/workflows/settings.md": "d96ddf01fb85e61e", "gsd-core/workflows/ship.md": "7b8fe9f89143e648", diff --git a/tests/fixtures/golden-install-parity/claude-local.json b/tests/fixtures/golden-install-parity/claude-local.json index 77feb0995..061264898 100644 --- a/tests/fixtures/golden-install-parity/claude-local.json +++ b/tests/fixtures/golden-install-parity/claude-local.json @@ -112,7 +112,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -359,7 +359,7 @@ "gsd-core/workflows/scan.md": "75c670d08cee8680", "gsd-core/workflows/secure-phase.md": "64ec4d06ca85720a", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "a44896a18f1f2edc", + "gsd-core/workflows/settings-advanced.md": "94d61da368e9f85b", "gsd-core/workflows/settings-integrations.md": "dfe3672c4fabf139", "gsd-core/workflows/settings.md": "acdd79110699a608", "gsd-core/workflows/ship.md": "44af1c72d86e153b", diff --git a/tests/fixtures/golden-install-parity/claude.json b/tests/fixtures/golden-install-parity/claude.json index d4f6f746d..5d487ada6 100644 --- a/tests/fixtures/golden-install-parity/claude.json +++ b/tests/fixtures/golden-install-parity/claude.json @@ -41,7 +41,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -288,7 +288,7 @@ "gsd-core/workflows/scan.md": "47371c2073d6c0be", "gsd-core/workflows/secure-phase.md": "59d3c50aba8c9a6c", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "fa4a285b6bcbc384", + "gsd-core/workflows/settings-advanced.md": "339def28c34b0797", "gsd-core/workflows/settings-integrations.md": "53649313d20694ae", "gsd-core/workflows/settings.md": "7e7458cdb2b68ec5", "gsd-core/workflows/ship.md": "12e8e58c077a891a", diff --git a/tests/fixtures/golden-install-parity/cline.json b/tests/fixtures/golden-install-parity/cline.json index 213d78a32..7d744ed74 100644 --- a/tests/fixtures/golden-install-parity/cline.json +++ b/tests/fixtures/golden-install-parity/cline.json @@ -45,7 +45,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -292,7 +292,7 @@ "gsd-core/workflows/scan.md": "dfd92717caea0ce7", "gsd-core/workflows/secure-phase.md": "cf78183f06a02582", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "7147ba291d13e007", + "gsd-core/workflows/settings-advanced.md": "69f3a19bf2c61160", "gsd-core/workflows/settings-integrations.md": "76eee76d6eb57657", "gsd-core/workflows/settings.md": "3701faed09d55247", "gsd-core/workflows/ship.md": "5f931a25ea9102a4", diff --git a/tests/fixtures/golden-install-parity/codebuddy.json b/tests/fixtures/golden-install-parity/codebuddy.json index fd4203926..8df634f4c 100644 --- a/tests/fixtures/golden-install-parity/codebuddy.json +++ b/tests/fixtures/golden-install-parity/codebuddy.json @@ -113,7 +113,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -360,7 +360,7 @@ "gsd-core/workflows/scan.md": "003883d71c37da7d", "gsd-core/workflows/secure-phase.md": "29fc6b62c5c5dc62", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "b6fa466a953dd3a2", + "gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44", "gsd-core/workflows/settings-integrations.md": "70515c5838fb9826", "gsd-core/workflows/settings.md": "d96ddf01fb85e61e", "gsd-core/workflows/ship.md": "7b8fe9f89143e648", diff --git a/tests/fixtures/golden-install-parity/codex.json b/tests/fixtures/golden-install-parity/codex.json index 0404c6786..fc35ccd2f 100644 --- a/tests/fixtures/golden-install-parity/codex.json +++ b/tests/fixtures/golden-install-parity/codex.json @@ -148,7 +148,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -395,7 +395,7 @@ "gsd-core/workflows/scan.md": "1a3caa5d724d39e9", "gsd-core/workflows/secure-phase.md": "db91810d16964b1e", "gsd-core/workflows/session-report.md": "dd8fa011c9394075", - "gsd-core/workflows/settings-advanced.md": "38566aab0c529f2c", + "gsd-core/workflows/settings-advanced.md": "2431433811616f76", "gsd-core/workflows/settings-integrations.md": "77730321d3d6d317", "gsd-core/workflows/settings.md": "054c8c31b3905ced", "gsd-core/workflows/ship.md": "d86233c9a365effd", diff --git a/tests/fixtures/golden-install-parity/copilot.json b/tests/fixtures/golden-install-parity/copilot.json index 17c7aef42..0076bec72 100644 --- a/tests/fixtures/golden-install-parity/copilot.json +++ b/tests/fixtures/golden-install-parity/copilot.json @@ -43,7 +43,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "10226e9512dd44bf", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -290,7 +290,7 @@ "gsd-core/workflows/scan.md": "dcc2f76d0850e2fb", "gsd-core/workflows/secure-phase.md": "d87bd706f85bcad6", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "8c88bbcb8fa0fa39", + "gsd-core/workflows/settings-advanced.md": "230a658de9c017a6", "gsd-core/workflows/settings-integrations.md": "a1d146d6bfd14db5", "gsd-core/workflows/settings.md": "f611f14f2f447f1e", "gsd-core/workflows/ship.md": "b9dc0aaee0ff68e7", diff --git a/tests/fixtures/golden-install-parity/cursor.json b/tests/fixtures/golden-install-parity/cursor.json index 9a5388888..5b43b78b1 100644 --- a/tests/fixtures/golden-install-parity/cursor.json +++ b/tests/fixtures/golden-install-parity/cursor.json @@ -113,7 +113,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -360,7 +360,7 @@ "gsd-core/workflows/scan.md": "47371c2073d6c0be", "gsd-core/workflows/secure-phase.md": "c55975672c4e1895", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "93a20fc634b05964", + "gsd-core/workflows/settings-advanced.md": "bf8ea69c8f7ae019", "gsd-core/workflows/settings-integrations.md": "166ca51b1f33c2a3", "gsd-core/workflows/settings.md": "0623c673eaf04799", "gsd-core/workflows/ship.md": "38830806d244fe9c", diff --git a/tests/fixtures/golden-install-parity/hermes.json b/tests/fixtures/golden-install-parity/hermes.json index 0dac7d2a8..4dd6d3631 100644 --- a/tests/fixtures/golden-install-parity/hermes.json +++ b/tests/fixtures/golden-install-parity/hermes.json @@ -42,7 +42,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -289,7 +289,7 @@ "gsd-core/workflows/scan.md": "b28f65d88c522767", "gsd-core/workflows/secure-phase.md": "f2957d4b88fb3746", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "7374ef968d5280f0", + "gsd-core/workflows/settings-advanced.md": "49be159144d7f426", "gsd-core/workflows/settings-integrations.md": "1dce76db0aca08a5", "gsd-core/workflows/settings.md": "0845d073009a4619", "gsd-core/workflows/ship.md": "dbf8bf636cb196c0", diff --git a/tests/fixtures/golden-install-parity/kilo.json b/tests/fixtures/golden-install-parity/kilo.json index 92e56cd37..c1021bce7 100644 --- a/tests/fixtures/golden-install-parity/kilo.json +++ b/tests/fixtures/golden-install-parity/kilo.json @@ -113,7 +113,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -360,7 +360,7 @@ "gsd-core/workflows/scan.md": "47371c2073d6c0be", "gsd-core/workflows/secure-phase.md": "e8855104c1e0417c", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "e00cd1a5d3fbba9c", + "gsd-core/workflows/settings-advanced.md": "edd858cd6cfddaf1", "gsd-core/workflows/settings-integrations.md": "3ea8095d5fad891b", "gsd-core/workflows/settings.md": "1925ecc2225c2216", "gsd-core/workflows/ship.md": "afd77be2093535f8", diff --git a/tests/fixtures/golden-install-parity/kimi.json b/tests/fixtures/golden-install-parity/kimi.json index 23239bd66..f96eaed8e 100644 --- a/tests/fixtures/golden-install-parity/kimi.json +++ b/tests/fixtures/golden-install-parity/kimi.json @@ -106,7 +106,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -353,7 +353,7 @@ "gsd-core/workflows/scan.md": "003883d71c37da7d", "gsd-core/workflows/secure-phase.md": "29fc6b62c5c5dc62", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "b6fa466a953dd3a2", + "gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44", "gsd-core/workflows/settings-integrations.md": "70515c5838fb9826", "gsd-core/workflows/settings.md": "d96ddf01fb85e61e", "gsd-core/workflows/ship.md": "7b8fe9f89143e648", diff --git a/tests/fixtures/golden-install-parity/opencode.json b/tests/fixtures/golden-install-parity/opencode.json index 22829d4aa..0ff4b6515 100644 --- a/tests/fixtures/golden-install-parity/opencode.json +++ b/tests/fixtures/golden-install-parity/opencode.json @@ -113,7 +113,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -360,7 +360,7 @@ "gsd-core/workflows/scan.md": "ad8ebcad4626d4a8", "gsd-core/workflows/secure-phase.md": "e9a488cec3b4efdc", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "92524bd2a53b43f5", + "gsd-core/workflows/settings-advanced.md": "252b0d3edc315339", "gsd-core/workflows/settings-integrations.md": "d1711a95f44fdbde", "gsd-core/workflows/settings.md": "2e42ee34c791378a", "gsd-core/workflows/ship.md": "924c79e3cfd1e42b", diff --git a/tests/fixtures/golden-install-parity/pi.json b/tests/fixtures/golden-install-parity/pi.json new file mode 100644 index 000000000..ca40d6239 --- /dev/null +++ b/tests/fixtures/golden-install-parity/pi.json @@ -0,0 +1,322 @@ +{ + ".gsd-profile": "0e716a5fef4e6dc1", + ".gsd/defaults.json": "560664b045e645cb", + "extensions/gsd.cjs": "619cec0af9cfdadf", + "gsd-core/VERSION": "ef0deccd81a6723c", + "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", + "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", + "gsd-core/bin/gsd-tools.cjs": "6a7616125440c8b1", + "gsd-core/bin/gsd_run": "62d9b647ede212e6", + "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", + "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", + "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", + "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", + "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", + "gsd-core/contexts/research.md": "b3285d8e7209cc3b", + "gsd-core/contexts/review.md": "dc578fdd74bbea11", + "gsd-core/references/agent-contracts.md": "ff65e633c656c0d2", + "gsd-core/references/agent-skills-bootstrap.md": "5ab875054b1adda9", + "gsd-core/references/ai-evals.md": "b5afa786b938671e", + "gsd-core/references/ai-frameworks.md": "f827de93dde124eb", + "gsd-core/references/api-coverage.md": "66264d41dfd9154a", + "gsd-core/references/artifact-types.md": "a6d2e1f9453ffbf5", + "gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785", + "gsd-core/references/checkpoints.md": "6aa620c6ca38bdf0", + "gsd-core/references/common-bug-patterns.md": "780145be56352626", + "gsd-core/references/context-budget.md": "f1ce57bf418824af", + "gsd-core/references/continuation-format.md": "ce73e57d053ab8a7", + "gsd-core/references/debugger-philosophy.md": "0466f95a3d6bfcd3", + "gsd-core/references/decimal-phase-calculation.md": "46b5ba045852c474", + "gsd-core/references/doc-conflict-engine.md": "67d019d23e17f934", + "gsd-core/references/domain-probes.md": "62d23ed1992c48a9", + "gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json": "72d1e29cedc854ec", + "gsd-core/references/edge-probe-fixtures/01-round-half-even/requirements.json": "fbc1b355d8625eeb", + "gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json": "fad67dcc8294f6da", + "gsd-core/references/edge-probe-fixtures/02-merge-intervals/requirements.json": "30a78ee9ce3473ea", + "gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json": "66dd60957fee45f0", + "gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/requirements.json": "47fca61f076835fa", + "gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json": "72d1e29cedc854ec", + "gsd-core/references/edge-probe-fixtures/04-money-rounding/requirements.json": "80f04f5c04fb24cf", + "gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json": "fad67dcc8294f6da", + "gsd-core/references/edge-probe-fixtures/05-list-dedupe/requirements.json": "d38147adb0e5b342", + "gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json": "bc552c01939bf4f8", + "gsd-core/references/edge-probe-fixtures/06-resolved-mixed/requirements.json": "30a78ee9ce3473ea", + "gsd-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json": "688ec62c13e08afe", + "gsd-core/references/edge-probe.md": "5687eba25a078561", + "gsd-core/references/execute-mvp-tdd.md": "a98a270a7ab126bc", + "gsd-core/references/execute-phase-between-wave-reset.md": "3ad96ca0f7fee37e", + "gsd-core/references/execute-phase-context-guard.md": "982006c1f3364242", + "gsd-core/references/execute-phase-wave-guard.md": "de9ac22cead4cfd8", + "gsd-core/references/executor-examples.md": "ba59243ed45c8ab1", + "gsd-core/references/few-shot-examples/plan-checker.md": "2574808188ac9de4", + "gsd-core/references/few-shot-examples/verifier.md": "5badee4560b14ae8", + "gsd-core/references/gate-prompts.md": "e69f5993ab944d80", + "gsd-core/references/gates.md": "bd79c7f90c8cb8d7", + "gsd-core/references/git-integration.md": "77bf9dff38b2c9d4", + "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "e71eb728b84641b1", + "gsd-core/references/honest-verifier.md": "8815c9fc18c35719", + "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", + "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", + "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", + "gsd-core/references/model-profile-resolution.md": "f32bb05102839767", + "gsd-core/references/model-profiles.md": "e067ad3df6770db1", + "gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118", + "gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34", + "gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac", + "gsd-core/references/planner-chunked.md": "79fe674221e738e6", + "gsd-core/references/planner-gap-closure.md": "76bee257911413e7", + "gsd-core/references/planner-graphify-auto-update.md": "6aeffd9097e25e22", + "gsd-core/references/planner-guidance.md": "96486cac2f7885e6", + "gsd-core/references/planner-human-verify-mode.md": "56d05e841630b3f4", + "gsd-core/references/planner-interface-context.md": "b28fa3da6ae739a8", + "gsd-core/references/planner-load-graph-context.md": "ca7a7af3f35ae61b", + "gsd-core/references/planner-mvp-mode.md": "ec33050db81101a8", + "gsd-core/references/planner-reviews.md": "dda0193a0fbd4947", + "gsd-core/references/planner-revision.md": "86ba8a511f081f05", + "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", + "gsd-core/references/planning-config.md": "ac409835e8260a3e", + "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", + "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", + "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", + "gsd-core/references/prohibition-probe.md": "605dc3f5a118ff3b", + "gsd-core/references/project-skills-discovery.md": "c155e03dce8dc3c2", + "gsd-core/references/questioning.md": "a8c988cab05f4651", + "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", + "gsd-core/references/research-philosophy.md": "62930e66cc979c1a", + "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "385501e4f9bbb31d", + "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", + "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", + "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", + "gsd-core/references/skeleton-template.md": "528691d1f0efa878", + "gsd-core/references/sketch-interactivity.md": "7d982fe877e1e1cc", + "gsd-core/references/sketch-theme-system.md": "33e2e96e450456f8", + "gsd-core/references/sketch-tooling.md": "df6c4f24c1c27611", + "gsd-core/references/sketch-variant-patterns.md": "66c197aa4fb52810", + "gsd-core/references/specless-probe-fallback.md": "5e400dc05a15e972", + "gsd-core/references/spidr-splitting.md": "074ac154c0e4f906", + "gsd-core/references/tdd.md": "e4708ede157478b6", + "gsd-core/references/thinking-models-debug.md": "2da61022b16c4e7c", + "gsd-core/references/thinking-models-execution.md": "dcc650a8b5f3e049", + "gsd-core/references/thinking-models-planning.md": "7e19462313fa028f", + "gsd-core/references/thinking-models-research.md": "5f6bf3f3b889c6e4", + "gsd-core/references/thinking-models-verification.md": "a71a933d51ca3d8d", + "gsd-core/references/thinking-partner.md": "41069529ef776e39", + "gsd-core/references/ui-brand.md": "48717bcfcd63bd27", + "gsd-core/references/ui-consideration-probe.md": "7e019dfaae47f4c4", + "gsd-core/references/universal-anti-patterns.md": "6a1245050b21df01", + "gsd-core/references/untrusted-input-boundary.md": "d33b80d4d348599a", + "gsd-core/references/user-profiling.md": "b50416fe57c1b321", + "gsd-core/references/user-story-template.md": "0cc50e06a144ff8a", + "gsd-core/references/verification-overrides.md": "8213de9bd62283b6", + "gsd-core/references/verification-patterns.md": "72f4d4814fdae9a8", + "gsd-core/references/verify-mvp-mode.md": "534bdc7f2432903a", + "gsd-core/references/workstream-flag.md": "ca99ca79e716f0f5", + "gsd-core/references/worktree-branch-check.md": "21d9c31bf6542b93", + "gsd-core/references/worktree-path-safety.md": "3c8d74756f9b16a8", + "gsd-core/templates/AI-SPEC.md": "efa1f8354bd3a24b", + "gsd-core/templates/DEBUG.md": "a13470b82b1935e7", + "gsd-core/templates/README.md": "93d3426fc64e2c12", + "gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f", + "gsd-core/templates/UAT.md": "9e296471b97ebcec", + "gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec", + "gsd-core/templates/VALIDATION.md": "f53e0ca061d3528e", + "gsd-core/templates/claude-md.md": "d1d333e4b963c0d2", + "gsd-core/templates/codebase/architecture.md": "6be88214162fdd89", + "gsd-core/templates/codebase/concerns.md": "efa26d1fb5132f25", + "gsd-core/templates/codebase/conventions.md": "c2e07698dad6b364", + "gsd-core/templates/codebase/integrations.md": "39bd23c71eedd564", + "gsd-core/templates/codebase/stack.md": "116e7e67dd87ddec", + "gsd-core/templates/codebase/structure.md": "222997133232a6f5", + "gsd-core/templates/codebase/testing.md": "76abff7f2050c9ea", + "gsd-core/templates/config.json": "a4b783ef759a0f37", + "gsd-core/templates/context.md": "69b01e7909ea3f66", + "gsd-core/templates/continue-here.md": "f522a51b6895fba8", + "gsd-core/templates/copilot-instructions.md": "aea34bc52ff548ea", + "gsd-core/templates/debug-subagent-prompt.md": "920656683dedb869", + "gsd-core/templates/dev-preferences.md": "88d0a65ec0993a3a", + "gsd-core/templates/discovery.md": "9a0e0935cc825dbc", + "gsd-core/templates/discussion-log.md": "cac1b48ec0f4dcb8", + "gsd-core/templates/milestone-archive.md": "591b6decdc0c0e51", + "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", + "gsd-core/templates/phase-prompt.md": "b811bf951092df2f", + "gsd-core/templates/planner-subagent-prompt.md": "ebf29dbb27042370", + "gsd-core/templates/project.md": "4f311fb1b05b823b", + "gsd-core/templates/requirements.md": "a44de4c2f146e473", + "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", + "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", + "gsd-core/templates/research-project/PITFALLS.md": "3ef75fa768422eec", + "gsd-core/templates/research-project/STACK.md": "82c85799ac4dd344", + "gsd-core/templates/research-project/SUMMARY.md": "dceb2f346388839d", + "gsd-core/templates/research.md": "88ce0920417091d0", + "gsd-core/templates/retrospective.md": "03981e30dd760103", + "gsd-core/templates/roadmap.md": "e4e35a9eb5dd4d4f", + "gsd-core/templates/spec.md": "26d55bce940f0288", + "gsd-core/templates/state.md": "4d123aa6cea167fe", + "gsd-core/templates/summary-complex.md": "a5e40574fd8894dc", + "gsd-core/templates/summary-minimal.md": "7d09b5e709e2e67c", + "gsd-core/templates/summary-standard.md": "e8d9cf4a8377cdff", + "gsd-core/templates/summary.md": "23c40f6503b3ea98", + "gsd-core/templates/user-profile.md": "20749f23e4c413fc", + "gsd-core/templates/user-setup.md": "78b7d718b6e8d67c", + "gsd-core/templates/verification-report.md": "dd5faa6254183731", + "gsd-core/workflows/_runtime-launcher.snippet.sh": "bf2dd5d1debd5335", + "gsd-core/workflows/add-backlog.md": "1bc7377b105194fc", + "gsd-core/workflows/add-phase.md": "46e0551ffdd8ce1a", + "gsd-core/workflows/add-tests.md": "2c1da65d41dc12d2", + "gsd-core/workflows/add-todo.md": "cc0efe270004c8fb", + "gsd-core/workflows/ai-integration-phase.md": "40c217869a06981f", + "gsd-core/workflows/analyze-dependencies.md": "52942af10f140717", + "gsd-core/workflows/audit-fix.md": "eedb2da4bffb7575", + "gsd-core/workflows/audit-milestone.md": "6866ce260980c21e", + "gsd-core/workflows/audit-uat.md": "fcfbec501620b564", + "gsd-core/workflows/autonomous.md": "7e5683728ef33707", + "gsd-core/workflows/check-todos.md": "bdeaf43f9c61e0cc", + "gsd-core/workflows/cleanup.md": "0daa2f2720c111f1", + "gsd-core/workflows/code-review-fix.md": "2e113d1f4350a075", + "gsd-core/workflows/code-review.md": "334c90c401f291f8", + "gsd-core/workflows/complete-milestone.md": "c1f91b77f4ace7f2", + "gsd-core/workflows/debug.md": "849d7e5b9c12dcae", + "gsd-core/workflows/diagnose-issues.md": "d6d978fddfd5da8d", + "gsd-core/workflows/discovery-phase.md": "3ba7cfb89fb1e761", + "gsd-core/workflows/discuss-phase-assumptions.md": "f5b765d33eba4f88", + "gsd-core/workflows/discuss-phase-power.md": "0841f7dc6e9a054a", + "gsd-core/workflows/discuss-phase.md": "acfe2baa4c8bebbe", + "gsd-core/workflows/discuss-phase/modes/advisor.md": "db488d74d080c653", + "gsd-core/workflows/discuss-phase/modes/all.md": "fa70d79066562e54", + "gsd-core/workflows/discuss-phase/modes/analyze.md": "da0788f3be7f8105", + "gsd-core/workflows/discuss-phase/modes/auto.md": "d06f0bea2ba0b240", + "gsd-core/workflows/discuss-phase/modes/batch.md": "6946597770e2d448", + "gsd-core/workflows/discuss-phase/modes/chain.md": "5502a67de7776853", + "gsd-core/workflows/discuss-phase/modes/default.md": "67d1b67f61f03966", + "gsd-core/workflows/discuss-phase/modes/power.md": "fdc7a728eaaa2261", + "gsd-core/workflows/discuss-phase/modes/text.md": "da6d45207da4a988", + "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", + "gsd-core/workflows/discuss-phase/templates/context.md": "6cd929e989fe2b0f", + "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "1bbd7703f11128e1", + "gsd-core/workflows/do.md": "7512dd1892f118a1", + "gsd-core/workflows/docs-update.md": "f35922d15b7061c9", + "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", + "gsd-core/workflows/eval-review.md": "f898936e2cfe4130", + "gsd-core/workflows/execute-phase.md": "8f6c2443655aceb1", + "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e", + "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", + "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581", + "gsd-core/workflows/execute-phase/steps/regression-gate.md": "8ccc16a6c7cf6000", + "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md": "be84efbd71e1513e", + "gsd-core/workflows/execute-plan.md": "ff172c3540b52e9d", + "gsd-core/workflows/explore.md": "6c04f2e658d93261", + "gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4", + "gsd-core/workflows/fast.md": "94136fb570d20a9d", + "gsd-core/workflows/forensics.md": "0d500a3f5ab26913", + "gsd-core/workflows/graduation.md": "47f1594c88c08501", + "gsd-core/workflows/health.md": "107e3c72e76d9535", + "gsd-core/workflows/help.md": "5d040504b9ab35e3", + "gsd-core/workflows/help/modes/brief.md": "924860e1f07defb0", + "gsd-core/workflows/help/modes/default.md": "08a02976c0c5cc50", + "gsd-core/workflows/help/modes/full.md": "87951776f730390d", + "gsd-core/workflows/help/modes/topic.md": "5c160093f3cbf35d", + "gsd-core/workflows/import.md": "3d3fa603ceb8bc9f", + "gsd-core/workflows/inbox.md": "437f981ef9ae7b26", + "gsd-core/workflows/ingest-docs.md": "c859921c811ac11b", + "gsd-core/workflows/insert-phase.md": "08dca838d831bb5b", + "gsd-core/workflows/list-phase-assumptions.md": "53dd4b69536c1bc3", + "gsd-core/workflows/list-seeds.md": "b687f2a843032d65", + "gsd-core/workflows/list-workspaces.md": "7f94e18b5c549453", + "gsd-core/workflows/manager.md": "3ba7a3e8213c4b23", + "gsd-core/workflows/map-codebase.md": "83d750aade709983", + "gsd-core/workflows/milestone-summary.md": "e0cdfddbd39a9043", + "gsd-core/workflows/mvp-phase.md": "344dd300e0cc1e74", + "gsd-core/workflows/new-milestone.md": "a616efb0f82d1a11", + "gsd-core/workflows/new-project.md": "4c32c69910d55ec0", + "gsd-core/workflows/new-workspace.md": "017688423110ed66", + "gsd-core/workflows/next.md": "13fb800f2472d970", + "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", + "gsd-core/workflows/note.md": "5a99eb396c744619", + "gsd-core/workflows/onboard.md": "6f9e6c0b484271a9", + "gsd-core/workflows/pause-work.md": "f2b33bba5593d422", + "gsd-core/workflows/plan-milestone-gaps.md": "852f6d7c0c4299dc", + "gsd-core/workflows/plan-phase.md": "5a841e26f1d6f9ae", + "gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "b36f77ac7344a072", + "gsd-core/workflows/plan-phase/steps/prd-express-path.md": "197c0590326371b2", + "gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "49f58c3f75be3eb5", + "gsd-core/workflows/plan-review-convergence.md": "c635ecb8590a724e", + "gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede", + "gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b", + "gsd-core/workflows/profile-user.md": "1bac7f69142801ef", + "gsd-core/workflows/progress.md": "893aa3c36983f74b", + "gsd-core/workflows/quick.md": "4471707540842a41", + "gsd-core/workflows/reapply-patches.md": "39050f72601aec89", + "gsd-core/workflows/remove-phase.md": "df9a45f0b1880999", + "gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c", + "gsd-core/workflows/resume-project.md": "f28da1200e4545f4", + "gsd-core/workflows/review.md": "93fe46bd0b5dd3d1", + "gsd-core/workflows/scan.md": "003883d71c37da7d", + "gsd-core/workflows/secure-phase.md": "29fc6b62c5c5dc62", + "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", + "gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44", + "gsd-core/workflows/settings-integrations.md": "70515c5838fb9826", + "gsd-core/workflows/settings.md": "d96ddf01fb85e61e", + "gsd-core/workflows/ship.md": "7b8fe9f89143e648", + "gsd-core/workflows/sketch-wrap-up.md": "838c701bd072ae73", + "gsd-core/workflows/sketch.md": "c7725562b3efd311", + "gsd-core/workflows/smart-entry.md": "449238eb94abe187", + "gsd-core/workflows/spec-phase.md": "8c480bd91f3cef6f", + "gsd-core/workflows/spike-wrap-up.md": "0b24057c340a8a17", + "gsd-core/workflows/spike.md": "9134cdc3b75282c9", + "gsd-core/workflows/stats.md": "76a42cbeaf6007c2", + "gsd-core/workflows/sync-skills.md": "b505e6f8331c0918", + "gsd-core/workflows/thread.md": "927e7eeefd2fcf5c", + "gsd-core/workflows/transition.md": "cb8ec5affb7ebba1", + "gsd-core/workflows/ui-phase.md": "d77fa19403a4684b", + "gsd-core/workflows/ui-review.md": "7acfc485526d064b", + "gsd-core/workflows/ultraplan-phase.md": "0fb8291153e3937d", + "gsd-core/workflows/undo.md": "96d2775f008b3a85", + "gsd-core/workflows/update.md": "7e69d278375a2493", + "gsd-core/workflows/validate-phase.md": "2c6d7671fcaabcaa", + "gsd-core/workflows/verify-phase.md": "22f18492581f1da5", + "gsd-core/workflows/verify-work.md": "63b3f680d8f0a6f3", + "hooks/gsd-check-update-worker.js": "55376b5b9335a580", + "hooks/gsd-check-update.js": "a89562537a41f83d", + "hooks/gsd-config-reload.js": "96546e0e8bb47904", + "hooks/gsd-context-monitor.js": "1c48eb0f38a24318", + "hooks/gsd-cursor-post-tool.js": "9168e0a09de1972a", + "hooks/gsd-cursor-pre-tool.js": "873998b25e308c29", + "hooks/gsd-cursor-session-start.js": "9b2e6f4f0c405375", + "hooks/gsd-cursor-stop.js": "bfaaf60f419e3238", + "hooks/gsd-cursor-subagent-start.js": "06d77fde5c1372b6", + "hooks/gsd-cursor-subagent-stop.js": "4bbf22917da4d389", + "hooks/gsd-ensure-canonical-path.js": "62d0819a51b55fc4", + "hooks/gsd-graphify-update.sh": "e4c6e14fe6ad64ff", + "hooks/gsd-phase-boundary.sh": "32739d5fbe0d0a1c", + "hooks/gsd-prompt-guard.js": "a749b8cb2c5248de", + "hooks/gsd-read-guard.js": "9e423cd03e2d1b16", + "hooks/gsd-read-injection-scanner.js": "f454242c010804cf", + "hooks/gsd-session-state.sh": "e54379ba86bf1b6d", + "hooks/gsd-statusline.js": "5539e1ae859b987e", + "hooks/gsd-update-banner.js": "55143a25f978f301", + "hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38", + "hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd", + "hooks/gsd-windsurf-pre-write.js": "92d4dbfbc36ab0cf", + "hooks/gsd-workflow-guard.js": "91ae24a15d2bca6f", + "hooks/gsd-worktree-path-guard.js": "2a2a7515c01ef998", + "hooks/lib/git-cmd.js": "268ba15992ca0b23", + "hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9", + "hooks/managed-hooks-registry.cjs": "ac720a2b548ba200", + "package.json": "dbf8353f77358bc1", + "scripts/changeset/README.md": "86ff89331dfd94b2", + "scripts/changeset/cli.cjs": "68f92a344b199271", + "scripts/changeset/github-release-notes.cjs": "795677f0c009b132", + "scripts/changeset/lint.cjs": "0066faed159154f0", + "scripts/changeset/new.cjs": "4991e21fd17f5541", + "scripts/changeset/parse.cjs": "f9a949cbcab56445", + "scripts/changeset/render.cjs": "e47bc3e1587c3cae", + "scripts/changeset/serialize.cjs": "ac0b8fe6f87cdb0e", + "scripts/fix-slash-commands.cjs": "0519742531ff3529", + "scripts/gen-capability-registry.cjs": "c52201ff4d1c2cd7", + "scripts/gen-loop-host-contract.cjs": "c7f15237234811a0", + "scripts/lib/allowlist-ratchet.cjs": "ffaceaac3efc2660", + "scripts/lib/cli-exit.cjs": "612d0c372c75b7e7" +} diff --git a/tests/fixtures/golden-install-parity/qwen.json b/tests/fixtures/golden-install-parity/qwen.json index 962bb305c..e3db0de4e 100644 --- a/tests/fixtures/golden-install-parity/qwen.json +++ b/tests/fixtures/golden-install-parity/qwen.json @@ -42,7 +42,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -289,7 +289,7 @@ "gsd-core/workflows/scan.md": "949692db4834dd27", "gsd-core/workflows/secure-phase.md": "6758f1acf4113e9e", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "d80d7acc4058cb40", + "gsd-core/workflows/settings-advanced.md": "5e05212fb5cba531", "gsd-core/workflows/settings-integrations.md": "29c5de27fbbb18e9", "gsd-core/workflows/settings.md": "26b9b7979d3a5747", "gsd-core/workflows/ship.md": "9f94d0b155eb041e", diff --git a/tests/fixtures/golden-install-parity/trae.json b/tests/fixtures/golden-install-parity/trae.json index 3109e4263..4d1e45b35 100644 --- a/tests/fixtures/golden-install-parity/trae.json +++ b/tests/fixtures/golden-install-parity/trae.json @@ -42,7 +42,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -289,7 +289,7 @@ "gsd-core/workflows/scan.md": "63631467651d9ca8", "gsd-core/workflows/secure-phase.md": "6cc236e53c2e7d56", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "90d913043cd11502", + "gsd-core/workflows/settings-advanced.md": "39e66386f6c48025", "gsd-core/workflows/settings-integrations.md": "f8f756709ec02363", "gsd-core/workflows/settings.md": "44b10c59215633b8", "gsd-core/workflows/ship.md": "c08f0fe3025d2c86", diff --git a/tests/fixtures/golden-install-parity/windsurf.json b/tests/fixtures/golden-install-parity/windsurf.json index 65baae3bc..8311cb82e 100644 --- a/tests/fixtures/golden-install-parity/windsurf.json +++ b/tests/fixtures/golden-install-parity/windsurf.json @@ -42,7 +42,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -289,7 +289,7 @@ "gsd-core/workflows/scan.md": "12c11b2edc165df9", "gsd-core/workflows/secure-phase.md": "7bf923689bf58288", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "18eff9c17e0b1948", + "gsd-core/workflows/settings-advanced.md": "2f86ec7b998f9485", "gsd-core/workflows/settings-integrations.md": "b082fc518b484c07", "gsd-core/workflows/settings.md": "002eb0ce3c10c741", "gsd-core/workflows/ship.md": "c035bb8b3bb7efbb", diff --git a/tests/fixtures/golden-install-parity/zcode.json b/tests/fixtures/golden-install-parity/zcode.json index d4d792860..04fb029d6 100644 --- a/tests/fixtures/golden-install-parity/zcode.json +++ b/tests/fixtures/golden-install-parity/zcode.json @@ -113,7 +113,7 @@ "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", - "gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a", + "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", "gsd-core/contexts/dev.md": "dcb0de9dce33cf41", @@ -360,7 +360,7 @@ "gsd-core/workflows/scan.md": "003883d71c37da7d", "gsd-core/workflows/secure-phase.md": "29fc6b62c5c5dc62", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", - "gsd-core/workflows/settings-advanced.md": "b6fa466a953dd3a2", + "gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44", "gsd-core/workflows/settings-integrations.md": "70515c5838fb9826", "gsd-core/workflows/settings.md": "d96ddf01fb85e61e", "gsd-core/workflows/ship.md": "7b8fe9f89143e648", diff --git a/tests/fixtures/pi-host-plugin.cjs b/tests/fixtures/pi-host-plugin.cjs index b9082c5cf..8d21f74f2 100644 --- a/tests/fixtures/pi-host-plugin.cjs +++ b/tests/fixtures/pi-host-plugin.cjs @@ -14,10 +14,18 @@ * behaviorally testable without a live pi runtime. The default export matches * pi's extension entry shape: `export default function (pi: ExtensionAPI) { … }`. * + * #2102 Stage 2: registerCommand takes `handler(args, ctx)` (args is the raw + * string after the command) — NOT `execute(ctx)`, which the original #1682 + * cut used. gsd_invoke's `execute` takes pi's real 5-arg tool-execute + * signature `(toolCallId, params, signal, onUpdate, ctx)`. See pi/gsd.cjs for + * the full production binding this reference fixture mirrors. + * * NOTE: this is the reference binding that proves the ExtensionAPI imperative - * adapter (#1682 AC). Full `--pi` installable-runtime integration (descriptor + - * installer wiring + golden parity 16→17) is a larger follow-up tracked - * separately — it is intentionally NOT added to the runtime registry here. + * adapter (#1682 AC), kept as a mock-friendly fixture independent of the real + * `pi/gsd.cjs` extension. Full `--pi` installable-runtime integration + * (descriptor + installer wiring + golden parity 16→17) shipped in #2102 + * Stage 1 — see capabilities/pi/capability.json and + * tests/fixtures/golden-install-parity/pi.json. * * @param {object} pi pi ExtensionAPI (registerTool/registerCommand/on/…) */ @@ -30,9 +38,12 @@ module.exports = function gsdPiPlugin(pi) { // imperative adapter (createImperativeAdapter({runtime:'pi'}) + dispatch). pi.registerCommand('gsd', { description: 'Invoke a GSD command via the embedded engine (imperative adapter).', - execute: async function /* ctx */ () { + handler: async function (args, ctx) { + void args; + void ctx; // Engine dispatch is wired by the host at load (createImperativeAdapter). - // Kept declarative in the reference; the real binding dispatches the hub. + // Kept declarative in the reference; the real binding (pi/gsd.cjs) + // dispatches through gsd-tools.cjs via dispatchGsdCommand. }, }); @@ -41,7 +52,12 @@ module.exports = function gsdPiPlugin(pi) { pi.registerTool({ name: 'gsd_invoke', description: 'Invoke a GSD command family/subcommand through the engine.', - execute: async function () { + execute: async function (toolCallId, params, signal, onUpdate, ctx) { + void toolCallId; + void params; + void signal; + void onUpdate; + void ctx; return 'ok'; }, }); diff --git a/tests/global-config-home-fragment.test.cjs b/tests/global-config-home-fragment.test.cjs index b7e6428b5..d3a97bf45 100644 --- a/tests/global-config-home-fragment.test.cjs +++ b/tests/global-config-home-fragment.test.cjs @@ -47,6 +47,7 @@ const GOLDEN_FRAGMENT_MAP = { cline: "'.cline'", kimi: "'.config', 'agents'", zcode: "'.zcode'", + pi: "'.pi', 'agent'", }; // Runtimes intentionally NOT in the table: claude is the default; antigravity is diff --git a/tests/gsd-mcp-server.test.cjs b/tests/gsd-mcp-server.test.cjs index e76b6353c..fde999b8d 100644 --- a/tests/gsd-mcp-server.test.cjs +++ b/tests/gsd-mcp-server.test.cjs @@ -54,6 +54,36 @@ test('tools/call gsd_invoke_command: dispatches to the command hub (point 1); un assert.strictEqual(res.jsonrpc, '2.0'); const payload = JSON.parse(res.result.content[0].text); assert.strictEqual(payload.ok, false, 'an unknown command dispatches to the hub and returns ok:false'); + assert.strictEqual(res.result.isError, true, 'unknown family surfaces as isError:true (#2102)'); +}); + +// REGRESSION #2102: gsd_invoke_command previously called +// `commandRoutingHub.createHub()` with NO arguments, which always hits +// `if (!_cjsRegistry) return makeUnknownCommand()` — every dispatch, valid +// family or not, returned UnknownCommand. The test above (family: +// 'no-such-family') could not catch this: an UnknownCommand result is +// EXACTLY what an unknown family is supposed to return, whether or not +// dispatch actually worked — it is vacuous by construction. This test proves +// the fix: a VALID read-only family/subcommand must reach gsd-tools.cjs for +// real and come back with actual data (not a crash, not UnknownCommand). +test('tools/call gsd_invoke_command: REGRESSION #2102 — a valid read-only family dispatches for real (not the createHub()-with-no-args UnknownCommand bug)', () => { + const dir = createTempDir(); + try { + const res = handleMessage( + { jsonrpc: '2.0', id: 9, method: 'tools/call', params: { name: 'gsd_invoke_command', arguments: { family: 'progress', subcommand: 'json' } } }, + { cwd: dir }, + ); + assert.strictEqual(res.jsonrpc, '2.0'); + assert.notStrictEqual(res.result.isError, true, 'a valid family must not surface as an error'); + const text = res.result.content[0].text; + const parsed = JSON.parse(text); + // Fail-first proof: under the bug, this would be + // { ok: false, kind: 'UnknownCommand', command: 'progress json' } instead + // of the real progress payload — `percent` would not exist. + assert.strictEqual(typeof parsed.percent, 'number', 'the real "progress json" command ran (the engine was reached)'); + } finally { + cleanup(dir); + } }); test('tools/call: unknown tool name surfaces a tool error (isError), not a JSON-RPC protocol error', () => { diff --git a/tests/helpers/install-shared.cjs b/tests/helpers/install-shared.cjs index 060571854..9b9a5e8aa 100644 --- a/tests/helpers/install-shared.cjs +++ b/tests/helpers/install-shared.cjs @@ -57,6 +57,7 @@ const RUNTIME_META = { kimi: { localDir: '.kimi-code', globalSuffix: path.join('.config', 'agents') }, kilo: { localDir: '.kilo', globalSuffix: path.join('.config', 'kilo') }, opencode: { localDir: '.opencode', globalSuffix: path.join('.config', 'opencode') }, + pi: { localDir: '.pi', globalSuffix: path.join('.pi', 'agent') }, qwen: { localDir: '.qwen', globalSuffix: '.qwen' }, trae: { localDir: '.trae', globalSuffix: '.trae' }, windsurf: { localDir: '.windsurf', globalSuffix: path.join('.codeium', 'windsurf') }, diff --git a/tests/host-integration-descriptors.test.cjs b/tests/host-integration-descriptors.test.cjs index 8e4b74d4e..898e27e21 100644 --- a/tests/host-integration-descriptors.test.cjs +++ b/tests/host-integration-descriptors.test.cjs @@ -38,7 +38,7 @@ const DISPATCH_KEYS = ['namedDispatch', 'nested', 'maxDepth', 'background', 'sub const RUNTIME_IDS = Object.keys(registry.runtimes); // Contract-pinned profile split (derived from .host-cli-final.json): -// programmatic-cli: claude, cline, cursor, hermes, kilo, kimi, opencode, qwen, trae (9) +// programmatic-cli: claude, cline, cursor, hermes, kilo, kimi, opencode, pi, qwen, trae (10) // declarative-cli: antigravity, augment, codebuddy, codex, copilot, windsurf, zcode (7) // ide: 0 const EXPECTED_PROFILES = { @@ -49,6 +49,7 @@ const EXPECTED_PROFILES = { kilo: 'programmatic-cli', kimi: 'programmatic-cli', opencode: 'programmatic-cli', + pi: 'programmatic-cli', qwen: 'programmatic-cli', trae: 'programmatic-cli', antigravity: 'declarative-cli', @@ -283,6 +284,9 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => { // #2087: OpenCode background subagents (v1.15 param, v1.17 default-on) → // dispatch.background/backgroundDispatch true → NOT force-flattened. opencode: false, + // #2102: pi's dispatch.background/backgroundDispatch are both false + // (undocumented background-subagent primitive) → force-flattened. + pi: true, qwen: true, trae: true, windsurf: true, diff --git a/tests/install-minimal-hooks.test.cjs b/tests/install-minimal-hooks.test.cjs index 72297e7b7..73d1dea3e 100644 --- a/tests/install-minimal-hooks.test.cjs +++ b/tests/install-minimal-hooks.test.cjs @@ -674,12 +674,14 @@ describe('#1755: .sh hooks are copied and executable after install', () => { // ─── #1821: Kilo/ZCode (hooksSurface:none, no plugin) receive no dead hooks ──── // // #1821 reported dead hook scripts staged for runtimes with hooksSurface:'none'. -// OpenCode ALSO declares hooksSurface:'none', but its #1914 native plugin adapter -// (plugins/gsd-core.js) spawns the staged hooks/*.js via OpenCode's event bus — -// so for OpenCode the hooks are LIVE and must keep being copied. Kilo and ZCode -// have no plugin surface, so their staged hooks are genuinely dead: this is the -// case the fix removes. These tests assert the split: Kilo/ZCode get no hooks; -// OpenCode (and Claude) still do. +// OpenCode and pi ALSO declare hooksSurface:'none', but each has a native plugin +// adapter that spawns the staged hooks/*.js scripts as subprocesses (OpenCode's +// #1914 plugins/gsd-core.js via OpenCode's event bus; pi's #2102 Stage 2 +// pi/gsd.cjs → extensions/gsd.cjs via pi.on(...) bridges) — so for both, the +// hooks are LIVE and must keep being copied. Kilo and ZCode have no plugin +// surface at all, so their staged hooks are genuinely dead: this is the case +// the fix removes. These tests assert the split: Kilo/ZCode get no hooks; +// OpenCode/pi (and Claude) still do. describe('#1821: Kilo/ZCode receive no dead hook files; OpenCode/Claude keep their hooks', () => { function gsdHookFilesUnder(configDir) { @@ -691,7 +693,7 @@ describe('#1821: Kilo/ZCode receive no dead hook files; OpenCode/Claude keep the }); } - function installAndCollect(runtime) { + function installAndCollect(runtime, opts = {}) { const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-1821-${runtime}-`)); try { const result = spawnSync( @@ -702,10 +704,12 @@ describe('#1821: Kilo/ZCode receive no dead hook files; OpenCode/Claude keep the assert.strictEqual(result.status, 0, `installer exited with status ${result.status} for --${runtime} --global\nstdout: ${result.stdout}\nstderr: ${result.stderr}`); // Collect results while targetDir still exists — cleanup() below removes it. + const pluginRelPath = opts.pluginRelPath || path.join('plugins', 'gsd-core.js'); return { hookFiles: gsdHookFilesUnder(targetDir), hooksLibExists: fs.existsSync(path.join(targetDir, 'hooks', 'lib')), - pluginExists: fs.existsSync(path.join(targetDir, 'plugins', 'gsd-core.js')), + gitCmdExists: fs.existsSync(path.join(targetDir, 'hooks', 'lib', 'git-cmd.js')), + pluginExists: fs.existsSync(path.join(targetDir, pluginRelPath)), }; } finally { cleanup(targetDir); @@ -713,7 +717,8 @@ describe('#1821: Kilo/ZCode receive no dead hook files; OpenCode/Claude keep the } // Kilo and ZCode both declare hooksSurface:'none' with no plugin surface, so - // their staged hooks are dead weight (#1821). + // their staged hooks are genuinely dead weight (#1821) — this is the case + // the fix removes. for (const runtime of ['kilo', 'zcode']) { test(`${runtime} --global install creates no gsd-*.js/.sh hook files or hooks/lib`, () => { const { hookFiles, hooksLibExists } = installAndCollect(runtime); @@ -735,6 +740,31 @@ describe('#1821: Kilo/ZCode receive no dead hook files; OpenCode/Claude keep the assert.ok(pluginExists, 'opencode install must install plugins/gsd-core.js (#1914 hook bridge)'); }); + // pi ALSO declares hooksSurface:'none', but — like OpenCode — it is NOT a + // dead-weight case: pi's native extension (pi/gsd.cjs → extensions/gsd.cjs) + // spawns the staged hooks/*.js scripts as bounded subprocesses (session_start + // → gsd-ensure-canonical-path.js, before_agent_start → gsd-workflow-guard.js, + // session_before_compact → gsd-context-monitor.js — #2102 Stage 2), and its + // /gsd command handler tokenizes raw args via the shared hooks/lib/git-cmd.js + // tokenizer. hostBehaviors.skipSharedHooksInstall is therefore NOT set for + // pi (unlike Kilo/ZCode/Cursor/Cline/Trae/Copilot/Windsurf/Kimi) — pi is in + // the OpenCode group, not the Kilo/ZCode group. + test('pi --global install still copies hooks (spawned by the native extension) + hooks/lib/git-cmd.js + the extension itself', () => { + const { hookFiles, hooksLibExists, gitCmdExists, pluginExists } = installAndCollect('pi', { + pluginRelPath: path.join('extensions', 'gsd.cjs'), + }); + const basenames = hookFiles.map((f) => path.basename(f)); + for (const expected of ['gsd-ensure-canonical-path.js', 'gsd-workflow-guard.js', 'gsd-context-monitor.js']) { + assert.ok( + basenames.includes(expected), + `pi install must copy ${expected} (spawned by pi/gsd.cjs's event bridges), found: ${basenames.join(', ')}`, + ); + } + assert.ok(hooksLibExists, 'pi install must create hooks/lib/'); + assert.ok(gitCmdExists, 'pi install must copy hooks/lib/git-cmd.js (the /gsd command tokenizer)'); + assert.ok(pluginExists, 'pi install must install extensions/gsd.cjs (the native-extension hook bridge)'); + }); + // Positive control: guards against over-exclusion breaking runtimes that // legitimately need hooks (hooksSurface !== 'none'). test('claude --global install still copies gsd-*.js hooks', () => { diff --git a/tests/installer-migration-install.integration.test.cjs b/tests/installer-migration-install.integration.test.cjs index b03c655f1..abdbaae3a 100644 --- a/tests/installer-migration-install.integration.test.cjs +++ b/tests/installer-migration-install.integration.test.cjs @@ -39,6 +39,20 @@ const RUNTIME_INSTALL_CONTRACTS = { // dead hook scripts or the CommonJS package.json marker. kilo: { surface: 'flat-command', settings: false, packageJson: false }, opencode: { surface: 'flat-command', settings: true, packageJson: true }, + // #2102 Stage 1/2: pi is a PLUGIN-ONLY install (hostBehaviors.pluginOnlyInstall) + // for commands/agents/skills — NO commands/, agents/, or skills/ dir. pi's + // /gsd command is registered programmatically by the native extension + // (extensions/gsd.cjs) and dispatches via a bounded subprocess to + // gsd-tools.cjs; it has no host-read markdown surface. Stage 2 (adversarial- + // review fix): pi's native extension DOES spawn the shared hooks/*.js bundle + // as bounded subprocesses (session_start/before_agent_start/session_before_ + // compact bridges) and its /gsd tokenizer requires hooks/lib/git-cmd.js, so + // `hostBehaviors.skipSharedHooksInstall` was removed — pi now receives + // hooks/ + hooks/lib/ + the {"type":"commonjs"} package.json marker, exactly + // like OpenCode (architecturally identical: hooksSurface:'none' + a native + // plugin that spawns the staged hooks), NOT like Kilo/ZCode (no plugin + // surface, where the same hooks are genuinely dead weight). + pi: { surface: 'plugin-only', settings: false, packageJson: true }, qwen: { surface: 'flat-skills', settings: true, packageJson: true }, trae: { surface: 'flat-skills', settings: false, packageJson: false }, windsurf: { surface: 'global-artifacts-noop', settings: false, packageJson: false }, @@ -262,6 +276,43 @@ function assertFreshInstallContract(runtime, targetDir) { listDirNames(targetDir, 'command').some((name) => name.startsWith('gsd-') && name.endsWith('.md')), `${runtime} should install flattened command markdown files` ); + } else if (contract.surface === 'plugin-only') { + // #2102 Stage 1/2: pi — PLUGIN-ONLY install for commands/agents/skills + // (hostBehaviors.pluginOnlyInstall). pi's /gsd command is registered + // programmatically by the native extension and dispatches via a bounded + // subprocess to gsd-tools.cjs — pi has no host-read markdown surface, so + // NO commands/, agents/, or skills/ dir is written. The extension DOES + // spawn the shared hooks/*.js bundle as bounded subprocesses (Stage 2 + // adversarial-review fix — hooksSurface:'none' no longer implies + // skipSharedHooksInstall for pi, mirroring OpenCode), so hooks/ + the + // git-cmd.js tokenizer helper ARE part of the artifact surface now. + assert.ok( + fs.existsSync(path.join(targetDir, 'extensions', 'gsd.cjs')), + `${runtime} should install the native extension file at extensions/gsd.cjs` + ); + assert.ok( + fs.existsSync(path.join(targetDir, 'hooks', 'gsd-ensure-canonical-path.js')), + `${runtime} should install the shared hooks/ bundle (spawned by the native extension's event bridges)` + ); + assert.ok( + fs.existsSync(path.join(targetDir, 'hooks', 'lib', 'git-cmd.js')), + `${runtime} should install hooks/lib/git-cmd.js (the /gsd command tokenizer)` + ); + assert.equal( + fs.existsSync(path.join(targetDir, 'commands')), + false, + `${runtime} should NOT install a commands/ dir (plugin-only, no host-read markdown surface)` + ); + assert.equal( + fs.existsSync(path.join(targetDir, 'agents')), + false, + `${runtime} should NOT install an agents/ dir (plugin-only, no named-dispatch toolkit)` + ); + assert.equal( + fs.existsSync(path.join(targetDir, 'skills')), + false, + `${runtime} should NOT install a skills/ dir (plugin-only)` + ); } else if (contract.surface === 'commands-gsd') { assert.ok( listDirNames(targetDir, path.join('commands', 'gsd')).length > 0, @@ -301,7 +352,7 @@ function assertFreshInstallContract(runtime, targetDir) { ); } - if (contract.surface !== 'kimi-skills-agents' && contract.surface !== 'global-artifacts-noop') { + if (contract.surface !== 'kimi-skills-agents' && contract.surface !== 'global-artifacts-noop' && contract.surface !== 'plugin-only') { assert.ok( listDirNames(targetDir, 'agents').some((name) => name.startsWith('gsd-')), `${runtime} full install should install agents` diff --git a/tests/multi-runtime-select.test.cjs b/tests/multi-runtime-select.test.cjs index 78b48dc9e..365ae2182 100644 --- a/tests/multi-runtime-select.test.cjs +++ b/tests/multi-runtime-select.test.cjs @@ -15,6 +15,10 @@ * now hermes, and the "All" shortcut moved from 17 to 16. * * #1925: ZCode (Z.ai) added as option 16; the "All" shortcut moved from 16 to 17. + * + * #2102: pi added as option 13 (alphabetical slot between opencode and qwen) — + * qwen/trae/windsurf/zcode each shift up one slot (14/15/16/17), and the "All" + * shortcut moves from 17 to 18. */ process.env.GSD_TEST_MODE = '1'; @@ -76,39 +80,43 @@ describe('multi-runtime selection parsing', () => { assert.deepStrictEqual(parseRuntimeInput('12'), ['opencode']); }); + test('single choice for pi', () => { + assert.deepStrictEqual(parseRuntimeInput('13'), ['pi']); + }); + test('single choice for qwen', () => { - assert.deepStrictEqual(parseRuntimeInput('13'), ['qwen']); + assert.deepStrictEqual(parseRuntimeInput('14'), ['qwen']); }); test('single choice for trae', () => { - assert.deepStrictEqual(parseRuntimeInput('14'), ['trae']); + assert.deepStrictEqual(parseRuntimeInput('15'), ['trae']); }); test('single choice for windsurf', () => { - assert.deepStrictEqual(parseRuntimeInput('15'), ['windsurf']); + assert.deepStrictEqual(parseRuntimeInput('16'), ['windsurf']); }); test('single choice for zcode', () => { - assert.deepStrictEqual(parseRuntimeInput('16'), ['zcode']); + assert.deepStrictEqual(parseRuntimeInput('17'), ['zcode']); }); test('single choice for kimi', () => { assert.deepStrictEqual(parseRuntimeInput('10'), ['kimi']); }); - test('choice 17 returns all runtimes', () => { - assert.deepStrictEqual(parseRuntimeInput('17'), allRuntimes); + test('choice 18 returns all runtimes', () => { + assert.deepStrictEqual(parseRuntimeInput('18'), allRuntimes); }); - test('choice 17 returns all runtimes when mixed with separators or other tokens', () => { - // CR feedback: tokenized inputs that include 17 (e.g. trailing comma, or + test('choice 18 returns all runtimes when mixed with separators or other tokens', () => { + // CR feedback: tokenized inputs that include 18 (e.g. trailing comma, or // alongside other choices) must still expand to all-runtimes — previously - // only the bare all-runtimes option matched, so "17," or "17 1" silently installed a + // only the bare all-runtimes option matched, so "18," or "18 1" silently installed a // subset. - assert.deepStrictEqual(parseRuntimeInput('17,'), allRuntimes); - assert.deepStrictEqual(parseRuntimeInput('17 1'), allRuntimes); - assert.deepStrictEqual(parseRuntimeInput('1,17'), allRuntimes); - assert.deepStrictEqual(parseRuntimeInput(' 17 '), allRuntimes); + assert.deepStrictEqual(parseRuntimeInput('18,'), allRuntimes); + assert.deepStrictEqual(parseRuntimeInput('18 1'), allRuntimes); + assert.deepStrictEqual(parseRuntimeInput('1,18'), allRuntimes); + assert.deepStrictEqual(parseRuntimeInput(' 18 '), allRuntimes); }); test('empty input defaults to claude', () => { @@ -117,13 +125,13 @@ describe('multi-runtime selection parsing', () => { }); test('invalid choices are ignored, falls back to claude if all invalid', () => { - assert.deepStrictEqual(parseRuntimeInput('18'), ['claude']); + assert.deepStrictEqual(parseRuntimeInput('19'), ['claude']); assert.deepStrictEqual(parseRuntimeInput('0'), ['claude']); assert.deepStrictEqual(parseRuntimeInput('abc'), ['claude']); }); test('invalid choices mixed with valid are filtered out', () => { - assert.deepStrictEqual(parseRuntimeInput('1,18,7'), ['claude', 'copilot']); + assert.deepStrictEqual(parseRuntimeInput('1,19,7'), ['claude', 'copilot']); assert.deepStrictEqual(parseRuntimeInput('abc 3 xyz'), ['augment']); }); @@ -152,14 +160,15 @@ describe('install.js exports multi-select runtime metadata', () => { '10': 'kimi', '11': 'kilo', '12': 'opencode', - '13': 'qwen', - '14': 'trae', - '15': 'windsurf', - '16': 'zcode', + '13': 'pi', + '14': 'qwen', + '15': 'trae', + '16': 'windsurf', + '17': 'zcode', }; const expectedRuntimes = [ 'claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', - 'copilot', 'cursor', 'hermes', 'kimi', 'kilo', 'opencode', + 'copilot', 'cursor', 'hermes', 'kimi', 'kilo', 'opencode', 'pi', 'qwen', 'trae', 'windsurf', 'zcode', ]; @@ -177,8 +186,8 @@ describe('install.js exports multi-select runtime metadata', () => { 'allRuntimes has no duplicates'); }); - test('"All" shortcut (option 17) selects every runtime', () => { - assert.deepStrictEqual(parseRuntimeInput('17'), allRuntimes); + test('"All" shortcut (option 18) selects every runtime', () => { + assert.deepStrictEqual(parseRuntimeInput('18'), allRuntimes); }); test('--kimi flag selects Kimi without interactive prompt', () => { @@ -189,6 +198,10 @@ describe('install.js exports multi-select runtime metadata', () => { assert.deepStrictEqual(selectRuntimesFromArgs(['--zcode']), ['zcode']); }); + test('--pi flag selects pi without interactive prompt', () => { + assert.deepStrictEqual(selectRuntimesFromArgs(['--pi']), ['pi']); + }); + test('--all flag includes Kimi exactly once', () => { const selected = selectRuntimesFromArgs(['--all']); assert.ok(selected.includes('kimi'), '--all includes kimi'); @@ -203,7 +216,14 @@ describe('install.js exports multi-select runtime metadata', () => { '--all includes zcode exactly once'); }); - test('prompt lists ZCode (16), and All (17)', () => { + test('--all flag includes pi exactly once', () => { + const selected = selectRuntimesFromArgs(['--all']); + assert.ok(selected.includes('pi'), '--all includes pi'); + assert.strictEqual(selected.filter((runtime) => runtime === 'pi').length, 1, + '--all includes pi exactly once'); + }); + + test('prompt lists pi (13), ZCode (17), and All (18)', () => { const prompt = stripAnsi(buildRuntimePromptText()); assert.ok(/\b9\)\s*Hermes Agent\b/.test(prompt), 'prompt lists Hermes Agent as option 9'); @@ -211,14 +231,16 @@ describe('install.js exports multi-select runtime metadata', () => { 'prompt lists Kimi as option 10'); assert.ok(/Kimi\s+\(~\/\.config\/agents, then ~\/\.agents if existing\)/.test(prompt), 'prompt shows the Kimi first-existing generic root policy'); - assert.ok(/\b13\)\s*Qwen Code\b/.test(prompt), - 'prompt lists Qwen Code as option 13'); - assert.ok(/\b14\)\s*Trae\b/.test(prompt), - 'prompt lists Trae as option 14'); - assert.ok(/\b16\)\s*ZCode\b/.test(prompt), - 'prompt lists ZCode as option 16'); - assert.ok(/\b17\)\s*All\b/.test(prompt), - 'prompt lists All as option 17'); + assert.ok(/\b13\)\s*pi\b/.test(prompt), + 'prompt lists pi as option 13'); + assert.ok(/\b14\)\s*Qwen Code\b/.test(prompt), + 'prompt lists Qwen Code as option 14'); + assert.ok(/\b15\)\s*Trae\b/.test(prompt), + 'prompt lists Trae as option 15'); + assert.ok(/\b17\)\s*ZCode\b/.test(prompt), + 'prompt lists ZCode as option 17'); + assert.ok(/\b18\)\s*All\b/.test(prompt), + 'prompt lists All as option 18'); }); test('prompt does not list Gemini (removed #1928)', () => { diff --git a/tests/pi-extension-reachability.test.cjs b/tests/pi-extension-reachability.test.cjs index e41025de0..8d51730cb 100644 --- a/tests/pi-extension-reachability.test.cjs +++ b/tests/pi-extension-reachability.test.cjs @@ -1,57 +1,115 @@ 'use strict'; /** - * pi extension reachability test — ADR-1239 Phase D / #1944. + * pi extension reachability test — ADR-1239 Phase D / #1944, upgraded #2102. * - * Proves the pi extension is keystone-WIRED: the registered /gsd command - * handler dispatches through the GSD command-routing hub and returns a result - * (not just a registration on a mock). This is the "user can invoke X" proof. + * Proves the pi extension is keystone-WIRED: the registered /gsd command's + * `handler(args, ctx)` (pi's REAL ExtensionAPI shape — NOT the `execute(ctx)` + * shape the original #1944 cut used) dispatches through gsd-tools.cjs + * (subprocess-reuse — dispatchGsdCommand) and returns real output, not just a + * registration on a mock. This is the "user can invoke X" proof. + * + * Dispatch is exercised with a real read-only family/subcommand + * (progress/json) against a real temp project, matching the sibling + * tests/vscode-extension-reachability.test.cjs pattern — no fake dispatcher + * injected, because the whole point of "reachability" is that the real + * engine is reached. */ const { test } = require('node:test'); const assert = require('node:assert/strict'); const gsdPiExtension = require('../pi/gsd.cjs'); +const { _internals } = require('../pi/gsd.cjs'); +const { createTempDir, cleanup } = require('./helpers.cjs'); function mockPi() { - const recorded = { commands: {}, tools: {}, events: [] }; + const recorded = { commands: {}, tools: {}, events: {} }; return { registerCommand(name, def) { recorded.commands[name] = def; }, registerTool(def) { if (def && def.name) recorded.tools[def.name] = def; }, - on(event) { recorded.events.push(event); }, + on(event, handler) { (recorded.events[event] = recorded.events[event] || []).push(handler); }, _recorded: recorded, }; } -test('the pi extension registers /gsd + gsd_invoke + tool_call via ExtensionAPI', () => { +test('the pi extension registers /gsd (with getArgumentCompletions + handler) + gsd_invoke + the event surface via ExtensionAPI', () => { const pi = mockPi(); gsdPiExtension(pi); - assert.ok(pi._recorded.commands['gsd'], 'registers /gsd command'); + const gsdCommand = pi._recorded.commands['gsd']; + assert.ok(gsdCommand, 'registers /gsd command'); + assert.equal(typeof gsdCommand.handler, 'function', '/gsd registers a handler(args, ctx) — pi\'s REAL ExtensionAPI shape, not execute(ctx)'); + assert.equal(typeof gsdCommand.getArgumentCompletions, 'function', '/gsd registers getArgumentCompletions'); assert.ok(pi._recorded.tools['gsd_invoke'], 'registers gsd_invoke tool'); - assert.ok(pi._recorded.events.includes('tool_call'), 'subscribes to tool_call'); + assert.equal(typeof pi._recorded.tools['gsd_invoke'].execute, 'function'); + assert.ok(pi._recorded.events['tool_call'], 'subscribes to tool_call'); + assert.ok(pi._recorded.events['before_provider_request'], 'subscribes to before_provider_request'); }); -test('REACHABILITY: the /gsd handler dispatches through the engine hub (keystone wired)', async () => { - const pi = mockPi(); - gsdPiExtension(pi); - // Invoke the registered /gsd handler — it must dispatch through createHub - // and return a JSON result (not throw). This is the keystone-wired proof. - const result = await pi._recorded.commands['gsd'].execute({ - family: 'query', - subcommand: 'help', - }); - assert.equal(typeof result, 'string', '/gsd handler returns a string result'); - const parsed = JSON.parse(result); - assert.ok(parsed !== null && typeof parsed === 'object', - '/gsd dispatch produced a result object (the engine was reached)'); +test('REACHABILITY: parseGsdCommandArgs tokenizes a raw args string into {family, subcommand, args}', () => { + const parsed = _internals.parseGsdCommandArgs('phase add --name test'); + assert.deepEqual(parsed, { family: 'phase', subcommand: 'add', args: ['--name', 'test'] }); }); -test('REACHABILITY: the gsd_invoke tool dispatches through the engine hub', async () => { +test('REACHABILITY: empty args dispatch a working default (gsd-tools.cjs --help), not the broken "query help"', () => { + const parsed = _internals.parseGsdCommandArgs(''); + assert.equal(parsed.family, '--help'); + assert.equal(parsed.subcommand, undefined); +}); + +test('REACHABILITY: the /gsd handler dispatches a real family through gsd-tools.cjs and returns real output (keystone wired)', async () => { const pi = mockPi(); gsdPiExtension(pi); - const result = await pi._recorded.tools['gsd_invoke'].execute(); - assert.equal(typeof result, 'string'); - JSON.parse(result); // must be valid JSON (engine was reached) + const dir = createTempDir(); + try { + const result = await pi._recorded.commands['gsd'].handler('progress json', { cwd: dir }); + assert.equal(typeof result, 'string', '/gsd handler returns a string result'); + const parsed = JSON.parse(result); + assert.equal(typeof parsed.percent, 'number', '/gsd dispatch reached gsd-tools.cjs for real (the engine was reached)'); + } finally { + cleanup(dir); + } +}); + +test('REACHABILITY: an unknown family surfaces a clear GSD error string, not a throw', async () => { + const pi = mockPi(); + gsdPiExtension(pi); + const dir = createTempDir(); + try { + const result = await pi._recorded.commands['gsd'].handler('no-such-family-8675309', { cwd: dir }); + assert.equal(typeof result, 'string'); + assert.match(result, /GSD error:/); + assert.match(result, /no-such-family-8675309|Unknown command/); + } finally { + cleanup(dir); + } +}); + +test('REACHABILITY: the gsd_invoke tool dispatches through the engine and returns real content', async () => { + const pi = mockPi(); + gsdPiExtension(pi); + const dir = createTempDir(); + try { + const result = await pi._recorded.tools['gsd_invoke'].execute( + 'call-1', + { family: 'progress', subcommand: 'json' }, + null, + null, + { cwd: dir }, + ); + assert.ok(result && Array.isArray(result.content), 'gsd_invoke returns {content:[...]}'); + const parsed = JSON.parse(result.content[0].text); + assert.equal(typeof parsed.percent, 'number', 'gsd_invoke dispatch reached gsd-tools.cjs for real'); + } finally { + cleanup(dir); + } +}); + +test('gsd_invoke rejects a missing "family" without dispatching', async () => { + const pi = mockPi(); + gsdPiExtension(pi); + const result = await pi._recorded.tools['gsd_invoke'].execute('call-2', {}, null, null, {}); + assert.match(result.content[0].text, /requires a non-empty string "family"/); }); test('gsdPiExtension throws without pi ExtensionAPI (fail-closed)', () => { diff --git a/tests/pi-imperative-reference.test.cjs b/tests/pi-imperative-reference.test.cjs index 8b7eee676..8212bf4c9 100644 --- a/tests/pi-imperative-reference.test.cjs +++ b/tests/pi-imperative-reference.test.cjs @@ -10,8 +10,10 @@ * 2. pi's axes (imperative + bun) classify as 'programmatic-cli' (the reference profile). * 3. the reference pi host-plugin binds GSD via ExtensionAPI (registers command + tool + event). * - * Full `--pi` installable-runtime integration is a larger follow-up (descriptor - * + installer + golden parity 16→17); this slice proves the imperative binding. + * Full `--pi` installable-runtime integration shipped in #2102 Stage 1 + * (capabilities/pi/capability.json + bin/install.js wiring + golden parity + * 16→17, see tests/fixtures/golden-install-parity/pi.json); this slice + * continues to prove the imperative ExtensionAPI binding in isolation. */ const { test } = require('node:test'); @@ -22,12 +24,13 @@ const { profileOf } = require('../gsd-core/bin/lib/host-integration.cjs'); const gsdPiPlugin = require('./fixtures/pi-host-plugin.cjs'); // Mock pi ExtensionAPI: records registrations so the plugin is testable without -// a live pi runtime. +// a live pi runtime. Records the full command/tool definitions (not just +// names) so #2102's handler/execute SHAPE can be asserted, not just presence. function mockPi() { - const recorded = { commands: [], tools: [], events: [] }; + const recorded = { commands: [], tools: [], events: [], commandDefs: {}, toolDefs: {} }; return { - registerCommand(name) { recorded.commands.push(name); }, - registerTool(def) { if (def && def.name) recorded.tools.push(def.name); }, + registerCommand(name, def) { recorded.commands.push(name); recorded.commandDefs[name] = def; }, + registerTool(def) { if (def && def.name) { recorded.tools.push(def.name); recorded.toolDefs[def.name] = def; } }, registerShortcut() {}, registerFlag() {}, on(event) { recorded.events.push(event); }, @@ -58,6 +61,27 @@ test('the pi reference host-plugin binds GSD via the ExtensionAPI (command + too assert.ok(pi._recorded.events.includes('tool_call'), 'subscribes to the tool_call event'); }); +// #2102 Stage 2: pi's REAL ExtensionAPI shape is `handler(args, ctx)` for +// registerCommand (NOT `execute(ctx)`, which the original #1682 cut used) and +// a 5-arg `execute(toolCallId, params, signal, onUpdate, ctx)` for +// registerTool. Locks the shape so a future drift back to the wrong contract +// is a visible test failure. +test('the /gsd command registers a handler(args, ctx) — not execute(ctx)', () => { + const pi = mockPi(); + gsdPiPlugin(pi); + const def = pi._recorded.commandDefs['gsd']; + assert.equal(typeof def.handler, 'function', 'registerCommand takes handler(args, ctx), pi\'s real ExtensionAPI shape'); + assert.equal(def.execute, undefined, 'must not use the wrong execute(ctx) shape'); +}); + +test('the gsd_invoke tool registers a 5-arg execute(toolCallId, params, signal, onUpdate, ctx)', () => { + const pi = mockPi(); + gsdPiPlugin(pi); + const def = pi._recorded.toolDefs['gsd_invoke']; + assert.equal(typeof def.execute, 'function'); + assert.equal(def.execute.length, 5, 'gsd_invoke.execute must declare pi\'s real 5-arg tool-execute signature'); +}); + test('gsdPiPlugin throws if the pi ExtensionAPI is not provided (fail-closed)', () => { assert.throws(() => gsdPiPlugin(null), /ExtensionAPI is required/); assert.throws(() => gsdPiPlugin(undefined), /ExtensionAPI is required/); diff --git a/tests/pi-upgrades.test.cjs b/tests/pi-upgrades.test.cjs new file mode 100644 index 000000000..8d1cbac94 --- /dev/null +++ b/tests/pi-upgrades.test.cjs @@ -0,0 +1,172 @@ +'use strict'; + +/** + * pi upgrades — ADR-1239 Phase D / #2102 Stage 2 (EoS/pi). + * + * Mirrors tests/opencode-imperative-reference.test.cjs's structure (Host- + * Integration axes classification/negotiation + the Context7-verified + * upgrades) for pi's three additive upgrades: + * 1. EXTENSION_EVENT_SURFACES.pi — the full ~30-event ExtensionAPI surface + * (was a single-event ['tool_call'] placeholder). + * 2. Event bindings — pi/gsd.cjs actually binds session_start, + * before_agent_start, session_before_compact (+ tool_call) via pi.on(), + * not just declaring the surface in host-integration.cts. + * 3. Active-model steering — before_provider_request resolves GSD's + * tier→model via the model-catalog's pi entries (populated this stage) + * and returns a bare anthropic model id pi's built-in models accept; + * fails open (returns undefined) when resolution comes back null. + * + * Plus the command-surface completions (getArgumentCompletions) and the + * standard fail-closed negotiation guarantee. + */ + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const { + extensionEventSurfaceFor, + negotiateHostCapabilities, + UNDOCUMENTED, +} = require('../gsd-core/bin/lib/host-integration.cjs'); +const { RUNTIME_PROFILE_MAP } = require('../gsd-core/bin/lib/model-catalog.cjs'); + +const gsdPiExtension = require('../pi/gsd.cjs'); +const { _internals } = require('../pi/gsd.cjs'); + +const PI_CAP = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'pi', 'capability.json'), 'utf8'), +); +const PI_AXES = PI_CAP.runtime.hostIntegration; + +function mockPi() { + const recorded = { commands: {}, tools: {}, events: {} }; + return { + registerCommand(name, def) { recorded.commands[name] = def; }, + registerTool(def) { if (def && def.name) recorded.tools[def.name] = def; }, + registerProvider() { + throw new Error('gsdPiExtension must NOT call registerProvider — GSD steers pi\'s existing anthropic models, it does not add a new provider'); + }, + on(event, handler) { (recorded.events[event] = recorded.events[event] || []).push(handler); }, + _recorded: recorded, + }; +} + +// -- (1) EXTENSION_EVENT_SURFACES.pi has all 30 events ----------------------- + +const EXPECTED_PI_EVENTS = [ + 'session_start', 'project_trust', 'resources_discover', 'input', + 'before_agent_start', 'agent_start', 'message_start', 'message_update', + 'message_end', 'turn_start', 'context', 'before_provider_request', + 'after_provider_response', 'tool_execution_start', 'tool_execution_update', + 'tool_execution_end', 'tool_call', 'tool_result', 'turn_end', 'agent_end', + 'session_before_switch', 'session_shutdown', 'session_before_fork', + 'session_info_changed', 'session_before_compact', 'session_compact', + 'session_before_tree', 'session_tree', 'thinking_level_select', 'model_select', +]; + +test('pi extension-event surface declares all 30 documented ExtensionAPI events (#2102)', () => { + const surface = extensionEventSurfaceFor('pi'); + assert.ok(surface, 'pi is a consumed extensionEvents dialect'); + assert.equal(surface.length, 30, `expected exactly 30 events, got ${surface.length}`); + for (const ev of EXPECTED_PI_EVENTS) { + assert.ok(surface.includes(ev), `expected pi extension-event surface to include "${ev}"`); + } + assert.deepEqual([...surface].sort(), [...EXPECTED_PI_EVENTS].sort()); +}); + +// -- (2) the binding actually binds session_start/before_agent_start/ ------- +// session_before_compact (not just declared in host-integration.cts) + +test('gsdPiExtension binds session_start, before_agent_start, session_before_compact, tool_call, before_provider_request', () => { + const pi = mockPi(); + gsdPiExtension(pi); + for (const ev of ['session_start', 'before_agent_start', 'session_before_compact', 'tool_call', 'before_provider_request']) { + assert.ok(Array.isArray(pi._recorded.events[ev]) && pi._recorded.events[ev].length > 0, + `expected gsdPiExtension to bind pi.on("${ev}", ...)`); + } +}); + +test('gsdPiExtension does NOT call registerProvider (GSD steers pi\'s existing anthropic models, not a new provider)', () => { + const pi = mockPi(); + // If gsdPiExtension called registerProvider, mockPi's registerProvider throws. + assert.doesNotThrow(() => gsdPiExtension(pi)); +}); + +// Finding #3 (adversarial review): the tests below previously only exercised +// buildBeforeProviderRequestHandler() directly (the builder), never the +// handler ACTUALLY REGISTERED via pi.on('before_provider_request', ...) in +// gsdPiExtension. This ties the bound handler (default tier = 'sonnet') to +// the real model-catalog steering end-to-end. +test('the ACTUALLY-REGISTERED before_provider_request handler steers to the default-tier model-catalog pi id', async () => { + const pi = mockPi(); + gsdPiExtension(pi); + const boundHandler = pi._recorded.events['before_provider_request'][0]; + assert.equal(typeof boundHandler, 'function'); + + const out = await boundHandler({ payload: {} }, { cwd: __dirname }); + assert.ok(out, 'expected a modified payload, not undefined'); + assert.equal(out.model, RUNTIME_PROFILE_MAP.pi.sonnet.model, 'the bound handler steers to the default (sonnet) tier\'s model-catalog pi id'); + assert.equal(out.model, 'claude-sonnet-5'); +}); + +// -- (3) before_provider_request: active-model steering ---------------------- + +test('before_provider_request resolves a tier that maps to a model → returns a payload with the bare anthropic model id (model-catalog pi ids)', async () => { + const handler = _internals.buildBeforeProviderRequestHandler({ tier: 'sonnet' }); + const result = await handler({ payload: { existing: 'field' } }, { cwd: __dirname }); + assert.ok(result, 'expected a modified payload, not undefined'); + assert.equal(result.existing, 'field', 'original payload fields are preserved'); + assert.equal(result.model, RUNTIME_PROFILE_MAP.pi.sonnet.model, 'model id matches the model-catalog pi entry'); + assert.equal(result.model, 'claude-sonnet-5'); +}); + +test('before_provider_request resolves opus/haiku tiers to their model-catalog pi ids too', async () => { + const opusHandler = _internals.buildBeforeProviderRequestHandler({ tier: 'opus' }); + const opusResult = await opusHandler({ payload: {} }, { cwd: __dirname }); + assert.equal(opusResult.model, RUNTIME_PROFILE_MAP.pi.opus.model); + + const haikuHandler = _internals.buildBeforeProviderRequestHandler({ tier: 'haiku' }); + const haikuResult = await haikuHandler({ payload: {} }, { cwd: __dirname }); + assert.equal(haikuResult.model, RUNTIME_PROFILE_MAP.pi.haiku.model); +}); + +test('before_provider_request given a tier that resolves to null returns undefined (fail-open, never a wrong/empty id)', async () => { + const handler = _internals.buildBeforeProviderRequestHandler({ tier: 'not-a-real-tier-8675309' }); + const result = await handler({ payload: { existing: 'field' } }, { cwd: __dirname }); + assert.equal(result, undefined); +}); + +// -- getArgumentCompletions returns family suggestions ----------------------- + +test('getArgumentCompletions filters PI_COMMAND_FAMILIES by prefix and returns null when empty', () => { + const matches = _internals.getArgumentCompletions('mi'); + assert.ok(Array.isArray(matches) && matches.length > 0); + assert.ok(matches.some((m) => m.value === 'milestone')); + for (const m of matches) { + assert.equal(typeof m.value, 'string'); + assert.equal(typeof m.label, 'string'); + } + + const all = _internals.getArgumentCompletions(''); + assert.ok(Array.isArray(all) && all.length > 0); + assert.deepEqual(all.map((m) => m.value), [..._internals.PI_COMMAND_FAMILIES]); + + const none = _internals.getArgumentCompletions('zzz-no-such-family-8675309'); + assert.equal(none, null); +}); + +// -- fail-closed negotiation for pi ------------------------------------------- + +test('negotiateHostCapabilities never throws for pi, even on an undeclared/corrupted axis', () => { + assert.doesNotThrow(() => negotiateHostCapabilities({})); + assert.doesNotThrow(() => negotiateHostCapabilities({ ...PI_AXES, embeddingMode: UNDOCUMENTED })); + assert.doesNotThrow(() => negotiateHostCapabilities({ ...PI_AXES, embeddingMode: 'future-unknown-axis-value' })); + assert.doesNotThrow(() => negotiateHostCapabilities({ ...PI_AXES, dispatch: undefined })); +}); + +test('pi axes negotiate modelMode:"active" (the active-model steering axis)', () => { + const result = negotiateHostCapabilities(PI_AXES); + assert.equal(result.effective.modelMode, 'active'); +}); diff --git a/tests/runtime-flags.test.cjs b/tests/runtime-flags.test.cjs index 6147407e7..97a2929e1 100644 --- a/tests/runtime-flags.test.cjs +++ b/tests/runtime-flags.test.cjs @@ -14,7 +14,7 @@ const registry = require('../gsd-core/bin/lib/capability-registry.cjs'); const EXPECTED_FLAGS = [ 'isOpencode', 'isKilo', 'isCodex', 'isCopilot', 'isAntigravity', 'isCursor', 'isWindsurf', 'isAugment', 'isTrae', 'isQwen', 'isHermes', - 'isCodebuddy', 'isCline', 'isKimi', 'isZcode', + 'isCodebuddy', 'isCline', 'isKimi', 'isZcode', 'isPi', ]; test('runtimeFlags: every known non-claude runtime sets exactly its own flag true', () => { @@ -35,12 +35,12 @@ test('runtimeFlags: claude / unknown / empty → all flags false (fail-closed)', } }); -test('runtimeFlags: all 15 flags present + boolean + the object is frozen', () => { +test('runtimeFlags: all 16 flags present + boolean + the object is frozen', () => { const flags = runtimeFlags('opencode'); for (const f of EXPECTED_FLAGS) { assert.strictEqual(typeof flags[f], 'boolean', `${f} must be boolean`); } - assert.deepStrictEqual(Object.keys(flags).sort(), [...EXPECTED_FLAGS].sort(), 'exactly the 15 flags'); + assert.deepStrictEqual(Object.keys(flags).sort(), [...EXPECTED_FLAGS].sort(), 'exactly the 16 flags'); assert.ok(Object.isFrozen(flags), 'flags object must be frozen'); }); diff --git a/tests/shell-command-projection-dispatch.test.cjs b/tests/shell-command-projection-dispatch.test.cjs index b393e69b5..3103b5597 100644 --- a/tests/shell-command-projection-dispatch.test.cjs +++ b/tests/shell-command-projection-dispatch.test.cjs @@ -14,6 +14,8 @@ const { platformWriteSync, platformReadSync, platformEnsureDir, + dispatchGsdCommand, + resolveGsdToolsPath, } = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'shell-command-projection.cjs')); const { createTempGitProject, createTempDir, cleanup } = require('./helpers.cjs'); @@ -99,6 +101,78 @@ describe('execTool', () => { }); }); +// ─── dispatchGsdCommand (#2102 Stage 2 — subprocess-shim dispatch to gsd-tools.cjs) ── +// +// The command-routing hub (`createHub()`) has no fully-populated factory +// anywhere in the tree — every caller builds a single-family hub — so the +// only dispatch path covering the FULL family/subcommand surface is the +// gsd-tools.cjs CLI itself. This is the shared helper pi/gsd.cjs and the +// companion MCP server both dispatch through. + +describe('dispatchGsdCommand', () => { + let tmpDir; + + beforeEach(() => { tmpDir = createTempDir(); }); + afterEach(() => { cleanup(tmpDir); }); + + test('resolveGsdToolsPath resolves to the real gsd-tools.cjs on disk', () => { + const toolsPath = resolveGsdToolsPath(); + assert.ok(fs.existsSync(toolsPath), `expected gsd-tools.cjs to exist at ${toolsPath}`); + assert.equal(path.basename(toolsPath), 'gsd-tools.cjs'); + }); + + test('a valid read-only family/subcommand dispatches for real and returns ok:true + non-empty stdout', () => { + const result = dispatchGsdCommand({ family: 'progress', subcommand: 'json', cwd: tmpDir }); + assert.equal(result.ok, true, `expected ok:true, got: ${JSON.stringify(result)}`); + assert.equal(typeof result.stdout, 'string'); + assert.ok(result.stdout.length > 0, 'stdout must be non-empty'); + const parsed = JSON.parse(result.stdout); + assert.equal(typeof parsed.percent, 'number', 'the real progress command ran (proves the engine was reached)'); + assert.equal(result.code, 0); + assert.equal(result.timedOut, false); + }); + + test('an unknown family returns ok:false without throwing', () => { + assert.doesNotThrow(() => { + const result = dispatchGsdCommand({ family: 'no-such-family-8675309', cwd: tmpDir }); + assert.equal(result.ok, false); + assert.notEqual(result.code, 0); + assert.equal(typeof result.stderr, 'string'); + assert.ok(result.stderr.length > 0); + // --json-errors gives a structured, parseable error envelope. + const parsedErr = JSON.parse(result.stderr); + assert.equal(parsedErr.ok, false); + }); + }); + + test('a missing/bogus gsd-tools.cjs path degrades to ok:false without throwing', () => { + assert.doesNotThrow(() => { + const result = dispatchGsdCommand({ + family: 'progress', + cwd: tmpDir, + gsdToolsPath: path.join(tmpDir, 'definitely-not-a-real-gsd-tools-8675309.cjs'), + }); + assert.equal(result.ok, false); + assert.equal(result.timedOut, false); + }); + }); + + test('a missing/empty "family" is rejected locally without spawning a subprocess', () => { + const result = dispatchGsdCommand({ cwd: tmpDir }); + assert.equal(result.ok, false); + assert.equal(result.code, null); + assert.match(result.stderr, /requires a non-empty string "family"/); + }); + + test('a wall-clock timeout is reported via timedOut:true, ok:false — never throws', () => { + assert.doesNotThrow(() => { + const result = dispatchGsdCommand({ family: 'progress', subcommand: 'json', cwd: tmpDir, timeout: 1 }); + assert.equal(result.ok, false); + assert.equal(result.timedOut, true); + }); + }); +}); + // ─── probeTty ──────────────────────────────────────────────────────────────── describe('probeTty', () => { diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index daae9caa5..e90823c69 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -68,7 +68,7 @@ "scan.md": 7732, "secure-phase.md": 13520, "session-report.md": 4044, - "settings-advanced.md": 39908, + "settings-advanced.md": 40019, "settings-integrations.md": 15892, "settings.md": 33467, "ship.md": 24691,