@opengsd/gsd-core 1.7.0-rc.4 → 1.7.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +20 -0
  4. package/agents/gsd-doc-classifier.md +105 -0
  5. package/agents/gsd-doc-synthesizer.md +61 -0
  6. package/agents/gsd-ui-checker.md +30 -0
  7. package/agents/gsd-ui-researcher.md +1 -0
  8. package/bin/install.js +1568 -622
  9. package/gsd-core/bin/gsd-tools.cjs +40 -1
  10. package/gsd-core/bin/lib/api-coverage.cjs +466 -0
  11. package/gsd-core/bin/lib/audit.cjs +6 -3
  12. package/gsd-core/bin/lib/capability-loader.cjs +11 -9
  13. package/gsd-core/bin/lib/capability-registry.cjs +761 -84
  14. package/gsd-core/bin/lib/capability-validator.cjs +56 -18
  15. package/gsd-core/bin/lib/capability-writer.cjs +10 -1
  16. package/gsd-core/bin/lib/check-command-router.cjs +242 -3
  17. package/gsd-core/bin/lib/commands.cjs +7 -5
  18. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  19. package/gsd-core/bin/lib/config.cjs +96 -0
  20. package/gsd-core/bin/lib/core-utils.cjs +4 -1
  21. package/gsd-core/bin/lib/host-integration-adapters/cline-sdk-binding.cjs +234 -0
  22. package/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs +145 -0
  23. package/gsd-core/bin/lib/host-integration.cjs +45 -4
  24. package/gsd-core/bin/lib/init.cjs +76 -39
  25. package/gsd-core/bin/lib/install-effort-resolver.cjs +213 -0
  26. package/gsd-core/bin/lib/install-engine.cjs +228 -18
  27. package/gsd-core/bin/lib/installer-migration-report.cjs +7 -0
  28. package/gsd-core/bin/lib/loop-resolver.cjs +68 -17
  29. package/gsd-core/bin/lib/markdown-sectionizer.cjs +50 -11
  30. package/gsd-core/bin/lib/mcp-server.cjs +18 -7
  31. package/gsd-core/bin/lib/milestone.cjs +3 -3
  32. package/gsd-core/bin/lib/normalize-test-command.cjs +187 -0
  33. package/gsd-core/bin/lib/phase-id.cjs +132 -3
  34. package/gsd-core/bin/lib/phase.cjs +78 -16
  35. package/gsd-core/bin/lib/planning-workspace.cjs +17 -0
  36. package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
  37. package/gsd-core/bin/lib/roadmap-command-router.cjs +5 -4
  38. package/gsd-core/bin/lib/roadmap-parser.cjs +21 -30
  39. package/gsd-core/bin/lib/roadmap-upgrade.cjs +9 -9
  40. package/gsd-core/bin/lib/roadmap.cjs +42 -56
  41. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +248 -44
  42. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -2
  43. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +39 -23
  44. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
  45. package/gsd-core/bin/lib/runtime-homes.cjs +30 -0
  46. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +534 -37
  47. package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
  48. package/gsd-core/bin/lib/security.cjs +6 -36
  49. package/gsd-core/bin/lib/shell-command-projection.cjs +115 -2
  50. package/gsd-core/bin/lib/spec-section.cjs +111 -0
  51. package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
  52. package/gsd-core/bin/lib/state-transition.cjs +1 -1
  53. package/gsd-core/bin/lib/state.cjs +24 -24
  54. package/gsd-core/bin/lib/surface.cjs +40 -6
  55. package/gsd-core/bin/lib/uat.cjs +4 -1
  56. package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
  57. package/gsd-core/bin/lib/validate.cjs +15 -6
  58. package/gsd-core/bin/lib/verify.cjs +33 -37
  59. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  60. package/gsd-core/bin/shared/model-catalog.json +14 -9
  61. package/gsd-core/references/api-coverage.md +104 -0
  62. package/gsd-core/references/model-profiles.md +2 -2
  63. package/gsd-core/references/planning-config.md +2 -0
  64. package/gsd-core/references/specless-probe-fallback.md +172 -0
  65. package/gsd-core/references/ui-consideration-probe.md +73 -0
  66. package/gsd-core/templates/UI-SPEC.md +25 -0
  67. package/gsd-core/templates/VALIDATION.md +2 -0
  68. package/gsd-core/templates/config.json +2 -1
  69. package/gsd-core/workflows/audit-fix.md +9 -1
  70. package/gsd-core/workflows/audit-milestone.md +7 -4
  71. package/gsd-core/workflows/code-review-fix.md +7 -3
  72. package/gsd-core/workflows/code-review.md +4 -1
  73. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -1
  74. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +8 -4
  75. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +42 -0
  76. package/gsd-core/workflows/execute-phase.md +1 -25
  77. package/gsd-core/workflows/plan-phase.md +37 -2
  78. package/gsd-core/workflows/quick.md +2 -2
  79. package/gsd-core/workflows/review.md +59 -13
  80. package/gsd-core/workflows/settings-advanced.md +12 -9
  81. package/gsd-core/workflows/settings.md +2 -2
  82. package/gsd-core/workflows/ui-phase.md +146 -1
  83. package/gsd-core/workflows/validate-phase.md +2 -2
  84. package/gsd-core/workflows/verify-phase.md +3 -2
  85. package/gsd-core/workflows/verify-work.md +38 -0
  86. package/hooks/dist/gsd-cursor-pre-tool.js +76 -0
  87. package/hooks/dist/gsd-cursor-stop.js +48 -0
  88. package/hooks/dist/gsd-cursor-subagent-start.js +50 -0
  89. package/hooks/dist/gsd-cursor-subagent-stop.js +40 -0
  90. package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
  91. package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
  92. package/hooks/dist/managed-hooks-registry.cjs +6 -0
  93. package/hooks/gsd-cursor-pre-tool.js +76 -0
  94. package/hooks/gsd-cursor-stop.js +48 -0
  95. package/hooks/gsd-cursor-subagent-start.js +50 -0
  96. package/hooks/gsd-cursor-subagent-stop.js +40 -0
  97. package/hooks/gsd-windsurf-pre-command.js +275 -0
  98. package/hooks/gsd-windsurf-pre-write.js +132 -0
  99. package/hooks/managed-hooks-registry.cjs +6 -0
  100. package/package.json +9 -4
  101. package/pi/gsd.cjs +354 -0
  102. package/scripts/build-hooks.js +8 -1
  103. package/scripts/gen-golden-install-parity-zcode.cjs +11 -1
  104. package/scripts/gen-registry.cjs +128 -0
  105. package/scripts/lint-phase-id-drift.cjs +150 -0
  106. package/scripts/lint-test-file-count.allowlist.json +2 -1
  107. package/scripts/registry-schema.cjs +565 -0
  108. package/scripts/run-tests.cjs +21 -1
  109. package/scripts/validate-registry.cjs +117 -0
  110. package/vscode/browser.js +197 -0
  111. package/vscode/extension.js +383 -0
  112. package/vscode/host-binding.js +113 -0
  113. package/vscode/package.json +96 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-core",
