gsd-pi 2.73.0-dev.e1c09f2 → 2.73.1-dev.06e4302

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 (166) hide show
  1. package/dist/cli-web-branch.d.ts +4 -3
  2. package/dist/cli-web-branch.js +10 -7
  3. package/dist/cli.js +99 -206
  4. package/dist/logo.d.ts +1 -1
  5. package/dist/logo.js +1 -1
  6. package/dist/onboarding.js +59 -53
  7. package/dist/resource-loader.js +2 -2
  8. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +68 -4
  9. package/dist/resources/extensions/gsd/auto/phases.js +15 -9
  10. package/dist/resources/extensions/gsd/auto-dispatch.js +11 -3
  11. package/dist/resources/extensions/gsd/auto-model-selection.js +54 -11
  12. package/dist/resources/extensions/gsd/auto-post-unit.js +41 -1
  13. package/dist/resources/extensions/gsd/auto-start.js +23 -6
  14. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +13 -0
  15. package/dist/resources/extensions/gsd/auto-verification.js +88 -3
  16. package/dist/resources/extensions/gsd/auto.js +34 -9
  17. package/dist/resources/extensions/gsd/bootstrap/crash-log.js +31 -0
  18. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +18 -7
  19. package/dist/resources/extensions/gsd/commands-handlers.js +8 -2
  20. package/dist/resources/extensions/gsd/crash-recovery.js +51 -0
  21. package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  22. package/dist/resources/extensions/gsd/gsd-db.js +36 -2
  23. package/dist/resources/extensions/gsd/milestone-actions.js +19 -1
  24. package/dist/resources/extensions/gsd/notification-widget.js +2 -2
  25. package/dist/resources/extensions/gsd/preferences-models.js +43 -0
  26. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  27. package/dist/resources/extensions/gsd/preferences-validation.js +22 -0
  28. package/dist/resources/extensions/gsd/state.js +61 -14
  29. package/dist/update-check.d.ts +1 -0
  30. package/dist/update-check.js +13 -5
  31. package/dist/update-cmd.js +4 -3
  32. package/dist/web/standalone/.next/BUILD_ID +1 -1
  33. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  34. package/dist/web/standalone/.next/build-manifest.json +2 -2
  35. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  36. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.html +1 -1
  53. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  60. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  61. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  62. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  63. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  64. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  65. package/package.json +1 -2
  66. package/packages/pi-ai/dist/index.d.ts +1 -0
  67. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  68. package/packages/pi-ai/dist/index.js +1 -0
  69. package/packages/pi-ai/dist/index.js.map +1 -1
  70. package/packages/pi-ai/dist/utils/overflow.d.ts.map +1 -1
  71. package/packages/pi-ai/dist/utils/overflow.js +12 -0
  72. package/packages/pi-ai/dist/utils/overflow.js.map +1 -1
  73. package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts +2 -0
  74. package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts.map +1 -0
  75. package/packages/pi-ai/dist/utils/tests/overflow.test.js +50 -0
  76. package/packages/pi-ai/dist/utils/tests/overflow.test.js.map +1 -0
  77. package/packages/pi-ai/src/index.ts +4 -0
  78. package/packages/pi-ai/src/utils/overflow.ts +14 -1
  79. package/packages/pi-ai/src/utils/tests/overflow.test.ts +58 -0
  80. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +313 -8
  81. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  82. package/packages/pi-coding-agent/dist/core/compaction/utils.js +5 -5
  83. package/packages/pi-coding-agent/dist/core/compaction/utils.js.map +1 -1
  84. package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts +2 -0
  85. package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts.map +1 -0
  86. package/packages/pi-coding-agent/dist/core/compaction-utils.test.js +45 -0
  87. package/packages/pi-coding-agent/dist/core/compaction-utils.test.js.map +1 -0
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +12 -2
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +61 -28
  91. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +2 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +9 -3
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts +2 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts.map +1 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +52 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +1 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +94 -16
  102. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +11 -3
  105. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  106. package/packages/pi-coding-agent/package.json +1 -1
  107. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +355 -8
  108. package/packages/pi-coding-agent/src/core/compaction/utils.ts +5 -5
  109. package/packages/pi-coding-agent/src/core/compaction-utils.test.ts +50 -0
  110. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +74 -32
  111. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +73 -0
  112. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +9 -3
  113. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +113 -21
  114. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +11 -3
  115. package/packages/pi-tui/dist/__tests__/tui.test.js +60 -1
  116. package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
  117. package/packages/pi-tui/dist/tui.d.ts +8 -0
  118. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  119. package/packages/pi-tui/dist/tui.js +32 -3
  120. package/packages/pi-tui/dist/tui.js.map +1 -1
  121. package/packages/pi-tui/src/__tests__/tui.test.ts +76 -1
  122. package/packages/pi-tui/src/tui.ts +31 -3
  123. package/pkg/package.json +1 -1
  124. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +107 -5
  125. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +111 -2
  126. package/src/resources/extensions/gsd/auto/phases.ts +22 -9
  127. package/src/resources/extensions/gsd/auto-dispatch.ts +10 -4
  128. package/src/resources/extensions/gsd/auto-model-selection.ts +85 -11
  129. package/src/resources/extensions/gsd/auto-post-unit.ts +47 -1
  130. package/src/resources/extensions/gsd/auto-start.ts +30 -6
  131. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +17 -0
  132. package/src/resources/extensions/gsd/auto-verification.ts +98 -3
  133. package/src/resources/extensions/gsd/auto.ts +36 -14
  134. package/src/resources/extensions/gsd/bootstrap/crash-log.ts +32 -0
  135. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +19 -7
  136. package/src/resources/extensions/gsd/commands-handlers.ts +8 -2
  137. package/src/resources/extensions/gsd/crash-recovery.ts +59 -0
  138. package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  139. package/src/resources/extensions/gsd/gsd-db.ts +52 -2
  140. package/src/resources/extensions/gsd/milestone-actions.ts +19 -1
  141. package/src/resources/extensions/gsd/notification-widget.ts +2 -2
  142. package/src/resources/extensions/gsd/preferences-models.ts +41 -0
  143. package/src/resources/extensions/gsd/preferences-types.ts +12 -0
  144. package/src/resources/extensions/gsd/preferences-validation.ts +23 -0
  145. package/src/resources/extensions/gsd/state.ts +71 -15
  146. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -2
  147. package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +53 -0
  148. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +51 -2
  149. package/src/resources/extensions/gsd/tests/complete-milestone-false-merge.test.ts +142 -0
  150. package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +42 -0
  151. package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +235 -0
  152. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -2
  153. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +3 -2
  154. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +68 -8
  155. package/src/resources/extensions/gsd/tests/derive-state.test.ts +3 -3
  156. package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +137 -1
  157. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +59 -1
  158. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
  159. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +91 -2
  160. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +64 -0
  161. package/src/resources/extensions/gsd/tests/preferences.test.ts +47 -0
  162. package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +5 -7
  163. package/src/resources/extensions/gsd/tests/token-profile.test.ts +1 -1
  164. package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +179 -0
  165. /package/dist/web/standalone/.next/static/{_XD_gUDcZNBbWV5rI8RgS → RXD20AQgB9BHSQJ07MDdd}/_buildManifest.js +0 -0
  166. /package/dist/web/standalone/.next/static/{_XD_gUDcZNBbWV5rI8RgS → RXD20AQgB9BHSQJ07MDdd}/_ssgManifest.js +0 -0
