@opengsd/gsd-core 1.7.0-rc.5 → 1.7.0

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 (112) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-executor.md +2 -1
  4. package/agents/gsd-security-auditor.md +13 -15
  5. package/agents/gsd-ui-checker.md +2 -0
  6. package/agents/gsd-ui-researcher.md +1 -0
  7. package/bin/install.js +975 -196
  8. package/commands/gsd/mempalace-capture.md +27 -1
  9. package/commands/gsd/surface.md +6 -6
  10. package/gsd-core/bin/gsd-tools.cjs +63 -2
  11. package/gsd-core/bin/lib/api-coverage.cjs +3 -4
  12. package/gsd-core/bin/lib/audit.cjs +7 -6
  13. package/gsd-core/bin/lib/capability-registry.cjs +503 -87
  14. package/gsd-core/bin/lib/capability-validator.cjs +56 -18
  15. package/gsd-core/bin/lib/check-command-router.cjs +1 -1
  16. package/gsd-core/bin/lib/clock.cjs +19 -0
  17. package/gsd-core/bin/lib/commands.cjs +48 -9
  18. package/gsd-core/bin/lib/config-loader.cjs +6 -2
  19. package/gsd-core/bin/lib/config.cjs +12 -0
  20. package/gsd-core/bin/lib/core-utils.cjs +8 -2
  21. package/gsd-core/bin/lib/drift.cjs +4 -4
  22. package/gsd-core/bin/lib/frontmatter.cjs +22 -0
  23. package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
  24. package/gsd-core/bin/lib/host-integration.cjs +33 -8
  25. package/gsd-core/bin/lib/init.cjs +60 -53
  26. package/gsd-core/bin/lib/install-engine.cjs +93 -22
  27. package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
  28. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  29. package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
  30. package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
  31. package/gsd-core/bin/lib/markdown-table.cjs +698 -0
  32. package/gsd-core/bin/lib/mcp-server.cjs +18 -7
  33. package/gsd-core/bin/lib/milestone.cjs +217 -31
  34. package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
  35. package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
  36. package/gsd-core/bin/lib/phase-locator.cjs +23 -2
  37. package/gsd-core/bin/lib/phase.cjs +436 -61
  38. package/gsd-core/bin/lib/plan-scan.cjs +3 -0
  39. package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
  40. package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
  41. package/gsd-core/bin/lib/roadmap.cjs +100 -49
  42. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +242 -44
  43. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
  44. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -14
  45. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
  46. package/gsd-core/bin/lib/runtime-homes.cjs +22 -0
  47. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +526 -29
  48. package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
  49. package/gsd-core/bin/lib/schema-detect.cjs +2 -1
  50. package/gsd-core/bin/lib/security.cjs +7 -37
  51. package/gsd-core/bin/lib/shell-command-projection.cjs +176 -27
  52. package/gsd-core/bin/lib/smart-entry.cjs +4 -3
  53. package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
  54. package/gsd-core/bin/lib/state-transition.cjs +100 -45
  55. package/gsd-core/bin/lib/state.cjs +391 -126
  56. package/gsd-core/bin/lib/surface.cjs +12 -8
  57. package/gsd-core/bin/lib/template.cjs +2 -1
  58. package/gsd-core/bin/lib/uat.cjs +54 -8
  59. package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
  60. package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
  61. package/gsd-core/bin/lib/verify.cjs +4 -3
  62. package/gsd-core/bin/lib/workstream.cjs +3 -2
  63. package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
  64. package/gsd-core/bin/lib/write-set.cjs +38 -0
  65. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  66. package/gsd-core/bin/shared/model-catalog.json +8 -3
  67. package/gsd-core/references/checkpoints.md +12 -0
  68. package/gsd-core/references/ui-consideration-probe.md +73 -0
  69. package/gsd-core/templates/UI-SPEC.md +25 -0
  70. package/gsd-core/templates/VALIDATION.md +2 -0
  71. package/gsd-core/workflows/add-tests.md +1 -1
  72. package/gsd-core/workflows/audit-milestone.md +7 -4
  73. package/gsd-core/workflows/debug.md +2 -0
  74. package/gsd-core/workflows/execute-phase.md +5 -3
  75. package/gsd-core/workflows/fast.md +8 -22
  76. package/gsd-core/workflows/plan-phase.md +6 -0
  77. package/gsd-core/workflows/progress.md +2 -2
  78. package/gsd-core/workflows/quick.md +2 -0
  79. package/gsd-core/workflows/review.md +42 -3
  80. package/gsd-core/workflows/secure-phase.md +1 -1
  81. package/gsd-core/workflows/settings-advanced.md +7 -4
  82. package/gsd-core/workflows/ship.md +8 -2
  83. package/gsd-core/workflows/spec-phase.md +1 -1
  84. package/gsd-core/workflows/transition.md +1 -1
  85. package/gsd-core/workflows/ui-phase.md +146 -1
  86. package/gsd-core/workflows/validate-phase.md +2 -2
  87. package/hooks/dist/gsd-statusline.js +164 -14
  88. package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
  89. package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
  90. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  91. package/hooks/gsd-statusline.js +164 -14
  92. package/hooks/gsd-windsurf-pre-command.js +275 -0
  93. package/hooks/gsd-windsurf-pre-write.js +132 -0
  94. package/hooks/managed-hooks-registry.cjs +2 -0
  95. package/package.json +10 -4
  96. package/pi/gsd.cjs +354 -0
  97. package/scripts/build-hooks.js +3 -0
  98. package/scripts/ci-test-scope.cjs +39 -1
  99. package/scripts/gen-golden-install-parity-zcode.cjs +35 -35
  100. package/scripts/gen-install-tree-fixtures.cjs +75 -0
  101. package/scripts/gen-registry.cjs +128 -0
  102. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  103. package/scripts/lint-table-schema-drift.cjs +157 -0
  104. package/scripts/lint-test-file-count.allowlist.json +2 -1
  105. package/scripts/registry-schema.cjs +565 -0
  106. package/scripts/validate-registry.cjs +117 -0
  107. package/skills/gsd-mempalace-capture/SKILL.md +27 -1
  108. package/skills/gsd-surface/SKILL.md +6 -6
  109. package/vscode/browser.js +197 -0
  110. package/vscode/extension.js +383 -0
  111. package/vscode/host-binding.js +113 -0
  112. package/vscode/package.json +96 -0
