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
@@ -358,7 +358,7 @@ function reconcileMergedNodeModules(agentNodeModules, hoisted, internal) {
358
358
  if (entry.name.startsWith('.'))
359
359
  continue;
360
360
  try {
361
- symlinkSync(join(hoisted, entry.name), join(agentNodeModules, entry.name));
361
+ symlinkSync(join(hoisted, entry.name), join(agentNodeModules, entry.name), 'junction');
362
362
  linkedCount++;
363
363
  }
364
364
  catch { /* skip individual */ }
@@ -382,7 +382,7 @@ function reconcileMergedNodeModules(agentNodeModules, hoisted, internal) {
382
382
  }
383
383
  catch { /* didn't exist — will create below */ }
384
384
  try {
385
- symlinkSync(join(internal, entry.name), link);
385
+ symlinkSync(join(internal, entry.name), link, 'junction');
386
386
  linkedCount++;
387
387
  }
388
388
  catch { /* skip individual */ }
@@ -6,7 +6,7 @@
6
6
  * AssistantMessageEvents for TUI rendering, then strips tool-call blocks from
7
7
  * the final AssistantMessage so GSD's agent loop doesn't try to dispatch them.
8
8
  */
9
- import { EventStream } from "@gsd/pi-ai";
9
+ import { EventStream, mapThinkingLevelToEffort, supportsAdaptiveThinking } from "@gsd/pi-ai";
10
10
  import { execSync } from "node:child_process";
11
11
  import { PartialMessageBuilder, ZERO_USAGE, mapUsage } from "./partial-builder.js";
12
12
  import { buildWorkflowMcpServers } from "../gsd/workflow-mcp.js";
@@ -123,6 +123,63 @@ export function buildPromptFromContext(context) {
123
123
  }
124
124
  return parts.join("\n\n");
125
125
  }
126
+ function stripDataUriPrefix(value) {
127
+ const commaIndex = value.indexOf(",");
128
+ if (value.startsWith("data:") && commaIndex !== -1) {
129
+ return value.slice(commaIndex + 1);
130
+ }
131
+ return value;
132
+ }
133
+ function inferMimeTypeFromDataUri(value) {
134
+ const match = /^data:([^;,]+);base64,/.exec(value);
135
+ return match?.[1] ?? null;
136
+ }
137
+ export function extractImageBlocksFromContext(context) {
138
+ const imageBlocks = [];
139
+ for (const msg of context.messages) {
140
+ if (msg.role !== "user" || !Array.isArray(msg.content))
141
+ continue;
142
+ for (const part of msg.content) {
143
+ if (!part || typeof part !== "object")
144
+ continue;
145
+ const block = part;
146
+ if (block.type !== "image" || typeof block.data !== "string")
147
+ continue;
148
+ const mimeType = typeof block.mimeType === "string" && block.mimeType.length > 0
149
+ ? block.mimeType
150
+ : inferMimeTypeFromDataUri(block.data);
151
+ if (!mimeType)
152
+ continue;
153
+ imageBlocks.push({
154
+ type: "image",
155
+ source: {
156
+ type: "base64",
157
+ media_type: mimeType,
158
+ data: stripDataUriPrefix(block.data),
159
+ },
160
+ });
161
+ }
162
+ }
163
+ return imageBlocks;
164
+ }
165
+ export function buildSdkQueryPrompt(context, textPrompt = buildPromptFromContext(context)) {
166
+ const imageBlocks = extractImageBlocksFromContext(context);
167
+ if (imageBlocks.length === 0) {
168
+ return textPrompt;
169
+ }
170
+ const content = [...imageBlocks];
171
+ if (textPrompt) {
172
+ content.push({ type: "text", text: textPrompt });
173
+ }
174
+ const sdkMessage = {
175
+ type: "user",
176
+ message: { role: "user", content },
177
+ parent_tool_use_id: null,
178
+ };
179
+ return (async function* () {
180
+ yield sdkMessage;
181
+ })();
182
+ }
126
183
  // ---------------------------------------------------------------------------
127
184
  // Error helper
128
185
  // ---------------------------------------------------------------------------
