gsd-pi 2.82.0-dev.dfbc5f58f → 2.82.0-dev.e7a7f1ed5

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 (182) hide show
  1. package/README.md +1 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +1 -1
  4. package/dist/resources/extensions/gsd/auto/phases.js +73 -30
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +66 -1
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -0
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +10 -16
  8. package/dist/resources/extensions/gsd/auto-recovery.js +40 -13
  9. package/dist/resources/extensions/gsd/auto-start.js +3 -3
  10. package/dist/resources/extensions/gsd/auto-verification.js +17 -4
  11. package/dist/resources/extensions/gsd/auto-worktree.js +65 -9
  12. package/dist/resources/extensions/gsd/auto.js +7 -2
  13. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +27 -6
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -2
  15. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +7 -2
  16. package/dist/resources/extensions/gsd/crash-recovery.js +16 -4
  17. package/dist/resources/extensions/gsd/db/milestone-leases.js +24 -0
  18. package/dist/resources/extensions/gsd/doctor-git-checks.js +46 -1
  19. package/dist/resources/extensions/gsd/git-service.js +6 -2
  20. package/dist/resources/extensions/gsd/gsd-db.js +20 -6
  21. package/dist/resources/extensions/gsd/guided-flow-queue.js +4 -3
  22. package/dist/resources/extensions/gsd/guided-flow.js +95 -116
  23. package/dist/resources/extensions/gsd/guided-unit-context.js +23 -0
  24. package/dist/resources/extensions/gsd/migration-auto-check.js +12 -17
  25. package/dist/resources/extensions/gsd/pending-auto-start.js +52 -0
  26. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  28. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -8
  29. package/dist/resources/extensions/gsd/prompts/discuss.md +9 -9
  30. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +4 -4
  31. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +3 -3
  32. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  33. package/dist/resources/extensions/gsd/prompts/queue.md +4 -4
  34. package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  35. package/dist/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
  36. package/dist/resources/extensions/gsd/queue-reorder-ui.js +30 -13
  37. package/dist/resources/extensions/gsd/smart-entry-routing.js +36 -0
  38. package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +9 -14
  39. package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +19 -24
  40. package/dist/resources/extensions/gsd/status-guards.js +7 -0
  41. package/dist/resources/extensions/gsd/workflow-mcp.js +17 -1
  42. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  43. package/dist/web/standalone/.next/BUILD_ID +1 -1
  44. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  45. package/dist/web/standalone/.next/build-manifest.json +2 -2
  46. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  47. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/index.html +1 -1
  65. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  72. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/package.json +1 -1
  77. package/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
  78. package/packages/pi-ai/dist/providers/google-gemini-cli.js +5 -0
  79. package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
  80. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts +2 -0
  81. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts.map +1 -0
  82. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js +41 -0
  83. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js.map +1 -0
  84. package/packages/pi-ai/src/providers/google-gemini-cli.test.ts +49 -0
  85. package/packages/pi-ai/src/providers/google-gemini-cli.ts +7 -0
  86. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +24 -6
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  90. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +23 -7
  91. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  92. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts +2 -0
  93. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts.map +1 -0
  94. package/packages/pi-tui/dist/__tests__/terminal.test.js +103 -0
  95. package/packages/pi-tui/dist/__tests__/terminal.test.js.map +1 -0
  96. package/packages/pi-tui/dist/terminal.d.ts +2 -0
  97. package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
  98. package/packages/pi-tui/dist/terminal.js +12 -0
  99. package/packages/pi-tui/dist/terminal.js.map +1 -1
  100. package/packages/pi-tui/src/__tests__/terminal.test.ts +121 -0
  101. package/packages/pi-tui/src/terminal.ts +11 -0
  102. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  103. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +1 -1
  104. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +9 -0
  105. package/src/resources/extensions/gsd/auto/phases.ts +83 -37
  106. package/src/resources/extensions/gsd/auto-dashboard.ts +72 -1
  107. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -0
  108. package/src/resources/extensions/gsd/auto-dispatch.ts +10 -16
  109. package/src/resources/extensions/gsd/auto-recovery.ts +45 -11
  110. package/src/resources/extensions/gsd/auto-start.ts +2 -3
  111. package/src/resources/extensions/gsd/auto-verification.ts +22 -2
  112. package/src/resources/extensions/gsd/auto-worktree.ts +74 -9
  113. package/src/resources/extensions/gsd/auto.ts +8 -2
  114. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +36 -6
  115. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -2
  116. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +8 -3
  117. package/src/resources/extensions/gsd/crash-recovery.ts +16 -2
  118. package/src/resources/extensions/gsd/db/milestone-leases.ts +26 -0
  119. package/src/resources/extensions/gsd/doctor-git-checks.ts +45 -1
  120. package/src/resources/extensions/gsd/doctor-types.ts +1 -0
  121. package/src/resources/extensions/gsd/git-service.ts +6 -3
  122. package/src/resources/extensions/gsd/gsd-db.ts +18 -6
  123. package/src/resources/extensions/gsd/guided-flow-queue.ts +4 -3
  124. package/src/resources/extensions/gsd/guided-flow.ts +128 -133
  125. package/src/resources/extensions/gsd/guided-unit-context.ts +30 -0
  126. package/src/resources/extensions/gsd/migration-auto-check.ts +15 -23
  127. package/src/resources/extensions/gsd/pending-auto-start.ts +79 -0
  128. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  129. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  130. package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -8
  131. package/src/resources/extensions/gsd/prompts/discuss.md +9 -9
  132. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +4 -4
  133. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +3 -3
  134. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  135. package/src/resources/extensions/gsd/prompts/queue.md +4 -4
  136. package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  137. package/src/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
  138. package/src/resources/extensions/gsd/queue-reorder-ui.ts +31 -13
  139. package/src/resources/extensions/gsd/smart-entry-routing.ts +77 -0
  140. package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +12 -15
  141. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +17 -25
  142. package/src/resources/extensions/gsd/status-guards.ts +8 -0
  143. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +71 -0
  144. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
  145. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +29 -1
  146. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +53 -2
  147. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +76 -5
  148. package/src/resources/extensions/gsd/tests/auto-stop-notification.test.ts +20 -0
  149. package/src/resources/extensions/gsd/tests/checkout-branch-stash-guard.test.ts +87 -0
  150. package/src/resources/extensions/gsd/tests/clear-stale-autostart.test.ts +11 -2
  151. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +5 -9
  152. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -0
  153. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +2 -0
  154. package/src/resources/extensions/gsd/tests/db-authority-regression.test.ts +208 -0
  155. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +27 -0
  156. package/src/resources/extensions/gsd/tests/doctor-empty-worktree.test.ts +65 -0
  157. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +11 -0
  158. package/src/resources/extensions/gsd/tests/guided-discuss-project-prompt-rendering.test.ts +2 -0
  159. package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +106 -0
  160. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +59 -11
  161. package/src/resources/extensions/gsd/tests/guided-tool-contract.test.ts +65 -0
  162. package/src/resources/extensions/gsd/tests/headless-milestone-parity.test.ts +7 -7
  163. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +9 -0
  164. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +46 -0
  165. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +179 -0
  166. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +26 -18
  167. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +29 -5
  168. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +2 -0
  169. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +59 -0
  170. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +37 -1
  171. package/src/resources/extensions/gsd/tests/queue-reorder-ui.test.ts +54 -0
  172. package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +43 -0
  173. package/src/resources/extensions/gsd/tests/run-uat-replay-cap.test.ts +2 -3
  174. package/src/resources/extensions/gsd/tests/smart-entry-routing.test.ts +113 -0
  175. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +22 -1
  176. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +119 -23
  177. package/src/resources/extensions/gsd/tests/status-guards.test.ts +13 -1
  178. package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +29 -2
  179. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +18 -0
  180. package/src/resources/extensions/gsd/workflow-mcp.ts +18 -1
  181. /package/dist/web/standalone/.next/static/{q0WYuDVbHeFFYbdd-fei2 → 4dSwdrs__8NwCZggxP9KF}/_buildManifest.js +0 -0
  182. /package/dist/web/standalone/.next/static/{q0WYuDVbHeFFYbdd-fei2 → 4dSwdrs__8NwCZggxP9KF}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -503,7 +503,7 @@ On first run, GSD launches a branded setup wizard that walks you through LLM pro
