gsd-pi 2.67.0-dev.5399650 → 2.67.0-dev.fe39184

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 (99) hide show
  1. package/README.md +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +152 -70
  3. package/dist/resources/extensions/gsd/auto-start.js +12 -0
  4. package/dist/resources/extensions/gsd/commands/catalog.js +2 -1
  5. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -1
  6. package/dist/resources/extensions/gsd/commands-mcp-status.js +43 -7
  7. package/dist/resources/extensions/gsd/doctor-git-checks.js +4 -4
  8. package/dist/resources/extensions/gsd/doctor-proactive.js +3 -3
  9. package/dist/resources/extensions/gsd/init-wizard.js +15 -12
  10. package/dist/resources/extensions/gsd/mcp-project-config.js +83 -0
  11. package/dist/resources/extensions/gsd/workflow-mcp.js +64 -24
  12. package/dist/web/standalone/.next/BUILD_ID +1 -1
  13. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  14. package/dist/web/standalone/.next/build-manifest.json +3 -3
  15. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  16. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.html +1 -1
  34. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  41. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  42. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  43. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  44. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  45. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  46. package/dist/web/standalone/.next/static/chunks/6502.5dcdcf1e1432e20d.js +9 -0
  47. package/dist/web/standalone/.next/static/chunks/{webpack-b49b09f97429b5d0.js → webpack-42a66876b763aa26.js} +1 -1
  48. package/package.json +1 -1
  49. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  50. package/packages/mcp-server/dist/workflow-tools.js +10 -4
  51. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  52. package/packages/mcp-server/src/workflow-tools.ts +13 -2
  53. package/packages/pi-agent-core/dist/agent-loop.js +14 -6
  54. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  55. package/packages/pi-agent-core/src/agent-loop.test.ts +53 -0
  56. package/packages/pi-agent-core/src/agent-loop.ts +20 -6
  57. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts +2 -0
  58. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts.map +1 -0
  59. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +28 -0
  60. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -0
  61. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  62. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  63. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -12
  64. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  65. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  66. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +19 -0
  67. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  68. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  69. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +2 -12
  70. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  71. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +54 -0
  72. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -12
  73. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +21 -0
  74. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +2 -15
  75. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +190 -93
  76. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +89 -116
  77. package/src/resources/extensions/gsd/auto-start.ts +15 -1
  78. package/src/resources/extensions/gsd/commands/catalog.ts +2 -1
  79. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -1
  80. package/src/resources/extensions/gsd/commands-mcp-status.ts +53 -7
  81. package/src/resources/extensions/gsd/doctor-git-checks.ts +4 -4
  82. package/src/resources/extensions/gsd/doctor-proactive.ts +3 -3
  83. package/src/resources/extensions/gsd/init-wizard.ts +17 -11
  84. package/src/resources/extensions/gsd/mcp-project-config.ts +128 -0
  85. package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +2 -9
  86. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +0 -33
  87. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +85 -0
  88. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +15 -0
  89. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +178 -17
  90. package/src/resources/extensions/gsd/workflow-mcp.ts +76 -23
  91. package/dist/web/standalone/.next/static/chunks/6502.b804e48b7919f55e.js +0 -9
  92. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts +0 -13
  93. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts.map +0 -1
  94. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js +0 -27
  95. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js.map +0 -1
  96. package/packages/pi-coding-agent/src/modes/interactive/provider-auth-setup.ts +0 -40
  97. package/src/resources/extensions/gsd/tests/init-bootstrap-completeness.test.ts +0 -121
  98. /package/dist/web/standalone/.next/static/{6_QPFhgX0DQnDhhquheRc → gbSATDX4Jt2ufxzUr5nYm}/_buildManifest.js +0 -0
  99. /package/dist/web/standalone/.next/static/{6_QPFhgX0DQnDhhquheRc → gbSATDX4Jt2ufxzUr5nYm}/_ssgManifest.js +0 -0
@@ -4,15 +4,15 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "nod
4
4
  import { join, resolve } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import {
7
+ makeStreamExhaustedErrorMessage,
7
8
  buildPromptFromContext,
8
- buildFinalClaudeCodeContent,
9
9
  buildSdkOptions,
10
+ extractToolResultsFromSdkUserMessage,
10
11
  getClaudeLookupCommand,
11
- makeStreamExhaustedErrorMessage,
12
12
  parseClaudeLookupOutput,
13
- sanitizeClaudeCodeStreamingEvent,
14
13
  } from "../stream-adapter.ts";