@@ -437,6 +494,7 @@ export async function resolveClaudePermissionMode(env = process.env) {
437
494
  * behaviour pass `permissionMode: "bypassPermissions"` explicitly.
438
495
  */
439
496
  export function buildSdkOptions(modelId, prompt, overrides, extraOptions = {}) {
497
+ const { reasoning, ...sdkExtraOptions } = extraOptions;
440
498
  const mcpServers = buildWorkflowMcpServers();
441
499
  const permissionMode = overrides?.permissionMode ?? "bypassPermissions";
442
500
  const disallowedTools = ["AskUserQuestion"];
@@ -455,6 +513,9 @@ export function buildSdkOptions(modelId, prompt, overrides, extraOptions = {}) {
455
513
  "Bash(pwd)",
456
514
  ...(mcpServers ? Object.keys(mcpServers).map((serverName) => `mcp__${serverName}__*`) : []),
457
515
  ];
516
+ const effort = reasoning && supportsAdaptiveThinking(modelId)
517
+ ? mapThinkingLevelToEffort(reasoning, modelId)
518
+ : undefined;
458
519
  return {
459
520
  pathToClaudeCodeExecutable: getClaudePath(),
460
521
  model: modelId,
@@ -469,7 +530,8 @@ export function buildSdkOptions(modelId, prompt, overrides, extraOptions = {}) {
469
530
  ...(allowedTools.length > 0 ? { allowedTools } : {}),
470
531
  ...(mcpServers ? { mcpServers } : {}),
471
532
  betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
472
- ...extraOptions,
533
+ ...(effort ? { effort } : {}),
534
+ ...sdkExtraOptions,
473
535
  };
474
536
  }
475
537
  function normalizeToolResultContent(content) {
@@ -617,14 +679,16 @@ async function pumpSdkMessages(model, context, options, stream) {
617
679
  options.signal.addEventListener("abort", () => controller.abort(), { once: true });
618
680
  }
619
681
  const prompt = buildPromptFromContext(context);
682
+ const queryPrompt = buildSdkQueryPrompt(context, prompt);
620
683
  const permissionMode = await resolveClaudePermissionMode();
621
684
  const sdkOpts = buildSdkOptions(modelId, prompt, { permissionMode }, typeof options?.extensionUIContext === "object"
622
685
  ? {
686
+ reasoning: options?.reasoning,
623
687
  onElicitation: createClaudeCodeElicitationHandler(options?.extensionUIContext),
624
688
  }
625
- : {});
689
+ : { reasoning: options?.reasoning });
626
690
  const queryResult = sdk.query({
627
- prompt,
691
+ prompt: queryPrompt,
628
692
  options: {
629
693
  ...sdkOpts,
630
694
  abortController: controller,
@@ -320,10 +320,13 @@ export async function runPreDispatch(ic, loopState) {
320
320
  }
321
321
  else if (state.phase === "blocked") {
322
322
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
323
- await deps.stopAuto(ctx, pi, blockerMsg);
324
- ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
325
- deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention", basename(s.originalBasePath || s.basePath));
326
- deps.logCmuxEvent(prefs, blockerMsg, "error");
323
+ // Pause instead of hard-stop so the session is resumable with `/gsd auto`.
324
+ // Hard-stop here was causing premature termination when slice dependencies
325
+ // were temporarily unresolvable (e.g. after reassessment added new slices).
326
+ await deps.pauseAuto(ctx, pi);
327
+ ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto to resume.`, "warning");
328
+ deps.sendDesktopNotification("GSD", blockerMsg, "warning", "attention", basename(s.originalBasePath || s.basePath));
329
+ deps.logCmuxEvent(prefs, blockerMsg, "warning");
327
330
  }
328
331
  else {
329
332
  const ids = incomplete.map((m) => m.id).join(", ");
@@ -392,13 +395,16 @@ export async function runPreDispatch(ic, loopState) {
392
395
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "milestone-complete", milestoneId: mid } });
393
396
  return { action: "break", reason: "milestone-complete" };
394
397
  }
395
- // Terminal: blocked
398
+ // Terminal: blocked — pause instead of hard-stop so the session is resumable.
396
399
  if (state.phase === "blocked") {
397
400
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
398
- await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
399
- ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
400
- deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention", basename(s.originalBasePath || s.basePath));
401
- deps.logCmuxEvent(prefs, blockerMsg, "error");
401
+ if (s.currentUnit) {
402
+ await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
403
+ }
404
+ await deps.pauseAuto(ctx, pi);
405
+ ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto to resume.`, "warning");
406
+ deps.sendDesktopNotification("GSD", blockerMsg, "warning", "attention", basename(s.originalBasePath || s.basePath));
407
+ deps.logCmuxEvent(prefs, blockerMsg, "warning");
402
408
  debugLog("autoLoop", { phase: "exit", reason: "blocked" });
403
409
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "blocked", blockers: state.blockers } });
404
410
  return { action: "break", reason: "blocked" };
@@ -216,7 +216,12 @@ export const DISPATCH_RULES = [
216
216
  {
217
217
  name: "reassess-roadmap (post-completion)",
218
218
  match: async ({ state, mid, midTitle, basePath, prefs }) => {
219
- if (prefs?.phases?.skip_reassess || !prefs?.phases?.reassess_after_slice)
219
+ if (prefs?.phases?.skip_reassess)
220
+ return null;
221
+ // Default reassess_after_slice to true — reassessment after slice completion
222
+ // is essential for roadmap integrity. Opt-out via explicit `false`.
223
+ const reassessEnabled = prefs?.phases?.reassess_after_slice ?? true;
224
+ if (!reassessEnabled)
220
225
  return null;
221
226
  const needsReassess = await checkNeedsReassessment(basePath, mid, state);
222
227
  if (!needsReassess)
@@ -710,11 +715,14 @@ export async function resolveDispatch(ctx) {
710
715
  return result;
711
716
  }
712
717
  }
713
- // No rule matched — unhandled phase
718
+ // No rule matched — unhandled phase.
719
+ // Use level "warning" so the loop pauses (resumable) instead of hard-stopping.
720
+ // Hard-stop here was causing premature termination for transient phase gaps
721
+ // (e.g. after reassessment modifies the roadmap and state needs re-derivation).
714
722
  return {
715
723
  action: "stop",
716
724
  reason: `Unhandled phase "${ctx.state.phase}" — run /gsd doctor to diagnose.`,
717
- level: "info",
725
+ level: "warning",
718
726
  matchedRule: "<no-match>",
719
727
  };
720
728
  }
@@ -9,10 +9,8 @@ import { resolveModelForComplexity, escalateTier, getEligibleModels, loadCapabil
9
9
  import { getLedger, getProjectTotals } from "./metrics.js";
10
10
  import { unitPhaseLabel } from "./auto-dashboard.js";
11
11
  import { getSessionModelOverride } from "./session-model-override.js";
12
- export function resolvePreferredModelConfig(unitType, autoModeStartModel,
13
- /** When false, only return explicit per-phase model configs — do not
14
- * synthesize a routing ceiling from dynamic_routing.tier_models (#3962). */
15
- isAutoMode = true) {
12
+ import { logWarning } from "./workflow-logger.js";
13
+ export function resolvePreferredModelConfig(unitType, autoModeStartModel, isAutoMode = true) {
16
14
  const explicitConfig = resolveModelWithFallbacksForUnit(unitType);
17
15
  if (explicitConfig)
18
16
  return explicitConfig;
@@ -24,7 +22,7 @@ isAutoMode = true) {
24
22
  if (!routingConfig.enabled || !routingConfig.tier_models)
25
23
  return undefined;
26
24
  // Don't synthesize a routing config for flat-rate providers (#3453).
27
- if (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider))
25
+ if (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider, autoModeStartModel.flatRateCtx))
28
26
  return undefined;
29
27
  const ceilingModel = routingConfig.tier_models.heavy
30
28
  ?? (autoModeStartModel ? `${autoModeStartModel.provider}/${autoModeStartModel.id}` : undefined);
@@ -51,6 +49,17 @@ sessionModelOverride) {
51
49
  const effectiveSessionModelOverride = sessionModelOverride === undefined
52
50
  ? getSessionModelOverride(ctx.sessionManager.getSessionId())
53
51
  : (sessionModelOverride ?? undefined);
52
+ // Enrich the start model with a flat-rate context up front so routing
53
+ // synthesis and the dispatch-time guard see the same signals (built-in
54
+ // list + user `flat_rate_providers` preference + externalCli auto-
55
+ // detection). The dispatch-time primary-model check below builds its
56
+ // own per-provider context when it has a resolved primary model.
57
+ if (autoModeStartModel) {
58
+ autoModeStartModel = {
59
+ ...autoModeStartModel,
60
+ flatRateCtx: buildFlatRateContext(autoModeStartModel.provider, ctx, prefs),
61
+ };
62
+ }
54
63
  const modelConfig = effectiveSessionModelOverride
55
64
  ? undefined
56
65
  : resolvePreferredModelConfig(unitType, autoModeStartModel, isAutoMode);
@@ -76,12 +85,13 @@ sessionModelOverride) {
76
85
  if (routingConfig.enabled) {
77
86
  const primaryModel = resolveModelId(modelConfig.primary, availableModels, ctx.model?.provider);
78
87
  if (primaryModel) {
79
- if (isFlatRateProvider(primaryModel.provider)) {
88
+ const primaryFlatRateCtx = buildFlatRateContext(primaryModel.provider, ctx, prefs);
89
+ if (isFlatRateProvider(primaryModel.provider, primaryFlatRateCtx)) {
80
90
  routingConfig.enabled = false;
81
91
  }
82
92
  }
83
- else if ((autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider))
84
- || (ctx.model?.provider && isFlatRateProvider(ctx.model.provider))) {
93
+ else if ((autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider, autoModeStartModel.flatRateCtx))
94
+ || (ctx.model?.provider && isFlatRateProvider(ctx.model.provider, buildFlatRateContext(ctx.model.provider, ctx, prefs)))) {
85
95
  // Primary model unresolvable but provider signals indicate flat-rate —
86
96
  // disable routing to prevent quality degradation.
87
97
  routingConfig.enabled = false;
@@ -331,7 +341,40 @@ export function resolveModelId(modelId, availableModels, currentProvider) {
331
341
  * Uses case-insensitive matching with alias support to prevent fail-open on
332
342
  * provider naming variations (e.g. "copilot" vs "github-copilot").
333
343
  */
334
- const FLAT_RATE_PROVIDERS = new Set(["github-copilot", "copilot", "claude-code"]);
335
- export function isFlatRateProvider(provider) {
336
- return FLAT_RATE_PROVIDERS.has(provider.toLowerCase());
344
+ const BUILTIN_FLAT_RATE = new Set(["github-copilot", "copilot", "claude-code"]);
345
+ export function isFlatRateProvider(provider, opts) {
346
+ const p = provider.toLowerCase();
347
+ if (BUILTIN_FLAT_RATE.has(p))
348
+ return true;
349
+ if (opts?.userFlatRate?.some(id => id.toLowerCase() === p))
350
+ return true;
351
+ if (opts?.authMode === "externalCli")
352
+ return true;
353
+ return false;
354
+ }
355
+ /**
356
+ * Build a FlatRateContext for a given provider from live runtime state.
357
+ * Safe to call when ctx or prefs are undefined — missing pieces are
358
+ * treated as "no signal".
359
+ */
360
+ export function buildFlatRateContext(provider, ctx, prefs) {
361
+ let authMode;
362
+ const getAuthMode = ctx?.modelRegistry?.getProviderAuthMode;
363
+ if (typeof getAuthMode === "function") {
364
+ try {
365
+ const mode = getAuthMode(provider);
366
+ if (mode === "apiKey" || mode === "oauth" || mode === "externalCli" || mode === "none") {
367
+ authMode = mode;
368
+ }
369
+ }
370
+ catch (err) {
371
+ // Registry lookup failure must never break flat-rate detection —
372
+ // fall through with authMode undefined and surface the cause.
373
+ logWarning("dispatch", `flat-rate auth-mode lookup failed for ${provider}: ${err instanceof Error ? err.message : String(err)}`);
374
+ }
375
+ }
376
+ return {
377
+ authMode,
378
+ userFlatRate: prefs?.flat_rate_providers,
379
+ };
337
380
  }
@@ -67,6 +67,7 @@ const LIFECYCLE_ONLY_UNITS = new Set([
67
67
  "replan-slice", "complete-slice", "run-uat",
68
68
  "reassess-roadmap", "rewrite-docs",
69
69
  ]);
70
+ import { describeNextUnit, } from "./auto-dashboard.js";
70
71
  import { existsSync, unlinkSync } from "node:fs";
71
72
  import { join } from "node:path";
72
73
  import { _resetHasChangesCache } from "./native-git-bridge.js";
@@ -179,6 +180,15 @@ export function detectRogueFileWrites(unitType, unitId, basePath) {
179
180
  }
180
181
  return rogues;
181
182
  }
183
+ export const STEP_COMPLETE_FALLBACK_MESSAGE = "Step complete. Run /clear, then /gsd to continue (or /gsd auto to run continuously).";
184
+ export function buildStepCompleteMessage(nextState) {
185
+ if (nextState.phase === "complete") {
186
+ return "Step complete — milestone finished. Run /gsd status to review, or start the next milestone.";
187
+ }
188
+ const next = describeNextUnit(nextState);
189
+ return `Step complete. Next: ${next.label}\n`
190
+ + `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`;
191
+ }
182
192
  /**
183
193
  * Pre-verification processing: parallel worker signal check, cache invalidation,
184
194
  * auto-commit, doctor run, state rebuild, worktree sync, artifact verification.
@@ -509,6 +519,26 @@ export async function postUnitPreVerification(pctx, opts) {
509
519
  const attempt = (s.verificationRetryCount.get(retryKey) ?? 0) + 1;
510
520
  s.verificationRetryCount.set(retryKey, attempt);
511
521
  if (attempt > MAX_VERIFICATION_RETRIES) {
522
+ // #4175: For complete-milestone, a blocker placeholder is harmful —
523
+ // the stub SUMMARY has no recovery value (milestone is terminal),
524
+ // it does not update DB status (so deriveState never advances),
525
+ // and it fools stopAuto's presence check into merging a milestone
526
+ // that was never legitimately completed. Pause auto-mode with a
527
+ // clear single failure signal and preserve the worktree branch.
528
+ if (s.currentUnit.type === "complete-milestone") {
529
+ debugLog("postUnit", {
530
+ phase: "artifact-verify-pause-complete-milestone",
531
+ unitType: s.currentUnit.type,
532
+ unitId: s.currentUnit.id,
533
+ attempt,
534
+ maxRetries: MAX_VERIFICATION_RETRIES,
535
+ });
536
+ s.verificationRetryCount.delete(retryKey);
537
+ s.pendingVerificationRetry = null;
538
+ ctx.ui.notify(`Milestone ${s.currentUnit.id} verification failed after ${MAX_VERIFICATION_RETRIES} retries — worktree branch preserved. Re-run /gsd auto once blockers are resolved.`, "error");
539
+ await pauseAuto(ctx, pi);
540
+ return "dispatched";
541
+ }
512
542
  // Retries exhausted — write a blocker placeholder so the pipeline
513
543
  // can advance past this stuck unit (#2653).
514
544
  debugLog("postUnit", {
@@ -836,8 +866,18 @@ export async function postUnitPostVerification(pctx) {
836
866
  debugLog("postUnit", { phase: "quick-task-dispatch", error: String(e) });
837
867
  }
838
868
  }
839
- // Step mode → show wizard instead of dispatch
869
+ // Step mode → show wizard instead of dispatch.
870
+ // Without this notify(), /gsd in step mode finishes a unit and silently
871
+ // exits the loop, leaving the user with no hint to /clear and /gsd again.
840
872
  if (s.stepMode) {
873
+ try {
874
+ const nextState = await deriveState(s.basePath);
875
+ ctx.ui.notify(buildStepCompleteMessage(nextState), "info");
876
+ }
877
+ catch (e) {
878
+ debugLog("postUnit", { phase: "step-wizard-notify", error: String(e) });
879
+ ctx.ui.notify(STEP_COMPLETE_FALLBACK_MESSAGE, "info");
880
+ }
841
881
  return "step-wizard";
842
882
  }
843
883
  return "continue";
@@ -38,7 +38,7 @@ import { existsSync, mkdirSync, readdirSync, rmSync, statSync, unlinkSync, } fro
38
38
  import { join } from "node:path";
39
39
  import { sep as pathSep } from "node:path";
40
40
  import { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js";
41
- import { resolveDefaultSessionModel, resolveDynamicRoutingConfig } from "./preferences-models.js";
41
+ import { isCustomProvider, resolveDefaultSessionModel, resolveDynamicRoutingConfig, } from "./preferences-models.js";
42
42
  import { getSessionModelOverride } from "./session-model-override.js";
43
43
  /**
44
44
  * Bootstrap a fresh auto-mode session. Handles everything from git init
@@ -195,8 +195,18 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
195
195
  //
196
196
  // This preserves #3517 defaults while honoring explicit runtime model
197
197
  // selection for subsequent /gsd runs in the same session.
198
+ //
199
+ // Exception (#4122): when the session provider is a custom provider declared
200
+ // in ~/.gsd/agent/models.json (Ollama, vLLM, OpenAI-compatible proxy, etc.),
201
+ // PREFERENCES.md is skipped entirely. PREFERENCES.md cannot reference custom
202
+ // providers, so honoring it would silently reroute auto-mode to a built-in
203
+ // provider the user is not logged into and surface as "Not logged in · Please
204
+ // run /login" before pausing and resetting to claude-code/claude-sonnet-4-6.
198
205
  const manualSessionOverride = getSessionModelOverride(ctx.sessionManager.getSessionId());
199
- const preferredModel = resolveDefaultSessionModel(ctx.model?.provider);
206
+ const sessionProviderIsCustom = isCustomProvider(ctx.model?.provider);
207
+ const preferredModel = sessionProviderIsCustom
208
+ ? null
209
+ : resolveDefaultSessionModel(ctx.model?.provider);
200
210
  // Validate the preferred model against the live registry + provider auth so
201
211
  // an unconfigured PREFERENCES.md entry (no API key / OAuth) can't become the
202
212
  // start-model snapshot. Without this, every subsequent unit would try to
@@ -622,6 +632,9 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
622
632
  }
623
633
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
624
634
  ctx.ui.setFooter(hideFooter);
635
+ // Hide gsd-health during AUTO — gsd-progress is the single source of truth
636
+ // for last-commit / cost / health signal while auto is running.
637
+ ctx.ui.setWidget("gsd-health", undefined);
625
638
  const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
626
639
  const pendingCount = (state.registry ?? []).filter((m) => m.status !== "complete" && m.status !== "parked").length;
627
640
  const scopeMsg = pendingCount > 1
@@ -636,12 +649,16 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
636
649
  const startModelLabel = s.autoModeStartModel
637
650
  ? `${s.autoModeStartModel.provider}/${s.autoModeStartModel.id}`
638
651
  : ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "default";
639
- // Flat-rate providers (e.g. GitHub Copilot, claude-code) suppress routing
640
- // at dispatch time (#3453) reflect that in the banner.
641
- const { isFlatRateProvider } = await import("./auto-model-selection.js");
652
+ // Flat-rate providers (e.g. GitHub Copilot, claude-code, user-declared
653
+ // subscription proxies, externalCli CLIs) suppress routing at dispatch
654
+ // time (#3453) reflect that in the banner. Thread the same
655
+ // FlatRateContext used by selectAndApplyModel so user-declared
656
+ // flat-rate providers and externalCli auto-detection are respected.
657
+ const { isFlatRateProvider, buildFlatRateContext } = await import("./auto-model-selection.js");
658
+ const bannerPrefs = loadEffectiveGSDPreferences()?.preferences;
642
659
  const effectiveProvider = s.autoModeStartModel?.provider ?? ctx.model?.provider;
643
660
  const effectivelyEnabled = routingConfig.enabled
644
- && !(effectiveProvider && isFlatRateProvider(effectiveProvider));
661
+ && !(effectiveProvider && isFlatRateProvider(effectiveProvider, buildFlatRateContext(effectiveProvider, ctx, bannerPrefs)));
645
662
  // The actual ceiling may come from tier_models.heavy, not the start model.
646
663
  const effectiveCeiling = (routingConfig.enabled && routingConfig.tier_models?.heavy)
647
664
  ? routingConfig.tier_models.heavy
@@ -156,6 +156,19 @@ export async function recoverTimedOutUnit(ctx, pi, unitType, unitId, reason, rct
156
156
  ctx.ui.notify(`${reason === "idle" ? "Idle" : "Timeout"} recovery: steering ${unitType} ${unitId} to produce ${expected} (attempt ${attemptNumber}, session ${recoveryAttempts + 1}/${maxRecoveryAttempts}).`, "warning");
157
157
  return "recovered";
158
158
  }
159
+ // #4175: For complete-milestone, never write a blocker placeholder — a stub
160
+ // SUMMARY has no recovery value (milestone is terminal), it does not update
161
+ // DB status, and downstream merge paths can treat the stub as a legitimate
162
+ // completion signal. Pause instead so the worktree branch is preserved.
163
+ if (unitType === "complete-milestone") {
164
+ writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnitStartedAt, {
165
+ phase: "paused",
166
+ recoveryAttempts: recoveryAttempts + 1,
167
+ lastRecoveryReason: reason,
168
+ });
169
+ ctx.ui.notify(`Milestone ${unitId} ${reason}-recovery exhausted ${maxRecoveryAttempts} attempt(s) — worktree branch preserved. Re-run /gsd auto once blockers are resolved.`, "error");
170
+ return "paused";
171
+ }
159
172
  // Retries exhausted — write a blocker placeholder and advance the pipeline
160
173
  // instead of silently stalling.
161
174
  const placeholder = writeBlockerPlaceholder(unitType, unitId, basePath, `${reason} recovery exhausted ${maxRecoveryAttempts} attempts without producing the artifact.`);
@@ -10,10 +10,15 @@
10
10
  * checks the result and handles control flow.
11
11
  */
12
12
  import { mkdirSync, writeFileSync } from "node:fs";
13
- import { resolveSlicePath } from "./paths.js";
13
+ import { resolveSlicePath, resolveMilestoneFile } from "./paths.js";
14
14
  import { parseUnitId } from "./unit-id.js";
15
- import { isDbAvailable, getTask, getSliceTasks } from "./gsd-db.js";
15
+ import { isDbAvailable, getTask, getSliceTasks, getMilestoneSlices } from "./gsd-db.js";
16
16
  import { loadEffectiveGSDPreferences } from "./preferences.js";
17
+ import { extractVerdict } from "./verdict-parser.js";
18
+ import { isClosedStatus } from "./status-guards.js";
19
+ import { loadFile } from "./files.js";
20
+ import { parseRoadmap } from "./parsers-legacy.js";
21
+ import { isMilestoneComplete } from "./state.js";
17
22
  import { runVerificationGate, formatFailureContext, captureRuntimeErrors, runDependencyAudit, } from "./verification-gate.js";
18
23
  import { writeVerificationJSON } from "./verification-evidence.js";
19
24
  import { logWarning } from "./workflow-logger.js";
@@ -22,6 +27,80 @@ import { join } from "node:path";
22
27
  function isInfraVerificationFailure(stderr) {
23
28
  return /\b(ENOENT|ENOTFOUND|ETIMEDOUT|ECONNRESET|EAI_AGAIN|spawn\s+\S+\s+ENOENT|command not found)\b/i.test(stderr);
24
29
  }
30
+ /**
31
+ * Post-unit guard for `validate-milestone` units (#4094).
32
+ *
33
+ * When validate-milestone writes verdict=needs-remediation, the agent is
34
+ * expected to also call gsd_reassess_roadmap in the same turn to add
35
+ * remediation slices. If they don't, the state machine re-derives
36
+ * `phase: validating-milestone` indefinitely (all slices still complete +
37
+ * verdict still needs-remediation), wasting ~3 dispatches before the stuck
38
+ * detector fires.
39
+ *
40
+ * This guard fires immediately on the first occurrence: if VALIDATION.md
41
+ * verdict is needs-remediation and no incomplete slices exist for the
42
+ * milestone, pause the auto-loop with a clear blocker.
43
+ */
44
+ async function runValidateMilestonePostCheck(vctx, pauseAuto) {
45
+ const { s, ctx, pi } = vctx;
46
+ if (!s.currentUnit)
47
+ return "continue";
48
+ const { milestone: mid } = parseUnitId(s.currentUnit.id);
49
+ if (!mid)
50
+ return "continue";
51
+ const validationFile = resolveMilestoneFile(s.basePath, mid, "VALIDATION");
52
+ if (!validationFile)
53
+ return "continue";
54
+ const validationContent = await loadFile(validationFile);
55
+ if (!validationContent)
56
+ return "continue";
57
+ const verdict = extractVerdict(validationContent);
58
+ if (verdict !== "needs-remediation")
59
+ return "continue";
60
+ const incompleteSliceCount = await countIncompleteSlices(s.basePath, mid);
61
+ // If any non-closed slices exist, the agent successfully queued remediation
62
+ // work — proceed normally. The state machine will execute those slices and
63
+ // re-validate per the #3596/#3670 fix.
64
+ if (incompleteSliceCount > 0)
65
+ return "continue";
66
+ ctx.ui.notify(`Milestone ${mid} validation returned verdict=needs-remediation but no remediation slices were added. Pausing for human review.`, "error");
67
+ process.stderr.write(`validate-milestone: pausing — verdict=needs-remediation with no incomplete slices for ${mid}. ` +
68
+ `The agent must call gsd_reassess_roadmap to add remediation slices before re-validation.\n`);
69
+ await pauseAuto(ctx, pi);
70
+ return "pause";
71
+ }
72
+ /**
73
+ * Count slices for a milestone that are not in a closed status.
74
+ * DB-backed projects are authoritative (#4094 peer review); falls back to
75
+ * roadmap parsing only when the DB is unavailable.
76
+ */
77
+ async function countIncompleteSlices(basePath, milestoneId) {
78
+ if (isDbAvailable()) {
79
+ const slices = getMilestoneSlices(milestoneId);
80
+ if (slices.length === 0) {
81
+ // No DB rows — treat as "unknown", do not pause.
82
+ return 1;
83
+ }
84
+ return slices.filter((slice) => !isClosedStatus(slice.status)).length;
85
+ }
86
+ // Filesystem fallback: parse the roadmap markdown.
87
+ try {
88
+ const roadmapFile = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
89
+ if (!roadmapFile)
90
+ return 1;
91
+ const roadmapContent = await loadFile(roadmapFile);
92
+ if (!roadmapContent)
93
+ return 1;
94
+ const roadmap = parseRoadmap(roadmapContent);
95
+ if (roadmap.slices.length === 0)
96
+ return 1;
97
+ return isMilestoneComplete(roadmap) ? 0 : 1;
98
+ }
99
+ catch {
100
+ // Parsing failures should not cause false-positive pauses.
101
+ return 1;
102
+ }
103
+ }
25
104
  /**
26
105
  * Run the verification gate for the current execute-task unit.
27
106
  * Returns:
@@ -31,7 +110,13 @@ function isInfraVerificationFailure(stderr) {
31
110
  */
32
111
  export async function runPostUnitVerification(vctx, pauseAuto) {
33
112
  const { s, ctx, pi } = vctx;
34
- if (!s.currentUnit || s.currentUnit.type !== "execute-task") {
113
+ if (!s.currentUnit) {
114
+ return "continue";
115
+ }
116
+ if (s.currentUnit.type === "validate-milestone") {
117
+ return await runValidateMilestonePostCheck(vctx, pauseAuto);
118
+ }
119
+ if (s.currentUnit.type !== "execute-task") {
35
120
  return "continue";
36
121
  }
37
122
  try {