503
503
  | `/gsd mcp` | MCP server status and connectivity |
504
504
  | `/gsd status` | Progress dashboard |
505
505
  | `/gsd brief <mode>` | Generate a visual HTML brief (diagram, plan, diff, recap, table, slides) |
506
- | `/gsd queue` | Queue future milestones (safe during auto mode) |
506
+ | `/gsd queue` | Queue/reorder future milestones (`pending`, `queued`, or legacy `planned`; safe during auto mode) |
507
507
  | `/gsd prefs` | Model selection, timeouts, budget ceiling |
508
508
  | `/gsd migrate` | Migrate a v1 `.planning` directory to `.gsd` format |
509
509
  | `/gsd help` | Categorized command reference for all GSD subcommands |
@@ -1 +1 @@
1
- 5c0ebf59b3b9fce3
1
+ 68e342bd44fe9191
@@ -269,7 +269,7 @@ function makeErrorMessage(model, errorMsg) {
269
269
  export function isClaudeCodeAbortErrorMessage(message) {
270
270
  if (!message)
271
271
  return false;
272
- return /\b(?:claude code process aborted by user|request aborted by user|process aborted by user)\b/i.test(message);
272
+ return /\b(?:claude code process aborted by user|request aborted by user|process aborted by user|aborterror)\b/i.test(message);
273
273
  }
274
274
  function isBareClaudeCodeAbortErrorMessage(message) {
275
275
  if (!message)
@@ -44,6 +44,7 @@ import { resolveManifest } from "../unit-context-manifest.js";
44
44
  import { createWorktreeSafetyModule } from "../worktree-safety.js";
45
45
  import { isSuspiciousGhostCompletion } from "../auto-unit-closeout.js";
46
46
  import { decideVerificationRetry, verificationRetryKey } from "./verification-retry-policy.js";
47
+ import { buildPhaseHandoffOutcome, setAutoOutcomeWidget } from "../auto-dashboard.js";
47
48
  // ─── Path Comparison Helper ───────────────────────────────────────────────
48
49
  /** Compare two paths for physical identity, tolerating trailing slashes and symlinks. */
49
50
  function isSamePathLocal(a, b) {
@@ -103,6 +104,12 @@ function unitWritesSource(unitType) {
103
104
  function formatWorktreeSafetyFailure(result) {
104
105
  return `Worktree Safety failed (${result.kind}): ${result.reason} ${result.remediation}`;
105
106
  }
107
+ function formatWorktreeSafetyStopReason(result) {
108
+ if (result.kind === "empty-worktree-with-project-content") {
109
+ return `Worktree Safety failed (${result.kind}). Run /gsd doctor fix, then /gsd auto.`;
110
+ }
111
+ return `Worktree Safety failed (${result.kind}).`;
112
+ }
106
113
  function resolveEmptyWorktreeWithProjectContent(unitRoot, projectRoot) {
107
114
  if (isSamePathLocal(unitRoot, projectRoot))
108
115
  return false;
@@ -176,7 +183,7 @@ async function validateSourceWriteWorktreeSafety(ic, unitType, unitId, milestone
176
183
  projectRoot,
177
184
  });
178
185
  ctx.ui.notify(msg, "error");
179
- await deps.stopAuto(ctx, pi, msg);
186
+ await deps.stopAuto(ctx, pi, formatWorktreeSafetyStopReason(result));
180
187
  return { action: "break", reason: result.kind };
181
188
  }
182
189
  // ─── Session timeout auto-resume state ────────────────────────────────────────
@@ -1331,35 +1338,8 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1331
1338
  s.currentUnit.type === unitType &&
1332
1339
  s.currentUnit.id === unitId);
1333
1340
  const previousTier = s.currentUnitRouting?.tier;
1334
- // Scope workflow-logger buffer to this unit so post-finalize drains are
1335
- // per-unit. Without this, the module-level _buffer accumulates across every
1336
- // unit in the same Node process (see workflow-logger.ts module header).
1337
- _resetLogs();
1338
1341
  const dispatchKey = `${unitType}/${unitId}`;
1339
- s.unitDispatchCount.set(dispatchKey, (s.unitDispatchCount.get(dispatchKey) ?? 0) + 1);
1340
- s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
1341
- s.lastGitActionFailure = null;
1342
- s.lastGitActionStatus = null;
1343
- s.lastUnitAgentEndMessages = null;
1344
- setCurrentPhase(unitType, {
1345
- basePath: s.basePath,
1346
- traceId: ic.flowId,
1347
- turnId: `iter-${ic.iteration}`,
1348
- causedBy: "unit-start",
1349
- });
1350
- s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
1351
- const unitStartSeq = ic.nextSeq();
1352
- deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
1353
- deps.captureAvailableSkills();
1354
- writeUnitRuntimeRecord(s.basePath, unitType, unitId, s.currentUnit.startedAt, {
1355
- phase: "dispatched",
1356
- wrapupWarningSent: false,
1357
- timeoutAt: null,
1358
- lastProgressAt: s.currentUnit.startedAt,
1359
- progressCount: 0,
1360
- lastProgressKind: "dispatch",
1361
- recoveryAttempts: 0, // Reset so re-dispatched units get full recovery budget (#2322)
1362
- });
1342
+ const nextDispatchCount = (s.unitDispatchCount.get(dispatchKey) ?? 0) + 1;
1363
1343
  // Status bar (widget + preconditions deferred until after model selection — see #2899)
1364
1344
  ctx.ui.setStatus("gsd-auto", "auto");
1365
1345
  if (mid)
@@ -1413,7 +1393,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1413
1393
  finalPrompt = `${capped}\n\n---\n\n${finalPrompt}`;
1414
1394
  s.pendingCrashRecovery = null;
1415
1395
  }
1416
- else if ((s.unitDispatchCount.get(dispatchKey) ?? 0) > 1) {
1396
+ else if (nextDispatchCount > 1) {
1417
1397
  const diagnostic = deps.getDeepDiagnostic(s.basePath);
1418
1398
  if (diagnostic) {
1419
1399
  const cappedDiag = diagnostic.length > MAX_RECOVERY_CHARS
@@ -1452,6 +1432,11 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1452
1432
  logWarning("engine", "Prompt reorder failed", { error: msg });
1453
1433
  }
1454
1434
  // Select and apply model (with tier escalation on retry — normal units only)
1435
+ const prevUnitRouting = s.currentUnitRouting;
1436
+ const prevUnitModel = s.currentUnitModel;
1437
+ const prevDispatchedModelId = s.currentDispatchedModelId;
1438
+ const prevSessionModel = ctx.model;
1439
+ const prevSessionThinkingLevel = pi.getThinkingLevel();
1455
1440
  const modelResult = await deps.selectAndApplyModel(ctx, pi, unitType, unitId, s.basePath, prefs, s.verbose, s.autoModeStartModel, sidecarItem ? undefined : { isRetry, previousTier }, undefined, s.manualSessionModelOverride, s.autoModeStartThinkingLevel);
1456
1441
  s.currentUnitRouting =
1457
1442
  modelResult.routing;
@@ -1495,12 +1480,58 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1495
1480
  ? ctx.modelRegistry.getProviderAuthMode(ctx.model.provider)
1496
1481
  : undefined,
1497
1482
  baseUrl: s.currentUnitModel?.baseUrl ?? ctx.model?.baseUrl,
1483
+ activeTools: typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [],
1498
1484
  });
1499
1485
  if (compatibilityError) {
1486
+ s.currentUnitRouting = prevUnitRouting;
1487
+ s.currentUnitModel = prevUnitModel;
1488
+ s.currentDispatchedModelId = prevDispatchedModelId;
1489
+ if (s.checkpointSha) {
1490
+ cleanupCheckpoint(s.basePath, unitId);
1491
+ s.checkpointSha = null;
1492
+ }
1493
+ if (prevSessionModel) {
1494
+ const ok = await pi.setModel(prevSessionModel, { persist: false });
1495
+ if (!ok) {
1496
+ ctx.ui.notify("Failed to restore previous session model after compatibility check failure.", "warning");
1497
+ }
1498
+ if (prevSessionThinkingLevel) {
1499
+ pi.setThinkingLevel(prevSessionThinkingLevel);
1500
+ }
1501
+ }
1500
1502
  ctx.ui.notify(compatibilityError, "error");
1501
1503
  await deps.stopAuto(ctx, pi, compatibilityError);
1502
1504
  return { action: "break", reason: "workflow-capability" };
1503
1505
  }
1506
+ // Scope workflow-logger buffer to this unit so post-finalize drains are
1507
+ // per-unit. Without this, the module-level _buffer accumulates across every
1508
+ // unit in the same Node process (see workflow-logger.ts module header).
1509
+ _resetLogs();
1510
+ const unitStartedAt = Date.now();
1511
+ s.unitDispatchCount.set(dispatchKey, nextDispatchCount);
1512
+ s.currentUnit = { type: unitType, id: unitId, startedAt: unitStartedAt };
1513
+ s.lastGitActionFailure = null;
1514
+ s.lastGitActionStatus = null;
1515
+ s.lastUnitAgentEndMessages = null;
1516
+ setCurrentPhase(unitType, {
1517
+ basePath: s.basePath,
1518
+ traceId: ic.flowId,
1519
+ turnId: `iter-${ic.iteration}`,
1520
+ causedBy: "unit-start",
1521
+ });
1522
+ s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
1523
+ const unitStartSeq = ic.nextSeq();
1524
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
1525
+ deps.captureAvailableSkills();
1526
+ writeUnitRuntimeRecord(s.basePath, unitType, unitId, unitStartedAt, {
1527
+ phase: "dispatched",
1528
+ wrapupWarningSent: false,
1529
+ timeoutAt: null,
1530
+ lastProgressAt: unitStartedAt,
1531
+ progressCount: 0,
1532
+ lastProgressKind: "dispatch",
1533
+ recoveryAttempts: 0, // Reset so re-dispatched units get full recovery budget (#2322)
1534
+ });
1504
1535
  // Progress widget + preconditions — deferred to after model selection so the
1505
1536
  // widget's first render tick shows the correct model (#2899).
1506
1537
  deps.updateProgressWidget(ctx, unitType, unitId, state);
@@ -1930,6 +1961,18 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
1930
1961
  lastProgressAt: Date.now(),
1931
1962
  lastProgressKind: "finalize-success",
1932
1963
  });