15
- import type { AssistantMessage, Context, Message } from "@gsd/pi-ai";
14
+ import type { Context, Message } from "@gsd/pi-ai";
15
+ import type { SDKUserMessage } from "../sdk-types.ts";
16
16
 
17
17
  // ---------------------------------------------------------------------------
18
18
  // Existing tests — exhausted stream fallback (#2575)
@@ -108,6 +108,65 @@ describe("stream-adapter — full context prompt (#2859)", () => {
108
108
  });
109
109
  });
110
110
 
111
+ describe("stream-adapter — Claude Code external tool results", () => {
112
+ test("extractToolResultsFromSdkUserMessage maps tool_result content to tool payloads", () => {
113
+ const message: SDKUserMessage = {
114
+ type: "user",
115
+ session_id: "sess-1",
116
+ parent_tool_use_id: "tool-bash-1",
117
+ message: {
118
+ role: "user",
119
+ content: [
120
+ {
121
+ type: "tool_result",
122
+ tool_use_id: "tool-bash-1",
123
+ content: "line 1\nline 2",
124
+ is_error: false,
125
+ },
126
+ ],
127
+ },
128
+ };
129
+
130
+ const results = extractToolResultsFromSdkUserMessage(message);
131
+ assert.deepEqual(results, [
132
+ {
133
+ toolUseId: "tool-bash-1",
134
+ result: {
135
+ content: [{ type: "text", text: "line 1\nline 2" }],
136
+ details: {},
137
+ isError: false,
138
+ },
139
+ },
140
+ ]);
141
+ });
142
+
143
+ test("extractToolResultsFromSdkUserMessage falls back to tool_use_result", () => {
144
+ const message: SDKUserMessage = {
145
+ type: "user",
146
+ session_id: "sess-1",
147
+ parent_tool_use_id: "tool-read-1",
148
+ message: { role: "user", content: [] },
149
+ tool_use_result: {
150
+ tool_use_id: "tool-read-1",
151
+ content: "file contents",
152
+ is_error: true,
153
+ },
154
+ };
155
+
156
+ const results = extractToolResultsFromSdkUserMessage(message);
157
+ assert.deepEqual(results, [
158
+ {
159
+ toolUseId: "tool-read-1",
160
+ result: {
161
+ content: [{ type: "text", text: "file contents" }],
162
+ details: {},
163
+ isError: true,
164
+ },
165
+ },
166
+ ]);
167
+ });
168
+ });
169
+
111
170
  describe("stream-adapter — session persistence (#2859)", () => {
112
171
  test("buildSdkOptions enables persistSession by default", () => {
113
172
  const options = buildSdkOptions("claude-sonnet-4-20250514", "test prompt");
@@ -149,18 +208,15 @@ describe("stream-adapter — session persistence (#2859)", () => {
149
208
  process.env.GSD_WORKFLOW_MCP_CWD = "/tmp/project";
150
209
 
151
210
  const options = buildSdkOptions("claude-sonnet-4-20250514", "test");
152
- assert.deepEqual(options.mcpServers, {
153
- "gsd-workflow": {
154
- command: "node",
155
- args: ["packages/mcp-server/dist/cli.js"],
156
- env: {
157
- GSD_CLI_PATH: "/tmp/gsd",
158
- GSD_PERSIST_WRITE_GATE_STATE: "1",
159
- GSD_WORKFLOW_PROJECT_ROOT: "/tmp/project",
160
- },
161
- cwd: "/tmp/project",
162
- },
163
- });
211
+ const mcpServers = options.mcpServers as Record<string, any>;
212
+ assert.ok(mcpServers?.["gsd-workflow"], "expected gsd-workflow server config");
213
+ const srv = mcpServers["gsd-workflow"];
214
+ assert.equal(srv.command, "node");
215
+ assert.deepEqual(srv.args, ["packages/mcp-server/dist/cli.js"]);
216
+ assert.equal(srv.cwd, "/tmp/project");
217
+ assert.equal(srv.env.GSD_CLI_PATH, "/tmp/gsd");
218
+ assert.equal(srv.env.GSD_PERSIST_WRITE_GATE_STATE, "1");
219
+ assert.equal(srv.env.GSD_WORKFLOW_PROJECT_ROOT, "/tmp/project");
164
220
  } finally {
165
221
  process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
166
222
  process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
@@ -170,7 +226,7 @@ describe("stream-adapter — session persistence (#2859)", () => {
170
226
  }
171
227
  });
172
228
 
173
- test("buildSdkOptions omits workflow MCP server config when env is unset", () => {
229
+ test("buildSdkOptions auto-discovers bundled MCP server even without env hints", () => {
174
230
  const prev = {
175
231
  GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
176
232
  GSD_WORKFLOW_MCP_NAME: process.env.GSD_WORKFLOW_MCP_NAME,
@@ -190,7 +246,13 @@ describe("stream-adapter — session persistence (#2859)", () => {
190
246
  process.chdir(emptyDir);
191
247
  const options = buildSdkOptions("claude-sonnet-4-20250514", "test");
192
248
  process.chdir(originalCwd);
193
- assert.equal((options as any).mcpServers, undefined);
249
+ // The bundled CLI may or may not be discoverable depending on
250
+ // whether the build output exists relative to import.meta.url.
251
+ // Either outcome is valid — the key invariant is no crash.
252
+ const mcpServers = (options as any).mcpServers;
253
+ if (mcpServers) {
254
+ assert.ok(mcpServers["gsd-workflow"], "if present, must be gsd-workflow");
255
+ }
194
256
  rmSync(emptyDir, { recursive: true, force: true });
195
257
  } finally {
196
258
  process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
@@ -227,18 +289,15 @@ describe("stream-adapter — session persistence (#2859)", () => {
227
289
  const resolvedRepoDir = realpathSync(repoDir);
228
290
 
229
291
  const options = buildSdkOptions("claude-sonnet-4-20250514", "test");
230
- assert.deepEqual(options.mcpServers, {
231
- "gsd-workflow": {
232
- command: process.execPath,
233
- args: [realpathSync(resolve(repoDir, "packages", "mcp-server", "dist", "cli.js"))],
234
- env: {
235
- GSD_CLI_PATH: "/tmp/gsd",
236
- GSD_PERSIST_WRITE_GATE_STATE: "1",
237
- GSD_WORKFLOW_PROJECT_ROOT: resolvedRepoDir,
238
- },
239
- cwd: resolvedRepoDir,
240
- },
241
- });
292
+ const mcpServers = options.mcpServers as Record<string, any>;
293
+ assert.ok(mcpServers?.["gsd-workflow"], "expected gsd-workflow server config");
294
+ const srv = mcpServers["gsd-workflow"];
295
+ assert.equal(srv.command, process.execPath);
296
+ assert.deepEqual(srv.args, [realpathSync(resolve(repoDir, "packages", "mcp-server", "dist", "cli.js"))]);
297
+ assert.equal(srv.cwd, resolvedRepoDir);
298
+ assert.equal(srv.env.GSD_CLI_PATH, "/tmp/gsd");
299
+ assert.equal(srv.env.GSD_PERSIST_WRITE_GATE_STATE, "1");
300
+ assert.equal(srv.env.GSD_WORKFLOW_PROJECT_ROOT, resolvedRepoDir);
242
301
  } finally {
243
302
  process.chdir(originalCwd);
244
303
  rmSync(repoDir, { recursive: true, force: true });
@@ -252,92 +311,6 @@ describe("stream-adapter — session persistence (#2859)", () => {
252
311
  });
253
312
  });
254
313
 
255
- describe("stream-adapter — final content filtering (#3861)", () => {
256
- test("buildFinalClaudeCodeContent strips intermediate tool calls from the final assistant message", () => {
257
- const finalContent = buildFinalClaudeCodeContent(
258
- [
259
- { type: "toolCall", id: "tc_1", name: "Read", arguments: {} },
260
- { type: "thinking", thinking: "Planning next step" },
261
- { type: "text", text: "Done." },
262
- ] as any,
263
- "",
264
- "",
265
- );
266
-
267
- assert.deepEqual(finalContent, [
268
- { type: "thinking", thinking: "Planning next step" },
269
- { type: "text", text: "Done." },
270
- ]);
271
- });
272
-
273
- test("buildFinalClaudeCodeContent falls back to cached text when the final turn only had tool calls", () => {
274
- const finalContent = buildFinalClaudeCodeContent(
275
- [
276
- { type: "toolCall", id: "tc_2", name: "Edit", arguments: { file_path: "app.ts" } },
277
- ] as any,
278
- "",
279
- "User-facing answer",
280
- );
281
-
282
- assert.deepEqual(finalContent, [{ type: "text", text: "User-facing answer" }]);
283
- });
284
- });
285
-
286
- describe("stream-adapter — streaming content filtering follow-up (#3867)", () => {
287
- function makePartial(content: AssistantMessage["content"]): AssistantMessage {
288
- return {
289
- role: "assistant",
290
- content,
291
- api: "anthropic-messages",
292
- provider: "claude-code",
293
- model: "claude-sonnet-4-20250514",
294
- usage: {
295
- input: 0,
296
- output: 0,
297
- cacheRead: 0,
298
- cacheWrite: 0,
299
- totalTokens: 0,
300
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
301
- },
302
- stopReason: "stop",
303
- timestamp: Date.now(),
304
- };
305
- }
306
-
307
- test("sanitizeClaudeCodeStreamingEvent strips tool calls from streamed partials and remaps contentIndex", () => {
308
- const event = sanitizeClaudeCodeStreamingEvent({
309
- type: "text_delta",
310
- contentIndex: 2,
311
- delta: "Done.",
312
- partial: makePartial([
313
- { type: "toolCall", id: "tc_1", name: "ToolSearch", arguments: {} },
314
- { type: "thinking", thinking: "Planning next step" },
315
- { type: "text", text: "Done." },
316
- ] as any),
317
- });
318
-
319
- assert.ok(event, "text events should still be forwarded");
320
- assert.equal(event!.type, "text_delta");
321
- assert.equal((event! as any).contentIndex, 1);
322
- assert.deepEqual((event! as any).partial.content, [
323
- { type: "thinking", thinking: "Planning next step" },
324
- { type: "text", text: "Done." },
325
- ]);
326
- });
327
-
328
- test("sanitizeClaudeCodeStreamingEvent suppresses internal tool streaming events entirely", () => {
329
- const event = sanitizeClaudeCodeStreamingEvent({
330
- type: "toolcall_start",
331
- contentIndex: 0,
332
- partial: makePartial([
333
- { type: "toolCall", id: "tc_1", name: "Bash", arguments: {} },
334
- ] as any),
335
- });
336
-
337
- assert.equal(event, null);
338
- });
339
- });
340
-
341
314
  describe("stream-adapter — Windows Claude path lookup (#3770)", () => {
342
315
  test("getClaudeLookupCommand uses where on Windows", () => {
343
316
  assert.equal(getClaudeLookupCommand("win32"), "where claude");
@@ -340,6 +340,21 @@ export async function bootstrapAutoSession(
340
340
  }
341
341
  }
342
342
 
343
+ if (ctx.model?.provider === "claude-code") {
344
+ try {
345
+ const { ensureProjectWorkflowMcpConfig } = await import("./mcp-project-config.js");
346
+ const result = ensureProjectWorkflowMcpConfig(base);
347
+ if (result.status !== "unchanged") {
348
+ ctx.ui.notify(`Claude Code MCP prepared at ${result.configPath}`, "info");
349
+ }
350
+ } catch (err) {
351
+ ctx.ui.notify(
352
+ `Claude Code MCP prep failed: ${err instanceof Error ? err.message : String(err)}`,
353
+ "warning",
354
+ );
355
+ }
356
+ }
357
+
343
358
  // Initialize GitServiceImpl
344
359
  s.gitService = new GitServiceImpl(
345
360
  s.basePath,
@@ -909,4 +924,3 @@ export async function bootstrapAutoSession(
909
924
  throw err;
910
925
  }
911
926
  }
912
-
@@ -70,7 +70,7 @@ export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
70
70
  { cmd: "templates", desc: "List available workflow templates" },
71
71
  { cmd: "extensions", desc: "Manage extensions (list, enable, disable, info)" },
72
72
  { cmd: "fast", desc: "Toggle OpenAI service tier (on/off/flex/status)" },
73
- { cmd: "mcp", desc: "MCP server status and connectivity check (status, check <server>)" },
73
+ { cmd: "mcp", desc: "MCP server status, connectivity, and local config bootstrap (status, check, init)" },
74
74
  { cmd: "rethink", desc: "Conversational project reorganization — reorder, park, discard, add milestones" },
75
75
  { cmd: "workflow", desc: "Custom workflow lifecycle (new, run, list, validate, pause, resume)" },
76
76
  { cmd: "codebase", desc: "Generate, refresh, and inspect the codebase map cache (.gsd/CODEBASE.md)" },
@@ -201,6 +201,7 @@ const NESTED_COMPLETIONS: CompletionMap = {
201
201
  mcp: [
202
202
  { cmd: "status", desc: "Show all MCP server statuses (default)" },
203
203
  { cmd: "check", desc: "Detailed status for a specific server" },
204
+ { cmd: "init", desc: "Write .mcp.json for the local GSD workflow MCP server" },
204
205
  ],
205
206
  doctor: [
206
207
  { cmd: "fix", desc: "Auto-fix detected issues" },
@@ -60,7 +60,7 @@ export function showHelp(ctx: ExtensionCommandContext): void {
60
60
  " /gsd hooks Show post-unit hook configuration",
61
61
  " /gsd extensions Manage extensions [list|enable|disable|info]",
62
62
  " /gsd fast Toggle OpenAI service tier [on|off|flex|status]",
63
- " /gsd mcp MCP server status and connectivity [status|check <server>]",
63
+ " /gsd mcp MCP server status and connectivity [status|check <server>|init [dir]]",
64
64
  "",
65
65
  "MAINTENANCE",
66
66
  " /gsd doctor Diagnose and repair .gsd/ state [audit|fix|heal] [scope]",
@@ -7,12 +7,15 @@
7
7
  * /gsd mcp — Overview of all servers (alias: /gsd mcp status)
8
8
  * /gsd mcp status — Same as bare /gsd mcp
9
9
  * /gsd mcp check <srv> — Detailed status for a specific server
10
+ * /gsd mcp init [dir] — Write project-local GSD workflow MCP config
10
11
  */
11
12
 
12
13
  import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
13
14
 
14
15
  import { existsSync, readFileSync } from "node:fs";
15
- import { join } from "node:path";
16
+ import { join, resolve } from "node:path";
17
+
18
+ import { ensureProjectWorkflowMcpConfig } from "./mcp-project-config.js";
16
19
 
17
20
  // ─── Types ──────────────────────────────────────────────────────────────────
18
21
 
@@ -28,6 +31,28 @@ export interface McpServerDetail extends McpServerStatus {
28
31
  tools: string[];
29
32
  }
30
33
 
34
+ export function formatMcpInitResult(
35
+ status: "created" | "updated" | "unchanged",
36
+ configPath: string,
37
+ targetPath: string,
38
+ ): string {
39
+ const summary =
40
+ status === "created"
41
+ ? "Created project MCP config."
42
+ : status === "updated"
43
+ ? "Updated project MCP config."
44
+ : "Project MCP config is already up to date.";
45
+
46
+ return [
47
+ summary,
48
+ "",
49
+ `Project: ${targetPath}`,
50
+ `Config: ${configPath}`,
51
+ "",
52
+ "Claude Code can now load the GSD workflow MCP server from this folder.",
53
+ ].join("\n");
54
+ }
55
+
31
56
  // ─── Config reader (standalone — does not import mcp-client internals) ──────
32
57
 
33
58
  interface McpServerRawConfig {
@@ -94,6 +119,7 @@ export function formatMcpStatusReport(servers: McpServerStatus[]): string {
94
119
  "No MCP servers configured.",
95
120
  "",
96
121
  "Add servers to .mcp.json or .gsd/mcp.json to enable MCP integrations.",
122
+ "Tip: run /gsd mcp init . to write the local GSD workflow MCP config.",
97
123
  "See: https://modelcontextprotocol.io/quickstart",
98
124
  ].join("\n");
99
125
  }
@@ -153,12 +179,31 @@ export async function handleMcpStatus(
153
179
  args: string,
154
180
  ctx: ExtensionCommandContext,
155
181
  ): Promise<void> {
156
- const trimmed = args.trim().toLowerCase();
182
+ const trimmed = args.trim();
183
+ const lowered = trimmed.toLowerCase();
157
184
  const configs = readMcpConfigs();
158
185
 
186
+ // /gsd mcp init [dir]
187
+ if (!lowered || lowered === "status") {
188
+ // handled below
189
+ } else if (lowered === "init" || lowered.startsWith("init ")) {
190
+ const rawPath = trimmed.slice("init".length).trim();
191
+ const targetPath = resolve(rawPath || ".");
192
+ try {
193
+ const result = ensureProjectWorkflowMcpConfig(targetPath);
194
+ ctx.ui.notify(formatMcpInitResult(result.status, result.configPath, targetPath), "info");
195
+ } catch (err) {
196
+ ctx.ui.notify(
197
+ `Failed to prepare MCP config for ${targetPath}: ${err instanceof Error ? err.message : String(err)}`,
198
+ "error",
199
+ );
200
+ }
201
+ return;
202
+ }
203
+
159
204
  // /gsd mcp check <server>
160
- if (trimmed.startsWith("check ")) {
161
- const serverName = args.trim().slice("check ".length).trim();
205
+ if (lowered.startsWith("check ")) {
206
+ const serverName = trimmed.slice("check ".length).trim();
162
207
  const config = configs.find((c) => c.name === serverName);
163
208
  if (!config) {
164
209
  const available = configs.map((c) => c.name).join(", ") || "(none)";
@@ -202,7 +247,7 @@ export async function handleMcpStatus(
202
247
  }
203
248
 
204
249
  // /gsd mcp or /gsd mcp status
205
- if (!trimmed || trimmed === "status") {
250
+ if (!lowered || lowered === "status") {
206
251
  // Build status for each server
207
252
  const statuses: McpServerStatus[] = [];
208
253
 
@@ -239,9 +284,10 @@ export async function handleMcpStatus(
239
284
 
240
285
  // Unknown subcommand
241
286
  ctx.ui.notify(
242
- "Usage: /gsd mcp [status|check <server>]\n\n" +
287
+ "Usage: /gsd mcp [status|check <server>|init [dir]]\n\n" +
243
288
  " status Show all MCP server statuses (default)\n" +
244
- " check <server> Detailed status for a specific server",
289
+ " check <server> Detailed status for a specific server\n" +
290
+ " init [dir] Write .mcp.json for the local GSD workflow MCP server",
245
291
  "warning",
246
292
  );
247
293
  }
@@ -10,7 +10,7 @@ import { deriveState, isMilestoneComplete } from "./state.js";
10
10
  import { listWorktrees, resolveGitDir, worktreesDir } from "./worktree-manager.js";
11
11
  import { abortAndReset } from "./git-self-heal.js";
12
12
  import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch, writeIntegrationBranch } from "./git-service.js";
13
- import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddAllWithExclusions, nativeCommit } from "./native-git-bridge.js";
13
+ import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
14
14
  import { getAllWorktreeHealth } from "./worktree-health.js";
15
15
  import { loadEffectiveGSDPreferences } from "./preferences.js";
16
16
 
@@ -386,19 +386,19 @@ export async function checkGitHealth(
386
386
  code: "stale_uncommitted_changes",
387
387
  scope: "project",
388
388
  unitId: "project",
389
- message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting uncommitted changes.`,
389
+ message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting tracked files.`,
390
390
  fixable: true,
391
391
  });
392
392
 
393
393
  if (shouldFix("stale_uncommitted_changes")) {
394
394
  try {
395
- nativeAddAllWithExclusions(basePath, RUNTIME_EXCLUSION_PATHS);
395
+ nativeAddTracked(basePath);
396
396
  const commitMsg = `gsd snapshot: uncommitted changes after ${mins}m inactivity`;
397
397
  const result = nativeCommit(basePath, commitMsg);
398
398
  if (result) {
399
399
  fixesApplied.push(`created gsd snapshot after ${mins}m of uncommitted changes`);
400
400
  } else {
401
- fixesApplied.push("gsd snapshot skipped — nothing to commit after staging changes");
401
+ fixesApplied.push("gsd snapshot skipped — nothing to commit after staging tracked files");
402
402
  }
403
403
  } catch {
404
404
  fixesApplied.push("failed to create gsd snapshot commit");
@@ -21,8 +21,8 @@ import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.j
21
21
  import { abortAndReset } from "./git-self-heal.js";
22
22
  import { rebuildState } from "./doctor.js";
23
23
  import { deriveState } from "./state.js";
24
- import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch } from "./git-service.js";
25
- import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddAllWithExclusions, nativeCommit } from "./native-git-bridge.js";
24
+ import { resolveMilestoneIntegrationBranch } from "./git-service.js";
25
+ import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
26
26
  import { loadEffectiveGSDPreferences } from "./preferences.js";
27
27
  import { runEnvironmentChecks } from "./doctor-environment.js";
28
28
 
@@ -312,7 +312,7 @@ export async function preDispatchHealthGate(basePath: string): Promise<PreDispat
312
312
  if (minutesSinceCommit >= thresholdMinutes) {
313
313
  const mins = Math.floor(minutesSinceCommit);
314
314
  try {
315
- nativeAddAllWithExclusions(basePath, RUNTIME_EXCLUSION_PATHS);
315
+ nativeAddTracked(basePath);
316
316
  const commitMsg = `gsd snapshot: pre-dispatch, uncommitted changes after ${mins}m inactivity`;
317
317
  const result = nativeCommit(basePath, commitMsg);
318
318
  if (result) {
@@ -238,15 +238,11 @@ export async function showProjectInit(
238
238
  // Initialize SQLite database so GSD starts in full-capability mode (#3880).
239
239
  // Without this, isDbAvailable() returns false and GSD enters degraded
240
240
  // markdown-only mode until a tool handler happens to call ensureDbOpen().
241
- let dbReady = false;
242
241
  try {
243
242
  const { ensureDbOpen } = await import("./bootstrap/dynamic-tools.js");
244
- dbReady = await ensureDbOpen(basePath);
243
+ await ensureDbOpen(basePath);
245
244
  } catch {
246
- // Swallowedwarning surfaced below
247
- }
248
- if (!dbReady) {
249
- ctx.ui.notify("Warning: database initialization failed — GSD will run in degraded mode until the next /gsd invocation.", "warning");
245
+ // Non-fatalDB creation failure should not block project init
250
246
  }
251
247
 
252
248
  // Ensure .gitignore
@@ -267,7 +263,6 @@ export async function showProjectInit(
267
263
  // Write initial STATE.md so it exists before the first /gsd invocation.
268
264
  // The explicit /gsd init path (ops.ts) returns without entering showSmartEntry(),
269
265
  // which would otherwise generate STATE.md at guided-flow.ts:1358.
270
- let stateReady = false;
271
266
  try {
272
267
  const { deriveState } = await import("./state.js");
273
268
  const { buildStateMarkdown } = await import("./doctor.js");
@@ -275,12 +270,23 @@ export async function showProjectInit(
275
270
  const { resolveGsdRootFile } = await import("./paths.js");
276
271
  const state = await deriveState(basePath);
277
272
  await saveFile(resolveGsdRootFile(basePath, "STATE"), buildStateMarkdown(state));
278
- stateReady = true;
279
273
  } catch {
280
- // Swallowedwarning surfaced below
274
+ // Non-fatalSTATE.md will be regenerated on next /gsd invocation
281
275
  }
282
- if (!stateReady) {
283
- ctx.ui.notify("Warning: initial STATE.md generation failed — it will be created on the next /gsd invocation.", "warning");
276
+
277
+ if (ctx.model?.provider === "claude-code") {
278
+ try {
279
+ const { ensureProjectWorkflowMcpConfig } = await import("./mcp-project-config.js");
280
+ const result = ensureProjectWorkflowMcpConfig(basePath);
281
+ if (result.status !== "unchanged") {
282
+ ctx.ui.notify(`Claude Code MCP prepared at ${result.configPath}`, "info");
283
+ }
284
+ } catch (err) {
285
+ ctx.ui.notify(
286
+ `Claude Code MCP prep failed: ${err instanceof Error ? err.message : String(err)}`,
287
+ "warning",
288
+ );
289
+ }
284
290
  }
285
291
 
286
292
  ctx.ui.notify("GSD initialized. Starting your first milestone...", "info");
@@ -0,0 +1,128 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { assertSafeDirectory } from "./validate-directory.js";
6
+ import { detectWorkflowMcpLaunchConfig } from "./workflow-mcp.js";
7
+
8
+ export const GSD_WORKFLOW_MCP_SERVER_NAME = "gsd-workflow";
9
+
10
+ export interface ProjectMcpServerConfig {
11
+ command?: string;
12
+ args?: string[];
13
+ cwd?: string;
14
+ env?: Record<string, string>;
15
+ url?: string;
16
+ }
17
+
18
+ export interface EnsureProjectWorkflowMcpConfigResult {
19
+ configPath: string;
20
+ serverName: string;
21
+ status: "created" | "updated" | "unchanged";
22
+ }
23
+
24
+ interface McpConfigFile {
25
+ mcpServers?: Record<string, ProjectMcpServerConfig>;
26
+ servers?: Record<string, ProjectMcpServerConfig>;
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ export function resolveBundledGsdCliPath(env: NodeJS.ProcessEnv = process.env): string | null {
31
+ const explicit = env.GSD_CLI_PATH?.trim() || env.GSD_BIN_PATH?.trim();
32
+ if (explicit) return explicit;
33
+
34
+ const candidates = [
35
+ resolve(fileURLToPath(new URL("../../../../scripts/dev-cli.js", import.meta.url))),
36
+ resolve(fileURLToPath(new URL("../../../../dist/loader.js", import.meta.url))),
37
+ resolve(fileURLToPath(new URL("../../../loader.js", import.meta.url))),
38
+ ];
39
+
40
+ for (const candidate of candidates) {
41
+ if (existsSync(candidate)) return candidate;
42
+ }
43
+
44
+ return null;
45
+ }
46
+
47
+ export function buildProjectWorkflowMcpServerConfig(
48
+ projectRoot: string,
49
+ env: NodeJS.ProcessEnv = process.env,
50
+ ): ProjectMcpServerConfig {
51
+ const resolvedProjectRoot = resolve(projectRoot);
52
+ const gsdCliPath = resolveBundledGsdCliPath(env);
53
+ const launch = detectWorkflowMcpLaunchConfig(resolvedProjectRoot, {
54
+ ...env,
55
+ ...(gsdCliPath ? { GSD_CLI_PATH: gsdCliPath, GSD_BIN_PATH: gsdCliPath } : {}),
56
+ });
57
+
58
+ if (!launch) {
59
+ throw new Error(
60
+ "Unable to resolve the GSD workflow MCP server. Build this checkout or install gsd-mcp-server on PATH.",
61
+ );
62
+ }
63
+
64
+ return {
65
+ command: launch.command,
66
+ ...(launch.args && launch.args.length > 0 ? { args: launch.args } : {}),
67
+ ...(launch.cwd ? { cwd: launch.cwd } : {}),
68
+ ...(launch.env ? { env: launch.env } : {}),
69
+ };
70
+ }
71
+
72
+ function readExistingConfig(configPath: string): McpConfigFile {
73
+ if (!existsSync(configPath)) return {};
74
+
75
+ const raw = readFileSync(configPath, "utf-8");
76
+ try {
77
+ const parsed = JSON.parse(raw) as McpConfigFile;
78
+ return parsed && typeof parsed === "object" ? parsed : {};
79
+ } catch (err) {
80
+ throw new Error(
81
+ `Failed to parse ${configPath}: ${err instanceof Error ? err.message : String(err)}`,
82
+ );
83
+ }
84
+ }
85
+
86
+ export function ensureProjectWorkflowMcpConfig(
87
+ projectRoot: string,
88
+ env: NodeJS.ProcessEnv = process.env,
89
+ ): EnsureProjectWorkflowMcpConfigResult {
90
+ const resolvedProjectRoot = resolve(projectRoot);
91
+ assertSafeDirectory(resolvedProjectRoot);
92
+
93
+ const configPath = resolve(resolvedProjectRoot, ".mcp.json");
94
+ const existing = readExistingConfig(configPath);
95
+ const desiredServer = buildProjectWorkflowMcpServerConfig(resolvedProjectRoot, env);
96
+ const previousServers = existing.mcpServers ?? {};
97
+ const nextServers = {
98
+ ...previousServers,
99
+ [GSD_WORKFLOW_MCP_SERVER_NAME]: desiredServer,
100
+ };
101
+
102
+ const alreadyPresent = existsSync(configPath);
103
+ const unchanged =
104
+ JSON.stringify(previousServers[GSD_WORKFLOW_MCP_SERVER_NAME] ?? null)
105
+ === JSON.stringify(desiredServer)
106
+ && existing.mcpServers !== undefined;
107
+
108
+ if (unchanged) {
109
+ return {
110
+ configPath,
111
+ serverName: GSD_WORKFLOW_MCP_SERVER_NAME,
112
+ status: "unchanged",
113
+ };
114
+ }
115
+
116
+ const nextConfig: McpConfigFile = {
117
+ ...existing,
118
+ mcpServers: nextServers,
119
+ };
120
+
121
+ writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf-8");
122
+
123
+ return {
124
+ configPath,
125
+ serverName: GSD_WORKFLOW_MCP_SERVER_NAME,
126
+ status: alreadyPresent ? "updated" : "created",
127
+ };
128
+ }