gsd-pi 2.70.0 → 2.70.1-dev.3591dcf

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/dist/loader.js +4 -0
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +150 -2
  3. package/dist/resources/extensions/gsd/auto-model-selection.js +33 -19
  4. package/dist/resources/extensions/gsd/auto-prompts.js +7 -3
  5. package/dist/resources/extensions/gsd/auto-start.js +25 -1
  6. package/dist/resources/extensions/gsd/auto.js +12 -8
  7. package/dist/resources/extensions/gsd/commands-handlers.js +22 -8
  8. package/dist/resources/extensions/gsd/doctor-engine-checks.js +12 -0
  9. package/dist/resources/extensions/gsd/doctor-format.js +2 -0
  10. package/dist/resources/extensions/gsd/guided-flow.js +21 -10
  11. package/dist/resources/extensions/gsd/pre-execution-checks.js +5 -3
  12. package/dist/resources/extensions/gsd/validate-directory.js +30 -12
  13. package/dist/resources/extensions/gsd/workflow-mcp.js +11 -0
  14. package/dist/resources/extensions/slash-commands/audit.js +2 -1
  15. package/dist/resources/extensions/subagent/isolation.js +4 -2
  16. package/dist/update-check.d.ts +1 -0
  17. package/dist/update-check.js +30 -27
  18. package/dist/update-cmd.js +3 -11
  19. package/dist/web/standalone/.next/BUILD_ID +1 -1
  20. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  21. package/dist/web/standalone/.next/build-manifest.json +3 -3
  22. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  23. package/dist/web/standalone/.next/required-server-files.json +4 -4
  24. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  25. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  35. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  45. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  46. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  47. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  48. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  49. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  50. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  51. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  53. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  63. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  83. package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  93. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  99. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  115. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +2 -2
  119. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  128. package/dist/web/standalone/.next/server/app/index.html +1 -1
  129. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  130. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  131. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  132. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  133. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  134. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  135. package/dist/web/standalone/.next/server/app/page.js +2 -2
  136. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  137. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  138. package/dist/web/standalone/.next/server/chunks/63.js +3 -3
  139. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  140. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  141. package/dist/web/standalone/.next/server/middleware.js +2 -2
  142. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  143. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  144. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  145. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  146. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  147. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  148. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  149. package/dist/web/standalone/.next/static/chunks/app/page-f1e30ab6bb269149.js +1 -0
  150. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  151. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  152. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  153. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  154. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  155. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  156. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  157. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  158. package/dist/web/standalone/server.js +1 -1
  159. package/dist/web-mode.js +4 -0
  160. package/package.json +11 -11
  161. package/packages/mcp-server/dist/workflow-tools.d.ts +2 -0
  162. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  163. package/packages/mcp-server/dist/workflow-tools.js +35 -3
  164. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  165. package/packages/mcp-server/src/import-candidates.test.ts +48 -0
  166. package/packages/mcp-server/src/workflow-tools.ts +34 -1
  167. package/packages/pi-agent-core/dist/agent.d.ts +8 -0
  168. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  169. package/packages/pi-agent-core/dist/agent.js +3 -0
  170. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  171. package/packages/pi-agent-core/src/agent.test.ts +82 -0
  172. package/packages/pi-agent-core/src/agent.ts +12 -0
  173. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts +1 -0
  174. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
  175. package/packages/pi-coding-agent/dist/core/lsp/config.js +38 -15
  176. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
  177. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  178. package/packages/pi-coding-agent/dist/core/sdk.js +10 -0
  179. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  180. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.d.ts.map +1 -1
  181. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +3 -1
  182. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
  183. package/packages/pi-coding-agent/package.json +1 -1
  184. package/packages/pi-coding-agent/src/core/lsp/config.ts +43 -17
  185. package/packages/pi-coding-agent/src/core/sdk.ts +8 -0
  186. package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +7 -5
  187. package/pkg/package.json +1 -1
  188. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +227 -2
  189. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +172 -0
  190. package/src/resources/extensions/gsd/auto-model-selection.ts +39 -25
  191. package/src/resources/extensions/gsd/auto-prompts.ts +7 -3
  192. package/src/resources/extensions/gsd/auto-start.ts +34 -1
  193. package/src/resources/extensions/gsd/auto.ts +12 -8
  194. package/src/resources/extensions/gsd/commands-handlers.ts +22 -7
  195. package/src/resources/extensions/gsd/doctor-engine-checks.ts +14 -0
  196. package/src/resources/extensions/gsd/doctor-format.ts +1 -0
  197. package/src/resources/extensions/gsd/doctor-types.ts +1 -0
  198. package/src/resources/extensions/gsd/guided-flow.ts +24 -8
  199. package/src/resources/extensions/gsd/pre-execution-checks.ts +6 -3
  200. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +43 -0
  201. package/src/resources/extensions/gsd/tests/interactive-routing-bypass.test.ts +207 -0
  202. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +48 -1
  203. package/src/resources/extensions/gsd/tests/resource-loader-import-path.test.ts +8 -7
  204. package/src/resources/extensions/gsd/tests/validate-directory.test.ts +33 -1
  205. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +87 -1
  206. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +25 -0
  207. package/src/resources/extensions/gsd/validate-directory.ts +33 -11
  208. package/src/resources/extensions/gsd/workflow-mcp.ts +15 -0
  209. package/src/resources/extensions/slash-commands/audit.ts +2 -1
  210. package/src/resources/extensions/subagent/isolation.ts +4 -3
  211. package/dist/web/standalone/.next/static/chunks/app/page-7115e62689b5fd84.js +0 -1
  212. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  213. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  214. /package/dist/web/standalone/.next/static/{Nl6lg7zP5dNgNBV1107v1 → KdlODhIktLmeRKpLpHdKb}/_buildManifest.js +0 -0
  215. /package/dist/web/standalone/.next/static/{Nl6lg7zP5dNgNBV1107v1 → KdlODhIktLmeRKpLpHdKb}/_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,7 +392,11 @@ 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(modelId: string, prompt: string): Record<string, unknown> {
395
+ export function buildSdkOptions(
396
+ modelId: string,
397
+ prompt: string,
398
+ extraOptions: Record<string, unknown> = {},
399
+ ): Record<string, unknown> {
186
400
  const mcpServers = buildWorkflowMcpServers();
187
401
  return {
188
402
  pathToClaudeCodeExecutable: getClaudePath(),
@@ -196,6 +410,7 @@ export function buildSdkOptions(modelId: string, prompt: string): Record<string,
196
410
  systemPrompt: { type: "preset", preset: "claude_code" },
197
411
  ...(mcpServers ? { mcpServers } : {}),
198
412
  betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
413
+ ...extraOptions,
199
414
  };
200
415
  }
201
416
 
@@ -359,7 +574,17 @@ async function pumpSdkMessages(
359
574
  }
360
575
 
361
576
  const prompt = buildPromptFromContext(context);
362
- const sdkOpts = buildSdkOptions(modelId, prompt);
577
+ const sdkOpts = buildSdkOptions(
578
+ modelId,
579
+ prompt,
580
+ typeof (options as ClaudeCodeStreamOptions | undefined)?.extensionUIContext === "object"
581
+ ? {
582
+ onElicitation: createClaudeCodeElicitationHandler(
583
+ (options as ClaudeCodeStreamOptions | undefined)?.extensionUIContext,
584
+ ),
585
+ }
586
+ : {},
587
+ );
363
588
 
364
589
  const queryResult = sdk.query({
365
590
  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";
@@ -309,6 +312,175 @@ describe("stream-adapter — session persistence (#2859)", () => {
309
312
  process.env.GSD_CLI_PATH = prev.GSD_CLI_PATH;
310
313
  }
311
314
  });
315
+
316
+ test("buildSdkOptions preserves runtime callbacks such as onElicitation", () => {
317
+ const prev = {
318
+ GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
319
+ GSD_WORKFLOW_MCP_NAME: process.env.GSD_WORKFLOW_MCP_NAME,
320
+ GSD_WORKFLOW_MCP_ARGS: process.env.GSD_WORKFLOW_MCP_ARGS,
321
+ GSD_WORKFLOW_MCP_ENV: process.env.GSD_WORKFLOW_MCP_ENV,
322
+ GSD_WORKFLOW_MCP_CWD: process.env.GSD_WORKFLOW_MCP_CWD,
323
+ };
324
+ const onElicitation = async () => ({ action: "decline" as const });
325
+ try {
326
+ delete process.env.GSD_WORKFLOW_MCP_COMMAND;
327
+ delete process.env.GSD_WORKFLOW_MCP_NAME;
328
+ delete process.env.GSD_WORKFLOW_MCP_ARGS;
329
+ delete process.env.GSD_WORKFLOW_MCP_ENV;
330
+ delete process.env.GSD_WORKFLOW_MCP_CWD;
331
+ const options = buildSdkOptions("claude-sonnet-4-20250514", "test", { onElicitation });
332
+ assert.equal(options.onElicitation, onElicitation);
333
+ } finally {
334
+ process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
335
+ process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
336
+ process.env.GSD_WORKFLOW_MCP_ARGS = prev.GSD_WORKFLOW_MCP_ARGS;
337
+ process.env.GSD_WORKFLOW_MCP_ENV = prev.GSD_WORKFLOW_MCP_ENV;
338
+ process.env.GSD_WORKFLOW_MCP_CWD = prev.GSD_WORKFLOW_MCP_CWD;
339
+ }
340
+ });
341
+ });
342
+
343
+ describe("stream-adapter — MCP elicitation bridge", () => {
344
+ const askUserQuestionsRequest = {
345
+ serverName: "gsd-workflow",
346
+ message: "Please answer the following question(s).",
347
+ mode: "form" as const,
348
+ requestedSchema: {
349
+ type: "object" as const,
350
+ properties: {
351
+ storage_scope: {
352
+ type: "string",
353
+ title: "Storage",
354
+ description: "Does this app need to sync across devices?",
355
+ oneOf: [
356
+ { const: "Local-only (Recommended)", title: "Local-only (Recommended)" },
357
+ { const: "Cloud-synced", title: "Cloud-synced" },
358
+ { const: "None of the above", title: "None of the above" },
359
+ ],
360
+ },
361
+ storage_scope__note: {
362
+ type: "string",
363
+ title: "Storage Note",
364
+ description: "Optional note for None of the above.",
365
+ },
366
+ platform: {
367
+ type: "array",
368
+ title: "Platform",
369
+ description: "Where should it run?",
370
+ items: {
371
+ anyOf: [
372
+ { const: "Web", title: "Web" },
373
+ { const: "Desktop", title: "Desktop" },
374
+ { const: "Mobile", title: "Mobile" },
375
+ ],
376
+ },
377
+ },
378
+ },
379
+ },
380
+ };
381
+
382
+ test("parseAskUserQuestionsElicitation rebuilds interview questions from the MCP schema", () => {
383
+ const questions = parseAskUserQuestionsElicitation(askUserQuestionsRequest);
384
+ assert.deepEqual(questions, [
385
+ {
386
+ id: "storage_scope",
387
+ header: "Storage",
388
+ question: "Does this app need to sync across devices?",
389
+ options: [
390
+ { label: "Local-only (Recommended)", description: "" },
391
+ { label: "Cloud-synced", description: "" },
392
+ ],
393
+ noteFieldId: "storage_scope__note",
394
+ },
395
+ {
396
+ id: "platform",
397
+ header: "Platform",
398
+ question: "Where should it run?",
399
+ options: [
400
+ { label: "Web", description: "" },
401
+ { label: "Desktop", description: "" },
402
+ { label: "Mobile", description: "" },
403
+ ],
404
+ allowMultiple: true,
405
+ },
406
+ ]);
407
+ });
408
+
409
+ test("roundResultToElicitationContent preserves notes for None of the above", () => {
410
+ const questions = parseAskUserQuestionsElicitation(askUserQuestionsRequest);
411
+ assert.ok(questions);
412
+
413
+ const content = roundResultToElicitationContent(questions, {
414
+ endInterview: false,
415
+ answers: {
416
+ storage_scope: {
417
+ selected: "None of the above",
418
+ notes: "Needs selective sync later",
419
+ },
420
+ platform: {
421
+ selected: ["Web", "Desktop"],
422
+ notes: "",
423
+ },
424
+ },
425
+ });
426
+
427
+ assert.deepEqual(content, {
428
+ storage_scope: "None of the above",
429
+ storage_scope__note: "Needs selective sync later",
430
+ platform: ["Web", "Desktop"],
431
+ });
432
+ });
433
+
434
+ test("createClaudeCodeElicitationHandler accepts interview-style answers from custom UI", async () => {
435
+ const handler = createClaudeCodeElicitationHandler({
436
+ custom: async (_factory: any) => ({
437
+ endInterview: false,
438
+ answers: {
439
+ storage_scope: {
440
+ selected: "Cloud-synced",
441
+ notes: "",
442
+ },
443
+ platform: {
444
+ selected: ["Web", "Mobile"],
445
+ notes: "",
446
+ },
447
+ },
448
+ }),
449
+ } as any);
450
+
451
+ assert.ok(handler);
452
+ const result = await handler!(askUserQuestionsRequest, { signal: new AbortController().signal });
453
+ assert.deepEqual(result, {
454
+ action: "accept",
455
+ content: {
456
+ storage_scope: "Cloud-synced",
457
+ platform: ["Web", "Mobile"],
458
+ },
459
+ });
460
+ });
461
+
462
+ test("createClaudeCodeElicitationHandler falls back to dialog prompts when custom UI is unavailable", async () => {
463
+ const ui = {
464
+ custom: async () => undefined,
465
+ select: async (_title: string, options: string[], opts?: { allowMultiple?: boolean }) => {
466
+ if (opts?.allowMultiple) return ["Desktop", "Mobile"];
467
+ return options.includes("None of the above") ? "None of the above" : options[0];
468
+ },
469
+ input: async () => "CLI-only deployment target",
470
+ };
471
+ const handler = createClaudeCodeElicitationHandler(ui as any);
472
+ assert.ok(handler);
473
+
474
+ const result = await handler!(askUserQuestionsRequest, { signal: new AbortController().signal });
475
+ assert.deepEqual(result, {
476
+ action: "accept",
477
+ content: {
478
+ storage_scope: "None of the above",
479
+ storage_scope__note: "CLI-only deployment target",
480
+ platform: ["Desktop", "Mobile"],
481
+ },
482
+ });
483
+ });
312
484
  });