1964
+ if (!preUnitSnapshot.type.startsWith("hook/") &&
1965
+ preUnitSnapshot.type !== "custom-step" &&
1966
+ preUnitSnapshot.type !== "complete-milestone") {
1967
+ setAutoOutcomeWidget(ctx, {
1968
+ ...buildPhaseHandoffOutcome({
1969
+ unitType: preUnitSnapshot.type,
1970
+ unitId: preUnitSnapshot.id,
1971
+ agentEndMessages: s.lastUnitAgentEndMessages,
1972
+ }),
1973
+ startedAt: s.autoStartTime,
1974
+ });
1975
+ }
1933
1976
  }
1934
1977
  s.currentUnit = null;
1935
1978
  clearCurrentPhase();
@@ -27,6 +27,19 @@ export function extractUatSliceId(unitId) {
27
27
  return slice;
28
28
  return null;
29
29
  }
30
+ export function buildPhaseHandoffOutcome(input) {
31
+ const phase = unitPhaseLabel(input.unitType);
32
+ const detail = extractLastAssistantSummary(input.agentEndMessages) ??
33
+ `Completed ${unitVerb(input.unitType)} ${input.unitId}.`;
34
+ return {
35
+ status: "complete",
36
+ title: `${phase} complete`,
37
+ detail,
38
+ unitLabel: `${unitVerb(input.unitType)} ${input.unitId}`,
39
+ nextAction: "Preparing the next phase. Review this handoff while the next session starts.",
40
+ commands: ["/gsd status for overview", "/gsd visualize to inspect", "/gsd notifications for history"],
41
+ };
42
+ }
30
43
  // ─── Unit Description Helpers ─────────────────────────────────────────────────