@@ -55,7 +55,33 @@ This step is `onError: skip` at `discuss:post` / `plan:post` / `verify:post` --
55
55
  On any error or timeout, stop and let the phase continue -- capture is best-effort.
56
56
 
57
57
  1. **Dedup first.** Interactive: `mempalace_check_duplicate` on the artifact's deterministic drawer id. Headless: rely on `mempalace mine`'s content-hash idempotency.
58
- 2. **Add the drawer (verbatim).** File the exact artifact text into `room: <room>` of `wing: <wing>` with provenance (`source_file`, phase id). Interactive: `mempalace_add_drawer`. Headless: `mempalace mine <path> --wing <wing> --room <room>`.
58
+ 2. **Add the drawer (verbatim).** File the exact artifact text into `room: <room>` of `wing: <wing>` with provenance (`source_file`, phase id). Interactive: `mempalace_add_drawer`. Headless: see below.
59
+
60
+ **`mempalace mine` has no `--room` flag** — only `search` accepts `--room` ([CLI reference: https://mempalaceofficial.com/reference/cli.html](https://mempalaceofficial.com/reference/cli.html)). Room assignment is driven by `detect_room()` matching folder-path segments against the `rooms:` list in `mempalace.yaml` ([mining guide: https://mempalaceofficial.com/guide/mining.html](https://mempalaceofficial.com/guide/mining.html) — "Rooms are auto-detected from your folder structure"; [config reference: https://mempalaceofficial.com/guide/configuration.html](https://mempalaceofficial.com/guide/configuration.html)). Stage the artifact under a room-named folder so `detect_room()` assigns it correctly:
61
+
62
+ ```bash
63
+ STAGE=".planning/.mempalace-stage"
64
+ # One-time: declare the GSD room taxonomy so detect_room() recognizes these folders
65
+ mkdir -p "$STAGE"
66
+ [ -f "$STAGE/mempalace.yaml" ] || cat > "$STAGE/mempalace.yaml" <<'YAML'
67
+ rooms:
68
+ - decisions
69
+ - planning
70
+ - milestones
71
+ - problems
72
+ - general
73
+ YAML
74
+ # Suppress MemPalace cache artifacts written into the scanned tree
75
+ [ -f "$STAGE/.gitignore" ] || echo "mempalace_embedder.json" > "$STAGE/.gitignore"
76
+ # Stage under <room>/<phase-id>/<basename> — stable path so mine's content-hash
77
+ # idempotency (file_already_mined keys on absolute source_file + mtime) deduplicates
78
+ # instead of creating duplicate drawers on re-runs.
79
+ ROOM_DIR="$STAGE/<room>/<phase-id>"
80
+ mkdir -p "$ROOM_DIR"
81
+ cp "<artifact-path>" "$ROOM_DIR/<basename>"
82
+ # Mine with --wing only — no --room flag; detect_room() assigns from the folder path
83
+ mempalace mine "$STAGE" --wing <wing>
84
+ ```
59
85
  3. **Mirror KG facts** when `config.mempalace.mirror_kg` is true: extract decision/delivery facts and `mempalace_kg_add` them with `valid_from` = the phase date (e.g. `(<project>, decided, <decision>)` from CONTEXT; `(<phase>, delivered, <capability>)` from SUMMARY). Under `augment` these are an *additive* mirror of GSD's native `.planning/graphs/`. Under `kg_backend`/`replace` the palace KG is the *authoritative* fact store — GSD still produces `.planning/graphs/` through its normal graphify, so an unreachable palace never loses a fact.
60
86
  4. Re-running a phase MUST NOT create duplicate drawers (deterministic ids + `check_duplicate`).
61
87
 
@@ -37,9 +37,9 @@ Parse the first token of $ARGUMENTS:
37
37
  ## list / status
38
38
 
39
39
  Load the capability registry and call `listSurface(runtimeConfigDir, manifest, CLUSTERS, registry)` from
40
- `gsd-core/bin/lib/surface.cjs`. The registry is loaded via:
40
+ the engine module at `${runtimeConfigDir}/gsd-core/bin/lib/surface.cjs`. The registry is loaded via:
41
41
  ```js
42
- const registry = require('gsd-core/bin/lib/capability-registry.cjs');
42
+ const registry = require(runtimeConfigDir + '/gsd-core/bin/lib/capability-registry.cjs');
43
43
  ```
44
44
  Display:
45
45
 
@@ -71,7 +71,7 @@ Install profile: standard (from .gsd-profile)
71
71
  3. `writeSurface(runtimeConfigDir, surfaceState)`.
72
72
  4. Resolve and re-apply:
73
73
  ```js
74
- const registry = require('gsd-core/bin/lib/capability-registry.cjs');
74
+ const registry = require(runtimeConfigDir + '/gsd-core/bin/lib/capability-registry.cjs');
75
75
  const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope);
76
76
  applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry);
77
77
  ```
@@ -89,7 +89,7 @@ Valid cluster names: `core_loop`, `audit_review`, `milestone`, `research_ideate`
89
89
  3. Add cluster to `surfaceState.disabledClusters` (deduplicate).
90
90
  4. `writeSurface` → resolve layout → `applySurface`:
91
91
  ```js
92
- const registry = require('gsd-core/bin/lib/capability-registry.cjs');
92
+ const registry = require(runtimeConfigDir + '/gsd-core/bin/lib/capability-registry.cjs');
93
93
  const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope);
94
94
  applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry);
95
95
  ```
@@ -103,7 +103,7 @@ Valid cluster names: `core_loop`, `audit_review`, `milestone`, `research_ideate`
103
103
  2. Remove cluster from `surfaceState.disabledClusters`.
104
104
  3. `writeSurface` → resolve layout → `applySurface`:
105
105
  ```js
106
- const registry = require('gsd-core/bin/lib/capability-registry.cjs');
106
+ const registry = require(runtimeConfigDir + '/gsd-core/bin/lib/capability-registry.cjs');
107
107
  const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope);
108
108
  applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry);
109
109
  ```
@@ -151,7 +151,7 @@ All paths can be overridden by reading the `CLAUDE_CONFIG_DIR` env var if set.
151
151
 
152
152
  - Unknown cluster name → list valid cluster names, exit without writing.
153
153
  - Unknown profile name → list known profiles (`core`, `standard`, `full`), exit.
154
- - Missing `surface.cjs` → prompt: "Run `npm i -g gsd-core` to reinstall GSD."
154
+ - Missing `surface.cjs` → prompt: "Run `npm i -g @opengsd/gsd-core` to reinstall GSD."
155
155
 
156
156
  <execution_context>
157
157
  Surface state file: `~/.claude/.gsd-surface.json`
@@ -0,0 +1,197 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * GSD extension for VS Code — WEB (browser) entry, #2103.
5
+ *
6
+ * This is the `browser` entry point (vscode/package.json `"browser": "./browser.js"`),
7
+ * loaded by VS Code Web / vscode.dev in a webworker context. It has ZERO Node
8
+ * APIs: no `require('fs')`, `require('path')`, or `require('child_process')`,
9
+ * and no Node globals (`process`, `Buffer`, `__dirname`, `__filename`). This is
10
+ * a HARD constraint, not a style preference — a Web Extension host does not
11
+ * have Node's core modules available at all; requiring one throws immediately
12
+ * at load time and breaks activation.
13
+ *
14
+ * WHY THIS FILE DOES NOT REQUIRE `./host-binding.js` OR `./extension.js`
15
+ * (a deliberate deviation from "compose the seams via bindGsdToVscode" — see
16
+ * the #2103 dispatch-crux note below):
17
+ *
18
+ * `host-binding.js`'s `bindGsdToVscode` is NOT actually web-safe once its
19
+ * transitive dependencies are checked — three of its four required engine-lib
20
+ * modules pull in Node's `fs`/`os`/`path` at module-load time (eagerly, on
21
+ * every `require()`, regardless of which code path runs):
22
+ * - gsd-core/bin/lib/state-io.cjs → requires 'node:fs' directly.
23
+ * - gsd-core/bin/lib/adapter-imperative.cjs → requires install-engine.cjs +
24
+ * capability-loader.cjs (fs/os/path).
25
+ * - gsd-core/bin/lib/model-adapter.cjs → requires model-resolver.cjs →
26
+ * config-loader.cjs (fs/os/path) +
27
+ * configuration.cjs (fs/path).
28
+ * (gsd-core/bin/lib/hook-bus.cjs alone has no requires and is genuinely
29
+ * web-safe.) Requiring `host-binding.js` here would transitively pull in
30
+ * `node:fs` and throw at web-worker load time — the opposite of "zero Node
31
+ * APIs". See host-binding.js's own header comment for the full chain. Fixing
32
+ * those engine-lib modules to be fs-free is a separate, much larger
33
+ * engine-wide refactor (config/capability loading genuinely reads files from
34
+ * disk for every OTHER host) — out of scope here; flagged rather than routed
35
+ * around silently.
36
+ *
37
+ * So this file implements its OWN minimal, independently-verified-zero-Node-API
38
+ * composition directly against `vscode.lm` — no engine-lib requires at all.
39
+ *
40
+ * DISPATCH STORY ON WEB (per the #2103 dispatch-crux design): full GSD engine
41
+ * dispatch (the gsd-tools.cjs subprocess-shim `dispatchGsdCommand` used by the
42
+ * desktop `extension.js`) is fundamentally a Node `child_process.spawnSync`
43
+ * call — there is no web-worker equivalent. On web, GSD command dispatch is
44
+ * available through VS Code's NATIVE MCP client connecting to the GSD
45
+ * companion MCP server (gsd-core/bin/lib/mcp-server.cjs, `gsd-mcp-server`
46
+ * bin entry — a separate, already-existing surface; this file does NOT
47
+ * implement an MCP client itself, it only points the user at that story).
48
+ * The chat participant and Language Model Tools registered below are
49
+ * therefore intentionally limited on web: they register (so the surface is
50
+ * discoverable and `#runSubagent`-eligible per VS Code's chat engine) but
51
+ * their handlers return an honest "configure the GSD MCP server for full
52
+ * dispatch on web" message rather than silently failing or faking success.
53
+ */
54
+
55
+ /**
56
+ * Tokenizes a raw chat/free-form prompt string. Kept local (not shared with
57
+ * extension.js) so this file has zero requires of any kind beyond `vscode`.
58
+ * @param {string} rawPrompt
59
+ * @returns {{family: string, subcommand: string|undefined, args: string[]}}
60
+ */
61
+ function parseChatPrompt(rawPrompt) {
62
+ const tokens = String(rawPrompt || '').trim().split(/\s+/).filter(Boolean);
63
+ return {
64
+ family: tokens[0] || '--help',
65
+ subcommand: tokens[1],
66
+ args: tokens.slice(2),
67
+ };
68
+ }
69
+
70
+ /**
71
+ * The honest "web mode" message every web-surface handler returns instead of
72
+ * attempting Node-only engine dispatch.
73
+ * @param {string} family
74
+ */
75
+ function webDispatchUnavailableMessage(family) {
76
+ return (
77
+ `GSD web mode: full engine dispatch for "${family}" is not available in the browser ` +
78
+ '(the VS Code Web/webworker host has no Node runtime, so the gsd-tools.cjs ' +
79
+ 'subprocess dispatch used on desktop cannot run here). Configure the GSD MCP ' +
80
+ 'server (gsd-mcp-server) as a VS Code MCP server for full command dispatch on web, ' +
81
+ 'or use the desktop GSD Core extension.'
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Registers the `@gsd` chat participant in web mode (#2103). Its handler is
87
+ * honest about the web dispatch limitation — see webDispatchUnavailableMessage.
88
+ * Exported separately so it is testable with a mock `vscode.chat`.
89
+ * @param {object} vscode
90
+ * @param {import('vscode').ExtensionContext} context
91
+ * @returns {object|null} the created participant, or null if vscode.chat is absent.
92
+ */
93
+ function registerChatParticipant(vscode, context) {
94
+ if (!vscode || !vscode.chat || typeof vscode.chat.createChatParticipant !== 'function') {
95
+ return null;
96
+ }
97
+ const participant = vscode.chat.createChatParticipant('gsd', async (request, _chatContext, stream, _token) => {
98
+ const { family } = parseChatPrompt(request && request.prompt);
99
+ if (stream && typeof stream.markdown === 'function') {
100
+ stream.markdown(webDispatchUnavailableMessage(family));
101
+ }
102
+ return { metadata: { command: family, mode: 'web' } };
103
+ });
104
+ if (context && Array.isArray(context.subscriptions)) context.subscriptions.push(participant);
105
+ return participant;
106
+ }
107
+
108
+ /**
109
+ * The same representative LM tool NAMES as the desktop extension (must match
110
+ * package.json's contributes.languageModelTools[].name — underscored, the
111
+ * vscode.lm.registerTool registration name — so the manifest is identical
112
+ * across both entry points), but with web-mode invoke() handlers.
113
+ */
114
+ const LM_TOOL_NAMES = ['gsd_progress', 'gsd_workstreams', 'gsd_plan_phase'];
115
+
116
+ /**
117
+ * Registers web-mode LM tools via vscode.lm.registerTool (#2103). Each
118
+ * invoke() returns the honest web-dispatch-unavailable message — no engine-lib
119
+ * requires, no Node APIs.
120
+ * @param {object} vscode
121
+ * @param {import('vscode').ExtensionContext} context
122
+ * @returns {number} count of tools registered (0 if vscode.lm is absent — fail-soft).
123
+ */
124
+ function registerLanguageModelTools(vscode, context) {
125
+ if (!vscode || !vscode.lm || typeof vscode.lm.registerTool !== 'function') return 0;
126
+ let count = 0;
127
+ for (const name of LM_TOOL_NAMES) {
128
+ const impl = {
129
+ async invoke(_options, _token) {
130
+ return new vscode.LanguageModelToolResult([
131
+ new vscode.LanguageModelTextPart(webDispatchUnavailableMessage(name)),
132
+ ]);
133
+ },
134
+ };
135
+ const disposable = vscode.lm.registerTool(name, impl);
136
+ if (context && Array.isArray(context.subscriptions)) context.subscriptions.push(disposable);
137
+ count++;
138
+ }
139
+ return count;
140
+ }
141
+
142
+ /**
143
+ * Detects `#runSubagent` feature availability (`chat.subagents.allowInvocationsFromSubagents`,
144
+ * VS Code 1.105+). Fail-soft: never throws. Identical detection logic to the
145
+ * desktop extension.js (duplicated, not shared, to keep this file at zero
146
+ * requires) — see extension.js's registerSubagentDispatch for the rationale
147
+ * that VS Code's chat engine itself surfaces registered participants/tools to
148
+ * `#runSubagent`, with no separate registration API.
149
+ * @param {object} vscode
150
+ * @returns {{available: boolean}}
151
+ */
152
+ function detectSubagentSupport(vscode) {
153
+ let available = false;
154
+ try {
155
+ const cfg = vscode && vscode.workspace && typeof vscode.workspace.getConfiguration === 'function'
156
+ ? vscode.workspace.getConfiguration('chat.subagents')
157
+ : null;
158
+ available = !!(cfg && typeof cfg.get === 'function' && cfg.get('allowInvocationsFromSubagents') !== undefined);
159
+ } catch {
160
+ available = false;
161
+ }
162
+ return { available };
163
+ }
164
+
165
+ /**
166
+ * VS Code Web extension activation. Registers the chat participant + Language
167
+ * Model Tools in web mode. Does NOT register the `gsd.invoke` command with a
168
+ * real-dispatch handler (there is no Node dispatch on web) — the command is
169
+ * still contributed (contributes.commands in package.json is shared across
170
+ * desktop/web), so it is registered here too, but its handler returns the
171
+ * same honest web-mode message.
172
+ * @param {import('vscode').ExtensionContext} context
173
+ */
174
+ function activate(context) {
175
+ const vscode = require('vscode');
176
+
177
+ const gsdCommand = vscode.commands.registerCommand('gsd.invoke', async (args) => {
178
+ const a = (args && typeof args === 'object') ? args : {};
179
+ const family = (typeof a.family === 'string' && a.family) ? a.family : '--help';
180
+ return JSON.stringify({ ok: false, stdout: '', stderr: webDispatchUnavailableMessage(family), code: null, timedOut: false });
181
+ });
182
+ context.subscriptions.push(gsdCommand);
183
+
184
+ registerChatParticipant(vscode, context);
185
+ registerLanguageModelTools(vscode, context);
186
+ detectSubagentSupport(vscode);
187
+ }
188
+
189
+ module.exports = {
190
+ activate,
191
+ parseChatPrompt,
192
+ webDispatchUnavailableMessage,
193
+ registerChatParticipant,
194
+ registerLanguageModelTools,
195
+ detectSubagentSupport,
196
+ LM_TOOL_NAMES,
197
+ };
@@ -0,0 +1,383 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * GSD extension for VS Code — ADR-1239 Phase D / #1942, dispatch fixed +
5
+ * extension surface (chat participant, Language Model Tools, #runSubagent
6
+ * wiring) added #2103.
7
+ *
8
+ * This is the DESKTOP (Node) `main` entry — see browser.js for the Web
9
+ * Extension `browser` entry, which is intentionally a SEPARATE, much more
10
+ * minimal file (zero Node APIs; does not require this file or host-binding.js).
11
+ *
12
+ * VS Code is the IDE-profile reference host. This extension binds GSD's command
13
+ * surface to VS Code's Command Palette + Chat participant via the imperative
14
+ * adapter path. Engine entry: in-process CJS require (the extension host runs
15
+ * Node). The engine seams (active model via vscode.lm, engine-owned hook bus,
16
+ * sandboxed-storage stateIO) are composed in activate() via host-binding.js
17
+ * per the #1933 binding.
18
+ *
19
+ * Installation: Marketplace/VSIX extension (see capabilities/vscode/capability.json
20
+ * — installSurface:'none', it is never CLI-installed by bin/install.js).
21
+ *
22
+ * Engine entry: dispatch is SUBPROCESS-REUSE to gsd-tools.cjs (bounded,
23
+ * no-throw — the shared `dispatchGsdCommand` in
24
+ * gsd-core/bin/lib/shell-command-projection.cjs), NOT an in-process
25
+ * command-routing hub. No fully-populated hub factory exists anywhere in
26
+ * gsd-core — every createHub() caller builds a single-family hub for its own
27
+ * narrow purpose — so calling createHub() with no args (the original #1942
28
+ * cut) always answered UnknownCommand. This mirrors the fix already applied
29
+ * to the pi extension (pi/gsd.cjs) and the companion MCP server
30
+ * (gsd-core/bin/lib/mcp-server.cjs), which dispatch through the SAME shared
31
+ * helper (#2102 Stage 2 / #2103).
32
+ *
33
+ * Extension surface (#2103):
34
+ * - Chat participant `@gsd` (contributes.chatParticipants) — dispatches free-form
35
+ * prompts through the same dispatchGsdCommand.
36
+ * - Language Model Tools (contributes.languageModelTools) — a representative
37
+ * set of GSD skills exposed as vscode.lm tools, each dispatching through the
38
+ * same shared helper (UPGRADE 1).
39
+ * - #runSubagent wiring — VS Code 1.105+ lets the primary chat agent invoke
40
+ * registered chat participants / languageModelTools as a nested agent turn
41
+ * via `#runSubagent`, gated by the `chat.subagents.allowInvocationsFromSubagents`
42
+ * setting. There is no separate "subagent contribution" registration API
43
+ * beyond the participant + tools already registered above — VS Code's own
44
+ * chat engine surfaces them. This extension's own contribution is a
45
+ * belt-and-suspenders depth cap (dispatchAsSubagent, GSD_MAX_SUBAGENT_DEPTH)
46
+ * mirroring capabilities/vscode/capability.json's
47
+ * hostIntegration.dispatch.maxDepth:5, independent of whatever VS Code
48
+ * itself enforces natively (UPGRADE 2).
49
+ */
50
+
51
+ const path = require('path');
52
+ const fs = require('fs');
53
+
54
+ // Resolve the GSD engine tree (walk up to find gsd-core/).
55
+ function resolveEngineRoot(startDir) {
56
+ let dir = startDir;
57
+ for (let i = 0; i < 6; i++) {
58
+ if (fs.existsSync(path.join(dir, 'gsd-core'))) return dir;
59
+ const parent = path.dirname(dir);
60
+ if (parent === dir) break;
61
+ dir = parent;
62
+ }
63
+ return path.resolve(startDir, '..');
64
+ }
65
+
66
+ const ENGINE_ROOT = resolveEngineRoot(__dirname);
67
+ const GSD_CORE = path.join(ENGINE_ROOT, 'gsd-core');
68
+
69
+ /**
70
+ * Pure command handler — dispatches through the SHARED subprocess-shim
71
+ * `dispatchGsdCommand` (gsd-core/bin/lib/shell-command-projection.cjs), the
72
+ * same helper the pi extension (pi/gsd.cjs) and the companion MCP server
73
+ * (gsd-core/bin/lib/mcp-server.cjs) dispatch through.
74
+ *
75
+ * Exported separately from activate() so it is testable WITHOUT a VS Code
76
+ * host. Preserves the original return CONTRACT — a JSON-stringified result
77
+ * object — but now backed by a REAL dispatch instead of an unconfigured
78
+ * `createHub()` that always answered UnknownCommand (#2103 fix).
79
+ *
80
+ * Empty/omitted args default to `--help` (a real, working, ok:true
81
+ * gsd-tools.cjs command) — NOT the `'query'`/`'help'` pairing the original
82
+ * cut used, which is not a valid gsd-tools.cjs command and always produced
83
+ * UnknownCommand (mirrors the same fix already applied to
84
+ * pi/gsd.cjs's parseGsdCommandArgs).
85
+ *
86
+ * @returns {Promise<string>} JSON-stringified dispatch result:
87
+ * `{ok, stdout, stderr, code, timedOut}` on a normal dispatch, or
88
+ * `{ok:false, stdout:'', stderr:'GSD engine unavailable: ...', code:null,
89
+ * timedOut:false}` if the shared helper itself cannot be loaded (e.g.
90
+ * gsd-core/ missing from the tree).
91
+ */
92
+ async function dispatchGsdCommand(args) {
93
+ const a = (args && typeof args === 'object') ? args : {};
94
+ const family = (typeof a.family === 'string' && a.family) ? a.family : '--help';
95
+ const subcommand = (typeof a.subcommand === 'string' && a.subcommand) ? a.subcommand : undefined;
96
+ const rest = Array.isArray(a.args) ? a.args : [];
97
+ const cwd = a.cwd || process.cwd();
98
+
99
+ let dispatchViaShim;
100
+ try {
101
+ ({ dispatchGsdCommand: dispatchViaShim } = require(path.join(GSD_CORE, 'bin', 'lib', 'shell-command-projection.cjs')));
102
+ } catch (e) {
103
+ return JSON.stringify({
104
+ ok: false,
105
+ stdout: '',
106
+ stderr: `GSD engine unavailable: ${e && e.message ? e.message : String(e)}`,
107
+ code: null,
108
+ timedOut: false,
109
+ });
110
+ }
111
+ const result = dispatchViaShim({ family, subcommand, args: rest, cwd });
112
+ return JSON.stringify(result);
113
+ }
114
+
115
+ /**
116
+ * Resolves the current workspace's root directory, per-invocation (never
117
+ * cached at activate() time), so GSD commands dispatch against the user's
118
+ * actual project instead of the extension host's own `process.cwd()` (#2103
119
+ * FIX — adversarial review: all three desktop dispatch surfaces previously
120
+ * omitted `cwd` entirely when calling dispatchGsdCommand, silently defaulting
121
+ * to the wrong directory). Falls back to `process.cwd()` only when no
122
+ * workspace folder is open (e.g. an empty window) — mirrors VS Code's own
123
+ * single-root convention of reading `workspaceFolders[0]`.
124
+ * @param {object} vscode
125
+ * @returns {string}
126
+ */
127
+ function resolveWorkspaceCwd(vscode) {
128
+ const folders = vscode && vscode.workspace && vscode.workspace.workspaceFolders;
129
+ const first = Array.isArray(folders) ? folders[0] : undefined;
130
+ const fsPath = first && first.uri && first.uri.fsPath;
131
+ return (typeof fsPath === 'string' && fsPath) ? fsPath : process.cwd();
132
+ }
133
+
134
+ /**
135
+ * Tokenizes a raw chat/free-form prompt string into {family, subcommand, args}.
136
+ * Mirrors pi/gsd.cjs's parseGsdCommandArgs (simple whitespace split — no shell
137
+ * quoting support needed for a chat prompt). Empty input defaults to `--help`
138
+ * (a real, working gsd-tools.cjs command), matching dispatchGsdCommand's own
139
+ * default so the two surfaces (palette vs. chat) never diverge on "no input".
140
+ * @param {string} rawPrompt
141
+ * @returns {{family: string, subcommand: string|undefined, args: string[]}}
142
+ */
143
+ function parseChatPrompt(rawPrompt) {
144
+ const tokens = String(rawPrompt || '').trim().split(/\s+/).filter(Boolean);
145
+ return {
146
+ family: tokens[0] || '--help',
147
+ subcommand: tokens[1],
148
+ args: tokens.slice(2),
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Registers the `@gsd` chat participant (#2103). Its handler dispatches the
154
+ * user's free-form prompt through the SAME dispatchGsdCommand as gsd.invoke —
155
+ * one dispatch path for every command surface (palette / chat / LM tools).
156
+ * Exported separately so it is testable with a mock `vscode.chat`.
157
+ * @param {object} vscode
158
+ * @param {import('vscode').ExtensionContext} context
159
+ * @returns {object|null} the created participant, or null if vscode.chat is absent
160
+ * (older VS Code — fail-soft, never throws).
161
+ */
162
+ function registerChatParticipant(vscode, context) {
163
+ if (!vscode || !vscode.chat || typeof vscode.chat.createChatParticipant !== 'function') {
164
+ return null;
165
+ }
166
+ const participant = vscode.chat.createChatParticipant('gsd', async (request, _chatContext, stream, _token) => {
167
+ // #2103 FIX: resolve the user's actual workspace, not the extension host's
168
+ // process.cwd() — computed per-invocation so it always reflects the
169
+ // CURRENT workspace (a chat request has no cwd field of its own; VS Code's
170
+ // ChatContext does not carry one).
171
+ const cwd = resolveWorkspaceCwd(vscode);
172
+ const { family, subcommand, args } = parseChatPrompt(request && request.prompt);
173
+ const result = JSON.parse(await dispatchGsdCommand({ family, subcommand, args, cwd }));
174
+ if (stream && typeof stream.markdown === 'function') {
175
+ stream.markdown(result.ok ? result.stdout : `GSD error: ${result.stderr || result.stdout || 'dispatch failed'}`);
176
+ }
177
+ return { metadata: { command: family } };
178
+ });
179
+ if (context && Array.isArray(context.subscriptions)) context.subscriptions.push(participant);
180
+ return participant;
181
+ }
182
+
183
+ /**
184
+ * #2103 UPGRADE 1 — Language Model Tools.
185
+ *
186
+ * A representative set of GSD skills (see skills/gsd-progress, skills/gsd-workstreams,
187
+ * skills/gsd-plan-phase) exposed as vscode.lm tools, matching the
188
+ * contributes.languageModelTools manifest entries in package.json. Each tool's
189
+ * invoke() dispatches through the SAME shared dispatchGsdCommand as gsd.invoke —
190
+ * the tool name maps to a real, verified family/subcommand pair (verified by
191
+ * direct `gsd-tools.cjs <family> [subcommand] --raw --json-errors` invocation;
192
+ * see the #2103 CORE-stage precedent for `progress json`).
193
+ *
194
+ * Kept to a small, curated set rather than all 71 shipped skills: these three
195
+ * both name a real skill AND map cleanly onto a single, safe, read-only
196
+ * gsd-tools.cjs command (the rest are multi-step agent workflows that do not
197
+ * reduce to one non-interactive CLI call, and stay slash-command-native).
198
+ */
199
+ // `name` uses underscores (vscode.lm.registerTool's registration name — MUST
200
+ // match contributes.languageModelTools[].name in package.json exactly); the
201
+ // hyphenated `#gsd-progress`-style mention alias is package.json's separate
202
+ // `toolReferenceName` field (VS Code owns that mapping internally).
203
+ const LM_TOOLS = [
204
+ {
205
+ name: 'gsd_progress',
206
+ // skills/gsd-progress — real, verified: `gsd-tools.cjs progress json`.
207
+ resolveCommand: () => ({ family: 'progress', subcommand: 'json', args: [] }),
208
+ },
209
+ {
210
+ name: 'gsd_workstreams',
211
+ // skills/gsd-workstreams — real, verified: `gsd-tools.cjs workstream list`.
212
+ resolveCommand: () => ({ family: 'workstream', subcommand: 'list', args: [] }),
213
+ },
214
+ {
215
+ name: 'gsd_plan_phase',
216
+ // skills/gsd-plan-phase — real, verified: `gsd-tools.cjs phase-plan-index <phase>`.
217
+ // (The full multi-step planning workflow stays slash-command-native; this
218
+ // tool exposes the read-only plan-index lookup the workflow itself queries first.)
219
+ resolveCommand: (input) => ({
220
+ family: 'phase-plan-index',
221
+ subcommand: undefined,
222
+ args: [input && input.phase ? String(input.phase) : ''],
223
+ }),
224
+ },
225
+ ];
226
+
227
+ /**
228
+ * Builds a vscode.lm tool implementation for one LM_TOOLS entry.
229
+ * @param {{name:string, resolveCommand:(input:object)=>{family:string,subcommand:string|undefined,args:string[]}}} toolDef
230
+ * @param {object} vscode
231
+ */
232
+ function createLanguageModelTool(toolDef, vscode) {
233
+ return {
234
+ async invoke(options, _token) {
235
+ const input = (options && options.input) || {};
236
+ // #2103 FIX: resolve the user's actual workspace, not the extension
237
+ // host's process.cwd() — computed per-invocation. LanguageModelToolInvocationOptions
238
+ // has no cwd field of its own.
239
+ const cwd = resolveWorkspaceCwd(vscode);
240
+ const { family, subcommand, args } = toolDef.resolveCommand(input);
241
+ const result = JSON.parse(await dispatchGsdCommand({ family, subcommand, args, cwd }));
242
+ const text = result.ok ? result.stdout : `GSD error: ${result.stderr || result.stdout || 'dispatch failed'}`;
243
+ return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(text)]);
244
+ },
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Registers the LM_TOOLS set via vscode.lm.registerTool (#2103 UPGRADE 1).
250
+ * Exported separately so it is testable with a mock `vscode.lm`.
251
+ * @param {object} vscode
252
+ * @param {import('vscode').ExtensionContext} context
253
+ * @returns {number} count of tools registered (0 if vscode.lm is absent — fail-soft).
254
+ */
255
+ function registerLanguageModelTools(vscode, context) {
256
+ if (!vscode || !vscode.lm || typeof vscode.lm.registerTool !== 'function') return 0;
257
+ let count = 0;
258
+ for (const toolDef of LM_TOOLS) {
259
+ const disposable = vscode.lm.registerTool(toolDef.name, createLanguageModelTool(toolDef, vscode));
260
+ if (context && Array.isArray(context.subscriptions)) context.subscriptions.push(disposable);
261
+ count++;
262
+ }
263
+ return count;
264
+ }
265
+
266
+ /** Mirrors capabilities/vscode/capability.json's hostIntegration.dispatch.maxDepth. */
267
+ const GSD_MAX_SUBAGENT_DEPTH = 5;
268
+
269
+ /**
270
+ * #2103 UPGRADE 2 — native subagent dispatch (#runSubagent).
271
+ *
272
+ * Dispatches a command as a (possibly nested) subagent turn, enforcing GSD's
273
+ * own maxDepth:5 ceiling (capabilities/vscode/capability.json) independent of
274
+ * whatever VS Code's chat engine enforces natively for `#runSubagent` /
275
+ * `chat.subagents.allowInvocationsFromSubagents` — belt-and-suspenders, never
276
+ * silently trusts the host's own depth accounting.
277
+ * @param {{family?:string, subcommand?:string, args?:string[], cwd?:string, depth?:number}} args
278
+ * @returns {Promise<string>} same JSON-stringified contract as dispatchGsdCommand.
279
+ */
280
+ async function dispatchAsSubagent(args) {
281
+ const a = (args && typeof args === 'object') ? args : {};
282
+ const depth = Number.isInteger(a.depth) ? a.depth : 0;
283
+ if (depth > GSD_MAX_SUBAGENT_DEPTH) {
284
+ return JSON.stringify({
285
+ ok: false,
286
+ stdout: '',
287
+ stderr: `GSD subagent dispatch refused: depth ${depth} exceeds maxDepth ${GSD_MAX_SUBAGENT_DEPTH}`,
288
+ code: null,
289
+ timedOut: false,
290
+ });
291
+ }
292
+ return dispatchGsdCommand(a);
293
+ }
294
+
295
+ /**
296
+ * Detects whether the host VS Code build exposes the `#runSubagent` feature
297
+ * surface (`chat.subagents.allowInvocationsFromSubagents`, VS Code 1.105+).
298
+ * Fail-soft: any missing/older API surface (including an Insiders-gated build
299
+ * where the setting does not exist yet) resolves `available:false` — never
300
+ * throws. There is no separate registration call: VS Code's chat engine
301
+ * surfaces the already-registered chat participant + languageModelTools to
302
+ * `#runSubagent` on its own; this function only reports/confirms availability.
303
+ * @param {object} vscode
304
+ * @returns {{available: boolean, dispatchAsSubagent: typeof dispatchAsSubagent, maxDepth: number}}
305
+ */
306
+ function registerSubagentDispatch(vscode) {
307
+ let available = false;
308
+ try {
309
+ const cfg = vscode && vscode.workspace && typeof vscode.workspace.getConfiguration === 'function'
310
+ ? vscode.workspace.getConfiguration('chat.subagents')
311
+ : null;
312
+ available = !!(cfg && typeof cfg.get === 'function' && cfg.get('allowInvocationsFromSubagents') !== undefined);
313
+ } catch {
314
+ available = false;
315
+ }
316
+ return { available, dispatchAsSubagent, maxDepth: GSD_MAX_SUBAGENT_DEPTH };
317
+ }
318
+
319
+ /**
320
+ * VS Code extension activation. Composes the IDE-profile seams (per the #1933
321
+ * reference binding) + registers the full command surface: palette
322
+ * (gsd.invoke), chat participant (@gsd), Language Model Tools, and the
323
+ * #runSubagent depth-cap wiring.
324
+ * @param {import('vscode').ExtensionContext} context
325
+ */
326
+ function activate(context) {
327
+ const vscode = require('vscode');
328
+
329
+ // ── Command surface: palette ──────────────────────────────────────────────
330
+ // #2103 FIX: wrap dispatchGsdCommand (rather than registering it directly as
331
+ // the handler) so a palette invocation that omits `cwd` resolves the user's
332
+ // actual workspace instead of silently defaulting to the extension host's
333
+ // own process.cwd(). An explicit `args.cwd` (e.g. from a programmatic
334
+ // vscode.commands.executeCommand('gsd.invoke', {..., cwd}) caller) still
335
+ // takes precedence.
336
+ const gsdCommand = vscode.commands.registerCommand('gsd.invoke', (args) => {
337
+ const a = (args && typeof args === 'object') ? args : {};
338
+ const cwd = a.cwd || resolveWorkspaceCwd(vscode);
339
+ return dispatchGsdCommand({ ...a, cwd });
340
+ });
341
+ context.subscriptions.push(gsdCommand);
342
+
343
+ // ── IDE-profile host binding (#1933 reference binding; desktop/Node only —
344
+ // see host-binding.js's header for why browser.js does NOT use this path).
345
+ // On a real desktop VS Code host this SUCCEEDS (#2103 fix: host-binding.js's
346
+ // guard checks vscode.lm.selectChatModels — the real API — not the
347
+ // nonexistent vscode.lm.sendRequest the pre-#2103 code assumed). Still
348
+ // wrapped fail-open so a genuinely older VS Code build (no vscode.lm at all)
349
+ // degrades to the palette command only, rather than blocking activation.
350
+ try {
351
+ const bindGsdToVscode = require('./host-binding.js');
352
+ const hostStorage = {
353
+ read: (key) => context.globalState.get(key),
354
+ write: (key, value) => context.globalState.update(key, value),
355
+ };
356
+ bindGsdToVscode(vscode, hostStorage);
357
+ } catch {
358
+ // fail-open — see doc comment above.
359
+ }
360
+
361
+ // ── Chat participant (@gsd) ───────────────────────────────────────────────
362
+ registerChatParticipant(vscode, context);
363
+
364
+ // ── Language Model Tools (#2103 UPGRADE 1) ────────────────────────────────
365
+ registerLanguageModelTools(vscode, context);
366
+
367
+ // ── #runSubagent wiring (#2103 UPGRADE 2) ─────────────────────────────────
368
+ registerSubagentDispatch(vscode);
369
+ }
370
+
371
+ module.exports = {
372
+ activate,
373
+ dispatchGsdCommand,
374
+ resolveEngineRoot,
375
+ resolveWorkspaceCwd,
376
+ parseChatPrompt,
377
+ registerChatParticipant,
378
+ registerLanguageModelTools,
379
+ registerSubagentDispatch,
380
+ dispatchAsSubagent,
381
+ LM_TOOLS,
382
+ GSD_MAX_SUBAGENT_DEPTH,
383
+ };