gsd-pi 2.73.1-dev.47ed32a → 2.73.1-dev.49397e5

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/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/headless-query.js +4 -1
  5. package/dist/logo.d.ts +1 -1
  6. package/dist/logo.js +1 -1
  7. package/dist/onboarding.js +59 -53
  8. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +59 -1
  9. package/dist/resources/extensions/gsd/auto/detect-stuck.js +11 -4
  10. package/dist/resources/extensions/gsd/auto/phases.js +60 -10
  11. package/dist/resources/extensions/gsd/auto-dispatch.js +11 -3
  12. package/dist/resources/extensions/gsd/auto-post-unit.js +93 -57
  13. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +13 -0
  14. package/dist/resources/extensions/gsd/auto-verification.js +88 -3
  15. package/dist/resources/extensions/gsd/auto.js +29 -10
  16. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +21 -8
  17. package/dist/resources/extensions/gsd/commands/catalog.js +26 -1
  18. package/dist/resources/extensions/gsd/commands/handlers/ops.js +20 -0
  19. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +68 -9
  20. package/dist/resources/extensions/gsd/commands-add-tests.js +111 -0
  21. package/dist/resources/extensions/gsd/commands-backlog.js +140 -0
  22. package/dist/resources/extensions/gsd/commands-do.js +79 -0
  23. package/dist/resources/extensions/gsd/commands-maintenance.js +6 -6
  24. package/dist/resources/extensions/gsd/commands-pr-branch.js +180 -0
  25. package/dist/resources/extensions/gsd/commands-session-report.js +82 -0
  26. package/dist/resources/extensions/gsd/commands-ship.js +187 -0
  27. package/dist/resources/extensions/gsd/db-writer.js +3 -5
  28. package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  29. package/dist/resources/extensions/gsd/gsd-db.js +321 -0
  30. package/dist/resources/extensions/gsd/index.js +15 -2
  31. package/dist/resources/extensions/gsd/md-importer.js +3 -4
  32. package/dist/resources/extensions/gsd/memory-store.js +19 -51
  33. package/dist/resources/extensions/gsd/milestone-validation-gates.js +13 -12
  34. package/dist/resources/extensions/gsd/native-git-bridge.js +7 -4
  35. package/dist/resources/extensions/gsd/notification-widget.js +2 -2
  36. package/dist/resources/extensions/gsd/prompts/add-tests.md +35 -0
  37. package/dist/resources/extensions/gsd/state.js +66 -15
  38. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +3 -14
  39. package/dist/resources/extensions/gsd/triage-resolution.js +2 -5
  40. package/dist/resources/extensions/gsd/workflow-manifest.js +8 -69
  41. package/dist/resources/extensions/gsd/workflow-migration.js +21 -22
  42. package/dist/resources/extensions/gsd/workflow-projections.js +4 -1
  43. package/dist/resources/extensions/gsd/workflow-reconcile.js +14 -11
  44. package/dist/tsconfig.extensions.tsbuildinfo +1 -0
  45. package/dist/web/standalone/.next/BUILD_ID +1 -1
  46. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  47. package/dist/web/standalone/.next/build-manifest.json +2 -2
  48. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  49. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.html +1 -1
  66. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  73. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  75. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  76. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  77. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  78. package/package.json +3 -3
  79. package/packages/mcp-server/tsconfig.json +1 -0
  80. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -0
  81. package/packages/native/tsconfig.tsbuildinfo +1 -0
  82. package/packages/pi-agent-core/tsconfig.json +1 -0
  83. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -0
  84. package/packages/pi-ai/tsconfig.json +1 -0
  85. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -0
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +10 -2
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +2 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +9 -3
  92. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts +2 -0
  94. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts.map +1 -0
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +52 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +1 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +5 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  100. package/packages/pi-coding-agent/package.json +1 -1
  101. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +12 -6
  102. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +73 -0
  103. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +9 -3
  104. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +5 -1
  105. package/packages/pi-coding-agent/tsconfig.json +1 -0
  106. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -0
  107. package/packages/pi-tui/dist/__tests__/tui.test.js +30 -0
  108. package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
  109. package/packages/pi-tui/dist/tui.d.ts +1 -0
  110. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  111. package/packages/pi-tui/dist/tui.js +22 -3
  112. package/packages/pi-tui/dist/tui.js.map +1 -1
  113. package/packages/pi-tui/src/__tests__/tui.test.ts +38 -0
  114. package/packages/pi-tui/src/tui.ts +20 -3
  115. package/packages/pi-tui/tsconfig.json +1 -0
  116. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -0
  117. package/packages/rpc-client/tsconfig.json +1 -0
  118. package/packages/rpc-client/tsconfig.tsbuildinfo +1 -0
  119. package/pkg/package.json +1 -1
  120. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +95 -1
  121. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +88 -0
  122. package/src/resources/extensions/gsd/auto/detect-stuck.ts +12 -4
  123. package/src/resources/extensions/gsd/auto/loop-deps.ts +6 -0
  124. package/src/resources/extensions/gsd/auto/phases.ts +90 -10
  125. package/src/resources/extensions/gsd/auto-dispatch.ts +10 -4
  126. package/src/resources/extensions/gsd/auto-post-unit.ts +107 -58
  127. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +17 -0
  128. package/src/resources/extensions/gsd/auto-verification.ts +98 -3
  129. package/src/resources/extensions/gsd/auto.ts +33 -14
  130. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +24 -8
  131. package/src/resources/extensions/gsd/commands/catalog.ts +26 -1
  132. package/src/resources/extensions/gsd/commands/handlers/ops.ts +20 -0
  133. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +74 -9
  134. package/src/resources/extensions/gsd/commands-add-tests.ts +137 -0
  135. package/src/resources/extensions/gsd/commands-backlog.ts +182 -0
  136. package/src/resources/extensions/gsd/commands-do.ts +109 -0
  137. package/src/resources/extensions/gsd/commands-maintenance.ts +6 -6
  138. package/src/resources/extensions/gsd/commands-pr-branch.ts +234 -0
  139. package/src/resources/extensions/gsd/commands-session-report.ts +101 -0
  140. package/src/resources/extensions/gsd/commands-ship.ts +219 -0
  141. package/src/resources/extensions/gsd/db-writer.ts +3 -5
  142. package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  143. package/src/resources/extensions/gsd/gsd-db.ts +467 -0
  144. package/src/resources/extensions/gsd/index.ts +18 -2
  145. package/src/resources/extensions/gsd/md-importer.ts +3 -5
  146. package/src/resources/extensions/gsd/memory-store.ts +31 -62
  147. package/src/resources/extensions/gsd/milestone-validation-gates.ts +13 -14
  148. package/src/resources/extensions/gsd/native-git-bridge.ts +11 -12
  149. package/src/resources/extensions/gsd/notification-widget.ts +2 -2
  150. package/src/resources/extensions/gsd/prompts/add-tests.md +35 -0
  151. package/src/resources/extensions/gsd/state.ts +80 -17
  152. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -2
  153. package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +53 -0
  154. package/src/resources/extensions/gsd/tests/commands-backlog.test.ts +158 -0
  155. package/src/resources/extensions/gsd/tests/commands-do.test.ts +127 -0
  156. package/src/resources/extensions/gsd/tests/commands-pr-branch.test.ts +68 -0
  157. package/src/resources/extensions/gsd/tests/commands-session-report.test.ts +82 -0
  158. package/src/resources/extensions/gsd/tests/commands-ship.test.ts +71 -0
  159. package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +14 -0
  160. package/src/resources/extensions/gsd/tests/complete-milestone-false-merge.test.ts +142 -0
  161. package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +42 -0
  162. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -2
  163. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +3 -2
  164. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +68 -8
  165. package/src/resources/extensions/gsd/tests/derive-state.test.ts +3 -3
  166. package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +154 -0
  167. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
  168. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +68 -1
  169. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +140 -0
  170. package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +180 -0
  171. package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +5 -7
  172. package/src/resources/extensions/gsd/tests/token-profile.test.ts +1 -1
  173. package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +179 -0
  174. package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +223 -0
  175. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +3 -11
  176. package/src/resources/extensions/gsd/triage-resolution.ts +2 -7
  177. package/src/resources/extensions/gsd/workflow-manifest.ts +9 -104
  178. package/src/resources/extensions/gsd/workflow-migration.ts +21 -29
  179. package/src/resources/extensions/gsd/workflow-projections.ts +8 -1
  180. package/src/resources/extensions/gsd/workflow-reconcile.ts +15 -15
  181. /package/dist/web/standalone/.next/static/{WawMb3TMcYLkGJLTg_x1u → tAsSblRKK1lG7MVOwsO92}/_buildManifest.js +0 -0
  182. /package/dist/web/standalone/.next/static/{WawMb3TMcYLkGJLTg_x1u → tAsSblRKK1lG7MVOwsO92}/_ssgManifest.js +0 -0
