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.
- package/README.md +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +152 -70
- package/dist/resources/extensions/gsd/auto-start.js +12 -0
- package/dist/resources/extensions/gsd/commands/catalog.js +2 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -1
- package/dist/resources/extensions/gsd/commands-mcp-status.js +43 -7
- package/dist/resources/extensions/gsd/doctor-git-checks.js +4 -4
- package/dist/resources/extensions/gsd/doctor-proactive.js +3 -3
- package/dist/resources/extensions/gsd/init-wizard.js +15 -12
- package/dist/resources/extensions/gsd/mcp-project-config.js +83 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +64 -24
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/6502.5dcdcf1e1432e20d.js +9 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-b49b09f97429b5d0.js → webpack-42a66876b763aa26.js} +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +10 -4
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.ts +13 -2
- package/packages/pi-agent-core/dist/agent-loop.js +14 -6
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.test.ts +53 -0
- package/packages/pi-agent-core/src/agent-loop.ts +20 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +28 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -12
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +19 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +2 -12
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +54 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -12
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +21 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +2 -15
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +190 -93
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +89 -116
- package/src/resources/extensions/gsd/auto-start.ts +15 -1
- package/src/resources/extensions/gsd/commands/catalog.ts +2 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -1
- package/src/resources/extensions/gsd/commands-mcp-status.ts +53 -7
- package/src/resources/extensions/gsd/doctor-git-checks.ts +4 -4
- package/src/resources/extensions/gsd/doctor-proactive.ts +3 -3
- package/src/resources/extensions/gsd/init-wizard.ts +17 -11
- package/src/resources/extensions/gsd/mcp-project-config.ts +128 -0
- package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +2 -9
- package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +0 -33
- package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/mcp-status.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +178 -17
- package/src/resources/extensions/gsd/workflow-mcp.ts +76 -23
- package/dist/web/standalone/.next/static/chunks/6502.b804e48b7919f55e.js +0 -9
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts +0 -13
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts.map +0 -1
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js +0 -27
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js.map +0 -1
- package/packages/pi-coding-agent/src/modes/interactive/provider-auth-setup.ts +0 -40
- package/src/resources/extensions/gsd/tests/init-bootstrap-completeness.test.ts +0 -121
- /package/dist/web/standalone/.next/static/{6_QPFhgX0DQnDhhquheRc → gbSATDX4Jt2ufxzUr5nYm}/_buildManifest.js +0 -0
- /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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
|
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()
|
|
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 (
|
|
116
|
-
const serverName =
|
|
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 (!
|
|
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
|
|
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"
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
24
|
-
import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch,
|
|
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
|
-
|
|
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
|
-
|
|
195
|
+
await ensureDbOpen(basePath);
|
|
197
196
|
}
|
|
198
197
|
catch {
|
|
199
|
-
//
|
|
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-fatal — DB 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
|
-
//
|
|
233
|
-
}
|
|
234
|
-
if (
|
|
235
|
-
|
|
226
|
+
// Non-fatal — STATE.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
|
+
}
|