gsd-pi 2.70.1-dev.3591dcf → 2.70.1-dev.3e19108
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/dist/resources/extensions/claude-code-cli/stream-adapter.js +2 -0
- package/dist/resources/extensions/gsd/auto-start.js +3 -11
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
- package/dist/resources/extensions/gsd/guided-flow.js +12 -10
- package/dist/resources/extensions/gsd/init-wizard.js +3 -11
- package/dist/resources/extensions/gsd/prompts/discuss.md +31 -13
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +34 -0
- package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +56 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
- 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/required-server-files.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 +17 -17
- 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/2826.dd3dc8bbd3025fa5.js +9 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-6e4d7e9a4f57bed4.js → webpack-b868033a5834586d.js} +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/env-writer.d.ts +39 -0
- package/packages/mcp-server/dist/env-writer.d.ts.map +1 -0
- package/packages/mcp-server/dist/env-writer.js +158 -0
- package/packages/mcp-server/dist/env-writer.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts +11 -2
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +102 -2
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/src/env-writer.test.ts +280 -0
- package/packages/mcp-server/src/env-writer.ts +183 -0
- package/packages/mcp-server/src/secure-env-collect.test.ts +265 -0
- package/packages/mcp-server/src/server.ts +137 -3
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +133 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -0
- 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 +58 -21
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +152 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +83 -27
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +2 -0
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +33 -0
- package/src/resources/extensions/gsd/auto-start.ts +3 -13
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
- package/src/resources/extensions/gsd/guided-flow.ts +12 -9
- package/src/resources/extensions/gsd/init-wizard.ts +3 -13
- package/src/resources/extensions/gsd/prompts/discuss.md +31 -13
- package/src/resources/extensions/gsd/tests/discuss-incremental-persistence.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +76 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +155 -1
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +22 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +60 -25
- package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +76 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +1 -1
- package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +0 -9
- /package/dist/web/standalone/.next/static/{KdlODhIktLmeRKpLpHdKb → cHCEWiRJM5bXJa9HkP1QU}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{KdlODhIktLmeRKpLpHdKb → cHCEWiRJM5bXJa9HkP1QU}/_ssgManifest.js +0 -0
|
@@ -9,6 +9,18 @@ import { appKey } from "../components/keybinding-hints.js";
|
|
|
9
9
|
// Tracks the last processed content index to avoid re-scanning all blocks on every message_update
|
|
10
10
|
let lastProcessedContentIndex = 0;
|
|
11
11
|
|
|
12
|
+
function hasVisibleAssistantContent(message: { content: Array<any> }): boolean {
|
|
13
|
+
return message.content.some(
|
|
14
|
+
(c) =>
|
|
15
|
+
(c.type === "text" && typeof c.text === "string" && c.text.trim().length > 0)
|
|
16
|
+
|| (c.type === "thinking" && typeof c.thinking === "string" && c.thinking.trim().length > 0),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hasAssistantToolBlocks(message: { content: Array<any> }): boolean {
|
|
21
|
+
return message.content.some((c) => c.type === "toolCall" || c.type === "serverToolUse");
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
13
25
|
init: () => Promise<void>;
|
|
14
26
|
getMarkdownThemeWithSettings: () => any;
|
|
@@ -104,45 +116,55 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
104
116
|
host.updatePendingMessagesDisplay();
|
|
105
117
|
host.ui.requestRender();
|
|
106
118
|
} else if (event.message.role === "assistant") {
|
|
107
|
-
host.streamingComponent = new AssistantMessageComponent(
|
|
108
|
-
undefined,
|
|
109
|
-
host.hideThinkingBlock,
|
|
110
|
-
host.getMarkdownThemeWithSettings(),
|
|
111
|
-
host.settingsManager.getTimestampFormat(),
|
|
112
|
-
);
|
|
113
119
|
host.streamingMessage = event.message;
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
// External-tool providers can stream multiple assistant turns through
|
|
121
|
+
// one response. Delay component creation until visible assistant text
|
|
122
|
+
// arrives so tool outputs keep chronological ordering.
|
|
116
123
|
host.ui.requestRender();
|
|
117
124
|
}
|
|
118
125
|
break;
|
|
119
126
|
|
|
120
127
|
case "message_update":
|
|
121
|
-
if (
|
|
128
|
+
if (event.message.role === "assistant") {
|
|
122
129
|
host.streamingMessage = event.message;
|
|
123
|
-
host.streamingComponent.updateContent(host.streamingMessage);
|
|
124
|
-
|
|
125
|
-
// When the stream adapter signals a completed tool call with an
|
|
126
|
-
// external result (from Claude Code SDK), update the pending
|
|
127
|
-
// ToolExecutionComponent immediately so output is visible in
|
|
128
|
-
// real-time instead of waiting for the session to end.
|
|
129
130
|
const innerEvent = event.assistantMessageEvent;
|
|
131
|
+
|
|
132
|
+
if (!host.streamingComponent && hasVisibleAssistantContent(host.streamingMessage)) {
|
|
133
|
+
host.streamingComponent = new AssistantMessageComponent(
|
|
134
|
+
undefined,
|
|
135
|
+
host.hideThinkingBlock,
|
|
136
|
+
host.getMarkdownThemeWithSettings(),
|
|
137
|
+
host.settingsManager.getTimestampFormat(),
|
|
138
|
+
);
|
|
139
|
+
host.chatContainer.addChild(host.streamingComponent);
|
|
140
|
+
}
|
|
141
|
+
if (host.streamingComponent) {
|
|
142
|
+
host.streamingComponent.updateContent(host.streamingMessage);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let externalToolResult:
|
|
146
|
+
| { toolCallId: string; content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; details: Record<string, unknown>; isError: boolean }
|
|
147
|
+
| undefined;
|
|
130
148
|
if (innerEvent.type === "toolcall_end" && innerEvent.toolCall) {
|
|
131
149
|
const tc = innerEvent.toolCall as any;
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
});
|
|
141
|
-
}
|
|
150
|
+
const ext = tc.externalResult;
|
|
151
|
+
if (ext) {
|
|
152
|
+
externalToolResult = {
|
|
153
|
+
toolCallId: tc.id,
|
|
154
|
+
content: ext.content ?? [{ type: "text", text: "" }],
|
|
155
|
+
details: ext.details ?? {},
|
|
156
|
+
isError: ext.isError ?? false,
|
|
157
|
+
};
|
|
142
158
|
}
|
|
143
159
|
}
|
|
144
160
|
|
|
145
161
|
const contentBlocks = host.streamingMessage.content;
|
|
162
|
+
// Some adapters reuse a single assistant lifecycle while internally
|
|
163
|
+
// spanning multiple provider turns. When a new turn starts, content
|
|
164
|
+
// length can shrink back to 0/1; reset scan index to avoid skipping.
|
|
165
|
+
if (lastProcessedContentIndex >= contentBlocks.length) {
|
|
166
|
+
lastProcessedContentIndex = 0;
|
|
167
|
+
}
|
|
146
168
|
for (let i = lastProcessedContentIndex; i < contentBlocks.length; i++) {
|
|
147
169
|
const content = contentBlocks[i];
|
|
148
170
|
if (content.type === "toolCall") {
|
|
@@ -192,6 +214,22 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
192
214
|
}
|
|
193
215
|
}
|
|
194
216
|
}
|
|
217
|
+
|
|
218
|
+
// When the stream adapter signals a completed tool call with an
|
|
219
|
+
// external result (from Claude Code SDK), update the pending
|
|
220
|
+
// ToolExecutionComponent immediately so output is visible in
|
|
221
|
+
// real-time instead of waiting for the session to end.
|
|
222
|
+
if (externalToolResult) {
|
|
223
|
+
const component = host.pendingTools.get(externalToolResult.toolCallId);
|
|
224
|
+
if (component) {
|
|
225
|
+
component.updateResult({
|
|
226
|
+
content: externalToolResult.content,
|
|
227
|
+
details: externalToolResult.details,
|
|
228
|
+
isError: externalToolResult.isError,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
195
233
|
// Update index: fully processed blocks won't need re-scanning.
|
|
196
234
|
// Keep the last block's index (it may still be accumulating data),
|
|
197
235
|
// so we re-check it next time but skip all earlier ones.
|
|
@@ -204,7 +242,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
204
242
|
|
|
205
243
|
case "message_end":
|
|
206
244
|
if (event.message.role === "user") break;
|
|
207
|
-
if (
|
|
245
|
+
if (event.message.role === "assistant") {
|
|
208
246
|
host.streamingMessage = event.message;
|
|
209
247
|
let errorMessage: string | undefined;
|
|
210
248
|
if (host.streamingMessage.stopReason === "aborted") {
|
|
@@ -214,7 +252,25 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
214
252
|
: "Operation aborted";
|
|
215
253
|
host.streamingMessage.errorMessage = errorMessage;
|
|
216
254
|
}
|
|
217
|
-
|
|
255
|
+
|
|
256
|
+
const shouldRenderAssistant = hasVisibleAssistantContent(host.streamingMessage)
|
|
257
|
+
|| (
|
|
258
|
+
(host.streamingMessage.stopReason === "aborted" || host.streamingMessage.stopReason === "error")
|
|
259
|
+
&& !hasAssistantToolBlocks(host.streamingMessage)
|
|
260
|
+
);
|
|
261
|
+
if (!host.streamingComponent && shouldRenderAssistant) {
|
|
262
|
+
host.streamingComponent = new AssistantMessageComponent(
|
|
263
|
+
undefined,
|
|
264
|
+
host.hideThinkingBlock,
|
|
265
|
+
host.getMarkdownThemeWithSettings(),
|
|
266
|
+
host.settingsManager.getTimestampFormat(),
|
|
267
|
+
);
|
|
268
|
+
host.chatContainer.addChild(host.streamingComponent);
|
|
269
|
+
}
|
|
270
|
+
if (host.streamingComponent) {
|
|
271
|
+
host.streamingComponent.updateContent(host.streamingMessage);
|
|
272
|
+
}
|
|
273
|
+
|
|
218
274
|
if (host.streamingMessage.stopReason === "aborted" || host.streamingMessage.stopReason === "error") {
|
|
219
275
|
if (!errorMessage) {
|
|
220
276
|
errorMessage = host.streamingMessage.errorMessage || "Error";
|
|
@@ -398,6 +398,7 @@ export function buildSdkOptions(
|
|
|
398
398
|
extraOptions: Record<string, unknown> = {},
|
|
399
399
|
): Record<string, unknown> {
|
|
400
400
|
const mcpServers = buildWorkflowMcpServers();
|
|
401
|
+
const disallowedTools = ["AskUserQuestion"];
|
|
401
402
|
return {
|
|
402
403
|
pathToClaudeCodeExecutable: getClaudePath(),
|
|
403
404
|
model: modelId,
|
|
@@ -408,6 +409,7 @@ export function buildSdkOptions(
|
|
|
408
409
|
allowDangerouslySkipPermissions: true,
|
|
409
410
|
settingSources: ["project"],
|
|
410
411
|
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
412
|
+
disallowedTools,
|
|
411
413
|
...(mcpServers ? { mcpServers } : {}),
|
|
412
414
|
betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
|
|
413
415
|
...extraOptions,
|
|
@@ -220,6 +220,35 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
220
220
|
assert.equal(srv.env.GSD_CLI_PATH, "/tmp/gsd");
|
|
221
221
|
assert.equal(srv.env.GSD_PERSIST_WRITE_GATE_STATE, "1");
|
|
222
222
|
assert.equal(srv.env.GSD_WORKFLOW_PROJECT_ROOT, "/tmp/project");
|
|
223
|
+
assert.deepEqual(options.disallowedTools, ["AskUserQuestion"]);
|
|
224
|
+
} finally {
|
|
225
|
+
process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
|
|
226
|
+
process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
|
|
227
|
+
process.env.GSD_WORKFLOW_MCP_ARGS = prev.GSD_WORKFLOW_MCP_ARGS;
|
|
228
|
+
process.env.GSD_WORKFLOW_MCP_ENV = prev.GSD_WORKFLOW_MCP_ENV;
|
|
229
|
+
process.env.GSD_WORKFLOW_MCP_CWD = prev.GSD_WORKFLOW_MCP_CWD;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("buildSdkOptions disables AskUserQuestion for custom workflow MCP server names", () => {
|
|
234
|
+
const prev = {
|
|
235
|
+
GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
|
|
236
|
+
GSD_WORKFLOW_MCP_NAME: process.env.GSD_WORKFLOW_MCP_NAME,
|
|
237
|
+
GSD_WORKFLOW_MCP_ARGS: process.env.GSD_WORKFLOW_MCP_ARGS,
|
|
238
|
+
GSD_WORKFLOW_MCP_ENV: process.env.GSD_WORKFLOW_MCP_ENV,
|
|
239
|
+
GSD_WORKFLOW_MCP_CWD: process.env.GSD_WORKFLOW_MCP_CWD,
|
|
240
|
+
};
|
|
241
|
+
try {
|
|
242
|
+
process.env.GSD_WORKFLOW_MCP_COMMAND = "node";
|
|
243
|
+
process.env.GSD_WORKFLOW_MCP_NAME = "custom-workflow";
|
|
244
|
+
process.env.GSD_WORKFLOW_MCP_ARGS = JSON.stringify(["packages/mcp-server/dist/cli.js"]);
|
|
245
|
+
process.env.GSD_WORKFLOW_MCP_ENV = JSON.stringify({ GSD_CLI_PATH: "/tmp/gsd" });
|
|
246
|
+
process.env.GSD_WORKFLOW_MCP_CWD = "/tmp/project";
|
|
247
|
+
|
|
248
|
+
const options = buildSdkOptions("claude-sonnet-4-20250514", "test");
|
|
249
|
+
const mcpServers = options.mcpServers as Record<string, any>;
|
|
250
|
+
assert.ok(mcpServers?.["custom-workflow"], "expected custom workflow server config");
|
|
251
|
+
assert.deepEqual(options.disallowedTools, ["AskUserQuestion"]);
|
|
223
252
|
} finally {
|
|
224
253
|
process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
|
|
225
254
|
process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
|
|
@@ -255,6 +284,9 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
255
284
|
const mcpServers = (options as any).mcpServers;
|
|
256
285
|
if (mcpServers) {
|
|
257
286
|
assert.ok(mcpServers["gsd-workflow"], "if present, must be gsd-workflow");
|
|
287
|
+
assert.deepEqual((options as any).disallowedTools, ["AskUserQuestion"]);
|
|
288
|
+
} else {
|
|
289
|
+
assert.deepEqual((options as any).disallowedTools, ["AskUserQuestion"]);
|
|
258
290
|
}
|
|
259
291
|
rmSync(emptyDir, { recursive: true, force: true });
|
|
260
292
|
} finally {
|
|
@@ -301,6 +333,7 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
301
333
|
assert.equal(srv.env.GSD_CLI_PATH, "/tmp/gsd");
|
|
302
334
|
assert.equal(srv.env.GSD_PERSIST_WRITE_GATE_STATE, "1");
|
|
303
335
|
assert.equal(srv.env.GSD_WORKFLOW_PROJECT_ROOT, resolvedRepoDir);
|
|
336
|
+
assert.deepEqual(options.disallowedTools, ["AskUserQuestion"]);
|
|
304
337
|
} finally {
|
|
305
338
|
process.chdir(originalCwd);
|
|
306
339
|
rmSync(repoDir, { recursive: true, force: true });
|
|
@@ -335,19 +335,9 @@ export async function bootstrapAutoSession(
|
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const result = ensureProjectWorkflowMcpConfig(base);
|
|
342
|
-
if (result.status !== "unchanged") {
|
|
343
|
-
ctx.ui.notify(`Claude Code MCP prepared at ${result.configPath}`, "info");
|
|
344
|
-
}
|
|
345
|
-
} catch (err) {
|
|
346
|
-
ctx.ui.notify(
|
|
347
|
-
`Claude Code MCP prep failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
348
|
-
"warning",
|
|
349
|
-
);
|
|
350
|
-
}
|
|
338
|
+
{
|
|
339
|
+
const { prepareWorkflowMcpForProject } = await import("./workflow-mcp-auto-prep.js");
|
|
340
|
+
prepareWorkflowMcpForProject(ctx, base);
|
|
351
341
|
}
|
|
352
342
|
|
|
353
343
|
// Initialize GitServiceImpl
|
|
@@ -45,6 +45,8 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
45
45
|
resetToolCallLoopGuard();
|
|
46
46
|
resetAskUserQuestionsCache();
|
|
47
47
|
await syncServiceTierStatus(ctx);
|
|
48
|
+
const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
|
|
49
|
+
prepareWorkflowMcpForProject(ctx, process.cwd());
|
|
48
50
|
|
|
49
51
|
// Apply show_token_cost preference (#1515)
|
|
50
52
|
try {
|
|
@@ -85,6 +87,8 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
85
87
|
resetAskUserQuestionsCache();
|
|
86
88
|
clearDiscussionFlowState();
|
|
87
89
|
await syncServiceTierStatus(ctx);
|
|
90
|
+
const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
|
|
91
|
+
prepareWorkflowMcpForProject(ctx, process.cwd());
|
|
88
92
|
loadToolApiKeys();
|
|
89
93
|
});
|
|
90
94
|
|
|
@@ -426,8 +426,9 @@ function resolveAvailableModel<T extends { id: string; provider: string }>(
|
|
|
426
426
|
* Build the discuss-and-plan prompt for a new milestone.
|
|
427
427
|
* Used by all three "new milestone" paths (first ever, no active, all complete).
|
|
428
428
|
*/
|
|
429
|
-
function buildDiscussPrompt(nextId: string, preamble: string, _basePath: string, preparationContext?: string): string {
|
|
429
|
+
function buildDiscussPrompt(nextId: string, preamble: string, _basePath: string, pi: ExtensionAPI, ctx: ExtensionCommandContext, preparationContext?: string): string {
|
|
430
430
|
const milestoneRel = `.gsd/milestones/${nextId}`;
|
|
431
|
+
const structuredQuestionsAvailable = getStructuredQuestionsAvailability(pi, ctx);
|
|
431
432
|
const inlinedTemplates = [
|
|
432
433
|
inlineTemplate("project", "Project"),
|
|
433
434
|
inlineTemplate("requirements", "Requirements"),
|
|
@@ -439,6 +440,7 @@ function buildDiscussPrompt(nextId: string, preamble: string, _basePath: string,
|
|
|
439
440
|
milestoneId: nextId,
|
|
440
441
|
preamble,
|
|
441
442
|
preparationContext: preparationContext ?? "",
|
|
443
|
+
structuredQuestionsAvailable,
|
|
442
444
|
contextPath: `${milestoneRel}/${nextId}-CONTEXT.md`,
|
|
443
445
|
roadmapPath: `${milestoneRel}/${nextId}-ROADMAP.md`,
|
|
444
446
|
inlinedTemplates,
|
|
@@ -486,6 +488,7 @@ function buildHeadlessDiscussPrompt(nextId: string, seedContext: string, _basePa
|
|
|
486
488
|
*/
|
|
487
489
|
async function prepareAndBuildDiscussPrompt(
|
|
488
490
|
ctx: ExtensionCommandContext,
|
|
491
|
+
pi: ExtensionAPI,
|
|
489
492
|
nextId: string,
|
|
490
493
|
preamble: string,
|
|
491
494
|
basePath: string,
|
|
@@ -520,7 +523,7 @@ async function prepareAndBuildDiscussPrompt(
|
|
|
520
523
|
}
|
|
521
524
|
}
|
|
522
525
|
|
|
523
|
-
return buildDiscussPrompt(nextId, preamble, basePath, preparationContext);
|
|
526
|
+
return buildDiscussPrompt(nextId, preamble, basePath, pi, ctx, preparationContext);
|
|
524
527
|
}
|
|
525
528
|
|
|
526
529
|
/**
|
|
@@ -780,7 +783,7 @@ export async function showDiscuss(
|
|
|
780
783
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
781
784
|
const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
|
|
782
785
|
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: false, createdAt: Date.now() });
|
|
783
|
-
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
|
|
786
|
+
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
|
|
784
787
|
}
|
|
785
788
|
return;
|
|
786
789
|
}
|
|
@@ -1185,7 +1188,7 @@ async function handleMilestoneActions(
|
|
|
1185
1188
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
1186
1189
|
const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
|
|
1187
1190
|
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
|
|
1188
|
-
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId,
|
|
1191
|
+
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
|
1189
1192
|
`New milestone ${nextId}.`,
|
|
1190
1193
|
basePath
|
|
1191
1194
|
), "gsd-run", ctx, "discuss-milestone");
|
|
@@ -1375,7 +1378,7 @@ export async function showSmartEntry(
|
|
|
1375
1378
|
if (isFirst) {
|
|
1376
1379
|
// First ever — skip wizard, just ask directly
|
|
1377
1380
|
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
|
|
1378
|
-
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId,
|
|
1381
|
+
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
|
1379
1382
|
`New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`,
|
|
1380
1383
|
basePath
|
|
1381
1384
|
), "gsd-run", ctx, "discuss-milestone");
|
|
@@ -1396,7 +1399,7 @@ export async function showSmartEntry(
|
|
|
1396
1399
|
|
|
1397
1400
|
if (choice === "new_milestone") {
|
|
1398
1401
|
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
|
|
1399
|
-
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId,
|
|
1402
|
+
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
|
1400
1403
|
`New milestone ${nextId}.`,
|
|
1401
1404
|
basePath
|
|
1402
1405
|
), "gsd-run", ctx, "discuss-milestone");
|
|
@@ -1435,7 +1438,7 @@ export async function showSmartEntry(
|
|
|
1435
1438
|
const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
|
|
1436
1439
|
|
|
1437
1440
|
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
|
|
1438
|
-
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId,
|
|
1441
|
+
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
|
1439
1442
|
`New milestone ${nextId}.`,
|
|
1440
1443
|
basePath
|
|
1441
1444
|
), "gsd-run", ctx, "discuss-milestone");
|
|
@@ -1502,7 +1505,7 @@ export async function showSmartEntry(
|
|
|
1502
1505
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
1503
1506
|
const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
|
|
1504
1507
|
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
|
|
1505
|
-
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId,
|
|
1508
|
+
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
|
1506
1509
|
`New milestone ${nextId}.`,
|
|
1507
1510
|
basePath
|
|
1508
1511
|
), "gsd-run", ctx, "discuss-milestone");
|
|
@@ -1599,7 +1602,7 @@ export async function showSmartEntry(
|
|
|
1599
1602
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
1600
1603
|
const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
|
|
1601
1604
|
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
|
|
1602
|
-
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId,
|
|
1605
|
+
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
|
1603
1606
|
`New milestone ${nextId}.`,
|
|
1604
1607
|
basePath
|
|
1605
1608
|
), "gsd-run", ctx, "discuss-milestone");
|
|
@@ -274,19 +274,9 @@ export async function showProjectInit(
|
|
|
274
274
|
// Non-fatal — STATE.md will be regenerated on next /gsd invocation
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
}
|
|
277
|
+
{
|
|
278
|
+
const { prepareWorkflowMcpForProject } = await import("./workflow-mcp-auto-prep.js");
|
|
279
|
+
prepareWorkflowMcpForProject(ctx, basePath);
|
|
290
280
|
}
|
|
291
281
|
|
|
292
282
|
ctx.ui.notify("GSD initialized. Starting your first milestone...", "info");
|
|
@@ -49,6 +49,26 @@ This happens ONCE, before the first round. The goal: your first questions should
|
|
|
49
49
|
|
|
50
50
|
For subsequent rounds, continue investigating between rounds — check docs, search, or scout as needed to make each round's questions smarter. But the first-round investigation is mandatory and explicit. Distribute searches across turns rather than clustering them in one turn.
|
|
51
51
|
|
|
52
|
+
## Question Rounds
|
|
53
|
+
|
|
54
|
+
Ask **1–3 questions per round**. Keep each round tightly focused on one or two of the depth checklist dimensions — do not try to cover all six in one round.
|
|
55
|
+
|
|
56
|
+
**If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions` for each round. 1–3 questions per call, each as a separate question object. Keep option labels short (3–5 words). Always include a freeform "Other / let me explain" option. When the user picks that option or writes a long freeform answer, switch to plain text follow-up for that thread before resuming structured questions. **IMPORTANT: Call `ask_user_questions` exactly once per turn. Never make multiple calls with the same or overlapping questions — wait for the user's response before asking the next round.**
|
|
57
|
+
|
|
58
|
+
**If `{{structuredQuestionsAvailable}}` is `false`:** ask questions in plain text. Keep each round to 1–3 focused questions. Wait for answers before asking the next round.
|
|
59
|
+
|
|
60
|
+
After each answer set, investigate further if any answer opens a new unknown, then ask the next round.
|
|
61
|
+
|
|
62
|
+
### Round cadence
|
|
63
|
+
|
|
64
|
+
After each round of answers, decide whether you already have enough depth to write strong output.
|
|
65
|
+
|
|
66
|
+
- **Incremental persistence:** After every 2 question rounds, silently save a `{{milestoneId}}-CONTEXT-DRAFT.md` using `gsd_summary_save` with `artifact_type: "CONTEXT-DRAFT"` and `milestone_id: "{{milestoneId}}"`. This protects confirmed work against session crashes. Do NOT mention this save to the user.
|
|
67
|
+
- If not ready, continue to the next round immediately. Do **not** ask a meta "ready to wrap up?" question after every round.
|
|
68
|
+
- **Depth-matching rule:** Simple, well-defined work needs fewer rounds — maybe 1–2. Large, ambiguous visions need more — maybe 4+. Do not pad rounds to hit a number. Stop when the Depth Enforcement checklist below is fully satisfied.
|
|
69
|
+
- Do not count the reflection step as a question round. Rounds start after reflection is confirmed.
|
|
70
|
+
- When you genuinely believe the depth checklist is satisfied, move to the Depth Verification step below. Do not ask a separate "ready to wrap up?" gate — the depth verification IS the gate.
|
|
71
|
+
|
|
52
72
|
## Questioning Philosophy
|
|
53
73
|
|
|
54
74
|
You are a thinking partner, not an interviewer.
|
|
@@ -94,29 +114,27 @@ Do NOT offer to proceed until ALL of the following are satisfied. Track these in
|
|
|
94
114
|
|
|
95
115
|
Before offering to proceed, demonstrate absorption: reference specific things the user emphasized, specific terminology they used, specific nuance they sharpened — and show how those shaped your understanding. Synthesize, don't recite. "Your emphasis on X led me to prioritize Y over Z" is good. "You said X, you said Y, you said Z" is not. The user should feel heard in the specifics, not just acknowledged in the abstract.
|
|
96
116
|
|
|
97
|
-
**Questioning depth should match scope.** Simple, well-defined work needs fewer rounds — maybe 1-2. Large, ambiguous visions need more — maybe 4+. Don't pad rounds to hit a number. Stop when the depth checklist is satisfied and you genuinely understand the work.
|
|
98
|
-
|
|
99
|
-
Do not count the reflection step as a question round. Rounds start after reflection is confirmed.
|
|
100
|
-
|
|
101
117
|
## Depth Verification
|
|
102
118
|
|
|
103
119
|
Before moving to the wrap-up gate, present a structured depth summary as a checkpoint.
|
|
104
120
|
|
|
105
121
|
**Print the summary as normal chat text first** — this is where the formatting renders properly. Structure the summary across the depth checklist dimensions using the user's own terminology and framing. Cover: what you understood them to be building, what shaped your understanding most (their emphasis, constraints, concerns), and any areas where you're least confident in your understanding.
|
|
106
122
|
|
|
107
|
-
**Then
|
|
123
|
+
**Then confirm:**
|
|
108
124
|
|
|
109
|
-
**
|
|
125
|
+
**If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions` with:
|
|
126
|
+
- header: "Depth Check"
|
|
127
|
+
- question: "Did I capture the depth right?"
|
|
128
|
+
- options: "Yes, you got it (Recommended)", "Not quite — let me clarify"
|
|
129
|
+
- **The question ID must contain `depth_verification`** (e.g., `depth_verification_confirm`) — this naming convention enables downstream mechanical detection and the write-gate.
|
|
110
130
|
|
|
111
|
-
|
|
112
|
-
1. Print in chat: the full depth summary with markdown formatting (headers, bold, bullets)
|
|
113
|
-
2. Call `ask_user_questions` with: header "Depth Check", question "Did I capture the depth right?", options "Yes, you got it (Recommended)" and "Not quite — let me clarify"
|
|
131
|
+
**If `{{structuredQuestionsAvailable}}` is `false`:** ask in plain text: "Did I capture that correctly? If not, tell me what I missed." Wait for explicit confirmation before proceeding. **The same non-bypassable gate applies to the plain-text path** — if the user does not respond, gives an ambiguous answer, or does not explicitly confirm, you MUST re-ask. Never rationalize past a missing confirmation.
|
|
114
132
|
|
|
115
133
|
If they clarify, absorb the correction and re-verify.
|
|
116
134
|
|
|
117
135
|
The depth verification is the required write-gate. Do **not** add another meta "ready to proceed?" checkpoint immediately after it unless there is still material ambiguity.
|
|
118
136
|
|
|
119
|
-
**CRITICAL — Non-bypassable gate:** The system mechanically blocks CONTEXT.md writes until the user selects the "(Recommended)" option. If the user declines, cancels, or the tool fails, you MUST re-ask — never rationalize past the block ("tool not responding, I'll proceed" is forbidden). The gate exists to protect the user's work; treat a block as an instruction, not an obstacle to work around.
|
|
137
|
+
**CRITICAL — Non-bypassable gate:** The system mechanically blocks CONTEXT.md writes until the user selects the "(Recommended)" option (structured path) or explicitly confirms (plain-text path). If the user declines, cancels, does not respond, or the tool fails, you MUST re-ask — never rationalize past the block ("tool not responding, I'll proceed" is forbidden). The gate exists to protect the user's work; treat a block as an instruction, not an obstacle to work around.
|
|
120
138
|
|
|
121
139
|
## Wrap-up Gate
|
|
122
140
|
|
|
@@ -244,7 +262,7 @@ If a milestone has no dependencies, omit the frontmatter. The dependency chain f
|
|
|
244
262
|
|
|
245
263
|
#### Phase 3: Sequential readiness gate for remaining milestones
|
|
246
264
|
|
|
247
|
-
For each remaining milestone **one at a time, in sequence**, decide the most likely readiness mode from the evidence you already have, then use `ask_user_questions`
|
|
265
|
+
For each remaining milestone **one at a time, in sequence**, decide the most likely readiness mode from the evidence you already have, then present the three options below to the user. **If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions`. **If `{{structuredQuestionsAvailable}}` is `false`:** present the options as a plain-text numbered list and ask the user to type their choice. **Non-bypassable:** If the user does not respond, gives an ambiguous answer, or the tool fails, you MUST re-ask — never rationalize past the block or auto-select a readiness mode. Present three options:
|
|
248
266
|
|
|
249
267
|
- **"Discuss now"** — The user wants to conduct a focused discussion for this milestone in the current session, while the context from the broader discussion is still fresh. Proceed with a focused discussion for this milestone (reflection → investigation → questioning → depth verification). When the discussion concludes, write a full `CONTEXT.md`. Then move to the gate for the next milestone.
|
|
250
268
|
- **"Write draft for later"** — This milestone has seed material from the current conversation but needs its own dedicated discussion in a future session. Write a `CONTEXT-DRAFT.md` capturing the seed material (what was discussed, key ideas, provisional scope, open questions). Mark it clearly as a draft, not a finalized context. **What happens downstream:** When auto-mode reaches this milestone, it pauses and notifies the user: "M00x has draft context — needs discussion. Run /gsd." The `/gsd` wizard shows a "Discuss from draft" option that seeds the new discussion with this draft, so nothing from the current conversation is lost. After the dedicated discussion produces a full CONTEXT.md, the draft file is automatically deleted.
|
|
@@ -256,9 +274,9 @@ Before writing each milestone's CONTEXT.md (whether primary or secondary), you M
|
|
|
256
274
|
|
|
257
275
|
1. **Read the actual code** for every file or module you reference. Confirm APIs exist, check what functions actually do, identify phantom capabilities (code that exists but isn't wired up).
|
|
258
276
|
2. **Check for stale assumptions** — the codebase changes. Verify referenced modules still work as described.
|
|
259
|
-
3. **Present findings** — use `ask_user_questions` with a question ID containing BOTH `depth_verification` AND the milestone ID (e.g., `depth_verification_M002`). Present: what you're about to write, key technical findings from investigation, risks the code review surfaced.
|
|
277
|
+
3. **Present findings** — **If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions` with a question ID containing BOTH `depth_verification` AND the milestone ID (e.g., `depth_verification_M002`). Present: what you're about to write, key technical findings from investigation, risks the code review surfaced. **If `{{structuredQuestionsAvailable}}` is `false`:** present the same findings in plain text and ask for explicit confirmation before proceeding.
|
|
260
278
|
|
|
261
|
-
**The system mechanically blocks CONTEXT.md writes until the per-milestone depth verification passes
|
|
279
|
+
**The system mechanically blocks CONTEXT.md writes until the per-milestone depth verification passes** (structured path: user selects "(Recommended)" option; plain-text path: user explicitly confirms). Each milestone needs its own verification — one global verification does not unlock all milestones.
|
|
262
280
|
|
|
263
281
|
**Why sequential, not batch:** After writing the primary milestone's context and roadmap, the agent still has context window capacity. Asking one milestone at a time lets the user decide per-milestone whether to invest that remaining capacity in a focused discussion now, or defer to a future session. A batch question ("Ready/Draft/Queue for M002, M003, M004?") forces the user to decide everything upfront without knowing how much session capacity remains.
|
|
264
282
|
|
|
@@ -27,10 +27,19 @@ describe("discuss incremental persistence (#2152)", () => {
|
|
|
27
27
|
assert.match(content, /Incremental persistence/, "should have incremental persistence section");
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
test("new-project discuss prompt includes CONTEXT-DRAFT save instruction", () => {
|
|
31
|
+
const content = readFileSync(join(promptsDir, "discuss.md"), "utf-8");
|
|
32
|
+
assert.match(content, /CONTEXT-DRAFT/, "should mention CONTEXT-DRAFT");
|
|
33
|
+
assert.match(content, /Incremental persistence/, "should have incremental persistence section");
|
|
34
|
+
assert.match(content, /gsd_summary_save/, "should use gsd_summary_save tool");
|
|
35
|
+
});
|
|
36
|
+
|
|
30
37
|
test("drafts are saved silently without user notification", () => {
|
|
31
38
|
const milestone = readFileSync(join(promptsDir, "guided-discuss-milestone.md"), "utf-8");
|
|
32
39
|
const slice = readFileSync(join(promptsDir, "guided-discuss-slice.md"), "utf-8");
|
|
40
|
+
const discuss = readFileSync(join(promptsDir, "discuss.md"), "utf-8");
|
|
33
41
|
assert.match(milestone, /Do NOT mention this save to the user/);
|
|
34
42
|
assert.match(slice, /Do NOT mention this to the user/);
|
|
43
|
+
assert.match(discuss, /Do NOT mention this save to the user/);
|
|
35
44
|
});
|
|
36
45
|
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { prepareWorkflowMcpForProject, shouldAutoPrepareWorkflowMcp } from "../workflow-mcp-auto-prep.ts";
|
|
5
|
+
|
|
6
|
+
test("shouldAutoPrepareWorkflowMcp enables prep for externalCli local transport", () => {
|
|
7
|
+
const result = shouldAutoPrepareWorkflowMcp({
|
|
8
|
+
model: { provider: "claude-code", baseUrl: "local://claude-code" },
|
|
9
|
+
modelRegistry: {
|
|
10
|
+
getProviderAuthMode: () => "externalCli",
|
|
11
|
+
isProviderRequestReady: () => false,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
assert.equal(result, true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("shouldAutoPrepareWorkflowMcp enables prep when claude-code provider is ready", () => {
|
|
19
|
+
const result = shouldAutoPrepareWorkflowMcp({
|
|
20
|
+
model: { provider: "openai", baseUrl: "https://api.openai.com" },
|
|
21
|
+
modelRegistry: {
|
|
22
|
+
getProviderAuthMode: () => "apiKey",
|
|
23
|
+
isProviderRequestReady: (provider: string) => provider === "claude-code",
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
assert.equal(result, true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("shouldAutoPrepareWorkflowMcp enables prep when claude-code provider is registered", () => {
|
|
31
|
+
const result = shouldAutoPrepareWorkflowMcp({
|
|
32
|
+
model: { provider: "openai", baseUrl: "https://api.openai.com" },
|
|
33
|
+
modelRegistry: {
|
|
34
|
+
getProviderAuthMode: (provider: string) => provider === "claude-code" ? "externalCli" : "apiKey",
|
|
35
|
+
isProviderRequestReady: () => false,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
assert.equal(result, true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("shouldAutoPrepareWorkflowMcp stays disabled when neither transport nor provider readiness match", () => {
|
|
43
|
+
const result = shouldAutoPrepareWorkflowMcp({
|
|
44
|
+
model: { provider: "openai", baseUrl: "https://api.openai.com" },
|
|
45
|
+
modelRegistry: {
|
|
46
|
+
getProviderAuthMode: () => "apiKey",
|
|
47
|
+
isProviderRequestReady: () => false,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
assert.equal(result, false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("prepareWorkflowMcpForProject warns with /gsd mcp init guidance when prep fails", () => {
|
|
55
|
+
const notifications: Array<{ message: string; level: "info" | "warning" | "error" | "success" }> = [];
|
|
56
|
+
const result = prepareWorkflowMcpForProject(
|
|
57
|
+
{
|
|
58
|
+
model: { provider: "claude-code", baseUrl: "local://claude-code" },
|
|
59
|
+
modelRegistry: {
|
|
60
|
+
getProviderAuthMode: () => "externalCli",
|
|
61
|
+
isProviderRequestReady: () => true,
|
|
62
|
+
},
|
|
63
|
+
ui: {
|
|
64
|
+
notify: (message: string, level?: "info" | "warning" | "error" | "success") => {
|
|
65
|
+
notifications.push({ message, level: level ?? "info" });
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
"/",
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
assert.equal(result, null);
|
|
73
|
+
assert.equal(notifications.length, 1);
|
|
74
|
+
assert.equal(notifications[0].level, "warning");
|
|
75
|
+
assert.match(notifications[0].message, /Please run \/gsd mcp init \./);
|
|
76
|
+
});
|