313
485
 
314
486
  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
- if (verbose) {
127
- ctx.ui.notify(
128
- `Tier escalation: ${retryContext.previousTier} → ${escalated} (retry after failure)`,
129
- "info",
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
- if (verbose) {
199
- if (routingResult.selectionMethod === "capability-scored" && routingResult.capabilityScores) {
200
- // Verbose scoring breakdown for capability-scored decisions (D-20)
201
- const tierLbl = tierLabel(classification.tier);
202
- const scores = Object.entries(routingResult.capabilityScores)
203
- .sort(([, a], [, b]) => b - a)
204
- .map(([id, score]) => `${id}: ${score.toFixed(1)}`)
205
- .join(", ");
206
- ctx.ui.notify(
207
- `Dynamic routing [${tierLbl}]: ${routingResult.modelId} (capability-scored) — ${scores}`,
208
- "info",
209
- );
210
- } else {
211
- ctx.ui.notify(
212
- `Dynamic routing [${tierLabel(classification.tier)}]: ${routingResult.modelId} (${classification.reason})`,
213
- "info",
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: "true",
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).map(s => s.id);
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).map(s => s.id);
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)}`);
@@ -83,7 +83,7 @@ import { join } from "node:path";
83
83
  import { sep as pathSep } from "node:path";
84
84
 
85
85
  import { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js";
86
- import { resolveDefaultSessionModel } from "./preferences-models.js";
86
+ import { resolveDefaultSessionModel, resolveDynamicRoutingConfig } from "./preferences-models.js";
87
87
  import type { WorktreeResolver } from "./worktree-resolver.js";
88
88
 
89
89
  export interface BootstrapDeps {
@@ -778,6 +778,39 @@ export async function bootstrapAutoSession(
778
778
  : "Will loop until milestone complete.";
779
779
  ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
780
780
 
781
+ // Show dynamic routing status so users know upfront if models will be
782
+ // downgraded for simple tasks (#3962).
783
+ // Use the same effective logic as selectAndApplyModel: check flat-rate
784
+ // provider suppression and resolve the actual ceiling model.
785
+ const routingConfig = resolveDynamicRoutingConfig();
786
+ const startModelLabel = s.autoModeStartModel
787
+ ? `${s.autoModeStartModel.provider}/${s.autoModeStartModel.id}`
788
+ : ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "default";
789
+
790
+ // Flat-rate providers (e.g. GitHub Copilot, claude-code) suppress routing
791
+ // at dispatch time (#3453) — reflect that in the banner.
792
+ const { isFlatRateProvider } = await import("./auto-model-selection.js");
793
+ const effectiveProvider = s.autoModeStartModel?.provider ?? ctx.model?.provider;
794
+ const effectivelyEnabled = routingConfig.enabled
795
+ && !(effectiveProvider && isFlatRateProvider(effectiveProvider));
796
+
797
+ // The actual ceiling may come from tier_models.heavy, not the start model.
798
+ const effectiveCeiling = (routingConfig.enabled && routingConfig.tier_models?.heavy)
799
+ ? routingConfig.tier_models.heavy
800
+ : startModelLabel;
801
+
802
+ if (effectivelyEnabled) {
803
+ ctx.ui.notify(
804
+ `Dynamic routing: enabled — simple tasks may use cheaper models (ceiling: ${effectiveCeiling})`,
805
+ "info",
806
+ );
807
+ } else {
808
+ ctx.ui.notify(
809
+ `Dynamic routing: disabled — all tasks will use ${startModelLabel}`,
810
+ "info",
811
+ );
812
+ }
813
+
781
814
  updateSessionLock(
782
815
  lockBase(),
783
816
  "starting",
@@ -125,9 +125,9 @@ import {
125
125
  } from "./metrics.js";
126
126
  import { setLogBasePath, logWarning, logError } from "./workflow-logger.js";
127
127
  import { homedir } from "node:os";
128
- import { join, dirname } from "node:path";
128
+ import { join } from "node:path";
129
+ import { pathToFileURL } from "node:url";
129
130
  import { readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
130
- import { createRequire } from "node:module";
131
131
  import { atomicWriteSync } from "./atomic-write.js";
132
132
  import {
133
133
  autoCommitCurrentBranch,
@@ -1334,13 +1334,17 @@ export async function startAuto(
1334
1334
  restoreHookState(s.basePath);
1335
1335
  // Re-sync managed resources on resume so long-lived auto sessions pick up
1336
1336
  // bundled extension updates before resume-time verification/state logic runs.
1337
+ // GSD_PKG_ROOT is set by loader.ts and points to the gsd-pi package root.
1338
+ // The relative import ("../../../resource-loader.js") only works from the source
1339
+ // tree; deployed extensions live at ~/.gsd/agent/extensions/gsd/ where the
1340
+ // relative path resolves to ~/.gsd/agent/resource-loader.js which doesn't exist.
1341
+ // Using GSD_PKG_ROOT constructs a correct absolute path in both contexts (#3949).
1337
1342
  const agentDir = process.env.GSD_CODING_AGENT_DIR || join(process.env.GSD_HOME || homedir(), ".gsd", "agent");
1338
- // Resolve resource-loader from the gsd-pi package root — the relative
1339
- // "../../../resource-loader.js" path only works from the source tree but
1340
- // breaks when extensions are deployed to ~/.gsd/agent/extensions/gsd/.
1341
- const _req = createRequire(import.meta.url);
1342
- const pkgRoot = dirname(_req.resolve("gsd-pi/package.json"));
1343
- const { initResources } = await import(join(pkgRoot, "dist", "resource-loader.js"));
1343
+ const pkgRoot = process.env.GSD_PKG_ROOT;
1344
+ const resourceLoaderPath = pkgRoot
1345
+ ? pathToFileURL(join(pkgRoot, "dist", "resource-loader.js")).href
1346
+ : new URL("../../../resource-loader.js", import.meta.url).href;
1347
+ const { initResources } = await import(resourceLoaderPath);
1344
1348
  initResources(agentDir);
1345
1349
  // Open the project DB before rebuild/derive so resume uses DB-backed
1346
1350
  // state instead of falling back to stale markdown parsing (#2940).