@@ -30,7 +30,15 @@ import { MergeConflictError } from "../git-service.js";
30
30
  import { setCurrentPhase, clearCurrentPhase } from "../../shared/gsd-phase-state.js";
31
31
  import { join, basename, dirname, parse as parsePath } from "node:path";
32
32
  import { existsSync, cpSync, readdirSync } from "node:fs";
33
- import { logWarning, logError } from "../workflow-logger.js";
33
+ import {
34
+ logWarning,
35
+ logError,
36
+ _resetLogs,
37
+ drainLogs,
38
+ drainAndSummarize,
39
+ formatForNotification,
40
+ hasAnyIssues,
41
+ } from "../workflow-logger.js";
34
42
  import { gsdRoot } from "../paths.js";
35
43
  import { atomicWriteSync } from "../atomic-write.js";
36
44
  import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.js";
@@ -159,6 +167,29 @@ async function closeoutAndStop(
159
167
  await deps.stopAuto(ctx, pi, reason);
160
168
  }
161
169
 
170
+ async function emitCancelledUnitEnd(
171
+ ic: IterationContext,
172
+ unitType: string,
173
+ unitId: string,
174
+ unitStartSeq: number,
175
+ errorContext?: { message: string; category: string; stopReason?: string; isTransient?: boolean; retryAfterMs?: number },
176
+ ): Promise<void> {
177
+ ic.deps.emitJournalEvent({
178
+ ts: new Date().toISOString(),
179
+ flowId: ic.flowId,
180
+ seq: ic.nextSeq(),
181
+ eventType: "unit-end",
182
+ data: {
183
+ unitType,
184
+ unitId,
185
+ status: "cancelled",
186
+ artifactVerified: false,
187
+ ...(errorContext ? { errorContext } : {}),
188
+ },
189
+ causedBy: { flowId: ic.flowId, seq: unitStartSeq },
190
+ });
191
+ }
192
+
162
193
  // ─── runPreDispatch ───────────────────────────────────────────────────────────
