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
package/README.md CHANGED
@@ -95,7 +95,7 @@ See the full [Changelog](./CHANGELOG.md) for details on every release.
95
95
 
96
96
  ## Documentation
97
97
 
98
- Full documentation is available in the [`docs/`](./docs/) directory:
98
+ Full documentation is available at **[gsd.build](https://gsd.build)** (powered by Mintlify) and in the [`docs/`](./docs/) directory:
99
99
 
100
100
  - **[Getting Started](./docs/getting-started.md)** — install, first run, basic usage
101
101
  - **[Auto Mode](./docs/auto-mode.md)** — autonomous execution deep-dive
@@ -124,70 +124,6 @@ export function makeStreamExhaustedErrorMessage(model, lastTextContent) {
124
124
  }
125
125
  return message;
126
126
  }
127
- /**
128
- * Claude Code executes its own internal tool loop inside the SDK call. The
129
- * streamed and final assistant messages should therefore contain only
130
- * user-facing content (text/thinking), not replayable tool blocks that GSD
131
- * would render again.
132
- */
133
- function isUserFacingClaudeCodeBlock(block) {
134
- return block.type === "text" || block.type === "thinking";
135
- }
136
- function filterUserFacingClaudeCodeContent(blocks) {
137
- return blocks.filter(isUserFacingClaudeCodeBlock);
138
- }
139
- function remapClaudeCodeContentIndex(blocks, contentIndex) {
140
- let visibleCount = 0;
141
- for (let i = 0; i <= contentIndex && i < blocks.length; i++) {
142
- if (isUserFacingClaudeCodeBlock(blocks[i]))
143
- visibleCount++;
144
- }
145
- return Math.max(0, visibleCount - 1);
146
- }
147
- function sanitizeClaudeCodePartial(partial) {
148
- return {
149
- ...partial,
150
- content: filterUserFacingClaudeCodeContent(partial.content),
151
- };
152
- }
153
- export function sanitizeClaudeCodeStreamingEvent(event) {
154
- switch (event.type) {
155
- case "toolcall_start":
156
- case "toolcall_delta":
157
- case "toolcall_end":
158
- case "server_tool_use":
159
- case "web_search_result":
160
- return null;
161
- case "text_start":
162
- case "text_delta":
163
- case "text_end":
164
- case "thinking_start":
165
- case "thinking_delta":
166
- case "thinking_end":
167
- return {
168
- ...event,
169
- contentIndex: remapClaudeCodeContentIndex(event.partial.content, event.contentIndex),
170
- partial: sanitizeClaudeCodePartial(event.partial),
171
- };
172
- default:
173
- return event;
174
- }
175
- }
176
- export function buildFinalClaudeCodeContent(blocks, lastThinkingContent, lastTextContent, resultText) {
177
- const finalContent = filterUserFacingClaudeCodeContent(blocks);
178
- if (finalContent.length > 0)
179
- return finalContent;
180
- if (lastThinkingContent) {
181
- finalContent.push({ type: "thinking", thinking: lastThinkingContent });
182
- }
183
- if (lastTextContent) {
184
- finalContent.push({ type: "text", text: lastTextContent });
185
- }
186
- if (finalContent.length === 0 && resultText) {
187
- finalContent.push({ type: "text", text: resultText });
188
- }
189
- return finalContent;
190
- }
191
127
  // ---------------------------------------------------------------------------
192
128
  // SDK options builder
193
129
  // ---------------------------------------------------------------------------
@@ -213,6 +149,94 @@ export function buildSdkOptions(modelId, prompt) {
213
149
  betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
214
150
  };
215
151
  }
