feat(#2102): make pi a first-class installable runtime + fix its dispatch (ADR-1239)

Net-new EoS/pi installable runtime — purely additive (no prior runtime==='pi'
branches). pi is a bun-runtime programmatic-CLI whose /gsd command is registered
by a native ExtensionAPI extension and dispatches through the embedded engine.

Stage 1 (install plumbing):
- capabilities/pi/capability.json: full hostIntegration descriptor (imperative /
  slash-programmatic / active-model / native-extension / bun) + hostBehaviors
  {nativePlugin, pluginOnlyInstall}.
- --pi flag + interactive-menu renumber (All 17->18); pi added to RUNTIME_FLAG_IDS,
  RUNTIME_LABELS, RUNTIME_META, allRuntimes/runtimeMap, model-catalog defaults.
- Install mirrors OpenCode: pi installs the gsd.cjs extension + the shared engine
  payload (gsd-core + scripts + config markers) + the shared hooks bundle (spawned
  by the extension at lifecycle events, like OpenCode's plugin). pluginOnlyInstall
  EXCLUDES declarative command/agent/skill markdown, which pi has no host-read
  surface for (its /gsd is programmatic). _installNativePluginIfDeclared (extracted
  from the opencode-family path) copies pi/gsd.cjs -> ~/.pi/agent/extensions/gsd.cjs
  (global) / .pi/extensions/ (local). pi added to package.json files.
- Golden: new pi.json (320 files: extension + engine + 27-file hooks bundle, no
  markdown); the 16 other fixtures + claude-local change only by the shared
  model-catalog hash line.

Stage 2 (real dispatch + upgrades):
- Shared dispatchGsdCommand() (shell-command-projection): bounded, no-throw
  subprocess-shim to gsd-tools.cjs (the only full-surface dispatch path; no
  in-process full-hub factory exists). Fixes pi/gsd.cjs's createHub()-no-args bug
  (every dispatch was UnknownCommand) AND the identical bug in mcp-server.cts's
  gsd_invoke_command, which a vacuous unknown-family-only test had masked (now has
  a real dispatch regression test).
- pi/gsd.cjs: /gsd handler now (args, ctx) - tokenizes (quote-aware, via the
  shipped hooks/lib/git-cmd.js) + dispatches real family/subcommand (not hardcoded
  query/help); gsd_invoke gets a TypeBox (JSON-schema-fallback) parameters schema +
  consumes params; getArgumentCompletions; before_provider_request active-model
  steering (fail-open on null resolution); functional session_start /
  before_agent_start / session_before_compact hook bridges (spawn the shipped GSD
  hook scripts).
- EXTENSION_EVENT_SURFACES.pi expanded from ['tool_call'] to the full 30-event
  vocabulary.

Docs (host-integration matrix + how-to) + changeset (Added).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-11 19:34:23 -04:00
parent 246f566ee1
commit 79d7657eff
49 changed files with 1771 additions and 198 deletions

View File

@@ -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 <family> <subcommand>` 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)

View File

@@ -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-<cmd>.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-<cmd>.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}
`;

View File

@@ -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
}
}
}

View File

@@ -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.

View File

@@ -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`:

View File

@@ -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 |

View File

@@ -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/<file>.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-<cmd>.md` + `agents/gsd-<name>.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 <family> [subcommand] ... --cwd <dir> --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.

View File

@@ -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": [],

View File

@@ -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": {

View File

@@ -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:

View File

@@ -20,7 +20,8 @@
".opencode",
"GEMINI.md",
"hooks",
"scripts"
"scripts",
"pi"
],
"keywords": [
"claude",

View File

@@ -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 (<root>/pi/gsd.cjs → <root>) 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 <args>` 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<object|undefined>}
*/
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 <hooks/hookFile>` 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 (<root>/pi/gsd.cjs → <root>) 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,
};

View File

@@ -584,7 +584,21 @@ const EXTENSION_EVENT_SURFACES: Readonly<Record<string, readonly string[]>> = 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([]),
});

View File

@@ -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,

View File

@@ -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);

View File

@@ -210,6 +210,7 @@ const RUNTIME_LABELS: Readonly<Record<string, string>> = {
codebuddy: 'CodeBuddy',
cline: 'Cline',
zcode: 'ZCode',
pi: 'pi',
};
/**
@@ -258,6 +259,12 @@ const GLOBAL_CONFIG_HOME_FRAGMENTS: Readonly<Record<string, string>> = {
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);
/**

View File

@@ -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;

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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"
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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';
},
});

View File

@@ -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

View File

@@ -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', () => {

View File

@@ -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') },

View File

@@ -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,

View File

@@ -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', () => {

View File

@@ -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`

View File

@@ -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)', () => {

View File

@@ -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)', () => {

View File

@@ -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/);

172
tests/pi-upgrades.test.cjs Normal file
View File

@@ -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');
});

View File

@@ -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');
});

View File

@@ -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', () => {

View File

@@ -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,