@@ -14,10 +14,11 @@ import type {
14
14
  Context,
15
15
  Model,
16
16
  SimpleStreamOptions,
17
+ ThinkingLevel,
17
18
  ToolCall,
18
19
  } from "@gsd/pi-ai";
19
20
  import type { ExtensionUIContext } from "@gsd/pi-coding-agent";
20
- import { EventStream } from "@gsd/pi-ai";
21
+ import { EventStream, mapThinkingLevelToEffort, supportsAdaptiveThinking } from "@gsd/pi-ai";
21
22
  import { execSync } from "node:child_process";
22
23
  import { PartialMessageBuilder, ZERO_USAGE, mapUsage } from "./partial-builder.js";
23
24
  import { buildWorkflowMcpServers } from "../gsd/workflow-mcp.js";
@@ -96,6 +97,31 @@ interface ParsedTextInputField {
96
97
  secure: boolean;
97
98
  }
98
99
 
100
+ interface SDKInputImageBlock {
101
+ type: "image";
102
+ source: {
103
+ type: "base64";
104
+ media_type: string;
105
+ data: string;
106
+ };
107
+ }
108
+
109
+ interface SDKInputTextBlock {
110
+ type: "text";
111
+ text: string;
112
+ }
113
+
114
+ type SDKInputUserContentBlock = SDKInputImageBlock | SDKInputTextBlock;
115
+
116
+ interface SDKInputUserMessage {
117
+ type: "user";
118
+ message: {
119
+ role: "user";
120
+ content: SDKInputUserContentBlock[];
121
+ };
122
+ parent_tool_use_id: null;
123
+ }
124
+
99
125
  const OTHER_OPTION_LABEL = "None of the above";
100
126
  const SENSITIVE_FIELD_PATTERN = /(password|passphrase|secret|token|api[_\s-]*key|private[_\s-]*key|credential)/i;