3
- "version": "1.7.0-rc.4",
3
+ "version": "1.7.0-rc.6",
4
4
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
5
5
  "main": ".opencode/plugins/gsd-core.js",
6
6
  "bin": {
@@ -20,7 +20,9 @@
20
20
  ".opencode",
21
21
  "GEMINI.md",
22
22
  "hooks",
23
- "scripts"
23
+ "scripts",
24
+ "pi",
25
+ "vscode"
24
26
  ],
25
27
  "keywords": [
26
28
  "claude",
@@ -78,6 +80,7 @@
78
80
  "check:env": "node scripts/check-env.cjs",
79
81
  "check:alias-drift": "node scripts/check-alias-drift.cjs",
80
82
  "check:identity-drift": "node scripts/lint-package-identity-drift.cjs",
83
+ "check:phase-id-drift": "node scripts/lint-phase-id-drift.cjs",
81
84
  "check:integrity": "node scripts/check-npm-integrity.cjs",
82
85
  "build": "npm run generate:identity && npm run build:lib && npm run gen:plugin-skills && npm run gen:loop-host-contract && npm run gen:capability-registry && npm run build:hooks",
83
86
  "build:hooks": "node scripts/build-hooks.js",
@@ -86,6 +89,8 @@
86
89
  "gen:loop-host-contract": "node scripts/gen-loop-host-contract.cjs --write",
87
90
  "gen:plugin-skills": "node scripts/gen-plugin-skills.cjs --write",
88
91
  "gen:capability-registry": "node scripts/gen-capability-registry.cjs --write",
92
+ "gen:registry": "node scripts/gen-registry.cjs --write",
93
+ "validate:registry": "node scripts/validate-registry.cjs",
89
94
  "prepack": "npm run build:lib",
90
95
  "prepare": "npm run build:lib",
91
96
  "version": "node scripts/sync-manifest-versions.cjs --stage && node scripts/gen-capability-registry.cjs --write && git add gsd-core/bin/lib/capability-registry.cjs",
@@ -94,7 +99,7 @@
94
99
  "pretest:coverage": "npm run build:lib && npm run lint:skill-deps",
95
100
  "lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
96
101
  "lint:fix": "eslint . --fix",
97
- "lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs",
102
+ "lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/validate-registry.cjs",
98
103
  "lint:allow-test-rule-refs": "node scripts/lint-allow-test-rule-refs.cjs",
99
104
  "lint:regression-names": "node scripts/lint-regression-test-names.cjs",
100
105
  "lint:descriptions": "node scripts/lint-descriptions.cjs",
@@ -102,7 +107,7 @@
102
107
  "lint:test-file-count": "node scripts/lint-test-file-count.cjs",
103
108
  "lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs",
104
109
  "lint:changeset": "node scripts/changeset/lint.cjs",
105
- "lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check",
110
+ "lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check",
106
111
  "lint:docs": "node scripts/lint-docs-required.cjs",
107
112
  "lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs",
108
113
  "ci:test-scope": "node scripts/ci-test-scope.cjs",
package/pi/gsd.cjs ADDED
@@ -0,0 +1,354 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * GSD extension for pi (pi.dev) — ADR-1239 Phase D / #1944, upgraded #2102 Stage 2.
5
+ *
6
+ * pi is a Programmatic-CLI host whose TS extensions implement the ExtensionAPI
7
+ * (`@earendil-works/pi-coding-agent`): registerCommand({handler(args, ctx)}) /
8
+ * registerTool({execute(toolCallId, params, signal, onUpdate, ctx)}) / pi.on(event, handler).
9
+ * This extension binds GSD's command surface to pi via the imperative adapter
10
+ * path — the programmatic-CLI peer of the OpenCode worked binding.
11
+ *
12
+ * Installation: copy this file to ~/.pi/agent/extensions/gsd.cjs (pi loads
13
+ * extensions via jiti from that dir). The engine is resolved from the installed
14
+ * GSD tree (walk-up like the OpenCode plugin). pi's shared hooks/ bundle
15
+ * (hooks/*.js + hooks/lib/git-cmd.js) is installed alongside the extension —
16
+ * capabilities/pi/capability.json does NOT set
17
+ * `hostBehaviors.skipSharedHooksInstall` (#2102 Stage 2 fix; pi is
18
+ * architecturally identical to OpenCode here: `hooksSurface: 'none'` + a
19
+ * native extension that spawns the staged hooks — not Kilo/ZCode's
20
+ * no-plugin-surface case, where the same hooks would be genuine dead weight).
21
+ * This is what makes the event bridges below (and the tokenizer require)
22
+ * resolve for real in an installed tree, not just in this dev repo.
23
+ *
24
+ * Engine entry: dispatch is SUBPROCESS-REUSE to gsd-tools.cjs (bounded,
25
+ * no-throw — dispatchGsdCommand in shell-command-projection.cjs), NOT an
26
+ * in-process command-routing hub. No fully-populated hub factory exists
27
+ * anywhere in gsd-core — every createHub() caller in the tree builds a
28
+ * single-family hub for its own narrow purpose — so the "in-process createHub"
29
+ * framing of the original #1944 cut was aspirational and is not achievable
30
+ * without a hub factory that doesn't exist. This mirrors the precedent already
31
+ * established for the OpenCode/Kilo hook bridge (.opencode/plugins/gsd-core.js
32
+ * header: "Architecture: SUBPROCESS REUSE ... spawns existing hook scripts as
33
+ * child processes") — the same pattern, applied to command dispatch. The
34
+ * companion MCP server (gsd-mcp-server) dispatches through the SAME shared
35
+ * helper for out-of-process hosts.
36
+ *
37
+ * @param {object} pi pi ExtensionAPI (registerTool/registerCommand/on/…)
38
+ */
39
+
40
+ const fs = require('fs');
41
+ const path = require('path');
42
+ const { spawnSync } = require('child_process');
43
+
44
+ // Resolve the GSD engine tree (the dir holding gsd-core/ + hooks/).
45
+ // Works across dev (<root>/pi/gsd.cjs → <root>) and installed layouts.
46
+ function resolveEngineRoot(startDir) {
47
+ let dir = startDir;
48
+ for (let i = 0; i < 6; i++) {
49
+ if (fs.existsSync(path.join(dir, 'gsd-core'))) return dir;
50
+ const parent = path.dirname(dir);
51
+ if (parent === dir) break;
52
+ dir = parent;
53
+ }
54
+ return path.resolve(startDir, '..');
55
+ }
56
+
57
+ const ENGINE_ROOT = resolveEngineRoot(__dirname);
58
+ const GSD_CORE = path.join(ENGINE_ROOT, 'gsd-core');
59
+
60
+ // ── curated top-level command families (gsd-tools.cjs TOP_LEVEL_USAGE) ──────
61
+ // readCmdNames() (scripts/fix-slash-commands.cjs) reads commands/, which pi
62
+ // does NOT install (it ships a single native-extension file, no shared
63
+ // commands/ dir) — it would always return []. This is a self-contained,
64
+ // hand-curated subset of the STABLE top-level families documented by
65
+ // `node gsd-core/bin/gsd-tools.cjs --help` (gsd-tools.cjs:689-705). Named +
66
+ // exported (via _internals) so a test can assert against it directly.
67
+ const PI_COMMAND_FAMILIES = Object.freeze([
68
+ 'agent', 'capability', 'check', 'commit', 'config-get', 'config-path',
69
+ 'config-set', 'effort', 'git', 'graphify', 'init', 'intel', 'learnings',
70
+ 'list-todos', 'loop', 'milestone', 'phase', 'phases', 'progress',
71
+ 'requirements', 'research-plan', 'research-store', 'resolve-granularity',
72
+ 'resolve-model', 'roadmap', 'scaffold', 'smart-entry', 'state', 'task',
73
+ 'template', 'user-story', 'validate', 'verify', 'workstream', 'worktree',
74
+ ]);
75
+
76
+ /**
77
+ * Filter PI_COMMAND_FAMILIES by prefix (startsWith). Returns null when there
78
+ * are no matches, per pi's `AutocompleteItem[]|null` contract.
79
+ * @param {string} prefix
80
+ * @returns {{value: string, label: string}[] | null}
81
+ */
82
+ function getArgumentCompletions(prefix) {
83
+ const p = typeof prefix === 'string' ? prefix : '';
84
+ const matches = PI_COMMAND_FAMILIES.filter((name) => name.startsWith(p));
85
+ if (matches.length === 0) return null;
86
+ return matches.map((value) => ({ value, label: value }));
87
+ }
88
+
89
+ /**
90
+ * Tokenize the raw `/gsd <args>` string into { family, subcommand, args }.
91
+ * Reuses the quote-aware whitespace tokenizer already shipped for hooks
92
+ * (hooks/lib/git-cmd.js's `tokenize`) rather than re-implementing shell-word
93
+ * splitting a second time. #2102 Stage 2: pi's capability descriptor no
94
+ * longer sets `hostBehaviors.skipSharedHooksInstall` (adversarial-review
95
+ * finding #1/#2 — pi ships NO hooks/ with that flag set, so this require was
96
+ * dead in a real install), so the shared hooks/ bundle — including
97
+ * hooks/lib/git-cmd.js — is installed alongside the extension for real
98
+ * (mirrors OpenCode, whose native plugin also spawns the staged hooks/*.js
99
+ * bundle). The require below is therefore the PRIMARY, live path in an
100
+ * installed pi tree; the whitespace-split fallback stays as defense-in-depth
101
+ * for a corrupted/partial install (e.g. a user who deleted hooks/lib/ by
102
+ * hand) rather than the only-ever-taken path.
103
+ * @param {string} rawArgs
104
+ * @returns {{ family: string, subcommand?: string, args: string[] }}
105
+ */
106
+ function parseGsdCommandArgs(rawArgs) {
107
+ let tokenize;
108
+ try {
109
+ ({ tokenize } = require(path.join(ENGINE_ROOT, 'hooks', 'lib', 'git-cmd.js')));
110
+ } catch {
111
+ tokenize = (s) => String(s || '').split(/\s+/).filter(Boolean);
112
+ }
113
+ const tokens = tokenize(typeof rawArgs === 'string' ? rawArgs : '');
114
+ return {
115
+ // Empty args → dispatch gsd-tools.cjs's own --help surface (a real,
116
+ // working, ok:true default — NOT the 'query'/'help' pairing the original
117
+ // #1944 cut used, which is not a valid gsd-tools.cjs command).
118
+ family: tokens[0] || '--help',
119
+ subcommand: tokens[1],
120
+ args: tokens.slice(2),
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Best-effort TypeBox schema for gsd_invoke's `parameters`, falling back to a
126
+ * plain JSON-Schema object when the `typebox` package is unavailable (it is
127
+ * NOT a gsd-core dependency — pi's own ExtensionAPI contract expects TypeBox,
128
+ * but nothing in this repo installs it). TypeBox schemas ARE JSON Schema, so
129
+ * the fallback object is structurally equivalent for hosts that accept plain
130
+ * JSON Schema; this is a best-effort shim for the flat-file extension case.
131
+ * @returns {object}
132
+ */
133
+ function buildGsdInvokeParameters() {
134
+ try {
135
+ const typebox = require('typebox');
136
+ const Type = typebox && typebox.Type;
137
+ if (Type) {
138
+ return Type.Object({
139
+ family: Type.String(),
140
+ subcommand: Type.Optional(Type.String()),
141
+ args: Type.Optional(Type.Array(Type.String())),
142
+ });
143
+ }
144
+ } catch {
145
+ // typebox is not installed in this environment — fall through.
146
+ }
147
+ process.stderr.write(
148
+ 'gsd: typebox unavailable — gsd_invoke "parameters" falling back to a plain JSON-schema object.\n',
149
+ );
150
+ return {
151
+ type: 'object',
152
+ properties: {
153
+ family: { type: 'string' },
154
+ subcommand: { type: 'string' },
155
+ args: { type: 'array', items: { type: 'string' } },
156
+ },
157
+ required: ['family'],
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Build the `before_provider_request` handler that steers pi's model
163
+ * selection to GSD's tier-resolved id (modelMode: 'active' per
164
+ * capabilities/pi/capability.json). GSD does NOT call `pi.registerProvider` —
165
+ * that registers a NEW model provider; GSD's job here is only to pick a
166
+ * tier-appropriate id AMONG pi's EXISTING built-in anthropic models, so
167
+ * registerProvider would be the wrong primitive (it would wrongly add a fake
168
+ * provider instead of steering the real one).
169
+ *
170
+ * v1 tier policy: GSD does not yet expose a per-turn/per-agent tier signal to
171
+ * this event, so a conservative fixed default tier is used (parameterized —
172
+ * default 'sonnet' — so a future richer signal, or a test, can override it).
173
+ *
174
+ * ASSUMPTION (flagged — verify against a live pi host): the event payload's
175
+ * model field is named `model`, matching the anthropic-messages payload shape
176
+ * (Context7-confirmed for the wire protocol; pi's own before_provider_request
177
+ * event schema was not independently verifiable in this environment). If pi's
178
+ * actual field name differs, this returns the WRONG key and pi's fail-open
179
+ * default takes over only because bare-model-id mismatches degrade to
180
+ * provider-level errors, not GSD-level ones — a discrepancy here needs a
181
+ * live-host smoke test before shipping past this stage.
182
+ *
183
+ * Fail-open: any resolution failure (or a null/falsy resolved model — e.g. an
184
+ * unrecognized tier) returns `undefined`, leaving pi's model choice untouched.
185
+ * NEVER returns a payload with a missing/empty model id.
186
+ *
187
+ * @param {{ tier?: string }} [opts]
188
+ * @returns {(event: object, ctx: object) => Promise<object|undefined>}
189
+ */
190
+ function buildBeforeProviderRequestHandler({ tier = 'sonnet' } = {}) {
191
+ return async function onBeforeProviderRequest(event, ctx) {
192
+ try {
193
+ const effectiveCwd = (ctx && ctx.cwd) || process.cwd();
194
+ const { resolveTierEntry } = require(path.join(GSD_CORE, 'bin', 'lib', 'model-resolver.cjs'));
195
+ const { loadConfig } = require(path.join(GSD_CORE, 'bin', 'lib', 'config-loader.cjs'));
196
+ const config = loadConfig(effectiveCwd);
197
+ const overrides = (config && config.model_profile_overrides) || undefined;
198
+ const entry = resolveTierEntry({ runtime: 'pi', tier, overrides });
199
+ const modelId = entry && typeof entry.model === 'string' && entry.model.length > 0 ? entry.model : null;
200
+ if (!modelId) return undefined; // fail-open — leave pi's model untouched
201
+ const basePayload = (event && typeof event === 'object' && event.payload && typeof event.payload === 'object')
202
+ ? event.payload
203
+ : {};
204
+ return { ...basePayload, model: modelId };
205
+ } catch {
206
+ return undefined; // fail-open on any resolution error
207
+ }
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Bounded subprocess bridge to GSD's Claude Code hook scripts. Mirrors
213
+ * .opencode/plugins/gsd-core.js's `runHook` (SUBPROCESS-REUSE): spawns
214
+ * `node <hooks/hookFile>` with the payload piped to stdin, on a bounded
215
+ * timeout. NEVER throws — a missing hook file, a spawn error, or a timeout
216
+ * all degrade to a silent-allow result so a hook problem can never block pi.
217
+ * @param {string} hookFile filename under hooks/, e.g. "gsd-context-monitor.js"
218
+ * @param {object} payload
219
+ * @param {{ timeout?: number, cwd?: string }} [opts]
220
+ * @returns {{ stdout: string, exitCode: number, timedOut: boolean }}
221
+ */
222
+ function runHook(hookFile, payload, opts = {}) {
223
+ const hookPath = path.join(ENGINE_ROOT, 'hooks', hookFile);
224
+ if (!fs.existsSync(hookPath)) return { stdout: '', exitCode: 0, timedOut: false };
225
+ const timeout = opts.timeout || 8000;
226
+ let result;
227
+ try {
228
+ result = spawnSync(process.execPath, [hookPath], {
229
+ input: JSON.stringify(payload || {}),
230
+ encoding: 'utf8',
231
+ timeout,
232
+ cwd: opts.cwd || process.cwd(),
233
+ windowsHide: true,
234
+ });
235
+ } catch {
236
+ return { stdout: '', exitCode: 0, timedOut: false };
237
+ }
238
+ const stdout = (result && typeof result.stdout === 'string') ? result.stdout.trim() : '';
239
+ const exitCode = (result && result.status != null) ? result.status : 0;
240
+ return { stdout, exitCode, timedOut: !!(result && result.signal === 'SIGTERM') };
241
+ }
242
+
243
+ module.exports = function gsdPiExtension(pi) {
244
+ if (!pi || typeof pi !== 'object') {
245
+ throw new TypeError('gsdPiExtension: pi ExtensionAPI is required');
246
+ }
247
+
248
+ // ── /gsd command: dispatch through gsd-tools.cjs (subprocess-reuse) ──────
249
+ pi.registerCommand('gsd', {
250
+ description: 'Invoke a GSD command via the embedded engine (subprocess-reuse adapter).',
251
+ getArgumentCompletions,
252
+ handler: async (args, ctx) => {
253
+ const cwd = (ctx && ctx.cwd) || process.cwd();
254
+ const { family, subcommand, args: rest } = parseGsdCommandArgs(args);
255
+ let dispatchGsdCommand;
256
+ try {
257
+ ({ dispatchGsdCommand } = require(path.join(GSD_CORE, 'bin', 'lib', 'shell-command-projection.cjs')));
258
+ } catch (e) {
259
+ return `GSD engine unavailable: ${e && e.message ? e.message : String(e)}`;
260
+ }
261
+ const result = dispatchGsdCommand({ family, subcommand, args: rest, cwd });
262
+ if (result.ok) return result.stdout;
263
+ return `GSD error: ${result.stderr || result.stdout || `dispatch failed (exit ${result.code})`}`;
264
+ },
265
+ });
266
+
267
+ // ── gsd_invoke tool: programmatic command invocation ────────────────────
268
+ pi.registerTool({
269
+ name: 'gsd_invoke',
270
+ label: 'GSD Invoke',
271
+ description: 'Invoke a GSD command family/subcommand through the engine.',
272
+ parameters: buildGsdInvokeParameters(),
273
+ execute: async (toolCallId, params, signal, onUpdate, ctx) => {
274
+ const p = (params && typeof params === 'object') ? params : {};
275
+ const family = typeof p.family === 'string' ? p.family : '';
276
+ if (!family) {
277
+ return { content: [{ type: 'text', text: 'gsd_invoke requires a non-empty string "family".' }] };
278
+ }
279
+ const subcommand = typeof p.subcommand === 'string' ? p.subcommand : undefined;
280
+ const invokeArgs = Array.isArray(p.args) ? p.args : [];
281
+ const cwd = (ctx && ctx.cwd) || process.cwd();
282
+ let dispatchGsdCommand;
283
+ try {
284
+ ({ dispatchGsdCommand } = require(path.join(GSD_CORE, 'bin', 'lib', 'shell-command-projection.cjs')));
285
+ } catch (e) {
286
+ return { content: [{ type: 'text', text: `GSD engine unavailable: ${e && e.message ? e.message : String(e)}` }] };
287
+ }
288
+ const result = dispatchGsdCommand({ family, subcommand, args: invokeArgs, cwd });
289
+ const text = result.ok ? result.stdout : (result.stderr || result.stdout || `dispatch failed (exit ${result.code})`);
290
+ return { content: [{ type: 'text', text }] };
291
+ },
292
+ });
293
+
294
+ // ── before_provider_request: active-model steering (modelMode: 'active') ──
295
+ // GSD steers pi's EXISTING built-in anthropic models; it does NOT call
296
+ // pi.registerProvider (that would wrongly register a NEW fake provider —
297
+ // see buildBeforeProviderRequestHandler's doc comment).
298
+ pi.on('before_provider_request', buildBeforeProviderRequestHandler());
299
+
300
+ // ── Event bindings: bounded subprocess bridge to GSD's hook scripts ──────
301
+ // Each binding fails open — a hook error/timeout/missing-file never blocks
302
+ // pi (mirrors .opencode/plugins/gsd-core.js's runHook SUBPROCESS-REUSE
303
+ // pattern, applied to pi's ExtensionAPI event names).
304
+
305
+ // session_start → SessionStart-equivalent bootstrap.
306
+ pi.on('session_start', async (event, ctx) => {
307
+ try {
308
+ const cwd = (ctx && ctx.cwd) || process.cwd();
309
+ runHook('gsd-ensure-canonical-path.js', { hook_event_name: 'SessionStart', cwd }, { cwd });
310
+ } catch { /* fail-open */ }
311
+ });
312
+
313
+ // before_agent_start → workflow-guard bridge. Forward-compatible binding:
314
+ // gsd-workflow-guard.js's current triggers are tool-scoped (Write/Edit/
315
+ // Bash via tool_name/tool_input), so with no tool_name in the payload it
316
+ // fires as a safe no-op today — wired so a future agent-start-scoped check
317
+ // can attach without a plugin change (mirrors the OpenCode session.idle
318
+ // recognized-but-unused sentinel pattern).
319
+ pi.on('before_agent_start', async (event, ctx) => {
320
+ try {
321
+ const cwd = (ctx && ctx.cwd) || process.cwd();
322
+ runHook('gsd-workflow-guard.js', { hook_event_name: 'before_agent_start', cwd }, { cwd });
323
+ } catch { /* fail-open */ }
324
+ });
325
+
326
+ // session_before_compact → PreCompact-equivalent (context-usage bridge).
327
+ pi.on('session_before_compact', async (event, ctx) => {
328
+ try {
329
+ const cwd = (ctx && ctx.cwd) || process.cwd();
330
+ runHook('gsd-context-monitor.js', { hook_event_name: 'PreCompact', cwd }, { cwd });
331
+ } catch { /* fail-open */ }
332
+ });
333
+
334
+ // tool_call event: lifecycle hook bridge attachment point (kept from the
335
+ // original cut — the PreToolUse/PostToolUse tool_name/tool_input mapping
336
+ // is a follow-up once pi's tool_call payload shape is verified against a
337
+ // live host).
338
+ pi.on('tool_call', async function () {
339
+ /* GSD hook bridge attachment point (PreToolUse/PostToolUse mapping). */
340
+ });
341
+ };
342
+
343
+ // Test-only internals (mirrors the OpenCode plugin pattern) — wired to the
344
+ // real functions (not stubs) so tests can exercise parsing/completions/model
345
+ // resolution WITHOUT a live pi runtime.
346
+ module.exports._internals = {
347
+ resolveEngineRoot,
348
+ parseGsdCommandArgs,
349
+ getArgumentCompletions,
350
+ PI_COMMAND_FAMILIES,
351
+ buildBeforeProviderRequestHandler,
352
+ buildGsdInvokeParameters,
353
+ runHook,
354
+ };
@@ -37,9 +37,16 @@ const HOOKS_TO_COPY = [
37
37
  // so require('./managed-hooks-registry.cjs') resolves in the installed hooks/ dir.
38
38
  'managed-hooks-registry.cjs',
39
39
  'gsd-context-monitor.js',
40
- // Cursor lifecycle hooks (issue #777): sessionStart context injection + postToolUse monitor
40
+ // Cursor lifecycle hooks (#777 + ADR-1239/#2089): 6 managed events
41
41
  'gsd-cursor-session-start.js',
42
42
  'gsd-cursor-post-tool.js',
43
+ 'gsd-cursor-pre-tool.js',
44
+ 'gsd-cursor-stop.js',
45
+ 'gsd-cursor-subagent-start.js',
46
+ 'gsd-cursor-subagent-stop.js',
47
+ // Windsurf/Cascade lifecycle hooks (ADR-1239/#2100 Stage 2): 2 blocking events
48
+ 'gsd-windsurf-pre-write.js',
49
+ 'gsd-windsurf-pre-command.js',
43
50
  // Claude Code FileChanged hook (#770) — hot-reloads gsd config when
44
51
  // .planning/config.json changes mid-session. Must ship to dist so the
45
52
  // installer can copy it to the target hooks/ dir and register FileChanged.
@@ -26,7 +26,16 @@ const VOLATILE_FILES = new Set([
26
26
  '.gsd-source',
27
27
  'gsd-core/CHANGELOG.md',
28
28
  ]);
29
- const HOOK_CONFIG_FILES = new Set(['settings.json', 'hooks.json']);
29
+ // Must match tests/golden-install-parity.test.cjs exactly — settings.local.json
30
+ // (Claude LOCAL hook surface, #338/#2086) embeds the same platform-varying
31
+ // node-runner command and is excluded there; omitting it here mis-generated the
32
+ // claude-local fixture (#2100).
33
+ const HOOK_CONFIG_FILES = new Set(['settings.json', 'settings.local.json', 'hooks.json']);
34
+ // Kimi's native config.toml (#2095) — see tests/golden-install-parity.test.cjs'
35
+ // HOOK_CONFIG_RELATIVE_PATHS comment for why this is an exact relative-path
36
+ // exclusion rather than a HOOK_CONFIG_FILES basename entry (a basename entry
37
+ // would also blind Codex's stable, platform-independent config.toml fixture).
38
+ const HOOK_CONFIG_RELATIVE_PATHS = new Set(['.kimi/config.toml']);
30
39
  const EXCLUDED_PREFIXES = ['gsd-core/bin/lib/'];
31
40
 
32
41
  function buildParityManifest(configDir, root) {
@@ -36,6 +45,7 @@ function buildParityManifest(configDir, root) {
36
45
  const rel = path.relative(configDir, full).split(path.sep).join('/');
37
46
  if (VOLATILE_FILES.has(rel)) continue;
38
47
  if (HOOK_CONFIG_FILES.has(path.basename(rel))) continue;
48
+ if (HOOK_CONFIG_RELATIVE_PATHS.has(rel)) continue;
39
49
  if (EXCLUDED_PREFIXES.some((p) => rel.startsWith(p))) continue;
40
50
  const content = fs.readFileSync(full);
41
51
  const normalized = content.toString('utf8').split(root).join('<HOME>').split(PKG_VERSION).join('<VERSION>');
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * scripts/gen-registry.cjs — generates docs/registries/capability-registry.md
6
+ * (and, once PR2 ships docs/registries/eos.json, docs/registries/eos-registry.md)
7
+ * from the corresponding source JSON, via registry-schema.cjs#renderMarkdown.
8
+ * Issue #2182.
9
+ *
10
+ * NOT to be confused with `scripts/gen-capability-registry.cjs`: that script
11
+ * generates the RUNTIME capability manifest consumed by the host at runtime
12
+ * (`gsd-core/bin/lib/capability-registry.cjs`, built from every
13
+ * `capabilities/<id>/capability.json` declaration). THIS script instead
14
+ * generates the human-facing DOCUMENTATION catalog pages
15
+ * (`docs/registries/*.md`) from the third-party discoverability registry
16
+ * source JSON (`docs/registries/{capabilities,eos}.json`). The two pipelines
17
+ * are independent — do not conflate them.
18
+ *
19
+ * Usage:
20
+ * node scripts/gen-registry.cjs # print rendered markdown(s) to stdout
21
+ * node scripts/gen-registry.cjs --write # write the *-registry.md file(s)
22
+ * node scripts/gen-registry.cjs --check # exit 1 if a committed *-registry.md is stale
23
+ *
24
+ * Root is resolved from process.cwd() (not __dirname) — mirrors
25
+ * scripts/validate-registry.cjs so both are drivable as subprocesses against
26
+ * isolated temp-fixture directories via `cwd`.
27
+ */
28
+
29
+ const fs = require('node:fs');
30
+ const path = require('node:path');
31
+
32
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
33
+ const { renderMarkdown } = require('./registry-schema.cjs');
34
+
35
+ const SOURCES = [
36
+ { type: 'capability', jsonFile: 'capabilities.json', mdFile: 'capability-registry.md' },
37
+ { type: 'eos', jsonFile: 'eos.json', mdFile: 'eos-registry.md' },
38
+ ];
39
+
40
+ /**
41
+ * The generator always writes LF; a Windows checkout (autocrlf) may present
42
+ * committed files with CRLF. Normalize before comparing so `--check` only
43
+ * fails on real content drift, not checkout-introduced line-ending noise.
44
+ *
45
+ * @param {string} content
46
+ * @returns {string}
47
+ */
48
+ function normalizeLineEndings(content) {
49
+ return content.replace(/\r/g, '');
50
+ }
51
+
52
+ function getRegistriesDir() {
53
+ return path.join(process.cwd(), 'docs', 'registries');
54
+ }
55
+
56
+ /**
57
+ * Render the markdown for a single registry type from its committed source
58
+ * JSON.
59
+ *
60
+ * Only `eos.json` is optional (pre-PR2, before that source JSON ships) —
61
+ * an absent `eos.json` returns null and callers treat that as "nothing to
62
+ * do". `capabilities.json` is the primary registry source: a missing
63
+ * `capabilities.json` is ALWAYS an error (never a silent "up to date"
64
+ * pass), mirroring the type distinction in `scripts/validate-registry.cjs`
65
+ * (`type === 'eos' && !exists → continue`).
66
+ *
67
+ * @param {'capability'|'eos'} type
68
+ * @returns {string|null}
69
+ */
70
+ function renderFor(type) {
71
+ const source = SOURCES.find((s) => s.type === type);
72
+ if (!source) throw new Error(`gen-registry: unknown registry type "${type}"`);
73
+
74
+ const jsonPath = path.join(getRegistriesDir(), source.jsonFile);
75
+ if (!fs.existsSync(jsonPath)) {
76
+ if (type === 'eos') return null;
77
+ throw new ExitError(
78
+ 1,
79
+ `${source.jsonFile} does not exist at ${jsonPath}. Run:\n node scripts/gen-registry.cjs --write\n(after adding docs/registries/${source.jsonFile})`,
80
+ );
81
+ }
82
+
83
+ const entries = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
84
+ return renderMarkdown(entries, { type, sourceFile: source.jsonFile });
85
+ }
86
+
87
+ function main() {
88
+ const [, , flag] = process.argv;
89
+ const registriesDir = getRegistriesDir();
90
+ let anyDrift = false;
91
+
92
+ for (const { type, mdFile } of SOURCES) {
93
+ const rendered = renderFor(type);
94
+ if (rendered === null) continue; // source JSON absent (eos.json before PR2)
95
+
96
+ const mdPath = path.join(registriesDir, mdFile);
97
+
98
+ if (flag === '--check') {
99
+ if (!fs.existsSync(mdPath)) {
100
+ process.stderr.write(`${mdFile} does not exist. Run:\n node scripts/gen-registry.cjs --write\n`);
101
+ anyDrift = true;
102
+ continue;
103
+ }
104
+ const committed = fs.readFileSync(mdPath, 'utf8');
105
+ if (normalizeLineEndings(committed) !== normalizeLineEndings(rendered)) {
106
+ process.stderr.write(`${mdFile} is stale. Run:\n node scripts/gen-registry.cjs --write\n`);
107
+ anyDrift = true;
108
+ }
109
+ } else if (flag === '--write') {
110
+ fs.mkdirSync(registriesDir, { recursive: true });
111
+ fs.writeFileSync(mdPath, rendered);
112
+ process.stdout.write(`Wrote ${mdPath}\n`);
113
+ } else {
114
+ process.stdout.write(rendered + '\n');
115
+ }
116
+ }
117
+
118
+ if (flag === '--check') {
119
+ if (anyDrift) throw new ExitError(1, 'registry markdown is stale');
120
+ process.stdout.write('docs/registries/*.md are up to date.\n');
121
+ }
122
+
123
+ return 0;
124
+ }
125
+
126
+ if (require.main === module) runMain(main);
127
+
128
+ module.exports = { main, renderFor, SOURCES, normalizeLineEndings };