152
+ function normalizeToolResultContent(content) {
153
+ if (typeof content === "string") {
154
+ return [{ type: "text", text: content }];
155
+ }
156
+ if (!Array.isArray(content)) {
157
+ if (content == null)
158
+ return [{ type: "text", text: "" }];
159
+ return [{ type: "text", text: JSON.stringify(content) }];
160
+ }
161
+ const blocks = [];
162
+ for (const item of content) {
163
+ if (typeof item === "string") {
164
+ blocks.push({ type: "text", text: item });
165
+ continue;
166
+ }
167
+ if (!item || typeof item !== "object") {
168
+ blocks.push({ type: "text", text: String(item) });
169
+ continue;
170
+ }
171
+ const block = item;
172
+ if (block.type === "text") {
173
+ blocks.push({ type: "text", text: typeof block.text === "string" ? block.text : "" });
174
+ continue;
175
+ }
176
+ if (block.type === "image"
177
+ && typeof block.data === "string"
178
+ && typeof block.mimeType === "string") {
179
+ blocks.push({ type: "image", data: block.data, mimeType: block.mimeType });
180
+ continue;
181
+ }
182
+ blocks.push({ type: "text", text: JSON.stringify(block) });
183
+ }
184
+ return blocks.length > 0 ? blocks : [{ type: "text", text: "" }];
185
+ }
186
+ export function extractToolResultsFromSdkUserMessage(message) {
187
+ const extracted = [];
188
+ const seen = new Set();
189
+ const rawMessage = message.message;
190
+ const content = Array.isArray(rawMessage?.content) ? rawMessage.content : [];
191
+ for (const item of content) {
192
+ if (!item || typeof item !== "object")
193
+ continue;
194
+ const block = item;
195
+ const type = typeof block.type === "string" ? block.type : "";
196
+ if (type !== "tool_result" && type !== "mcp_tool_result")
197
+ continue;
198
+ const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
199
+ if (!toolUseId || seen.has(toolUseId))
200
+ continue;
201
+ seen.add(toolUseId);
202
+ extracted.push({
203
+ toolUseId,
204
+ result: {
205
+ content: normalizeToolResultContent(block.content),
206
+ details: {},
207
+ isError: block.is_error === true,
208
+ },
209
+ });
210
+ }
211
+ if (extracted.length === 0) {
212
+ const fallback = message.tool_use_result;
213
+ if (fallback && typeof fallback === "object") {
214
+ const toolResult = fallback;
215
+ const toolUseId = typeof toolResult.tool_use_id === "string" ? toolResult.tool_use_id : "";
216
+ if (toolUseId) {
217
+ extracted.push({
218
+ toolUseId,
219
+ result: {
220
+ content: normalizeToolResultContent(toolResult.content),
221
+ details: {},
222
+ isError: toolResult.is_error === true,
223
+ },
224
+ });
225
+ }
226
+ }
227
+ }
228
+ return extracted;
229
+ }
230
+ function attachExternalResultsToToolCalls(toolCalls, toolResultsById) {
231
+ for (const block of toolCalls) {
232
+ if (block.type !== "toolCall")
233
+ continue;
234
+ const externalResult = toolResultsById.get(block.id);
235
+ if (!externalResult)
236
+ continue;
237
+ block.externalResult = externalResult;
238
+ }
239
+ }
216
240
  // ---------------------------------------------------------------------------
217
241
  // streamSimple implementation
218
242
  // ---------------------------------------------------------------------------
