gsd-pi 2.70.0 → 2.70.1-dev.bef631a
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/loader.js +4 -0
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +152 -2
- package/dist/resources/extensions/gsd/auto-model-selection.js +33 -19
- package/dist/resources/extensions/gsd/auto-prompts.js +7 -3
- package/dist/resources/extensions/gsd/auto-start.js +28 -12
- package/dist/resources/extensions/gsd/auto.js +12 -8
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
- package/dist/resources/extensions/gsd/commands-handlers.js +22 -8
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +12 -0
- package/dist/resources/extensions/gsd/doctor-format.js +2 -0
- package/dist/resources/extensions/gsd/guided-flow.js +33 -20
- package/dist/resources/extensions/gsd/init-wizard.js +3 -11
- package/dist/resources/extensions/gsd/pre-execution-checks.js +5 -3
- 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/validate-directory.js +30 -12
- package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +56 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +12 -1
- package/dist/resources/extensions/slash-commands/audit.js +2 -1
- package/dist/resources/extensions/subagent/isolation.js +4 -2
- package/dist/update-check.d.ts +1 -0
- package/dist/update-check.js +30 -27
- package/dist/update-cmd.js +3 -11
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- package/dist/web/standalone/.next/build-manifest.json +4 -4
- 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 +4 -4
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +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/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- 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 +3 -3
- 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/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- 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 +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/chunks/63.js +3 -3
- package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
- 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/middleware.js +2 -2
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +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/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-f1e30ab6bb269149.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-6e4d7e9a4f57bed4.js → webpack-b868033a5834586d.js} +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/dist/web-mode.js +4 -0
- package/package.json +11 -11
- package/packages/mcp-server/dist/workflow-tools.d.ts +2 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +35 -3
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/import-candidates.test.ts +48 -0
- package/packages/mcp-server/src/workflow-tools.ts +34 -1
- package/packages/pi-agent-core/dist/agent.d.ts +8 -0
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +3 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/src/agent.test.ts +82 -0
- package/packages/pi-agent-core/src/agent.ts +12 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.js +38 -15
- package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +10 -0
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +3 -1
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/lsp/config.ts +43 -17
- package/packages/pi-coding-agent/src/core/sdk.ts +8 -0
- package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +7 -5
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +229 -2
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +205 -0
- package/src/resources/extensions/gsd/auto-model-selection.ts +39 -25
- package/src/resources/extensions/gsd/auto-prompts.ts +7 -3
- package/src/resources/extensions/gsd/auto-start.ts +37 -14
- package/src/resources/extensions/gsd/auto.ts +12 -8
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
- package/src/resources/extensions/gsd/commands-handlers.ts +22 -7
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +14 -0
- package/src/resources/extensions/gsd/doctor-format.ts +1 -0
- package/src/resources/extensions/gsd/doctor-types.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow.ts +36 -17
- package/src/resources/extensions/gsd/init-wizard.ts +3 -13
- package/src/resources/extensions/gsd/pre-execution-checks.ts +6 -3
- 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/doctor-scope-db-unavailable.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/interactive-routing-bypass.test.ts +207 -0
- package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +48 -1
- package/src/resources/extensions/gsd/tests/resource-loader-import-path.test.ts +8 -7
- package/src/resources/extensions/gsd/tests/validate-directory.test.ts +33 -1
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +87 -1
- package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +76 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +180 -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/validate-directory.ts +33 -11
- package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +76 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +16 -1
- package/src/resources/extensions/slash-commands/audit.ts +2 -1
- package/src/resources/extensions/subagent/isolation.ts +4 -3
- package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +0 -9
- package/dist/web/standalone/.next/static/chunks/app/page-7115e62689b5fd84.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
- /package/dist/web/standalone/.next/static/{Nl6lg7zP5dNgNBV1107v1 → UlX0WGGZ8aBPN0uSZ5Ki4}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{Nl6lg7zP5dNgNBV1107v1 → UlX0WGGZ8aBPN0uSZ5Ki4}/_ssgManifest.js +0 -0
|
@@ -16,10 +16,12 @@ import type {
|
|
|
16
16
|
SimpleStreamOptions,
|
|
17
17
|
ToolCall,
|
|
18
18
|
} from "@gsd/pi-ai";
|
|
19
|
+
import type { ExtensionUIContext } from "@gsd/pi-coding-agent";
|
|
19
20
|
import { EventStream } from "@gsd/pi-ai";
|
|
20
21
|
import { execSync } from "node:child_process";
|
|
21
22
|
import { PartialMessageBuilder, ZERO_USAGE, mapUsage } from "./partial-builder.js";
|
|
22
23
|
import { buildWorkflowMcpServers } from "../gsd/workflow-mcp.js";
|
|
24
|
+
import { showInterviewRound, type Question, type RoundResult } from "../shared/tui.js";
|
|
23
25
|
import type {
|
|
24
26
|
SDKAssistantMessage,
|
|
25
27
|
SDKMessage,
|
|
@@ -45,6 +47,46 @@ type ToolCallWithExternalResult = ToolCall & {
|
|
|
45
47
|
externalResult?: ExternalToolResultPayload;
|
|
46
48
|
};
|
|
47
49
|
|
|
50
|
+
interface ClaudeCodeStreamOptions extends SimpleStreamOptions {
|
|
51
|
+
extensionUIContext?: ExtensionUIContext;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface SdkElicitationRequestOption {
|
|
55
|
+
const?: string;
|
|
56
|
+
title?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface SdkElicitationFieldSchema {
|
|
60
|
+
type?: string;
|
|
61
|
+
title?: string;
|
|
62
|
+
description?: string;
|
|
63
|
+
oneOf?: SdkElicitationRequestOption[];
|
|
64
|
+
items?: {
|
|
65
|
+
anyOf?: SdkElicitationRequestOption[];
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface SdkElicitationRequest {
|
|
70
|
+
serverName: string;
|
|
71
|
+
message: string;
|
|
72
|
+
mode?: "form" | "url";
|
|
73
|
+
requestedSchema?: {
|
|
74
|
+
type?: string;
|
|
75
|
+
properties?: Record<string, SdkElicitationFieldSchema>;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface SdkElicitationResult {
|
|
80
|
+
action: "accept" | "decline" | "cancel";
|
|
81
|
+
content?: Record<string, string | string[]>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ParsedElicitationQuestion extends Question {
|
|
85
|
+
noteFieldId?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const OTHER_OPTION_LABEL = "None of the above";
|
|
89
|
+
|
|
48
90
|
// ---------------------------------------------------------------------------
|
|
49
91
|
// Stream factory
|
|
50
92
|
// ---------------------------------------------------------------------------
|
|
@@ -172,6 +214,174 @@ export function makeStreamExhaustedErrorMessage(model: string, lastTextContent:
|
|
|
172
214
|
return message;
|
|
173
215
|
}
|
|
174
216
|
|
|
217
|
+
function readElicitationChoices(options: SdkElicitationRequestOption[] | undefined): string[] {
|
|
218
|
+
if (!Array.isArray(options)) return [];
|
|
219
|
+
return options
|
|
220
|
+
.map((option) => (typeof option?.const === "string" ? option.const : typeof option?.title === "string" ? option.title : ""))
|
|
221
|
+
.filter((option): option is string => option.length > 0);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function parseAskUserQuestionsElicitation(
|
|
225
|
+
request: Pick<SdkElicitationRequest, "mode" | "requestedSchema">,
|
|
226
|
+
): ParsedElicitationQuestion[] | null {
|
|
227
|
+
if (request.mode && request.mode !== "form") return null;
|
|
228
|
+
const properties = request.requestedSchema?.properties;
|
|
229
|
+
if (!properties || typeof properties !== "object") return null;
|
|
230
|
+
|
|
231
|
+
const questions: ParsedElicitationQuestion[] = [];
|
|
232
|
+
|
|
233
|
+
for (const [fieldId, rawField] of Object.entries(properties)) {
|
|
234
|
+
if (fieldId.endsWith("__note")) continue;
|
|
235
|
+
if (!rawField || typeof rawField !== "object") return null;
|
|
236
|
+
|
|
237
|
+
const header = typeof rawField.title === "string" && rawField.title.length > 0 ? rawField.title : fieldId;
|
|
238
|
+
const question = typeof rawField.description === "string" ? rawField.description : "";
|
|
239
|
+
|
|
240
|
+
if (rawField.type === "array") {
|
|
241
|
+
const options = readElicitationChoices(rawField.items?.anyOf).map((label) => ({ label, description: "" }));
|
|
242
|
+
if (options.length === 0) return null;
|
|
243
|
+
questions.push({
|
|
244
|
+
id: fieldId,
|
|
245
|
+
header,
|
|
246
|
+
question,
|
|
247
|
+
options,
|
|
248
|
+
allowMultiple: true,
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (rawField.type === "string") {
|
|
254
|
+
const noteFieldId = Object.prototype.hasOwnProperty.call(properties, `${fieldId}__note`)
|
|
255
|
+
? `${fieldId}__note`
|
|
256
|
+
: undefined;
|
|
257
|
+
const options = readElicitationChoices(rawField.oneOf)
|
|
258
|
+
.filter((label) => label !== OTHER_OPTION_LABEL)
|
|
259
|
+
.map((label) => ({ label, description: "" }));
|
|
260
|
+
if (options.length === 0) return null;
|
|
261
|
+
questions.push({
|
|
262
|
+
id: fieldId,
|
|
263
|
+
header,
|
|
264
|
+
question,
|
|
265
|
+
options,
|
|
266
|
+
noteFieldId,
|
|
267
|
+
});
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return questions.length > 0 ? questions : null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function roundResultToElicitationContent(
|
|
278
|
+
questions: ParsedElicitationQuestion[],
|
|
279
|
+
result: RoundResult,
|
|
280
|
+
): Record<string, string | string[]> {
|
|
281
|
+
const content: Record<string, string | string[]> = {};
|
|
282
|
+
|
|
283
|
+
for (const question of questions) {
|
|
284
|
+
const answer = result.answers[question.id];
|
|
285
|
+
if (!answer) continue;
|
|
286
|
+
|
|
287
|
+
if (question.allowMultiple) {
|
|
288
|
+
const selected = Array.isArray(answer.selected) ? answer.selected : [answer.selected];
|
|
289
|
+
content[question.id] = selected;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const selected = Array.isArray(answer.selected) ? answer.selected[0] ?? "" : answer.selected;
|
|
294
|
+
content[question.id] = selected;
|
|
295
|
+
if (question.noteFieldId && selected === OTHER_OPTION_LABEL && answer.notes.trim().length > 0) {
|
|
296
|
+
content[question.noteFieldId] = answer.notes.trim();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return content;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function buildElicitationPromptTitle(request: SdkElicitationRequest, question: ParsedElicitationQuestion): string {
|
|
304
|
+
const parts = [
|
|
305
|
+
request.serverName ? `[${request.serverName}]` : "",
|
|
306
|
+
question.header,
|
|
307
|
+
question.question,
|
|
308
|
+
].filter((part) => part && part.trim().length > 0);
|
|
309
|
+
return parts.join("\n\n");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function promptElicitationWithDialogs(
|
|
313
|
+
request: SdkElicitationRequest,
|
|
314
|
+
questions: ParsedElicitationQuestion[],
|
|
315
|
+
ui: ExtensionUIContext,
|
|
316
|
+
signal: AbortSignal,
|
|
317
|
+
): Promise<SdkElicitationResult> {
|
|
318
|
+
const content: Record<string, string | string[]> = {};
|
|
319
|
+
|
|
320
|
+
for (const question of questions) {
|
|
321
|
+
const title = buildElicitationPromptTitle(request, question);
|
|
322
|
+
|
|
323
|
+
if (question.allowMultiple) {
|
|
324
|
+
const selected = await ui.select(title, question.options.map((option) => option.label), {
|
|
325
|
+
allowMultiple: true,
|
|
326
|
+
signal,
|
|
327
|
+
});
|
|
328
|
+
if (Array.isArray(selected)) {
|
|
329
|
+
if (selected.length === 0) return { action: "cancel" };
|
|
330
|
+
content[question.id] = selected;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (typeof selected === "string" && selected.length > 0) {
|
|
334
|
+
content[question.id] = [selected];
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
return { action: "cancel" };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const selected = await ui.select(title, [...question.options.map((option) => option.label), OTHER_OPTION_LABEL], { signal });
|
|
341
|
+
if (typeof selected !== "string" || selected.length === 0) {
|
|
342
|
+
return { action: "cancel" };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
content[question.id] = selected;
|
|
346
|
+
if (question.noteFieldId && selected === OTHER_OPTION_LABEL) {
|
|
347
|
+
const note = await ui.input(`${question.header} note`, "Explain your answer", { signal });
|
|
348
|
+
if (note === undefined) return { action: "cancel" };
|
|
349
|
+
if (note.trim().length > 0) {
|
|
350
|
+
content[question.noteFieldId] = note.trim();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return { action: "accept", content };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function createClaudeCodeElicitationHandler(
|
|
359
|
+
ui: ExtensionUIContext | undefined,
|
|
360
|
+
): ((request: SdkElicitationRequest, options: { signal: AbortSignal }) => Promise<SdkElicitationResult>) | undefined {
|
|
361
|
+
if (!ui) return undefined;
|
|
362
|
+
|
|
363
|
+
return async (request, { signal }) => {
|
|
364
|
+
if (request.mode === "url") {
|
|
365
|
+
return { action: "decline" };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const questions = parseAskUserQuestionsElicitation(request);
|
|
369
|
+
if (!questions) {
|
|
370
|
+
return { action: "decline" };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const interviewResult = await showInterviewRound(questions, { signal }, { ui } as any).catch(() => undefined);
|
|
374
|
+
if (interviewResult && Object.keys(interviewResult.answers).length > 0) {
|
|
375
|
+
return {
|
|
376
|
+
action: "accept",
|
|
377
|
+
content: roundResultToElicitationContent(questions, interviewResult),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return promptElicitationWithDialogs(request, questions, ui, signal);
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
175
385
|
// ---------------------------------------------------------------------------
|
|
176
386
|
// SDK options builder
|
|
177
387
|
// ---------------------------------------------------------------------------
|
|
@@ -182,8 +392,13 @@ export function makeStreamExhaustedErrorMessage(model: string, lastTextContent:
|
|
|
182
392
|
* Extracted for testability — callers can verify session persistence,
|
|
183
393
|
* beta flags, and other configuration without mocking the full SDK.
|
|
184
394
|
*/
|
|
185
|
-
export function buildSdkOptions(
|
|
395
|
+
export function buildSdkOptions(
|
|
396
|
+
modelId: string,
|
|
397
|
+
prompt: string,
|
|
398
|
+
extraOptions: Record<string, unknown> = {},
|
|
399
|
+
): Record<string, unknown> {
|
|
186
400
|
const mcpServers = buildWorkflowMcpServers();
|
|
401
|
+
const disallowedTools = ["AskUserQuestion"];
|
|
187
402
|
return {
|
|
188
403
|
pathToClaudeCodeExecutable: getClaudePath(),
|
|
189
404
|
model: modelId,
|
|
@@ -194,8 +409,10 @@ export function buildSdkOptions(modelId: string, prompt: string): Record<string,
|
|
|
194
409
|
allowDangerouslySkipPermissions: true,
|
|
195
410
|
settingSources: ["project"],
|
|
196
411
|
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
412
|
+
disallowedTools,
|
|
197
413
|
...(mcpServers ? { mcpServers } : {}),
|
|
198
414
|
betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
|
|
415
|
+
...extraOptions,
|
|
199
416
|
};
|
|
200
417
|
}
|
|
201
418
|
|
|
@@ -359,7 +576,17 @@ async function pumpSdkMessages(
|
|
|
359
576
|
}
|
|
360
577
|
|
|
361
578
|
const prompt = buildPromptFromContext(context);
|
|
362
|
-
const sdkOpts = buildSdkOptions(
|
|
579
|
+
const sdkOpts = buildSdkOptions(
|
|
580
|
+
modelId,
|
|
581
|
+
prompt,
|
|
582
|
+
typeof (options as ClaudeCodeStreamOptions | undefined)?.extensionUIContext === "object"
|
|
583
|
+
? {
|
|
584
|
+
onElicitation: createClaudeCodeElicitationHandler(
|
|
585
|
+
(options as ClaudeCodeStreamOptions | undefined)?.extensionUIContext,
|
|
586
|
+
),
|
|
587
|
+
}
|
|
588
|
+
: {},
|
|
589
|
+
);
|
|
363
590
|
|
|
364
591
|
const queryResult = sdk.query({
|
|
365
592
|
prompt,
|
|
@@ -7,9 +7,12 @@ import {
|
|
|
7
7
|
makeStreamExhaustedErrorMessage,
|
|
8
8
|
buildPromptFromContext,
|
|
9
9
|
buildSdkOptions,
|
|
10
|
+
createClaudeCodeElicitationHandler,
|
|
10
11
|
extractToolResultsFromSdkUserMessage,
|
|
11
12
|
getClaudeLookupCommand,
|
|
13
|
+
parseAskUserQuestionsElicitation,
|
|
12
14
|
parseClaudeLookupOutput,
|
|
15
|
+
roundResultToElicitationContent,
|
|
13
16
|
} from "../stream-adapter.ts";
|
|
14
17
|
import type { Context, Message } from "@gsd/pi-ai";
|
|
15
18
|
import type { SDKUserMessage } from "../sdk-types.ts";
|
|
@@ -217,6 +220,35 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
217
220
|
assert.equal(srv.env.GSD_CLI_PATH, "/tmp/gsd");
|
|
218
221
|
assert.equal(srv.env.GSD_PERSIST_WRITE_GATE_STATE, "1");
|
|
219
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"]);
|
|
220
252
|
} finally {
|
|
221
253
|
process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
|
|
222
254
|
process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
|
|
@@ -252,6 +284,9 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
252
284
|
const mcpServers = (options as any).mcpServers;
|
|
253
285
|
if (mcpServers) {
|
|
254
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"]);
|
|
255
290
|
}
|
|
256
291
|
rmSync(emptyDir, { recursive: true, force: true });
|
|
257
292
|
} finally {
|
|
@@ -298,6 +333,7 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
298
333
|
assert.equal(srv.env.GSD_CLI_PATH, "/tmp/gsd");
|
|
299
334
|
assert.equal(srv.env.GSD_PERSIST_WRITE_GATE_STATE, "1");
|
|
300
335
|
assert.equal(srv.env.GSD_WORKFLOW_PROJECT_ROOT, resolvedRepoDir);
|
|
336
|
+
assert.deepEqual(options.disallowedTools, ["AskUserQuestion"]);
|
|
301
337
|
} finally {
|
|
302
338
|
process.chdir(originalCwd);
|
|
303
339
|
rmSync(repoDir, { recursive: true, force: true });
|
|
@@ -309,6 +345,175 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
309
345
|
process.env.GSD_CLI_PATH = prev.GSD_CLI_PATH;
|
|
310
346
|
}
|
|
311
347
|
});
|
|
348
|
+
|
|
349
|
+
test("buildSdkOptions preserves runtime callbacks such as onElicitation", () => {
|
|
350
|
+
const prev = {
|
|
351
|
+
GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
|
|
352
|
+
GSD_WORKFLOW_MCP_NAME: process.env.GSD_WORKFLOW_MCP_NAME,
|
|
353
|
+
GSD_WORKFLOW_MCP_ARGS: process.env.GSD_WORKFLOW_MCP_ARGS,
|
|
354
|
+
GSD_WORKFLOW_MCP_ENV: process.env.GSD_WORKFLOW_MCP_ENV,
|
|
355
|
+
GSD_WORKFLOW_MCP_CWD: process.env.GSD_WORKFLOW_MCP_CWD,
|
|
356
|
+
};
|
|
357
|
+
const onElicitation = async () => ({ action: "decline" as const });
|
|
358
|
+
try {
|
|
359
|
+
delete process.env.GSD_WORKFLOW_MCP_COMMAND;
|
|
360
|
+
delete process.env.GSD_WORKFLOW_MCP_NAME;
|
|
361
|
+
delete process.env.GSD_WORKFLOW_MCP_ARGS;
|
|
362
|
+
delete process.env.GSD_WORKFLOW_MCP_ENV;
|
|
363
|
+
delete process.env.GSD_WORKFLOW_MCP_CWD;
|
|
364
|
+
const options = buildSdkOptions("claude-sonnet-4-20250514", "test", { onElicitation });
|
|
365
|
+
assert.equal(options.onElicitation, onElicitation);
|
|
366
|
+
} finally {
|
|
367
|
+
process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
|
|
368
|
+
process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
|
|
369
|
+
process.env.GSD_WORKFLOW_MCP_ARGS = prev.GSD_WORKFLOW_MCP_ARGS;
|
|
370
|
+
process.env.GSD_WORKFLOW_MCP_ENV = prev.GSD_WORKFLOW_MCP_ENV;
|
|
371
|
+
process.env.GSD_WORKFLOW_MCP_CWD = prev.GSD_WORKFLOW_MCP_CWD;
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
describe("stream-adapter — MCP elicitation bridge", () => {
|
|
377
|
+
const askUserQuestionsRequest = {
|
|
378
|
+
serverName: "gsd-workflow",
|
|
379
|
+
message: "Please answer the following question(s).",
|
|
380
|
+
mode: "form" as const,
|
|
381
|
+
requestedSchema: {
|
|
382
|
+
type: "object" as const,
|
|
383
|
+
properties: {
|
|
384
|
+
storage_scope: {
|
|
385
|
+
type: "string",
|
|
386
|
+
title: "Storage",
|
|
387
|
+
description: "Does this app need to sync across devices?",
|
|
388
|
+
oneOf: [
|
|
389
|
+
{ const: "Local-only (Recommended)", title: "Local-only (Recommended)" },
|
|
390
|
+
{ const: "Cloud-synced", title: "Cloud-synced" },
|
|
391
|
+
{ const: "None of the above", title: "None of the above" },
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
storage_scope__note: {
|
|
395
|
+
type: "string",
|
|
396
|
+
title: "Storage Note",
|
|
397
|
+
description: "Optional note for None of the above.",
|
|
398
|
+
},
|
|
399
|
+
platform: {
|
|
400
|
+
type: "array",
|
|
401
|
+
title: "Platform",
|
|
402
|
+
description: "Where should it run?",
|
|
403
|
+
items: {
|
|
404
|
+
anyOf: [
|
|
405
|
+
{ const: "Web", title: "Web" },
|
|
406
|
+
{ const: "Desktop", title: "Desktop" },
|
|
407
|
+
{ const: "Mobile", title: "Mobile" },
|
|
408
|
+
],
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
test("parseAskUserQuestionsElicitation rebuilds interview questions from the MCP schema", () => {
|
|
416
|
+
const questions = parseAskUserQuestionsElicitation(askUserQuestionsRequest);
|
|
417
|
+
assert.deepEqual(questions, [
|
|
418
|
+
{
|
|
419
|
+
id: "storage_scope",
|
|
420
|
+
header: "Storage",
|
|
421
|
+
question: "Does this app need to sync across devices?",
|
|
422
|
+
options: [
|
|
423
|
+
{ label: "Local-only (Recommended)", description: "" },
|
|
424
|
+
{ label: "Cloud-synced", description: "" },
|
|
425
|
+
],
|
|
426
|
+
noteFieldId: "storage_scope__note",
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
id: "platform",
|
|
430
|
+
header: "Platform",
|
|
431
|
+
question: "Where should it run?",
|
|
432
|
+
options: [
|
|
433
|
+
{ label: "Web", description: "" },
|
|
434
|
+
{ label: "Desktop", description: "" },
|
|
435
|
+
{ label: "Mobile", description: "" },
|
|
436
|
+
],
|
|
437
|
+
allowMultiple: true,
|
|
438
|
+
},
|
|
439
|
+
]);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("roundResultToElicitationContent preserves notes for None of the above", () => {
|
|
443
|
+
const questions = parseAskUserQuestionsElicitation(askUserQuestionsRequest);
|
|
444
|
+
assert.ok(questions);
|
|
445
|
+
|
|
446
|
+
const content = roundResultToElicitationContent(questions, {
|
|
447
|
+
endInterview: false,
|
|
448
|
+
answers: {
|
|
449
|
+
storage_scope: {
|
|
450
|
+
selected: "None of the above",
|
|
451
|
+
notes: "Needs selective sync later",
|
|
452
|
+
},
|
|
453
|
+
platform: {
|
|
454
|
+
selected: ["Web", "Desktop"],
|
|
455
|
+
notes: "",
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
assert.deepEqual(content, {
|
|
461
|
+
storage_scope: "None of the above",
|
|
462
|
+
storage_scope__note: "Needs selective sync later",
|
|
463
|
+
platform: ["Web", "Desktop"],
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("createClaudeCodeElicitationHandler accepts interview-style answers from custom UI", async () => {
|
|
468
|
+
const handler = createClaudeCodeElicitationHandler({
|
|
469
|
+
custom: async (_factory: any) => ({
|
|
470
|
+
endInterview: false,
|
|
471
|
+
answers: {
|
|
472
|
+
storage_scope: {
|
|
473
|
+
selected: "Cloud-synced",
|
|
474
|
+
notes: "",
|
|
475
|
+
},
|
|
476
|
+
platform: {
|
|
477
|
+
selected: ["Web", "Mobile"],
|
|
478
|
+
notes: "",
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
}),
|
|
482
|
+
} as any);
|
|
483
|
+
|
|
484
|
+
assert.ok(handler);
|
|
485
|
+
const result = await handler!(askUserQuestionsRequest, { signal: new AbortController().signal });
|
|
486
|
+
assert.deepEqual(result, {
|
|
487
|
+
action: "accept",
|
|
488
|
+
content: {
|
|
489
|
+
storage_scope: "Cloud-synced",
|
|
490
|
+
platform: ["Web", "Mobile"],
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("createClaudeCodeElicitationHandler falls back to dialog prompts when custom UI is unavailable", async () => {
|
|
496
|
+
const ui = {
|
|
497
|
+
custom: async () => undefined,
|
|
498
|
+
select: async (_title: string, options: string[], opts?: { allowMultiple?: boolean }) => {
|
|
499
|
+
if (opts?.allowMultiple) return ["Desktop", "Mobile"];
|
|
500
|
+
return options.includes("None of the above") ? "None of the above" : options[0];
|
|
501
|
+
},
|
|
502
|
+
input: async () => "CLI-only deployment target",
|
|
503
|
+
};
|
|
504
|
+
const handler = createClaudeCodeElicitationHandler(ui as any);
|
|
505
|
+
assert.ok(handler);
|
|
506
|
+
|
|
507
|
+
const result = await handler!(askUserQuestionsRequest, { signal: new AbortController().signal });
|
|
508
|
+
assert.deepEqual(result, {
|
|
509
|
+
action: "accept",
|
|
510
|
+
content: {
|
|
511
|
+
storage_scope: "None of the above",
|
|
512
|
+
storage_scope__note: "CLI-only deployment target",
|
|
513
|
+
platform: ["Desktop", "Mobile"],
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
});
|
|
312
517
|
});
|
|
313
518
|
|
|
314
519
|
describe("stream-adapter — Windows Claude path lookup (#3770)", () => {
|
|
@@ -25,10 +25,17 @@ export interface ModelSelectionResult {
|
|
|
25
25
|
export function resolvePreferredModelConfig(
|
|
26
26
|
unitType: string,
|
|
27
27
|
autoModeStartModel: { provider: string; id: string } | null,
|
|
28
|
+
/** When false, only return explicit per-phase model configs — do not
|
|
29
|
+
* synthesize a routing ceiling from dynamic_routing.tier_models (#3962). */
|
|
30
|
+
isAutoMode = true,
|
|
28
31
|
) {
|
|
29
32
|
const explicitConfig = resolveModelWithFallbacksForUnit(unitType);
|
|
30
33
|
if (explicitConfig) return explicitConfig;
|
|
31
34
|
|
|
35
|
+
// In interactive mode, don't synthesize a routing-based model config.
|
|
36
|
+
// The user's session model (/model) should be used as-is (#3962).
|
|
37
|
+
if (!isAutoMode) return undefined;
|
|
38
|
+
|
|
32
39
|
const routingConfig = resolveDynamicRoutingConfig();
|
|
33
40
|
if (!routingConfig.enabled || !routingConfig.tier_models) return undefined;
|
|
34
41
|
|
|
@@ -62,8 +69,11 @@ export async function selectAndApplyModel(
|
|
|
62
69
|
verbose: boolean,
|
|
63
70
|
autoModeStartModel: { provider: string; id: string } | null,
|
|
64
71
|
retryContext?: { isRetry: boolean; previousTier?: string },
|
|
72
|
+
/** When false (interactive/guided-flow), skip dynamic routing and use the session model.
|
|
73
|
+
* Dynamic routing only applies in auto-mode where cost optimization is expected. (#3962) */
|
|
74
|
+
isAutoMode = true,
|
|
65
75
|
): Promise<ModelSelectionResult> {
|
|
66
|
-
const modelConfig = resolvePreferredModelConfig(unitType, autoModeStartModel);
|
|
76
|
+
const modelConfig = resolvePreferredModelConfig(unitType, autoModeStartModel, isAutoMode);
|
|
67
77
|
let routing: { tier: string; modelDowngraded: boolean } | null = null;
|
|
68
78
|
let appliedModel: Model<Api> | null = null;
|
|
69
79
|
|
|
@@ -71,7 +81,13 @@ export async function selectAndApplyModel(
|
|
|
71
81
|
const availableModels = ctx.modelRegistry.getAvailable();
|
|
72
82
|
|
|
73
83
|
// ─── Dynamic Model Routing ─────────────────────────────────────────
|
|
84
|
+
// Dynamic routing (complexity-based downgrading) only applies in auto-mode.
|
|
85
|
+
// Interactive/guided-flow dispatches use the user's session model directly,
|
|
86
|
+
// respecting their /model selection without silent downgrades (#3962).
|
|
74
87
|
const routingConfig = resolveDynamicRoutingConfig();
|
|
88
|
+
if (!isAutoMode) {
|
|
89
|
+
routingConfig.enabled = false;
|
|
90
|
+
}
|
|
75
91
|
let effectiveModelConfig = modelConfig;
|
|
76
92
|
let routingTierLabel = "";
|
|
77
93
|
|
|
@@ -123,12 +139,11 @@ export async function selectAndApplyModel(
|
|
|
123
139
|
const escalated = escalateTier(retryContext.previousTier as ComplexityTier);
|
|
124
140
|
if (escalated) {
|
|
125
141
|
classification = { ...classification, tier: escalated, reason: "escalated after failure" };
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
142
|
+
// Always notify on tier escalation — model changes should be visible (#3962)
|
|
143
|
+
ctx.ui.notify(
|
|
144
|
+
`Tier escalation: ${retryContext.previousTier} → ${escalated} (retry after failure)`,
|
|
145
|
+
"info",
|
|
146
|
+
);
|
|
132
147
|
}
|
|
133
148
|
}
|
|
134
149
|
|
|
@@ -195,24 +210,23 @@ export async function selectAndApplyModel(
|
|
|
195
210
|
primary: routingResult.modelId,
|
|
196
211
|
fallbacks: routingResult.fallbacks,
|
|
197
212
|
};
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
213
|
+
// Always notify on model downgrade — users should see when their
|
|
214
|
+
// model selection is overridden, not just in verbose mode (#3962).
|
|
215
|
+
if (routingResult.selectionMethod === "capability-scored" && routingResult.capabilityScores) {
|
|
216
|
+
const tierLbl = tierLabel(classification.tier);
|
|
217
|
+
const scores = Object.entries(routingResult.capabilityScores)
|
|
218
|
+
.sort(([, a], [, b]) => b - a)
|
|
219
|
+
.map(([id, score]) => `${id}: ${score.toFixed(1)}`)
|
|
220
|
+
.join(", ");
|
|
221
|
+
ctx.ui.notify(
|
|
222
|
+
`Dynamic routing [${tierLbl}]: ${routingResult.modelId} (capability-scored) — ${scores}`,
|
|
223
|
+
"info",
|
|
224
|
+
);
|
|
225
|
+
} else {
|
|
226
|
+
ctx.ui.notify(
|
|
227
|
+
`Dynamic routing [${tierLabel(classification.tier)}]: ${routingResult.modelId} (${classification.reason})`,
|
|
228
|
+
"info",
|
|
229
|
+
);
|
|
216
230
|
}
|
|
217
231
|
}
|
|
218
232
|
routingTierLabel = ` [${tierLabel(classification.tier)}]`;
|
|
@@ -997,7 +997,7 @@ export async function buildDiscussMilestonePrompt(mid: string, midTitle: string,
|
|
|
997
997
|
milestoneId: mid,
|
|
998
998
|
milestoneTitle: midTitle,
|
|
999
999
|
inlinedTemplates: discussTemplates,
|
|
1000
|
-
structuredQuestionsAvailable: "
|
|
1000
|
+
structuredQuestionsAvailable: "false",
|
|
1001
1001
|
commitInstruction: "Do not commit planning artifacts — .gsd/ is managed externally.",
|
|
1002
1002
|
fastPathInstruction: "",
|
|
1003
1003
|
});
|
|
@@ -1503,7 +1503,9 @@ export async function buildCompleteMilestonePrompt(
|
|
|
1503
1503
|
try {
|
|
1504
1504
|
const { isDbAvailable, getMilestoneSlices } = await import("./gsd-db.js");
|
|
1505
1505
|
if (isDbAvailable()) {
|
|
1506
|
-
sliceIds = getMilestoneSlices(mid)
|
|
1506
|
+
sliceIds = getMilestoneSlices(mid)
|
|
1507
|
+
.filter(s => s.status !== "skipped")
|
|
1508
|
+
.map(s => s.id);
|
|
1507
1509
|
}
|
|
1508
1510
|
} catch (err) {
|
|
1509
1511
|
logWarning("prompt", `buildCompleteMilestonePrompt DB lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1597,7 +1599,9 @@ export async function buildValidateMilestonePrompt(
|
|
|
1597
1599
|
try {
|
|
1598
1600
|
const { isDbAvailable, getMilestoneSlices } = await import("./gsd-db.js");
|
|
1599
1601
|
if (isDbAvailable()) {
|
|
1600
|
-
valSliceIds = getMilestoneSlices(mid)
|
|
1602
|
+
valSliceIds = getMilestoneSlices(mid)
|
|
1603
|
+
.filter(s => s.status !== "skipped")
|
|
1604
|
+
.map(s => s.id);
|
|
1601
1605
|
}
|
|
1602
1606
|
} catch (err) {
|
|
1603
1607
|
logWarning("prompt", `buildValidateMilestonePrompt slice IDs lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|