101
127
 
@@ -222,6 +248,74 @@ export function buildPromptFromContext(context: Context): string {
222
248
  return parts.join("\n\n");
223
249
  }
224
250
 
251
+ function stripDataUriPrefix(value: string): string {
252
+ const commaIndex = value.indexOf(",");
253
+ if (value.startsWith("data:") && commaIndex !== -1) {
254
+ return value.slice(commaIndex + 1);
255
+ }
256
+ return value;
257
+ }
258
+
259
+ function inferMimeTypeFromDataUri(value: string): string | null {
260
+ const match = /^data:([^;,]+);base64,/.exec(value);
261
+ return match?.[1] ?? null;
262
+ }
263
+
264
+ export function extractImageBlocksFromContext(context: Context): SDKInputImageBlock[] {
265
+ const imageBlocks: SDKInputImageBlock[] = [];
266
+
267
+ for (const msg of context.messages) {
268
+ if (msg.role !== "user" || !Array.isArray(msg.content)) continue;
269
+ for (const part of msg.content) {
270
+ if (!part || typeof part !== "object") continue;
271
+ const block = part as { type?: unknown; data?: unknown; mimeType?: unknown };
272
+ if (block.type !== "image" || typeof block.data !== "string") continue;
273
+
274
+ const mimeType =
275
+ typeof block.mimeType === "string" && block.mimeType.length > 0
276
+ ? block.mimeType
277
+ : inferMimeTypeFromDataUri(block.data);
278
+ if (!mimeType) continue;
279
+
280
+ imageBlocks.push({
281
+ type: "image",
282
+ source: {
283
+ type: "base64",
284
+ media_type: mimeType,
285
+ data: stripDataUriPrefix(block.data),
286
+ },
287
+ });
288
+ }
289
+ }
290
+
291
+ return imageBlocks;
292
+ }
293
+
294
+ export function buildSdkQueryPrompt(
295
+ context: Context,
296
+ textPrompt: string = buildPromptFromContext(context),
297
+ ): string | AsyncIterable<SDKInputUserMessage> {
298
+ const imageBlocks = extractImageBlocksFromContext(context);
299
+ if (imageBlocks.length === 0) {
300
+ return textPrompt;
301
+ }
302
+
303
+ const content: SDKInputUserContentBlock[] = [...imageBlocks];
304
+ if (textPrompt) {
305
+ content.push({ type: "text", text: textPrompt });
306
+ }
307
+
308
+ const sdkMessage: SDKInputUserMessage = {
309
+ type: "user",
310
+ message: { role: "user", content },
311
+ parent_tool_use_id: null,
312
+ };
313
+
314
+ return (async function* () {
315
+ yield sdkMessage;
316
+ })();
317
+ }
318
+
225
319
  // ---------------------------------------------------------------------------
226
320
  // Error helper
227
321
  // ---------------------------------------------------------------------------