@@ -234,6 +258,10 @@ async function pumpSdkMessages(model, context, options, stream) {
234
258
  /** Track the last text content seen across all assistant turns for the final message. */
235
259
  let lastTextContent = "";
236
260
  let lastThinkingContent = "";
261
+ /** Collect tool calls from intermediate SDK turns for tool_execution events. */
262
+ const intermediateToolCalls = [];
263
+ /** Preserve real external tool results from Claude Code's synthetic user messages. */
264
+ const toolResultsById = new Map();
237
265
  try {
238
266
  // Dynamic import — the SDK is an optional dependency.
239
267
  const sdkModule = "@anthropic-ai/claude-agent-sdk";
@@ -285,11 +313,9 @@ async function pumpSdkMessages(model, context, options, stream) {
285
313
  if (!builder)
286
314
  break;
287
315
  const assistantEvent = builder.handleEvent(event);
288
- const sanitizedEvent = assistantEvent
289
- ? sanitizeClaudeCodeStreamingEvent(assistantEvent)
290
- : null;
291
- if (sanitizedEvent)
292
- stream.push(sanitizedEvent);
316
+ if (assistantEvent) {
317
+ stream.push(assistantEvent);
318
+ }
293
319
  break;
294
320
  }
295
321
  // -- Complete assistant message (non-streaming fallback) --
@@ -317,6 +343,36 @@ async function pumpSdkMessages(model, context, options, stream) {
317
343
  else if (block.type === "thinking" && block.thinking) {
318
344
  lastThinkingContent = block.thinking;
319
345
  }
346
+ else if (block.type === "toolCall") {
347
+ // Collect tool calls for externalToolExecution rendering
348
+ intermediateToolCalls.push(block);
349
+ }
350
+ }
351
+ }
352
+ // Extract tool results from the SDK's synthetic user message
353
+ // and attach to corresponding tool call blocks immediately.
354
+ for (const { toolUseId, result } of extractToolResultsFromSdkUserMessage(msg)) {
355
+ toolResultsById.set(toolUseId, result);
356
+ }
357
+ attachExternalResultsToToolCalls(intermediateToolCalls, toolResultsById);
358
+ // Push a synthetic toolcall_end for each tool call from this turn
359
+ // so the TUI can render tool results in real-time during the SDK
360
+ // session instead of waiting until the entire session completes.
361
+ if (builder) {
362
+ for (const block of builder.message.content) {
363
+ if (block.type !== "toolCall")
364
+ continue;
365
+ const extResult = block.externalResult;
366
+ if (!extResult)
367
+ continue;
368
+ // Push a toolcall_end with result attached so the chat-controller
369
+ // can call updateResult on the pending ToolExecutionComponent.
370
+ stream.push({
371
+ type: "toolcall_end",
372
+ contentIndex: builder.message.content.indexOf(block),
373
+ toolCall: block,
374
+ partial: builder.message,
375
+ });
320
376
  }
321
377
  }
322
378
  builder = null;
@@ -325,7 +381,33 @@ async function pumpSdkMessages(model, context, options, stream) {
325
381
  // -- Result (terminal) --
326
382
  case "result": {
327
383
  const result = msg;
328
- const finalContent = buildFinalClaudeCodeContent(builder?.message.content ?? [], lastThinkingContent, lastTextContent, result.subtype === "success" ? result.result : undefined);
384
+ // Build final message. Include intermediate tool calls so the
385
+ // agent loop's externalToolExecution path emits tool_execution
386
+ // events for proper TUI rendering, followed by the text response.
387
+ const finalContent = [];
388
+ // Add tool calls from intermediate turns first (renders above text)
389
+ attachExternalResultsToToolCalls(intermediateToolCalls, toolResultsById);
390
+ finalContent.push(...intermediateToolCalls);
391
+ // Add text/thinking from the last turn
392
+ if (builder && builder.message.content.length > 0) {
393
+ for (const block of builder.message.content) {
394
+ if (block.type === "text" || block.type === "thinking") {
395
+ finalContent.push(block);
396
+ }
397
+ }
398
+ }
399
+ else {
400
+ if (lastThinkingContent) {
401
+ finalContent.push({ type: "thinking", thinking: lastThinkingContent });
402
+ }
403
+ if (lastTextContent) {
404
+ finalContent.push({ type: "text", text: lastTextContent });
405
+ }
406
+ }
407
+ // Fallback: use the SDK's result text if we have no content
408
+ if (finalContent.length === 0 && result.subtype === "success" && result.result) {
409
+ finalContent.push({ type: "text", text: result.result });
410
+ }
329
411
  const finalMessage = {
330
412
  role: "assistant",
331
413
  content: finalContent,
@@ -250,6 +250,18 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
250
250
  logWarning("engine", `mkdir failed: ${err instanceof Error ? err.message : String(err)}`);
251
251
  }
252
252
  }
253
+ if (ctx.model?.provider === "claude-code") {
254
+ try {
255
+ const { ensureProjectWorkflowMcpConfig } = await import("./mcp-project-config.js");
256
+ const result = ensureProjectWorkflowMcpConfig(base);
257
+ if (result.status !== "unchanged") {
258
+ ctx.ui.notify(`Claude Code MCP prepared at ${result.configPath}`, "info");
259
+ }
260
+ }
261
+ catch (err) {
262
+ ctx.ui.notify(`Claude Code MCP prep failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
263
+ }
264
+ }
253
265
  // Initialize GitServiceImpl
254
266
  s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
255
267
  // Check for crash from previous session. Skip our own fresh bootstrap lock.
@@ -58,7 +58,7 @@ export const TOP_LEVEL_SUBCOMMANDS = [
58
58
  { cmd: "templates", desc: "List available workflow templates" },
59
59
  { cmd: "extensions", desc: "Manage extensions (list, enable, disable, info)" },
60
60
  { cmd: "fast", desc: "Toggle OpenAI service tier (on/off/flex/status)" },
61
- { cmd: "mcp", desc: "MCP server status and connectivity check (status, check <server>)" },
61
+ { cmd: "mcp", desc: "MCP server status, connectivity, and local config bootstrap (status, check, init)" },
62
62
  { cmd: "rethink", desc: "Conversational project reorganization — reorder, park, discard, add milestones" },
63
63
  { cmd: "workflow", desc: "Custom workflow lifecycle (new, run, list, validate, pause, resume)" },
64
64
  { cmd: "codebase", desc: "Generate, refresh, and inspect the codebase map cache (.gsd/CODEBASE.md)" },
@@ -188,6 +188,7 @@ const NESTED_COMPLETIONS = {
188
188
  mcp: [
189
189
  { cmd: "status", desc: "Show all MCP server statuses (default)" },
190
190
  { cmd: "check", desc: "Detailed status for a specific server" },
191
+ { cmd: "init", desc: "Write .mcp.json for the local GSD workflow MCP server" },
191
192
  ],
192
193
  doctor: [
193
194
  { cmd: "fix", desc: "Auto-fix detected issues" },
@@ -55,7 +55,7 @@ export function showHelp(ctx) {
55
55
  " /gsd hooks Show post-unit hook configuration",
56
56
  " /gsd extensions Manage extensions [list|enable|disable|info]",
57
57
  " /gsd fast Toggle OpenAI service tier [on|off|flex|status]",
58
- " /gsd mcp MCP server status and connectivity [status|check <server>]",
58
+ " /gsd mcp MCP server status and connectivity [status|check <server>|init [dir]]",
59
59
  "",
60
60
  "MAINTENANCE",
61
61
  " /gsd doctor Diagnose and repair .gsd/ state [audit|fix|heal] [scope]",
@@ -7,9 +7,26 @@
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
  import { existsSync, readFileSync } from "node:fs";
12
- import { join } from "node:path";
13
+ import { join, resolve } from "node:path";
14
+ import { ensureProjectWorkflowMcpConfig } from "./mcp-project-config.js";
15
+ export function formatMcpInitResult(status, configPath, targetPath) {
16
+ const summary = status === "created"
17
+ ? "Created project MCP config."
18
+ : status === "updated"
19
+ ? "Updated project MCP config."
20
+ : "Project MCP config is already up to date.";
21
+ return [
22
+ summary,
23
+ "",
24
+ `Project: ${targetPath}`,
25
+ `Config: ${configPath}`,
26
+ "",
27
+ "Claude Code can now load the GSD workflow MCP server from this folder.",
28
+ ].join("\n");
29
+ }
13
30
  function readMcpConfigs() {
14
31
  const servers = [];
15
32
  const seen = new Set();
@@ -61,6 +78,7 @@ export function formatMcpStatusReport(servers) {
61
78
  "No MCP servers configured.",
62
79
  "",
63
80
  "Add servers to .mcp.json or .gsd/mcp.json to enable MCP integrations.",
81
+ "Tip: run /gsd mcp init . to write the local GSD workflow MCP config.",
64
82
  "See: https://modelcontextprotocol.io/quickstart",
65
83
  ].join("\n");
66
84
  }
@@ -109,11 +127,28 @@ export function formatMcpServerDetail(server) {
109
127
  * Handle `/gsd mcp [status|check <server>]`.
110
128
  */
111
129
  export async function handleMcpStatus(args, ctx) {
112
- const trimmed = args.trim().toLowerCase();
130
+ const trimmed = args.trim();
131
+ const lowered = trimmed.toLowerCase();
113
132
  const configs = readMcpConfigs();
133
+ // /gsd mcp init [dir]
134
+ if (!lowered || lowered === "status") {
135
+ // handled below
136
+ }
137
+ else if (lowered === "init" || lowered.startsWith("init ")) {
138
+ const rawPath = trimmed.slice("init".length).trim();
139
+ const targetPath = resolve(rawPath || ".");
140
+ try {
141
+ const result = ensureProjectWorkflowMcpConfig(targetPath);
142
+ ctx.ui.notify(formatMcpInitResult(result.status, result.configPath, targetPath), "info");
143
+ }
144
+ catch (err) {
145
+ ctx.ui.notify(`Failed to prepare MCP config for ${targetPath}: ${err instanceof Error ? err.message : String(err)}`, "error");
146
+ }
147
+ return;
148
+ }
114
149
  // /gsd mcp check <server>
115
- if (trimmed.startsWith("check ")) {
116
- const serverName = args.trim().slice("check ".length).trim();
150
+ if (lowered.startsWith("check ")) {
151
+ const serverName = trimmed.slice("check ".length).trim();
117
152
  const config = configs.find((c) => c.name === serverName);
118
153
  if (!config) {
119
154
  const available = configs.map((c) => c.name).join(", ") || "(none)";
@@ -149,7 +184,7 @@ export async function handleMcpStatus(args, ctx) {
149
184
  return;
150
185
  }
151
186
  // /gsd mcp or /gsd mcp status
152
- if (!trimmed || trimmed === "status") {
187
+ if (!lowered || lowered === "status") {
153
188
  // Build status for each server
154
189
  const statuses = [];
155
190
  for (const config of configs) {
@@ -181,7 +216,8 @@ export async function handleMcpStatus(args, ctx) {
181
216
  return;
182
217
  }
183
218
  // Unknown subcommand
184
- ctx.ui.notify("Usage: /gsd mcp [status|check <server>]\n\n" +
219
+ ctx.ui.notify("Usage: /gsd mcp [status|check <server>|init [dir]]\n\n" +
185
220
  " status Show all MCP server statuses (default)\n" +
186
- " check <server> Detailed status for a specific server", "warning");
221
+ " check <server> Detailed status for a specific server\n" +
222
+ " init [dir] Write .mcp.json for the local GSD workflow MCP server", "warning");
187
223
  }
@@ -8,7 +8,7 @@ import { deriveState, isMilestoneComplete } from "./state.js";
8
8
  import { listWorktrees, resolveGitDir, worktreesDir } from "./worktree-manager.js";
9
9
  import { abortAndReset } from "./git-self-heal.js";
10
10
  import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch, writeIntegrationBranch } from "./git-service.js";
11
- import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddAllWithExclusions, nativeCommit } from "./native-git-bridge.js";
11
+ import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
12
12
  import { getAllWorktreeHealth } from "./worktree-health.js";
13
13
  import { loadEffectiveGSDPreferences } from "./preferences.js";
14
14
  /**
@@ -380,19 +380,19 @@ export async function checkGitHealth(basePath, issues, fixesApplied, shouldFix,
380
380
  code: "stale_uncommitted_changes",
381
381
  scope: "project",
382
382
  unitId: "project",
383
- message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting uncommitted changes.`,
383
+ message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting tracked files.`,
384
384
  fixable: true,
385
385
  });
386
386
  if (shouldFix("stale_uncommitted_changes")) {
387
387
  try {
388
- nativeAddAllWithExclusions(basePath, RUNTIME_EXCLUSION_PATHS);
388
+ nativeAddTracked(basePath);
389
389
  const commitMsg = `gsd snapshot: uncommitted changes after ${mins}m inactivity`;
390
390
  const result = nativeCommit(basePath, commitMsg);
391
391
  if (result) {
392
392
  fixesApplied.push(`created gsd snapshot after ${mins}m of uncommitted changes`);
393
393
  }
394
394
  else {
395
- fixesApplied.push("gsd snapshot skipped — nothing to commit after staging changes");
395
+ fixesApplied.push("gsd snapshot skipped — nothing to commit after staging tracked files");
396
396
  }
397
397
  }
398
398
  catch {
@@ -20,8 +20,8 @@ import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.j
20
20
  import { abortAndReset } from "./git-self-heal.js";
21
21
  import { rebuildState } from "./doctor.js";
22
22
  import { deriveState } from "./state.js";
23
- import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch } from "./git-service.js";
24
- import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddAllWithExclusions, nativeCommit } from "./native-git-bridge.js";
23
+ import { resolveMilestoneIntegrationBranch } from "./git-service.js";
24
+ import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
25
25
  import { loadEffectiveGSDPreferences } from "./preferences.js";
26
26
  import { runEnvironmentChecks } from "./doctor-environment.js";
27
27
  /** In-memory health history for the current auto-mode session. */
@@ -247,7 +247,7 @@ export async function preDispatchHealthGate(basePath) {
247
247
  if (minutesSinceCommit >= thresholdMinutes) {
248
248
  const mins = Math.floor(minutesSinceCommit);
249
249
  try {
250
- nativeAddAllWithExclusions(basePath, RUNTIME_EXCLUSION_PATHS);
250
+ nativeAddTracked(basePath);
251
251
  const commitMsg = `gsd snapshot: pre-dispatch, uncommitted changes after ${mins}m inactivity`;
252
252
  const result = nativeCommit(basePath, commitMsg);
253
253
  if (result) {
@@ -190,16 +190,12 @@ export async function showProjectInit(ctx, pi, basePath, detection) {
190
190
  // Initialize SQLite database so GSD starts in full-capability mode (#3880).
191
191
  // Without this, isDbAvailable() returns false and GSD enters degraded
192
192
  // markdown-only mode until a tool handler happens to call ensureDbOpen().
193
- let dbReady = false;
194
193
  try {
195
194
  const { ensureDbOpen } = await import("./bootstrap/dynamic-tools.js");
196
- dbReady = await ensureDbOpen(basePath);
195
+ await ensureDbOpen(basePath);
197
196
  }
198
197
  catch {
199
- // Swallowedwarning surfaced below
200
- }
201
- if (!dbReady) {
202
- ctx.ui.notify("Warning: database initialization failed — GSD will run in degraded mode until the next /gsd invocation.", "warning");
198
+ // Non-fatalDB creation failure should not block project init
203
199
  }
204
200
  // Ensure .gitignore
205
201
  ensureGitignore(basePath);
@@ -218,7 +214,6 @@ export async function showProjectInit(ctx, pi, basePath, detection) {
218
214
  // Write initial STATE.md so it exists before the first /gsd invocation.
219
215
  // The explicit /gsd init path (ops.ts) returns without entering showSmartEntry(),
220
216
  // which would otherwise generate STATE.md at guided-flow.ts:1358.
221
- let stateReady = false;
222
217
  try {
223
218
  const { deriveState } = await import("./state.js");
224
219
  const { buildStateMarkdown } = await import("./doctor.js");
@@ -226,13 +221,21 @@ export async function showProjectInit(ctx, pi, basePath, detection) {
226
221
  const { resolveGsdRootFile } = await import("./paths.js");
227
222
  const state = await deriveState(basePath);
228
223
  await saveFile(resolveGsdRootFile(basePath, "STATE"), buildStateMarkdown(state));
229
- stateReady = true;
230
224
  }
231
225
  catch {
232
- // Swallowedwarning surfaced below
233
- }
234
- if (!stateReady) {
235
- ctx.ui.notify("Warning: initial STATE.md generation failed — it will be created on the next /gsd invocation.", "warning");
226
+ // Non-fatalSTATE.md will be regenerated on next /gsd invocation
227
+ }
228
+ if (ctx.model?.provider === "claude-code") {
229
+ try {
230
+ const { ensureProjectWorkflowMcpConfig } = await import("./mcp-project-config.js");
231
+ const result = ensureProjectWorkflowMcpConfig(basePath);
232
+ if (result.status !== "unchanged") {
233
+ ctx.ui.notify(`Claude Code MCP prepared at ${result.configPath}`, "info");
234
+ }
235
+ }
236
+ catch (err) {
237
+ ctx.ui.notify(`Claude Code MCP prep failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
238
+ }
236
239
  }
237
240
  ctx.ui.notify("GSD initialized. Starting your first milestone...", "info");
238
241
  return { completed: true, bootstrapped: true };
@@ -0,0 +1,83 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { assertSafeDirectory } from "./validate-directory.js";
5
+ import { detectWorkflowMcpLaunchConfig } from "./workflow-mcp.js";
6
+ export const GSD_WORKFLOW_MCP_SERVER_NAME = "gsd-workflow";
7
+ export function resolveBundledGsdCliPath(env = process.env) {
8
+ const explicit = env.GSD_CLI_PATH?.trim() || env.GSD_BIN_PATH?.trim();
9
+ if (explicit)
10
+ return explicit;
11
+ const candidates = [
12
+ resolve(fileURLToPath(new URL("../../../../scripts/dev-cli.js", import.meta.url))),
13
+ resolve(fileURLToPath(new URL("../../../../dist/loader.js", import.meta.url))),
14
+ resolve(fileURLToPath(new URL("../../../loader.js", import.meta.url))),
15
+ ];
16
+ for (const candidate of candidates) {
17
+ if (existsSync(candidate))
18
+ return candidate;
19
+ }
20
+ return null;
21
+ }
22
+ export function buildProjectWorkflowMcpServerConfig(projectRoot, env = process.env) {
23
+ const resolvedProjectRoot = resolve(projectRoot);
24
+ const gsdCliPath = resolveBundledGsdCliPath(env);
25
+ const launch = detectWorkflowMcpLaunchConfig(resolvedProjectRoot, {
26
+ ...env,
27
+ ...(gsdCliPath ? { GSD_CLI_PATH: gsdCliPath, GSD_BIN_PATH: gsdCliPath } : {}),
28
+ });
29
+ if (!launch) {
30
+ throw new Error("Unable to resolve the GSD workflow MCP server. Build this checkout or install gsd-mcp-server on PATH.");
31
+ }
32
+ return {
33
+ command: launch.command,
34
+ ...(launch.args && launch.args.length > 0 ? { args: launch.args } : {}),
35
+ ...(launch.cwd ? { cwd: launch.cwd } : {}),
36
+ ...(launch.env ? { env: launch.env } : {}),
37
+ };
38
+ }
39
+ function readExistingConfig(configPath) {
40
+ if (!existsSync(configPath))
41
+ return {};
42
+ const raw = readFileSync(configPath, "utf-8");
43
+ try {
44
+ const parsed = JSON.parse(raw);
45
+ return parsed && typeof parsed === "object" ? parsed : {};
46
+ }
47
+ catch (err) {
48
+ throw new Error(`Failed to parse ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
49
+ }
50
+ }
51
+ export function ensureProjectWorkflowMcpConfig(projectRoot, env = process.env) {
52
+ const resolvedProjectRoot = resolve(projectRoot);
53
+ assertSafeDirectory(resolvedProjectRoot);
54
+ const configPath = resolve(resolvedProjectRoot, ".mcp.json");
55
+ const existing = readExistingConfig(configPath);
56
+ const desiredServer = buildProjectWorkflowMcpServerConfig(resolvedProjectRoot, env);
57
+ const previousServers = existing.mcpServers ?? {};
58
+ const nextServers = {
59
+ ...previousServers,
60
+ [GSD_WORKFLOW_MCP_SERVER_NAME]: desiredServer,
61
+ };
62
+ const alreadyPresent = existsSync(configPath);
63
+ const unchanged = JSON.stringify(previousServers[GSD_WORKFLOW_MCP_SERVER_NAME] ?? null)
64
+ === JSON.stringify(desiredServer)
65
+ && existing.mcpServers !== undefined;
66
+ if (unchanged) {
67
+ return {
68
+ configPath,
69
+ serverName: GSD_WORKFLOW_MCP_SERVER_NAME,
70
+ status: "unchanged",
71
+ };
72
+ }
73
+ const nextConfig = {
74
+ ...existing,
75
+ mcpServers: nextServers,
76
+ };
77
+ writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf-8");
78
+ return {
79
+ configPath,
80
+ serverName: GSD_WORKFLOW_MCP_SERVER_NAME,
81
+ status: alreadyPresent ? "updated" : "created",
82
+ };
83
+ }