163
194
 
164
195
  /**
@@ -481,10 +512,13 @@ export async function runPreDispatch(
481
512
  );
482
513
  } else if (state.phase === "blocked") {
483
514
  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");
515
+ // Pause instead of hard-stop so the session is resumable with `/gsd auto`.
516
+ // Hard-stop here was causing premature termination when slice dependencies
517
+ // were temporarily unresolvable (e.g. after reassessment added new slices).
518
+ await deps.pauseAuto(ctx, pi);
519
+ ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto to resume.`, "warning");
520
+ deps.sendDesktopNotification("GSD", blockerMsg, "warning", "attention", basename(s.originalBasePath || s.basePath));
521
+ deps.logCmuxEvent(prefs, blockerMsg, "warning");
488
522
  } else {
489
523
  const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
490
524
  const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
@@ -583,13 +617,23 @@ export async function runPreDispatch(
583
617
  return { action: "break", reason: "milestone-complete" };
584
618
  }
585
619
 
586
- // Terminal: blocked
620
+ // Terminal: blocked — pause instead of hard-stop so the session is resumable.
587
621
  if (state.phase === "blocked") {
588
622
  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");
623
+ if (s.currentUnit) {
624
+ await deps.closeoutUnit(
625
+ ctx,
626
+ s.basePath,
627
+ s.currentUnit.type,
628
+ s.currentUnit.id,
629
+ s.currentUnit.startedAt,
630
+ deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
631
+ );
632
+ }
633
+ await deps.pauseAuto(ctx, pi);
634
+ ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto to resume.`, "warning");
635
+ deps.sendDesktopNotification("GSD", blockerMsg, "warning", "attention", basename(s.originalBasePath || s.basePath));
636
+ deps.logCmuxEvent(prefs, blockerMsg, "warning");
593
637
  debugLog("autoLoop", { phase: "exit", reason: "blocked" });
594
638
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "blocked", blockers: state.blockers } });
595
639
  return { action: "break", reason: "blocked" };
@@ -1068,6 +1112,10 @@ export async function runUnitPhase(
1068
1112
  );
1069
1113
  const previousTier = s.currentUnitRouting?.tier;
1070
1114
 
1115
+ // Scope workflow-logger buffer to this unit so post-finalize drains are
1116
+ // per-unit. Without this, the module-level _buffer accumulates across every
1117
+ // unit in the same Node process (see workflow-logger.ts module header).
1118
+ _resetLogs();
1071
1119
  s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
1072
1120
  setCurrentPhase(unitType);
1073
1121
  s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
@@ -1334,6 +1382,7 @@ export async function runUnitPhase(
1334
1382
  // Provider-error pause: pauseAuto already handled cleanup and scheduled
1335
1383
  // recovery. Don't hard-stop — just break out of the loop (#2762).
1336
1384
  if (unitResult.errorContext?.category === "provider") {
1385
+ await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
1337
1386
  debugLog("autoLoop", { phase: "exit", reason: "provider-pause", isTransient: unitResult.errorContext.isTransient });
1338
1387
  return { action: "break", reason: "provider-pause" };
1339
1388
  }
@@ -1352,9 +1401,23 @@ export async function runUnitPhase(
1352
1401
  );
1353
1402
  debugLog("autoLoop", { phase: "session-timeout-pause", unitType, unitId });
1354
1403
  await deps.pauseAuto(ctx, pi);
1404
+ await deps.autoCommitUnit?.(s.basePath, unitType, unitId, ctx);
1405
+ await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
1355
1406
  return { action: "break", reason: "session-timeout" };
1356
1407
  }
1357
1408
  // All other cancelled states (structural errors, non-transient failures): hard stop
1409
+ if (s.currentUnit) {
1410
+ await deps.closeoutUnit(
1411
+ ctx,
1412
+ s.basePath,
1413
+ unitType,
1414
+ unitId,
1415
+ s.currentUnit.startedAt,
1416
+ deps.buildSnapshotOpts(unitType, unitId),
1417
+ );
1418
+ }
1419
+ await deps.autoCommitUnit?.(s.basePath, unitType, unitId, ctx);
1420
+ await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
1358
1421
  ctx.ui.notify(
1359
1422
  `Session creation failed for ${unitType} ${unitId}: ${unitResult.errorContext?.message ?? "unknown"}. Stopping auto-mode.`,
1360
1423
  "warning",
@@ -1532,6 +1595,9 @@ export async function runFinalize(
1532
1595
  // cannot mutate state for the next unit (#3757).
1533
1596
  s.currentUnit = null;
1534
1597
  clearCurrentPhase();
1598
+ // Drop any logger entries from the timed-out unit so they don't bleed
1599
+ // into the next iteration's drain.
1600
+ drainLogs();
1535
1601
  loopState.consecutiveFinalizeTimeouts++;
1536
1602
  debugLog("autoLoop", {
1537
1603
  phase: "pre-verification-timeout",
@@ -1630,6 +1696,9 @@ export async function runFinalize(
1630
1696
  // cannot mutate state for the next unit (#3757).
1631
1697
  s.currentUnit = null;
1632
1698
  clearCurrentPhase();
1699
+ // Drop any logger entries from the timed-out unit so they don't bleed
1700
+ // into the next iteration's drain.
1701
+ drainLogs();
1633
1702
  loopState.consecutiveFinalizeTimeouts++;
1634
1703
  debugLog("autoLoop", {
1635
1704
  phase: "post-verification-timeout",
@@ -1674,5 +1743,16 @@ export async function runFinalize(
1674
1743
  // Both pre and post verification completed without timeout — reset counter
1675
1744
  loopState.consecutiveFinalizeTimeouts = 0;
1676
1745
 
1746
+ // Surface accumulated workflow-logger issues for this unit to the user.
1747
+ // Warnings/errors logged during the unit are buffered in the logger and
1748
+ // drained here so the user sees a single consolidated post-unit alert.
1749
+ if (hasAnyIssues()) {
1750
+ const { logs } = drainAndSummarize();
1751
+ if (logs.length > 0) {
1752
+ const severity = logs.some((e) => e.severity === "error") ? "error" : "warning";
1753
+ ctx.ui.notify(formatForNotification(logs), severity);
1754
+ }
1755
+ }
1756
+
1677
1757
  return { action: "next", data: undefined as void };
1678
1758
  }
@@ -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
  }
@@ -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;
@@ -249,6 +262,65 @@ export interface PostUnitContext {
249
262
  updateProgressWidget: (ctx: ExtensionContext, unitType: string, unitId: string, state: import("./types.js").GSDState) => void;
250
263
  }
251
264
 
265
+ export async function autoCommitUnit(
266
+ basePath: string,
267
+ unitType: string,
268
+ unitId: string,
269
+ ctx?: ExtensionContext,
270
+ ): Promise<string | null> {
271
+ try {
272
+ let taskContext: TaskCommitContext | undefined;
273
+
274
+ if (unitType === "execute-task") {
275
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
276
+ if (mid && sid && tid) {
277
+ const summaryPath = resolveTaskFile(basePath, mid, sid, tid, "SUMMARY");
278
+ if (summaryPath) {
279
+ try {
280
+ const summaryContent = await loadFile(summaryPath);
281
+ if (summaryContent) {
282
+ const summary = parseSummary(summaryContent);
283
+ let ghIssueNumber: number | undefined;
284
+ try {
285
+ const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
286
+ ghIssueNumber = getTaskIssueNumberForCommit(basePath, mid, sid, tid) ?? undefined;
287
+ } catch (err) {
288
+ logWarning("engine", `GitHub issue lookup failed: ${err instanceof Error ? err.message : String(err)}`);
289
+ }
290
+
291
+ taskContext = {
292
+ taskId: `${sid}/${tid}`,
293
+ taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
294
+ oneLiner: summary.oneLiner || undefined,
295
+ keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
296
+ issueNumber: ghIssueNumber,
297
+ };
298
+ }
299
+ } catch (e) {
300
+ debugLog("postUnit", { phase: "task-summary-parse", error: String(e) });
301
+ }
302
+ }
303
+ }
304
+ }
305
+
306
+ _resetHasChangesCache();
307
+
308
+ if (LIFECYCLE_ONLY_UNITS.has(unitType)) {
309
+ return null;
310
+ }
311
+
312
+ const commitMsg = autoCommitCurrentBranch(basePath, unitType, unitId, taskContext);
313
+ if (commitMsg) {
314
+ ctx?.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
315
+ }
316
+ return commitMsg;
317
+ } catch (e) {
318
+ debugLog("postUnit", { phase: "auto-commit", error: String(e) });
319
+ ctx?.ui.notify(`Auto-commit failed: ${String(e).split("\n")[0]}`, "warning");
320
+ return null;
321
+ }
322
+ }
323
+
252
324
  /**
253
325
  * Pre-verification processing: parallel worker signal check, cache invalidation,
254
326
  * auto-commit, doctor run, state rebuild, worktree sync, artifact verification.
@@ -288,63 +360,7 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
288
360
  // Auto-commit
289
361
  if (s.currentUnit) {
290
362
  const unit = s.currentUnit;
291
- try {
292
- let taskContext: TaskCommitContext | undefined;
293
-
294
- if (s.currentUnit.type === "execute-task") {
295
- const { milestone: mid, slice: sid, task: tid } = parseUnitId(s.currentUnit.id);
296
- if (mid && sid && tid) {
297
- const summaryPath = resolveTaskFile(s.basePath, mid, sid, tid, "SUMMARY");
298
- if (summaryPath) {
299
- try {
300
- const summaryContent = await loadFile(summaryPath);
301
- if (summaryContent) {
302
- const summary = parseSummary(summaryContent);
303
- // Look up GitHub issue number for commit linking
304
- let ghIssueNumber: number | undefined;
305
- try {
306
- const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
307
- ghIssueNumber = getTaskIssueNumberForCommit(s.basePath, mid, sid, tid) ?? undefined;
308
- } catch (err) {
309
- // GitHub sync not available — skip
310
- logWarning("engine", `GitHub issue lookup failed: ${err instanceof Error ? err.message : String(err)}`);
311
- }
312
-
313
- taskContext = {
314
- taskId: `${sid}/${tid}`,
315
- taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
316
- oneLiner: summary.oneLiner || undefined,
317
- keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
318
- issueNumber: ghIssueNumber,
319
- };
320
- }
321
- } catch (e) {
322
- debugLog("postUnit", { phase: "task-summary-parse", error: String(e) });
323
- }
324
- }
325
- }
326
- }
327
-
328
- // Invalidate the nativeHasChanges cache before auto-commit (#1853).
329
- // The cache has a 10-second TTL and is keyed by basePath. A stale
330
- // `false` result causes autoCommit to skip staging entirely, leaving
331
- // code files only in the working tree where they are destroyed by
332
- // `git worktree remove --force` during teardown.
333
- _resetHasChangesCache();
334
-
335
- // Skip auto-commit for lifecycle-only units (#2553) — they only touch
336
- // `.gsd/` internal state files. Those files are picked up by the next
337
- // actual task commit via smartStage().
338
- if (!LIFECYCLE_ONLY_UNITS.has(s.currentUnit.type)) {
339
- const commitMsg = autoCommitCurrentBranch(s.basePath, s.currentUnit.type, s.currentUnit.id, taskContext);
340
- if (commitMsg) {
341
- ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
342
- }
343
- }
344
- } catch (e) {
345
- debugLog("postUnit", { phase: "auto-commit", error: String(e) });
346
- ctx.ui.notify(`Auto-commit failed: ${String(e).split("\n")[0]}`, "warning");
347
- }
363
+ await autoCommitUnit(s.basePath, unit.type, unit.id, ctx);
348
364
 
349
365
  // GitHub sync (non-blocking, opt-in)
350
366
  await runSafely("postUnit", "github-sync", async () => {
@@ -619,6 +635,30 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
619
635
  s.verificationRetryCount.set(retryKey, attempt);
620
636
 
621
637
  if (attempt > MAX_VERIFICATION_RETRIES) {
638
+ // #4175: For complete-milestone, a blocker placeholder is harmful —
639
+ // the stub SUMMARY has no recovery value (milestone is terminal),
640
+ // it does not update DB status (so deriveState never advances),
641
+ // and it fools stopAuto's presence check into merging a milestone
642
+ // that was never legitimately completed. Pause auto-mode with a
643
+ // clear single failure signal and preserve the worktree branch.
644
+ if (s.currentUnit.type === "complete-milestone") {
645
+ debugLog("postUnit", {
646
+ phase: "artifact-verify-pause-complete-milestone",
647
+ unitType: s.currentUnit.type,
648
+ unitId: s.currentUnit.id,
649
+ attempt,
650
+ maxRetries: MAX_VERIFICATION_RETRIES,
651
+ });
652
+ s.verificationRetryCount.delete(retryKey);
653
+ s.pendingVerificationRetry = null;
654
+ ctx.ui.notify(
655
+ `Milestone ${s.currentUnit.id} verification failed after ${MAX_VERIFICATION_RETRIES} retries — worktree branch preserved. Re-run /gsd auto once blockers are resolved.`,
656
+ "error",
657
+ );
658
+ await pauseAuto(ctx, pi);
659
+ return "dispatched";
660
+ }
661
+
622
662
  // Retries exhausted — write a blocker placeholder so the pipeline
623
663
  // can advance past this stuck unit (#2653).
624
664
  debugLog("postUnit", {
@@ -1025,8 +1065,17 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
1025
1065
  }
1026
1066
  }
1027
1067
 
1028
- // Step mode → show wizard instead of dispatch
1068
+ // Step mode → show wizard instead of dispatch.
1069
+ // Without this notify(), /gsd in step mode finishes a unit and silently
1070
+ // exits the loop, leaving the user with no hint to /clear and /gsd again.
1029
1071
  if (s.stepMode) {
1072
+ try {
1073
+ const nextState = await deriveState(s.basePath);
1074
+ ctx.ui.notify(buildStepCompleteMessage(nextState), "info");
1075
+ } catch (e) {
1076
+ debugLog("postUnit", { phase: "step-wizard-notify", error: String(e) });
1077
+ ctx.ui.notify(STEP_COMPLETE_FALLBACK_MESSAGE, "info");
1078
+ }
1030
1079
  return "step-wizard";
1031
1080
  }
1032
1081
 
@@ -230,6 +230,23 @@ export async function recoverTimedOutUnit(
230
230
  return "recovered";
231
231
  }
232
232
 
233
+ // #4175: For complete-milestone, never write a blocker placeholder — a stub
234
+ // SUMMARY has no recovery value (milestone is terminal), it does not update
235
+ // DB status, and downstream merge paths can treat the stub as a legitimate
236
+ // completion signal. Pause instead so the worktree branch is preserved.
237
+ if (unitType === "complete-milestone") {
238
+ writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnitStartedAt, {
239
+ phase: "paused",
240
+ recoveryAttempts: recoveryAttempts + 1,
241
+ lastRecoveryReason: reason,
242
+ });
243
+ ctx.ui.notify(
244
+ `Milestone ${unitId} ${reason}-recovery exhausted ${maxRecoveryAttempts} attempt(s) — worktree branch preserved. Re-run /gsd auto once blockers are resolved.`,
245
+ "error",
246
+ );
247
+ return "paused";
248
+ }
249
+
233
250
  // Retries exhausted — write a blocker placeholder and advance the pipeline
234
251
  // instead of silently stalling.
235
252
  const placeholder = writeBlockerPlaceholder(
@@ -12,10 +12,15 @@
12
12
 
13
13
  import type { ExtensionContext, ExtensionAPI } from "@gsd/pi-coding-agent";
14
14
  import { mkdirSync, writeFileSync } from "node:fs";
15
- import { resolveSliceFile, resolveSlicePath } from "./paths.js";
15
+ import { resolveSliceFile, resolveSlicePath, resolveMilestoneFile } from "./paths.js";
16
16
  import { parseUnitId } from "./unit-id.js";
17
- import { isDbAvailable, getTask, getSliceTasks, type TaskRow } from "./gsd-db.js";
17
+ import { isDbAvailable, getTask, getSliceTasks, getMilestoneSlices, type TaskRow } from "./gsd-db.js";
18
18
  import { loadEffectiveGSDPreferences } from "./preferences.js";
19
+ import { extractVerdict } from "./verdict-parser.js";
20
+ import { isClosedStatus } from "./status-guards.js";
21
+ import { loadFile } from "./files.js";
22
+ import { parseRoadmap } from "./parsers-legacy.js";
23
+ import { isMilestoneComplete } from "./state.js";
19
24
  import {
20
25
  runVerificationGate,
21
26
  formatFailureContext,
@@ -43,6 +48,88 @@ function isInfraVerificationFailure(stderr: string): boolean {
43
48
  );
44
49
  }
45
50
 
51
+ /**
52
+ * Post-unit guard for `validate-milestone` units (#4094).
53
+ *
54
+ * When validate-milestone writes verdict=needs-remediation, the agent is
55
+ * expected to also call gsd_reassess_roadmap in the same turn to add
56
+ * remediation slices. If they don't, the state machine re-derives
57
+ * `phase: validating-milestone` indefinitely (all slices still complete +
58
+ * verdict still needs-remediation), wasting ~3 dispatches before the stuck
59
+ * detector fires.
60
+ *
61
+ * This guard fires immediately on the first occurrence: if VALIDATION.md
62
+ * verdict is needs-remediation and no incomplete slices exist for the
63
+ * milestone, pause the auto-loop with a clear blocker.
64
+ */
65
+ async function runValidateMilestonePostCheck(
66
+ vctx: VerificationContext,
67
+ pauseAuto: (ctx?: ExtensionContext, pi?: ExtensionAPI) => Promise<void>,
68
+ ): Promise<VerificationResult> {
69
+ const { s, ctx, pi } = vctx;
70
+ if (!s.currentUnit) return "continue";
71
+
72
+ const { milestone: mid } = parseUnitId(s.currentUnit.id);
73
+ if (!mid) return "continue";
74
+
75
+ const validationFile = resolveMilestoneFile(s.basePath, mid, "VALIDATION");
76
+ if (!validationFile) return "continue";
77
+
78
+ const validationContent = await loadFile(validationFile);
79
+ if (!validationContent) return "continue";
80
+
81
+ const verdict = extractVerdict(validationContent);
82
+ if (verdict !== "needs-remediation") return "continue";
83
+
84
+ const incompleteSliceCount = await countIncompleteSlices(s.basePath, mid);
85
+
86
+ // If any non-closed slices exist, the agent successfully queued remediation
87
+ // work — proceed normally. The state machine will execute those slices and
88
+ // re-validate per the #3596/#3670 fix.
89
+ if (incompleteSliceCount > 0) return "continue";
90
+
91
+ ctx.ui.notify(
92
+ `Milestone ${mid} validation returned verdict=needs-remediation but no remediation slices were added. Pausing for human review.`,
93
+ "error",
94
+ );
95
+ process.stderr.write(
96
+ `validate-milestone: pausing — verdict=needs-remediation with no incomplete slices for ${mid}. ` +
97
+ `The agent must call gsd_reassess_roadmap to add remediation slices before re-validation.\n`,
98
+ );
99
+ await pauseAuto(ctx, pi);
100
+ return "pause";
101
+ }
102
+
103
+ /**
104
+ * Count slices for a milestone that are not in a closed status.
105
+ * DB-backed projects are authoritative (#4094 peer review); falls back to
106
+ * roadmap parsing only when the DB is unavailable.
107
+ */
108
+ async function countIncompleteSlices(basePath: string, milestoneId: string): Promise<number> {
109
+ if (isDbAvailable()) {
110
+ const slices = getMilestoneSlices(milestoneId);
111
+ if (slices.length === 0) {
112
+ // No DB rows — treat as "unknown", do not pause.
113
+ return 1;
114
+ }
115
+ return slices.filter((slice) => !isClosedStatus(slice.status)).length;
116
+ }
117
+
118
+ // Filesystem fallback: parse the roadmap markdown.
119
+ try {
120
+ const roadmapFile = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
121
+ if (!roadmapFile) return 1;
122
+ const roadmapContent = await loadFile(roadmapFile);
123
+ if (!roadmapContent) return 1;
124
+ const roadmap = parseRoadmap(roadmapContent);
125
+ if (roadmap.slices.length === 0) return 1;
126
+ return isMilestoneComplete(roadmap) ? 0 : 1;
127
+ } catch {
128
+ // Parsing failures should not cause false-positive pauses.
129
+ return 1;
130
+ }
131
+ }
132
+
46
133
  /**
47
134
  * Run the verification gate for the current execute-task unit.
48
135
  * Returns:
@@ -56,7 +143,15 @@ export async function runPostUnitVerification(
56
143
  ): Promise<VerificationResult> {
57
144
  const { s, ctx, pi } = vctx;
58
145
 
59
- if (!s.currentUnit || s.currentUnit.type !== "execute-task") {
146
+ if (!s.currentUnit) {
147
+ return "continue";
148
+ }
149
+
150
+ if (s.currentUnit.type === "validate-milestone") {
151
+ return await runValidateMilestonePostCheck(vctx, pauseAuto);
152
+ }
153
+
154
+ if (s.currentUnit.type !== "execute-task") {
60
155
  return "continue";
61
156
  }
62
157
 
@@ -187,7 +187,7 @@ import {
187
187
  deregisterSigtermHandler as _deregisterSigtermHandler,
188
188
  detectWorkingTreeActivity,
189
189
  } from "./auto-supervisor.js";
190
- import { isDbAvailable } from "./gsd-db.js";
190
+ import { isDbAvailable, getMilestone } from "./gsd-db.js";
191
191
  import { countPendingCaptures } from "./captures.js";
192
192
  import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.js";
193
193
 
@@ -195,6 +195,7 @@ import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.j
195
195
  import { startUnitSupervision } from "./auto-timers.js";
196
196
  import { runPostUnitVerification } from "./auto-verification.js";
197
197
  import {
198
+ autoCommitUnit,
198
199
  postUnitPreVerification,
199
200
  postUnitPostVerification,
200
201
  } from "./auto-post-unit.js";
@@ -761,24 +762,36 @@ export async function stopAuto(
761
762
  : { notify: () => {} };
762
763
  const resolver = buildResolver();
763
764
 
764
- // Check if the milestone is complete SUMMARY file is the authoritative signal.
765
+ // Check if the milestone is complete. DB status is the authoritative
766
+ // signal — only a successful gsd_complete_milestone call flips it to
767
+ // "complete" (tools/complete-milestone.ts). SUMMARY file presence is
768
+ // NOT sufficient: a blocker placeholder stub or a partial write can
769
+ // leave a file behind without the milestone actually being done,
770
+ // which previously caused stopAuto to merge a failed milestone and
771
+ // emit a misleading metadata-only merge warning (#4175).
772
+ // DB-unavailable projects fall back to SUMMARY-file presence.
765
773
  let milestoneComplete = false;
766
774
  try {
767
- const summaryPath = resolveMilestoneFile(
768
- s.originalBasePath || s.basePath,
769
- s.currentMilestoneId,
770
- "SUMMARY",
771
- );
772
- if (!summaryPath) {
773
- // Also check in the worktree path (SUMMARY may not be synced yet)
774
- const wtSummaryPath = resolveMilestoneFile(
775
- s.basePath,
775
+ if (isDbAvailable()) {
776
+ const dbRow = getMilestone(s.currentMilestoneId);
777
+ milestoneComplete = dbRow?.status === "complete";
778
+ } else {
779
+ const summaryPath = resolveMilestoneFile(
780
+ s.originalBasePath || s.basePath,
776
781
  s.currentMilestoneId,
777
782
  "SUMMARY",
778
783
  );
779
- milestoneComplete = wtSummaryPath !== null;
780
- } else {
781
- milestoneComplete = true;
784
+ if (!summaryPath) {
785
+ // Also check in the worktree path (SUMMARY may not be synced yet)
786
+ const wtSummaryPath = resolveMilestoneFile(
787
+ s.basePath,
788
+ s.currentMilestoneId,
789
+ "SUMMARY",
790
+ );
791
+ milestoneComplete = wtSummaryPath !== null;
792
+ } else {
793
+ milestoneComplete = true;
794
+ }
782
795
  }
783
796
  } catch (err) {
784
797
  // Non-fatal — fall through to preserveBranch path
@@ -1168,6 +1181,7 @@ function buildLoopDeps(): LoopDeps {
1168
1181
  getMainBranch,
1169
1182
  // Unit closeout + runtime records
1170
1183
  closeoutUnit,
1184
+ autoCommitUnit,
1171
1185
  recordOutcome,
1172
1186
  writeLock,
1173
1187
  captureAvailableSkills,
@@ -1388,6 +1402,11 @@ export async function startAuto(
1388
1402
  s.stepMode = requestedStepMode;
1389
1403
  s.cmdCtx = ctx;
1390
1404
  s.basePath = base;
1405
+ // Ensure the workflow-logger audit log is pinned to the project root
1406
+ // even when auto-mode is entered via a path that bypasses the
1407
+ // bootstrap/dynamic-tools ensureDbOpen() → setLogBasePath() chain
1408
+ // (e.g. /clear resume, hot-reload).
1409
+ setLogBasePath(base);
1391
1410
  s.unitDispatchCount.clear();
1392
1411
  s.unitLifetimeDispatches.clear();
1393
1412
  if (!getLedger()) initMetrics(base);