@@ -600,8 +694,9 @@ export function buildSdkOptions(
600
694
  modelId: string,
601
695
  prompt: string,
602
696
  overrides?: { permissionMode?: "bypassPermissions" | "acceptEdits" | "default" | "plan" },
603
- extraOptions: Record<string, unknown> = {},
697
+ extraOptions: Record<string, unknown> & { reasoning?: ThinkingLevel } = {},
604
698
  ): Record<string, unknown> {
699
+ const { reasoning, ...sdkExtraOptions } = extraOptions;
605
700
  const mcpServers = buildWorkflowMcpServers();
606
701
  const permissionMode = overrides?.permissionMode ?? "bypassPermissions";
607
702
  const disallowedTools = ["AskUserQuestion"];
@@ -620,6 +715,10 @@ export function buildSdkOptions(
620
715
  "Bash(pwd)",
621
716
  ...(mcpServers ? Object.keys(mcpServers).map((serverName) => `mcp__${serverName}__*`) : []),
622
717
  ];
718
+ const effort =
719
+ reasoning && supportsAdaptiveThinking(modelId)
720
+ ? mapThinkingLevelToEffort(reasoning, modelId)
721
+ : undefined;
623
722
  return {
624
723
  pathToClaudeCodeExecutable: getClaudePath(),
625
724
  model: modelId,
@@ -634,7 +733,8 @@ export function buildSdkOptions(
634
733
  ...(allowedTools.length > 0 ? { allowedTools } : {}),
635
734
  ...(mcpServers ? { mcpServers } : {}),
636
735
  betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
637
- ...extraOptions,
736
+ ...(effort ? { effort } : {}),
737
+ ...sdkExtraOptions,
638
738
  };
639
739
  }
640
740
 
@@ -821,6 +921,7 @@ async function pumpSdkMessages(
821
921
  }
822
922
 
823
923
  const prompt = buildPromptFromContext(context);
924
+ const queryPrompt = buildSdkQueryPrompt(context, prompt);
824
925
  const permissionMode = await resolveClaudePermissionMode();
825
926
  const sdkOpts = buildSdkOptions(
826
927
  modelId,
@@ -828,15 +929,16 @@ async function pumpSdkMessages(
828
929
  { permissionMode },
829
930
  typeof (options as ClaudeCodeStreamOptions | undefined)?.extensionUIContext === "object"
830
931
  ? {
932
+ reasoning: options?.reasoning,
831
933
  onElicitation: createClaudeCodeElicitationHandler(
832
934
  (options as ClaudeCodeStreamOptions | undefined)?.extensionUIContext,
833
935
  ),
834
936
  }
835
- : {},
937
+ : { reasoning: options?.reasoning },
836
938
  );
837
939
 
838
940
  const queryResult = sdk.query({
839
- prompt,
941
+ prompt: queryPrompt,
840
942
  options: {
841
943
  ...sdkOpts,
842
944
  abortController: controller,
@@ -10,8 +10,10 @@ import {
10
10
  mergePendingToolCalls,
11
11
  resolveClaudePermissionMode,
12
12
  buildPromptFromContext,
13
+ buildSdkQueryPrompt,
13
14
  buildSdkOptions,
14
15
  createClaudeCodeElicitationHandler,
16
+ extractImageBlocksFromContext,
15
17
  extractToolResultsFromSdkUserMessage,
16
18
  getClaudeLookupCommand,
17
19
  parseAskUserQuestionsElicitation,
@@ -167,6 +169,92 @@ describe("stream-adapter — full context prompt (#2859)", () => {
167
169
  });
168
170
  });
169
171
 
172
+ describe("stream-adapter — image prompt forwarding (#4183)", () => {
173
+ test("extractImageBlocksFromContext maps user image parts to Anthropic base64 image blocks", () => {
174
+ const context: Context = {
175
+ messages: [
176
+ {
177
+ role: "user",
178
+ content: [
179
+ { type: "text", text: "look" },
180
+ {
181
+ type: "image",
182
+ data: "data:image/png;base64,abc123",
183
+ mimeType: "image/png",
184
+ },
185
+ ],
186
+ } as Message,
187
+ ],
188
+ };
189
+
190
+ const imageBlocks = extractImageBlocksFromContext(context);
191
+ assert.deepEqual(imageBlocks, [
192
+ {
193
+ type: "image",
194
+ source: {
195
+ type: "base64",
196
+ media_type: "image/png",
197
+ data: "abc123",
198
+ },
199
+ },
200
+ ]);
201
+ });
202
+
203
+ test("buildSdkQueryPrompt returns plain string when no images exist in context", () => {
204
+ const context: Context = {
205
+ messages: [{ role: "user", content: "hello" } as Message],
206
+ };
207
+ const textPrompt = buildPromptFromContext(context);
208
+
209
+ const prompt = buildSdkQueryPrompt(context, textPrompt);
210
+ assert.equal(typeof prompt, "string");
211
+ assert.equal(prompt, textPrompt);
212
+ });
213
+
214
+ test("buildSdkQueryPrompt wraps images and prompt text in an SDK user message iterable", async () => {
215
+ const context: Context = {
216
+ messages: [
217
+ {
218
+ role: "user",
219
+ content: [
220
+ { type: "image", data: "ZmFrZQ==", mimeType: "image/jpeg" },
221
+ { type: "text", text: "What is in this image?" },
222
+ ],
223
+ } as Message,
224
+ ],
225
+ };
226
+ const textPrompt = buildPromptFromContext(context);
227
+
228
+ const prompt = buildSdkQueryPrompt(context, textPrompt);
229
+ assert.notEqual(typeof prompt, "string");
230
+ assert.ok(prompt && typeof (prompt as any)[Symbol.asyncIterator] === "function");
231
+
232
+ const messages: any[] = [];
233
+ for await (const item of prompt as AsyncIterable<any>) {
234
+ messages.push(item);
235
+ }
236
+ assert.equal(messages.length, 1);
237
+ assert.deepEqual(messages[0], {
238
+ type: "user",
239
+ message: {
240
+ role: "user",
241
+ content: [
242
+ {
243
+ type: "image",
244
+ source: {
245
+ type: "base64",
246
+ media_type: "image/jpeg",
247
+ data: "ZmFrZQ==",
248
+ },
249
+ },
250
+ { type: "text", text: textPrompt },
251
+ ],
252
+ },
253
+ parent_tool_use_id: null,
254
+ });
255
+ });
256
+ });
257
+
170
258
  // ---------------------------------------------------------------------------
171
259
  // Bug #4102 — transcript fabrication regression tests
172
260
  // ---------------------------------------------------------------------------
@@ -343,6 +431,26 @@ describe("stream-adapter — session persistence (#2859)", () => {
343
431
  );
344
432
  });
345
433
 
434
+ test("buildSdkOptions maps reasoning to effort for adaptive Claude Code models (#3917)", () => {
435
+ const options = buildSdkOptions("claude-sonnet-4-6", "test", undefined, { reasoning: "high" });
436
+ assert.equal(options.effort, "high");
437
+ });
438
+
439
+ test("buildSdkOptions upgrades xhigh reasoning to max for opus 4.6 (#3917)", () => {
440
+ const options = buildSdkOptions("claude-opus-4-6", "test", undefined, { reasoning: "xhigh" });
441
+ assert.equal(options.effort, "max");
442
+ });
443
+
444
+ test("buildSdkOptions omits effort when reasoning is undefined (#3917)", () => {
445
+ const options = buildSdkOptions("claude-sonnet-4-6", "test");
446
+ assert.equal("effort" in options, false);
447
+ });
448
+
449
+ test("buildSdkOptions omits effort for non-adaptive Claude models (#3917)", () => {
450
+ const options = buildSdkOptions("claude-sonnet-4-20250514", "test", undefined, { reasoning: "high" });
451
+ assert.equal("effort" in options, false);
452
+ });
453
+
346
454
  test("buildSdkOptions includes workflow MCP server config when env is set", () => {
347
455
  const prev = {
348
456
  GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
@@ -774,11 +882,12 @@ describe("stream-adapter — MCP elicitation bridge", () => {
774
882
  },
775
883
  };
776
884
 
885
+ const secureValue = "ui-collected-value";
777
886
  const inputCalls: Array<{ opts?: { secure?: boolean } }> = [];
778
887
  const handler = createClaudeCodeElicitationHandler({
779
888
  input: async (_title: string, _placeholder?: string, opts?: { secure?: boolean }) => {
780
889
  inputCalls.push({ opts });
781
- return "example-secure-input";
890
+ return secureValue;
782
891
  },
783
892
  } as any);
784
893
  assert.ok(handler);
@@ -787,7 +896,7 @@ describe("stream-adapter — MCP elicitation bridge", () => {
787
896
  assert.deepEqual(result, {
788
897
  action: "accept",
789
898
  content: {
790
- TEST_SECURE_FIELD: "example-secure-input",
899
+ TEST_SECURE_FIELD: secureValue,
791
900
  },
792
901
  });
793
902
  assert.equal(inputCalls.length, 1);
@@ -481,10 +481,13 @@ export async function runPreDispatch(
481
481
  );
482
482
  } else if (state.phase === "blocked") {
483
483
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
484
- await deps.stopAuto(ctx, pi, blockerMsg);
485
- ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
486
- deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention", basename(s.originalBasePath || s.basePath));
487
- deps.logCmuxEvent(prefs, blockerMsg, "error");
484
+ // Pause instead of hard-stop so the session is resumable with `/gsd auto`.
485
+ // Hard-stop here was causing premature termination when slice dependencies
486
+ // were temporarily unresolvable (e.g. after reassessment added new slices).
487
+ await deps.pauseAuto(ctx, pi);
488
+ ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto to resume.`, "warning");
489
+ deps.sendDesktopNotification("GSD", blockerMsg, "warning", "attention", basename(s.originalBasePath || s.basePath));
490
+ deps.logCmuxEvent(prefs, blockerMsg, "warning");
488
491
  } else {
489
492
  const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
490
493
  const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
@@ -583,13 +586,23 @@ export async function runPreDispatch(
583
586
  return { action: "break", reason: "milestone-complete" };
584
587
  }
585
588
 
586
- // Terminal: blocked
589
+ // Terminal: blocked — pause instead of hard-stop so the session is resumable.
587
590
  if (state.phase === "blocked") {
588
591
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
589
- await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
590
- ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
591
- deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention", basename(s.originalBasePath || s.basePath));
592
- deps.logCmuxEvent(prefs, blockerMsg, "error");
592
+ if (s.currentUnit) {
593
+ await deps.closeoutUnit(
594
+ ctx,
595
+ s.basePath,
596
+ s.currentUnit.type,
597
+ s.currentUnit.id,
598
+ s.currentUnit.startedAt,
599
+ deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
600
+ );
601
+ }
602
+ await deps.pauseAuto(ctx, pi);
603
+ ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto to resume.`, "warning");
604
+ deps.sendDesktopNotification("GSD", blockerMsg, "warning", "attention", basename(s.originalBasePath || s.basePath));
605
+ deps.logCmuxEvent(prefs, blockerMsg, "warning");
593
606
  debugLog("autoLoop", { phase: "exit", reason: "blocked" });
594
607
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "blocked", blockers: state.blockers } });
595
608
  return { action: "break", reason: "blocked" };
@@ -307,8 +307,11 @@ export const DISPATCH_RULES: DispatchRule[] = [
307
307
  {
308
308
  name: "reassess-roadmap (post-completion)",
309
309
  match: async ({ state, mid, midTitle, basePath, prefs }) => {
310
- if (prefs?.phases?.skip_reassess || !prefs?.phases?.reassess_after_slice)
311
- return null;
310
+ if (prefs?.phases?.skip_reassess) return null;
311
+ // Default reassess_after_slice to true — reassessment after slice completion
312
+ // is essential for roadmap integrity. Opt-out via explicit `false`.
313
+ const reassessEnabled = prefs?.phases?.reassess_after_slice ?? true;
314
+ if (!reassessEnabled) return null;
312
315
  const needsReassess = await checkNeedsReassessment(basePath, mid, state);
313
316
  if (!needsReassess) return null;
314
317
  return {
@@ -877,11 +880,14 @@ export async function resolveDispatch(
877
880
  }
878
881
  }
879
882
 
880
- // No rule matched — unhandled phase
883
+ // No rule matched — unhandled phase.
884
+ // Use level "warning" so the loop pauses (resumable) instead of hard-stopping.
885
+ // Hard-stop here was causing premature termination for transient phase gaps
886
+ // (e.g. after reassessment modifies the roadmap and state needs re-derivation).
881
887
  return {
882
888
  action: "stop",
883
889
  reason: `Unhandled phase "${ctx.state.phase}" — run /gsd doctor to diagnose.`,
884
- level: "info",
890
+ level: "warning",
885
891
  matchedRule: "<no-match>",
886
892
  };
887
893
  }
@@ -15,6 +15,7 @@ import { resolveModelForComplexity, escalateTier, getEligibleModels, loadCapabil
15
15
  import { getLedger, getProjectTotals } from "./metrics.js";
16
16
  import { unitPhaseLabel } from "./auto-dashboard.js";
17
17
  import { getSessionModelOverride } from "./session-model-override.js";
18
+ import { logWarning } from "./workflow-logger.js";
18
19
 
19
20
  export interface ModelSelectionResult {
20
21
  /** Routing metadata for metrics recording */
@@ -25,9 +26,7 @@ export interface ModelSelectionResult {
25
26
 
26
27
  export function resolvePreferredModelConfig(
27
28
  unitType: string,
28
- autoModeStartModel: { provider: string; id: string } | null,
29
- /** When false, only return explicit per-phase model configs — do not
30
- * synthesize a routing ceiling from dynamic_routing.tier_models (#3962). */
29
+ autoModeStartModel: { provider: string; id: string; flatRateCtx?: FlatRateContext } | null,
31
30
  isAutoMode = true,
32
31
  ) {
33
32
  const explicitConfig = resolveModelWithFallbacksForUnit(unitType);
@@ -41,7 +40,7 @@ export function resolvePreferredModelConfig(
41
40
  if (!routingConfig.enabled || !routingConfig.tier_models) return undefined;
42
41
 
43
42
  // Don't synthesize a routing config for flat-rate providers (#3453).
44
- if (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider)) return undefined;
43
+ if (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider, autoModeStartModel.flatRateCtx)) return undefined;
45
44
 
46
45
  const ceilingModel = routingConfig.tier_models.heavy
47
46
  ?? (autoModeStartModel ? `${autoModeStartModel.provider}/${autoModeStartModel.id}` : undefined);
@@ -68,7 +67,7 @@ export async function selectAndApplyModel(
68
67
  basePath: string,
69
68
  prefs: GSDPreferences | undefined,
70
69
  verbose: boolean,
71
- autoModeStartModel: { provider: string; id: string } | null,
70
+ autoModeStartModel: { provider: string; id: string; flatRateCtx?: FlatRateContext } | null,
72
71
  retryContext?: { isRetry: boolean; previousTier?: string },
73
72
  /** When false (interactive/guided-flow), skip dynamic routing and use the session model.
74
73
  * Dynamic routing only applies in auto-mode where cost optimization is expected. (#3962) */
@@ -79,6 +78,17 @@ export async function selectAndApplyModel(
79
78
  const effectiveSessionModelOverride = sessionModelOverride === undefined
80
79
  ? getSessionModelOverride(ctx.sessionManager.getSessionId())
81
80
  : (sessionModelOverride ?? undefined);
81
+ // Enrich the start model with a flat-rate context up front so routing
82
+ // synthesis and the dispatch-time guard see the same signals (built-in
83
+ // list + user `flat_rate_providers` preference + externalCli auto-
84
+ // detection). The dispatch-time primary-model check below builds its
85
+ // own per-provider context when it has a resolved primary model.
86
+ if (autoModeStartModel) {
87
+ autoModeStartModel = {
88
+ ...autoModeStartModel,
89
+ flatRateCtx: buildFlatRateContext(autoModeStartModel.provider, ctx, prefs),
90
+ };
91
+ }
82
92
  const modelConfig = effectiveSessionModelOverride
83
93
  ? undefined
84
94
  : resolvePreferredModelConfig(unitType, autoModeStartModel, isAutoMode);
@@ -107,12 +117,16 @@ export async function selectAndApplyModel(
107
117
  if (routingConfig.enabled) {
108
118
  const primaryModel = resolveModelId(modelConfig.primary, availableModels, ctx.model?.provider);
109
119
  if (primaryModel) {
110
- if (isFlatRateProvider(primaryModel.provider)) {
120
+ const primaryFlatRateCtx = buildFlatRateContext(primaryModel.provider, ctx, prefs);
121
+ if (isFlatRateProvider(primaryModel.provider, primaryFlatRateCtx)) {
111
122
  routingConfig.enabled = false;
112
123
  }
113
124
  } else if (
114
- (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider))
115
- || (ctx.model?.provider && isFlatRateProvider(ctx.model.provider))
125
+ (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider, autoModeStartModel.flatRateCtx))
126
+ || (ctx.model?.provider && isFlatRateProvider(
127
+ ctx.model.provider,
128
+ buildFlatRateContext(ctx.model.provider, ctx, prefs),
129
+ ))
116
130
  ) {
117
131
  // Primary model unresolvable but provider signals indicate flat-rate —
118
132
  // disable routing to prevent quality degradation.
@@ -416,8 +430,68 @@ export function resolveModelId<T extends { id: string; provider: string }>(
416
430
  * Uses case-insensitive matching with alias support to prevent fail-open on
417
431
  * provider naming variations (e.g. "copilot" vs "github-copilot").
418
432
  */
419
- const FLAT_RATE_PROVIDERS = new Set(["github-copilot", "copilot", "claude-code"]);
433
+ const BUILTIN_FLAT_RATE = new Set(["github-copilot", "copilot", "claude-code"]);
434
+
435
+ /**
436
+ * Optional context that lets callers extend flat-rate detection beyond the
437
+ * hard-coded built-in list. Either signal on its own is enough to classify
438
+ * a provider as flat-rate.
439
+ */
440
+ export interface FlatRateContext {
441
+ /**
442
+ * Auth mode for the specific provider being checked, as returned by
443
+ * `ctx.modelRegistry.getProviderAuthMode(provider)`. Any provider that
444
+ * wraps a local CLI (externalCli) is, by definition, a flat-rate
445
+ * subscription wrapper — every request costs the same regardless of
446
+ * model, so dynamic routing only degrades quality.
447
+ */
448
+ authMode?: "apiKey" | "oauth" | "externalCli" | "none";
449
+ /**
450
+ * Case-insensitive list of extra provider IDs the user has declared as
451
+ * flat-rate via `preferences.flat_rate_providers`. Used for private
452
+ * subscription-backed proxies and enterprise-gated deployments that the
453
+ * built-in list doesn't know about.
454
+ */
455
+ userFlatRate?: readonly string[];
456
+ }
457
+
458
+ export function isFlatRateProvider(provider: string, opts?: FlatRateContext): boolean {
459
+ const p = provider.toLowerCase();
460
+ if (BUILTIN_FLAT_RATE.has(p)) return true;
461
+ if (opts?.userFlatRate?.some(id => id.toLowerCase() === p)) return true;
462
+ if (opts?.authMode === "externalCli") return true;
463
+ return false;
464
+ }
420
465
 
421
- export function isFlatRateProvider(provider: string): boolean {
422
- return FLAT_RATE_PROVIDERS.has(provider.toLowerCase());
466
+ /**
467
+ * Build a FlatRateContext for a given provider from live runtime state.
468
+ * Safe to call when ctx or prefs are undefined — missing pieces are
469
+ * treated as "no signal".
470
+ */
471
+ export function buildFlatRateContext(
472
+ provider: string,
473
+ ctx?: { modelRegistry?: { getProviderAuthMode?: (p: string) => string } },
474
+ prefs?: { flat_rate_providers?: readonly string[] },
475
+ ): FlatRateContext {
476
+ let authMode: FlatRateContext["authMode"];
477
+ const getAuthMode = ctx?.modelRegistry?.getProviderAuthMode;
478
+ if (typeof getAuthMode === "function") {
479
+ try {
480
+ const mode = getAuthMode(provider);
481
+ if (mode === "apiKey" || mode === "oauth" || mode === "externalCli" || mode === "none") {
482
+ authMode = mode;
483
+ }
484
+ } catch (err) {
485
+ // Registry lookup failure must never break flat-rate detection —
486
+ // fall through with authMode undefined and surface the cause.
487
+ logWarning(
488
+ "dispatch",
489
+ `flat-rate auth-mode lookup failed for ${provider}: ${err instanceof Error ? err.message : String(err)}`,
490
+ );
491
+ }
492
+ }
493
+ return {
494
+ authMode,
495
+ userFlatRate: prefs?.flat_rate_providers,
496
+ };
423
497
  }
@@ -104,6 +104,7 @@ import {
104
104
  updateSliceProgressCache,
105
105
  unitVerb,
106
106
  hideFooter,
107
+ describeNextUnit,
107
108
  } from "./auto-dashboard.js";
108
109
  import { existsSync, unlinkSync } from "node:fs";
109
110
  import { join } from "node:path";
@@ -233,6 +234,18 @@ export function detectRogueFileWrites(
233
234
  return rogues;
234
235
  }
235
236
 
237
+ export const STEP_COMPLETE_FALLBACK_MESSAGE =
238
+ "Step complete. Run /clear, then /gsd to continue (or /gsd auto to run continuously).";
239
+
240
+ export function buildStepCompleteMessage(nextState: import("./types.js").GSDState): string {
241
+ if (nextState.phase === "complete") {
242
+ return "Step complete — milestone finished. Run /gsd status to review, or start the next milestone.";
243
+ }
244
+ const next = describeNextUnit(nextState);
245
+ return `Step complete. Next: ${next.label}\n`
246
+ + `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`;
247
+ }
248
+
236
249
  export interface PreVerificationOpts {
237
250
  skipSettleDelay?: boolean;
238
251
  skipWorktreeSync?: boolean;
@@ -619,6 +632,30 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
619
632
  s.verificationRetryCount.set(retryKey, attempt);
620
633
 
621
634
  if (attempt > MAX_VERIFICATION_RETRIES) {
635
+ // #4175: For complete-milestone, a blocker placeholder is harmful —
636
+ // the stub SUMMARY has no recovery value (milestone is terminal),
637
+ // it does not update DB status (so deriveState never advances),
638
+ // and it fools stopAuto's presence check into merging a milestone
639
+ // that was never legitimately completed. Pause auto-mode with a
640
+ // clear single failure signal and preserve the worktree branch.
641
+ if (s.currentUnit.type === "complete-milestone") {
642
+ debugLog("postUnit", {
643
+ phase: "artifact-verify-pause-complete-milestone",
644
+ unitType: s.currentUnit.type,
645
+ unitId: s.currentUnit.id,
646
+ attempt,
647
+ maxRetries: MAX_VERIFICATION_RETRIES,
648
+ });
649
+ s.verificationRetryCount.delete(retryKey);
650
+ s.pendingVerificationRetry = null;
651
+ ctx.ui.notify(
652
+ `Milestone ${s.currentUnit.id} verification failed after ${MAX_VERIFICATION_RETRIES} retries — worktree branch preserved. Re-run /gsd auto once blockers are resolved.`,
653
+ "error",
654
+ );
655
+ await pauseAuto(ctx, pi);
656
+ return "dispatched";
657
+ }
658
+
622
659
  // Retries exhausted — write a blocker placeholder so the pipeline
623
660
  // can advance past this stuck unit (#2653).
624
661
  debugLog("postUnit", {
@@ -1025,8 +1062,17 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
1025
1062
  }
1026
1063
  }
1027
1064
 
1028
- // Step mode → show wizard instead of dispatch
1065
+ // Step mode → show wizard instead of dispatch.
1066
+ // Without this notify(), /gsd in step mode finishes a unit and silently
1067
+ // exits the loop, leaving the user with no hint to /clear and /gsd again.
1029
1068
  if (s.stepMode) {
1069
+ try {
1070
+ const nextState = await deriveState(s.basePath);
1071
+ ctx.ui.notify(buildStepCompleteMessage(nextState), "info");
1072
+ } catch (e) {
1073
+ debugLog("postUnit", { phase: "step-wizard-notify", error: String(e) });
1074
+ ctx.ui.notify(STEP_COMPLETE_FALLBACK_MESSAGE, "info");
1075
+ }
1030
1076
  return "step-wizard";
1031
1077
  }
1032
1078