31
44
  export function unitVerb(unitType) {
32
45
  if (unitType.startsWith("hook/"))
@@ -464,7 +477,6 @@ export function _resetWidgetModeForTests() {
464
477
  export function updateProgressWidget(ctx, unitType, unitId, state, accessors, tierBadge) {
465
478
  if (!ctx.hasUI)
466
479
  return;
467
- ctx.ui.setWidget("gsd-outcome", undefined);
468
480
  // Welcome header is a startup-only banner — permanently suppress it once
469
481
  // auto-mode activates. The dashboard widget owns all status from here.
470
482
  // Note: setHeader(undefined) restores the built-in header (logo +
@@ -927,3 +939,56 @@ function normalizeRollupText(value) {
927
939
  return null;
928
940
  return clean;
929
941
  }
942
+ function isAssistantMessage(value) {
943
+ if (!value || typeof value !== "object")
944
+ return false;
945
+ const record = value;
946
+ if (record.role === "assistant")
947
+ return true;
948
+ const message = record.message;
949
+ if (message && typeof message === "object") {
950
+ return message.role === "assistant";
951
+ }
952
+ return false;
953
+ }
954
+ function extractLastAssistantSummary(messages) {
955
+ if (!messages || messages.length === 0)
956
+ return null;
957
+ for (let i = messages.length - 1; i >= 0; i--) {
958
+ if (!isAssistantMessage(messages[i]))
959
+ continue;
960
+ const text = extractMessageText(messages[i]);
961
+ const clean = normalizeRollupText(text);
962
+ if (clean)
963
+ return truncateToWidth(clean, 220, "…");
964
+ }
965
+ return null;
966
+ }
967
+ function extractMessageText(value) {
968
+ if (typeof value === "string")
969
+ return value;
970
+ if (!value || typeof value !== "object")
971
+ return null;
972
+ const record = value;
973
+ if (typeof record.content === "string")
974
+ return record.content;
975
+ const message = record.message;
976
+ if (message && typeof message === "object") {
977
+ return extractMessageText(message);
978
+ }
979
+ const content = record.content;
980
+ if (Array.isArray(content)) {
981
+ const parts = content
982
+ .map((part) => {
983
+ if (typeof part === "string")
984
+ return part;
985
+ if (!part || typeof part !== "object")
986
+ return "";
987
+ const partRecord = part;
988
+ return typeof partRecord.text === "string" ? partRecord.text : "";
989
+ })
990
+ .filter(Boolean);
991
+ return parts.length > 0 ? parts.join(" ") : null;
992
+ }
993
+ return null;
994
+ }
@@ -224,6 +224,7 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
224
224
  unitType,
225
225
  authMode: ctx.model?.provider ? ctx.modelRegistry.getProviderAuthMode(ctx.model.provider) : undefined,
226
226
  baseUrl: ctx.model?.baseUrl,
227
+ activeTools: typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [],
227
228
  });
228
229
  if (compatibilityError) {
229
230
  ctx.ui.notify(compatibilityError, "error");
@@ -299,9 +299,7 @@ export const DISPATCH_RULES = [
299
299
  const attempts = incrementUatCount(basePath, mid, sliceId);
300
300
  if (attempts > MAX_UAT_ATTEMPTS) {
301
301
  return {
302
- action: "stop",
303
- reason: `run-uat for ${mid}/${sliceId} has been dispatched ${attempts - 1} times without producing a verdict. Verification commands may be broken — fix the UAT spec or manually write an ASSESSMENT verdict.`,
304
- level: "warning",
302
+ action: "skip",
305
303
  };
306
304
  }
307
305
  const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT");
@@ -1098,16 +1096,16 @@ export const DISPATCH_RULES = [
1098
1096
  return { action: "skip" };
1099
1097
  }
1100
1098
  }
1101
- // Safety guard (#2675, #5747): block completion when VALIDATION
1102
- // verdict is non-passing. The state machine treats these verdicts as
1103
- // terminal, but completing-milestone should NOT proceed — remediation
1104
- // or human attention is needed.
1099
+ // Safety guard (#2675, #5747, #5920): block completion when VALIDATION
1100
+ // verdict is anything other than pass. The state machine treats these
1101
+ // verdicts as terminal, but completing-milestone should NOT proceed —
1102
+ // remediation or human attention is needed.
1105
1103
  const validationFile = resolveMilestoneFile(basePath, mid, "VALIDATION");
1106
1104
  if (validationFile) {
1107
1105
  const validationContent = await loadFile(validationFile);
1108
1106
  if (validationContent) {
1109
1107
  const verdict = extractVerdict(validationContent);
1110
- if (verdict === "needs-remediation" || verdict === "needs-attention") {
1108
+ if (verdict !== "pass") {
1111
1109
  return {
1112
1110
  action: "stop",
1113
1111
  reason: `Cannot complete milestone ${mid}: VALIDATION verdict is "${verdict}". Address the validation findings and re-run validation, or update the verdict manually.`,
@@ -1125,16 +1123,12 @@ export const DISPATCH_RULES = [
1125
1123
  level: "error",
1126
1124
  };
1127
1125
  }
1128
- // Safety guard (#1703): verify the milestone produced implementation
1129
- // artifacts (non-.gsd/ files). A milestone with only plan files and
1130
- // zero implementation code should not be marked complete.
1126
+ // Safety signal (#1703, #5097): detect milestones with only .gsd/
1127
+ // artifacts. This no longer hard-blocks completion because some
1128
+ // milestones are intentionally planning/documentation-only.
1131
1129
  const artifactCheck = hasImplementationArtifacts(basePath, mid);
1132
1130
  if (artifactCheck === "absent") {
1133
- return {
1134
- action: "stop",
1135
- reason: `Cannot complete milestone ${mid}: no implementation files found outside .gsd/. The milestone has only plan files — actual code changes are required.`,
1136
- level: "error",
1137
- };
1131
+ logWarning("dispatch", `Milestone ${mid} has no implementation files outside .gsd/ — continuing complete-milestone dispatch (planning-only/documentation-only milestone).`);
1138
1132
  }
1139
1133
  if (artifactCheck === "unknown") {
1140
1134
  logWarning("dispatch", `Implementation artifact check inconclusive for ${mid} — proceeding (git context unavailable)`);
@@ -162,9 +162,16 @@ export function hasImplementationArtifacts(basePath, milestoneId) {
162
162
  // Strategy: check `git diff --name-only` against the merge-base with the
163
163
  // main branch. This captures ALL files changed during the milestone's
164
164
  // lifetime while running on a milestone branch.
165
- const integrationBranch = milestoneId
166
- ? readIntegrationBranch(basePath, milestoneId) ?? detectMainBranch(basePath)
167
- : detectMainBranch(basePath);
165
+ const recordedIntegrationBranch = milestoneId
166
+ ? readIntegrationBranch(basePath, milestoneId)
167
+ : null;
168
+ let integrationBranch;
169
+ if (recordedIntegrationBranch?.startsWith("milestone/")) {
170
+ integrationBranch = detectMainBranch(basePath);
171
+ }
172
+ else {
173
+ integrationBranch = recordedIntegrationBranch ?? detectMainBranch(basePath);
174
+ }
168
175
  const currentBranch = getCurrentBranch(basePath);
169
176
  const branchDiff = getChangedFilesSinceBranch(basePath, integrationBranch);
170
177
  if (!branchDiff.ok)
@@ -496,29 +503,49 @@ function commitMatchesMilestone(basePath, message, milestoneId, files) {
496
503
  // rather than Mxx/Sxx/Tyy. Bind those commits back to the milestone when
497
504
  // either the commit touched this milestone's artifacts, or — for projects
498
505
  // where .gsd/ is gitignored/external (#5033) — the message explicitly
499
- // names the milestone or local GSD state proves the task belongs here.
506
+ // names the milestone, local GSD state proves the task belongs here, or the
507
+ // commit is implementation-bearing evidence itself (#5100).
500
508
  if (/^GSD-Task:\s*S[^/\s]+\/T\S+/m.test(message)) {
501
509
  if (files.some((file) => isMilestoneArtifactPath(file, milestoneId)))
502
510
  return true;
503
511
  if (commitMessageMentionsMilestone(message, milestoneId))
504
512
  return true;
505
- if (commitTaskTrailerBelongsToMilestone(basePath, message, milestoneId))
513
+ const taskTrailerOwnership = getTaskOwnershipStatus(basePath, message, milestoneId);
514
+ if (taskTrailerOwnership === true)
515
+ return true;
516
+ if (taskTrailerOwnership === false)
517
+ return false;
518
+ // taskTrailerOwnership === null: unknown ownership. Apply fallback only
519
+ // in this case to avoid cross-milestone attribution.
520
+ if (MILESTONE_ID_RE.test(milestoneId) && classifyImplementationFiles(files) === "present")
506
521
  return true;
507
522
  }
508
523
  return false;
509
524
  }
510
- function commitTaskTrailerBelongsToMilestone(basePath, message, milestoneId) {
525
+ /**
526
+ * Tri-state task ownership probe.
527
+ * true => DB or local files confirm this milestone owns the task.
528
+ * false => DB is available and this milestone is registered, but task is absent.
529
+ * null => ownership unknown (milestone not in DB yet, or no DB + no local files).
530
+ */
531
+ function getTaskOwnershipStatus(basePath, message, milestoneId) {
511
532
  const match = message.match(/^GSD-Task:\s*(S[^/\s]+)\/(T[^\s]+)/m);
512
533
  if (!match)
513
- return false;
534
+ return null;
514
535
  const [, sliceId, taskId] = match;
515
- if (getTask(milestoneId, sliceId, taskId))
516
- return true;
536
+ if (isDbAvailable()) {
537
+ if (!getMilestone(milestoneId))
538
+ return null;
539
+ return getTask(milestoneId, sliceId, taskId) ? true : false;
540
+ }
541
+ // DB unavailable: fallback to local task-file presence.
517
542
  const tasksDir = resolveTasksDir(basePath, milestoneId, sliceId);
518
- if (!tasksDir)
519
- return false;
520
- return existsSync(join(tasksDir, `${taskId}-PLAN.md`))
521
- || existsSync(join(tasksDir, `${taskId}-SUMMARY.md`));
543
+ if (tasksDir
544
+ && (existsSync(join(tasksDir, `${taskId}-PLAN.md`))
545
+ || existsSync(join(tasksDir, `${taskId}-SUMMARY.md`)))) {
546
+ return true;
547
+ }
548
+ return null;
522
549
  }
523
550
  function commitMessageMentionsMilestone(message, milestoneId) {
524
551
  if (!MILESTONE_ID_RE.test(milestoneId))
@@ -21,10 +21,10 @@ import { invalidateAllCaches } from "./cache.js";
21
21
  import { writeLock, clearLock } from "./crash-recovery.js";
22
22
  import { acquireSessionLock, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
23
23
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
24
- import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch, nativeCheckoutBranch, nativeBranchList, nativeBranchExists, nativeBranchListMerged, nativeBranchDelete, nativeWorktreeRemove, nativeCommitCountBetween, } from "./native-git-bridge.js";
24
+ import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchList, nativeBranchExists, nativeBranchListMerged, nativeBranchDelete, nativeWorktreeRemove, nativeCommitCountBetween, } from "./native-git-bridge.js";
25
25
  import { GitServiceImpl } from "./git-service.js";
26
26
  import { captureIntegrationBranch, detectWorktreeName, setActiveMilestoneId, } from "./worktree.js";
27
- import { getAutoWorktreePath } from "./auto-worktree.js";
27
+ import { getAutoWorktreePath, checkoutBranchWithStashGuard } from "./auto-worktree.js";
28
28
  import { readResourceVersion, cleanStaleRuntimeUnits } from "./auto-worktree.js";
29
29
  import { worktreePath as getWorktreeDir, isInsideWorktreesDir } from "./worktree-manager.js";
30
30
  import { emitWorktreeOrphaned } from "./worktree-telemetry.js";
@@ -901,7 +901,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
901
901
  const integrationBranch = nativeDetectMainBranch(base);
902
902
  const branchToCheckout = resolveIsolationNoneBranchCheckout(currentBranch, integrationBranch, isolationMode, isRepo);
903
903
  if (branchToCheckout) {
904
- nativeCheckoutBranch(base, branchToCheckout);
904
+ checkoutBranchWithStashGuard(base, branchToCheckout, "isolation-none-recovery");
905
905
  logWarning("bootstrap", `Returned to "${branchToCheckout}" — HEAD was on stale milestone branch "${currentBranch}" (isolation: none does not use milestone branches).`);
906
906
  }
907
907
  }
@@ -65,12 +65,25 @@ async function runValidateMilestonePostCheck(vctx, pauseAuto) {
65
65
  const { milestone: mid } = parseUnitId(s.currentUnit.id);
66
66
  if (!mid)
67
67
  return "continue";
68
+ const setToolFailureRetry = (message) => {
69
+ const retryKey = verificationRetryKey(s.currentUnit.type, s.currentUnit.id);
70
+ const attempt = (s.verificationRetryCount.get(retryKey) ?? 0) + 1;
71
+ s.verificationRetryCount.set(retryKey, attempt);
72
+ s.pendingVerificationRetry = {
73
+ unitId: s.currentUnit.id,
74
+ failureContext: message,
75
+ attempt,
76
+ };
77
+ return "retry";
78
+ };
68
79
  const validationFile = resolveMilestoneFile(s.basePath, mid, "VALIDATION");
69
- if (!validationFile)
70
- return "continue";
80
+ if (!validationFile) {
81
+ return setToolFailureRetry("You must call gsd_validate_milestone to persist the validation results. No VALIDATION.md was created.");
82
+ }
71
83
  const validationContent = await loadFile(validationFile);
72
- if (!validationContent)
73
- return "continue";
84
+ if (!validationContent) {
85
+ return setToolFailureRetry("You must call gsd_validate_milestone to persist the validation results. VALIDATION.md exists but is empty.");
86
+ }
74
87
  const verdict = extractVerdict(validationContent);
75
88
  if (verdict !== "needs-remediation") {
76
89
  await persistMilestoneValidationGate("pass", "none", `milestone validation verdict is ${verdict}; no remediation loop risk`, "", mid);
@@ -870,7 +870,63 @@ export function enterBranchModeForMilestone(basePath, milestoneId) {
870
870
  reused: true,
871
871
  });
872
872
  }
873
- nativeCheckoutBranch(basePath, branch);
873
+ checkoutBranchWithStashGuard(basePath, branch, `enter-branch-mode:${milestoneId}`);
874
+ }
875
+ export function checkoutBranchWithStashGuard(basePath, branch, reason) {
876
+ let stashMarker = null;
877
+ let stashed = false;
878
+ const status = nativeWorkingTreeStatus(basePath).trim();
879
+ if (status.length > 0) {
880
+ stashMarker = `gsd-checkout-stash:${reason}:${process.pid}:${Date.now()}:${process.hrtime.bigint().toString(36)}`;
881
+ const stashListBefore = execFileSync("git", ["stash", "list"], {
882
+ cwd: basePath,
883
+ stdio: ["ignore", "pipe", "pipe"],
884
+ encoding: "utf-8",
885
+ });
886
+ execFileSync("git", ["stash", "push", "--include-untracked", "-m", `gsd: checkout stash [${stashMarker}]`], {
887
+ cwd: basePath,
888
+ stdio: ["ignore", "pipe", "pipe"],
889
+ encoding: "utf-8",
890
+ });
891
+ const stashListAfter = execFileSync("git", ["stash", "list"], {
892
+ cwd: basePath,
893
+ stdio: ["ignore", "pipe", "pipe"],
894
+ encoding: "utf-8",
895
+ });
896
+ stashed = stashListAfter !== stashListBefore;
897
+ }
898
+ // Checkout and stash-restore are split so we can distinguish two failure
899
+ // modes: (a) checkout failed → HEAD did not move, restore stash and rethrow;
900
+ // (b) checkout succeeded but stash pop failed → HEAD moved to `branch` but
901
+ // the working-tree changes remain in the stash list. We surface a distinct
902
+ // error in case (b) so callers don't assume the branch switch was rolled back.
903
+ try {
904
+ nativeCheckoutBranch(basePath, branch);
905
+ }
906
+ catch (checkoutErr) {
907
+ if (stashed) {
908
+ try {
909
+ popStashByRef(basePath, stashMarker);
910
+ }
911
+ catch (restoreErr) {
912
+ logWarning("worktree", `git stash pop failed during checkout restore: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`);
913
+ }
914
+ }
915
+ throw checkoutErr;
916
+ }
917
+ if (stashed) {
918
+ try {
919
+ popStashByRef(basePath, stashMarker);
920
+ }
921
+ catch (popErr) {
922
+ const msg = popErr instanceof Error ? popErr.message : String(popErr);
923
+ const wrapped = new Error(`checkout to '${branch}' succeeded but stash restore failed; working tree changes remain in the stash list. Original error: ${msg}`);
924
+ const ref = popErr?.stashRef;
925
+ if (ref)
926
+ wrapped.stashRef = ref;
927
+ throw wrapped;
928
+ }
929
+ }
874
930
  }
875
931
  // ─── Public API ────────────────────────────────────────────────────────────
876
932
  /**
@@ -1727,14 +1783,6 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
1727
1783
  // report the dirty tree if it fails.
1728
1784
  logWarning("worktree", `git stash failed: ${err instanceof Error ? err.message : String(err)}`);
1729
1785
  }
1730
- if (needsDbCycle && dbPathToReopen) {
1731
- try {
1732
- openDatabase(dbPathToReopen);
1733
- }
1734
- catch (err) {
1735
- logWarning("worktree", `post-stash db reopen failed: ${err instanceof Error ? err.message : String(err)}`);
1736
- }
1737
- }
1738
1786
  // 7b. Clean up stale merge state before attempting squash merge (#2912).
1739
1787
  // A leftover MERGE_HEAD (from a previous failed merge, libgit2 native path,
1740
1788
  // or interrupted operation) causes `git merge --squash` to refuse with
@@ -1743,6 +1791,14 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
1743
1791
  removeMergeStateFiles(originalBasePath_, "pre-merge");
1744
1792
  // 8. Squash merge — auto-resolve .gsd/ state file conflicts (#530)
1745
1793
  const mergeResult = nativeMergeSquash(originalBasePath_, milestoneBranch);
1794
+ if (needsDbCycle && dbPathToReopen) {
1795
+ try {
1796
+ openDatabase(dbPathToReopen);
1797
+ }
1798
+ catch (err) {
1799
+ logWarning("worktree", `post-merge db reopen failed: ${err instanceof Error ? err.message : String(err)}`);
1800
+ }
1801
+ }
1746
1802
  if (!mergeResult.success) {
1747
1803
  // Dirty working tree — the merge was rejected before it started (e.g.
1748
1804
  // untracked .gsd/ files left by syncStateToProjectRoot). Preserve the
@@ -127,6 +127,12 @@ import { normalizeRealPath } from "./paths.js";
127
127
  // ─────────────────────────────────────────────────────────────────────────────
128
128
  /** Throttle STATE.md rebuilds — at most once per 30 seconds */
129
129
  const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
130
+ export function formatAutoStopNotification(prefix, totals, unitCount) {
131
+ return [
132
+ `${prefix}.`,
133
+ `Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${unitCount} units`,
134
+ ].join("\n");
135
+ }
130
136
  /**
131
137
  * Phase B — register this auto-mode process in the workers table so other
132
138
  * workers and janitors can detect liveness via heartbeat. Best-effort: if
@@ -1030,7 +1036,7 @@ export async function stopAuto(ctx, pi, reason, options = {}) {
1030
1036
  : `Auto-mode stopped${reasonSuffix}`;
1031
1037
  if (ledger && ledger.units.length > 0) {
1032
1038
  const totals = getProjectTotals(ledger.units);
1033
- ctx?.ui.notify(`${notificationPrefix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`, "info");
1039
+ ctx?.ui.notify(formatAutoStopNotification(notificationPrefix, totals, ledger.units.length), "info");
1034
1040
  }
1035
1041
  else {
1036
1042
  ctx?.ui.notify(`${notificationPrefix}.`, "info");
@@ -1319,7 +1325,6 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
1319
1325
  restoreProjectRootEnv();
1320
1326
  restoreMilestoneLockEnv();
1321
1327
  s.pendingVerificationRetry = null;
1322
- s.verificationRetryCount.clear();
1323
1328
  ctx?.ui.setStatus("gsd-auto", "paused");
1324
1329
  ctx?.ui.setWidget("gsd-progress", undefined);
1325
1330
  const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";