gsd-pi 2.82.0-dev.3709f22a5 → 2.82.0-dev.4285182e8

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 (193) hide show
  1. package/README.md +2 -2
  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/loop.js +14 -1
  5. package/dist/resources/extensions/gsd/auto/phases.js +53 -29
  6. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  7. package/dist/resources/extensions/gsd/auto/workflow-kernel.js +3 -0
  8. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +13 -19
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +13 -6
  11. package/dist/resources/extensions/gsd/auto-recovery.js +40 -13
  12. package/dist/resources/extensions/gsd/auto-start.js +3 -3
  13. package/dist/resources/extensions/gsd/auto-verification.js +17 -4
  14. package/dist/resources/extensions/gsd/auto-worktree.js +65 -9
  15. package/dist/resources/extensions/gsd/auto.js +14 -8
  16. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +6 -1
  17. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +1 -1
  18. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -1
  19. package/dist/resources/extensions/gsd/commands/catalog.js +7 -1
  20. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
  21. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  22. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +7 -2
  23. package/dist/resources/extensions/gsd/commands-verdict.js +139 -0
  24. package/dist/resources/extensions/gsd/crash-recovery.js +16 -4
  25. package/dist/resources/extensions/gsd/db/milestone-leases.js +24 -0
  26. package/dist/resources/extensions/gsd/forensics.js +3 -3
  27. package/dist/resources/extensions/gsd/git-service.js +6 -2
  28. package/dist/resources/extensions/gsd/gsd-db.js +20 -6
  29. package/dist/resources/extensions/gsd/guided-flow-queue.js +4 -3
  30. package/dist/resources/extensions/gsd/guided-flow.js +8 -5
  31. package/dist/resources/extensions/gsd/markdown-renderer.js +10 -8
  32. package/dist/resources/extensions/gsd/paths.js +4 -0
  33. package/dist/resources/extensions/gsd/queue-reorder-ui.js +30 -13
  34. package/dist/resources/extensions/gsd/state.js +3 -3
  35. package/dist/resources/extensions/gsd/status-guards.js +7 -0
  36. package/dist/resources/extensions/gsd/templates/plan.md +1 -0
  37. package/dist/resources/extensions/gsd/templates/task-plan.md +6 -0
  38. package/dist/resources/extensions/gsd/tools/plan-slice.js +3 -5
  39. package/dist/resources/extensions/gsd/workflow-mcp.js +17 -1
  40. package/dist/resources/extensions/gsd/worktree-manager.js +1 -1
  41. package/dist/resources/extensions/ttsr/ttsr-manager.js +3 -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 +11 -11
  45. package/dist/web/standalone/.next/build-manifest.json +3 -3
  46. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  47. package/dist/web/standalone/.next/react-loadable-manifest.json +3 -3
  48. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +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-react-loadable-manifest.js +1 -1
  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/dist/web/standalone/.next/static/chunks/8359.65b24fac92188a6b.js +10 -0
  79. package/dist/web/standalone/.next/static/chunks/9441.ff70bb53f6835771.js +1 -0
  80. package/dist/web/standalone/.next/static/chunks/{webpack-9a4db269f9ed63ad.js → webpack-855d616060cb6e59.js} +1 -1
  81. package/package.json +1 -1
  82. package/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
  83. package/packages/pi-ai/dist/providers/google-gemini-cli.js +5 -0
  84. package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
  85. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts +2 -0
  86. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts.map +1 -0
  87. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js +41 -0
  88. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js.map +1 -0
  89. package/packages/pi-ai/src/providers/google-gemini-cli.test.ts +49 -0
  90. package/packages/pi-ai/src/providers/google-gemini-cli.ts +7 -0
  91. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  92. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +44 -3
  93. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/sdk.js +1 -1
  95. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +71 -97
  98. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js +25 -1
  100. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +24 -10
  104. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  105. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +53 -3
  106. package/packages/pi-coding-agent/src/core/sdk.ts +1 -1
  107. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +75 -102
  108. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-ordering.test.ts +30 -1
  109. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +29 -10
  110. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  111. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts +2 -0
  112. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts.map +1 -0
  113. package/packages/pi-tui/dist/__tests__/terminal.test.js +103 -0
  114. package/packages/pi-tui/dist/__tests__/terminal.test.js.map +1 -0
  115. package/packages/pi-tui/dist/terminal.d.ts +2 -0
  116. package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
  117. package/packages/pi-tui/dist/terminal.js +12 -0
  118. package/packages/pi-tui/dist/terminal.js.map +1 -1
  119. package/packages/pi-tui/src/__tests__/terminal.test.ts +121 -0
  120. package/packages/pi-tui/src/terminal.ts +11 -0
  121. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  122. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +1 -1
  123. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +9 -0
  124. package/src/resources/extensions/gsd/auto/loop.ts +14 -1
  125. package/src/resources/extensions/gsd/auto/phases.ts +60 -36
  126. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  127. package/src/resources/extensions/gsd/auto/workflow-kernel.ts +5 -1
  128. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -0
  129. package/src/resources/extensions/gsd/auto-dispatch.ts +13 -19
  130. package/src/resources/extensions/gsd/auto-post-unit.ts +14 -6
  131. package/src/resources/extensions/gsd/auto-recovery.ts +45 -11
  132. package/src/resources/extensions/gsd/auto-start.ts +2 -3
  133. package/src/resources/extensions/gsd/auto-verification.ts +22 -2
  134. package/src/resources/extensions/gsd/auto-worktree.ts +74 -9
  135. package/src/resources/extensions/gsd/auto.ts +13 -8
  136. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +9 -1
  137. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +1 -1
  138. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -1
  139. package/src/resources/extensions/gsd/commands/catalog.ts +7 -1
  140. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
  141. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  142. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +8 -3
  143. package/src/resources/extensions/gsd/commands-verdict.ts +202 -0
  144. package/src/resources/extensions/gsd/crash-recovery.ts +16 -2
  145. package/src/resources/extensions/gsd/db/milestone-leases.ts +26 -0
  146. package/src/resources/extensions/gsd/forensics.ts +3 -3
  147. package/src/resources/extensions/gsd/git-service.ts +6 -3
  148. package/src/resources/extensions/gsd/gsd-db.ts +18 -6
  149. package/src/resources/extensions/gsd/guided-flow-queue.ts +4 -3
  150. package/src/resources/extensions/gsd/guided-flow.ts +8 -5
  151. package/src/resources/extensions/gsd/markdown-renderer.ts +10 -8
  152. package/src/resources/extensions/gsd/paths.ts +5 -0
  153. package/src/resources/extensions/gsd/queue-reorder-ui.ts +31 -13
  154. package/src/resources/extensions/gsd/state.ts +3 -3
  155. package/src/resources/extensions/gsd/status-guards.ts +8 -0
  156. package/src/resources/extensions/gsd/templates/plan.md +1 -0
  157. package/src/resources/extensions/gsd/templates/task-plan.md +6 -0
  158. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
  159. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +139 -1
  160. package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +6 -5
  161. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +76 -5
  162. package/src/resources/extensions/gsd/tests/checkout-branch-stash-guard.test.ts +87 -0
  163. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +378 -0
  164. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -0
  165. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +2 -0
  166. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +27 -0
  167. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +11 -0
  168. package/src/resources/extensions/gsd/tests/gsdroot-worktree-detection.test.ts +5 -2
  169. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +9 -0
  170. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +46 -0
  171. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +179 -0
  172. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +2 -1
  173. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +26 -1
  174. package/src/resources/extensions/gsd/tests/post-unit-state-rebuild.test.ts +84 -0
  175. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +59 -0
  176. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +18 -1
  177. package/src/resources/extensions/gsd/tests/quality-gates.test.ts +6 -0
  178. package/src/resources/extensions/gsd/tests/queue-reorder-ui.test.ts +54 -0
  179. package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +43 -0
  180. package/src/resources/extensions/gsd/tests/run-uat-replay-cap.test.ts +2 -3
  181. package/src/resources/extensions/gsd/tests/status-guards.test.ts +13 -1
  182. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +17 -0
  183. package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +29 -2
  184. package/src/resources/extensions/gsd/tests/workflow-kernel.test.ts +7 -0
  185. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +18 -0
  186. package/src/resources/extensions/gsd/tools/plan-slice.ts +3 -4
  187. package/src/resources/extensions/gsd/workflow-mcp.ts +18 -1
  188. package/src/resources/extensions/gsd/worktree-manager.ts +1 -1
  189. package/src/resources/extensions/ttsr/ttsr-manager.ts +5 -1
  190. package/dist/web/standalone/.next/static/chunks/8359.7eb3bb8f8ecf4c01.js +0 -10
  191. package/dist/web/standalone/.next/static/chunks/9441.1081da1125d1764f.js +0 -1
  192. /package/dist/web/standalone/.next/static/{kkGf3_VaPFkiDNV_D7Dtl → 78uanrILNOKG-Jpi4itAE}/_buildManifest.js +0 -0
  193. /package/dist/web/standalone/.next/static/{kkGf3_VaPFkiDNV_D7Dtl → 78uanrILNOKG-Jpi4itAE}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -350,7 +350,7 @@ This is what makes GSD different. Run it, walk away, come back to built software
350
350
 
351
351
  Auto mode is a state machine driven by the GSD database at the project root. It derives the next unit of work from authoritative SQLite state, creates a fresh agent session, injects a focused prompt with all relevant context pre-inlined, and lets the LLM execute. When the LLM finishes, auto mode persists the result to the database, refreshes markdown projections such as `STATE.md`, and dispatches the next unit.
352
352
 
353
- The database is authoritative for milestones, slices, tasks, requirements, summaries, and completion status. Durable decisions and project knowledge are stored in the `memories` table: decisions are `architecture` memories, and KNOWLEDGE patterns/lessons are `pattern`/`gotcha` memories. Markdown under `.gsd/` is a rendered projection for review, prompts, and git-friendly history; it is not a runtime fallback unless you explicitly run a recovery/import command. In worktree mode, project-root DB state remains authoritative and worktree markdown projections are not synced back as state.
353
+ The database is authoritative for milestones, slices, tasks, requirements, summaries, and completion status. Durable decisions and project knowledge are stored in the `memories` table: decisions are `architecture` memories, and KNOWLEDGE patterns/lessons are `pattern`/`gotcha` memories. Markdown under `.gsd/` is a rendered projection for review, prompts, and git-friendly history; it is not a runtime fallback unless you explicitly run a recovery/import command. In worktree mode, artifact/projection writes are rendered under the active worktree-local `.gsd/`, while the project-root DB remains authoritative runtime state.
354
354
 
355
355
  `KNOWLEDGE.md` is hybrid: rules remain file-canonical, while patterns and lessons are stored in the `memories` table and rendered back into `KNOWLEDGE.md` on the next session-start projection. Existing pattern and lesson rows are backfilled into memories before projection, so newly captured patterns and lessons may appear in memory-backed prompt context before the file view refreshes.
356
356
 
@@ -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
- d2173e15ccf5aedf
1
+ 8102192ede112252
@@ -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)
@@ -1,3 +1,5 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: Main auto-mode execution loop.
1
3
  /**
2
4
  * auto/loop.ts — Main auto-mode execution loop.
3
5
  *
@@ -789,11 +791,18 @@ export async function autoLoop(ctx, pi, s, deps, options) {
789
791
  unitId: iterData.unitId,
790
792
  });
791
793
  const finalizeReason = finalizeResult.action === "break" ? finalizeResult.reason : undefined;
794
+ const finalizeStatus = finalizeReason === "step-wizard"
795
+ ? "completed"
796
+ : finalizeResult.action === "next"
797
+ ? "completed"
798
+ : finalizeResult.action === "continue"
799
+ ? "retry"
800
+ : "stopped";
792
801
  journalReporter.emit("post-unit-finalize-end", {
793
802
  iteration,
794
803
  unitType: iterData.unitType,
795
804
  unitId: iterData.unitId,
796
- status: finalizeResult.action === "next" ? "completed" : finalizeResult.action === "continue" ? "retry" : "stopped",
805
+ status: finalizeStatus,
797
806
  action: finalizeResult.action,
798
807
  ...(finalizeReason ? { reason: finalizeReason } : {}),
799
808
  });
@@ -837,6 +846,10 @@ export async function autoLoop(ctx, pi, s, deps, options) {
837
846
  }) || dispatchSettled;
838
847
  completeIteration();
839
848
  finishTurn("completed");
849
+ if (finalizeDecision.action === "complete-and-break") {
850
+ s.preserveStepSurfaceAfterLoopExit = true;
851
+ break;
852
+ }
840
853
  }
841
854
  catch (loopErr) {
842
855
  // ── Blanket catch: absorb unexpected exceptions, apply graduated recovery ──
@@ -1338,35 +1338,8 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1338
1338
  s.currentUnit.type === unitType &&
1339
1339
  s.currentUnit.id === unitId);
1340
1340
  const previousTier = s.currentUnitRouting?.tier;
1341
- // Scope workflow-logger buffer to this unit so post-finalize drains are
1342
- // per-unit. Without this, the module-level _buffer accumulates across every
1343
- // unit in the same Node process (see workflow-logger.ts module header).
1344
- _resetLogs();
1345
1341
  const dispatchKey = `${unitType}/${unitId}`;
1346
- s.unitDispatchCount.set(dispatchKey, (s.unitDispatchCount.get(dispatchKey) ?? 0) + 1);
1347
- s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
1348
- s.lastGitActionFailure = null;
1349
- s.lastGitActionStatus = null;
1350
- s.lastUnitAgentEndMessages = null;
1351
- setCurrentPhase(unitType, {
1352
- basePath: s.basePath,
1353
- traceId: ic.flowId,
1354
- turnId: `iter-${ic.iteration}`,
1355
- causedBy: "unit-start",
1356
- });
1357
- s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
1358
- const unitStartSeq = ic.nextSeq();
1359
- deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
1360
- deps.captureAvailableSkills();
1361
- writeUnitRuntimeRecord(s.basePath, unitType, unitId, s.currentUnit.startedAt, {
1362
- phase: "dispatched",
1363
- wrapupWarningSent: false,
1364
- timeoutAt: null,
1365
- lastProgressAt: s.currentUnit.startedAt,
1366
- progressCount: 0,
1367
- lastProgressKind: "dispatch",
1368
- recoveryAttempts: 0, // Reset so re-dispatched units get full recovery budget (#2322)
1369
- });
1342
+ const nextDispatchCount = (s.unitDispatchCount.get(dispatchKey) ?? 0) + 1;
1370
1343
  // Status bar (widget + preconditions deferred until after model selection — see #2899)
1371
1344
  ctx.ui.setStatus("gsd-auto", "auto");
1372
1345
  if (mid)
@@ -1420,7 +1393,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1420
1393
  finalPrompt = `${capped}\n\n---\n\n${finalPrompt}`;
1421
1394
  s.pendingCrashRecovery = null;
1422
1395
  }
1423
- else if ((s.unitDispatchCount.get(dispatchKey) ?? 0) > 1) {
1396
+ else if (nextDispatchCount > 1) {
1424
1397
  const diagnostic = deps.getDeepDiagnostic(s.basePath);
1425
1398
  if (diagnostic) {
1426
1399
  const cappedDiag = diagnostic.length > MAX_RECOVERY_CHARS
@@ -1459,6 +1432,11 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1459
1432
  logWarning("engine", "Prompt reorder failed", { error: msg });
1460
1433
  }
1461
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();
1462
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);
1463
1441
  s.currentUnitRouting =
1464
1442
  modelResult.routing;
@@ -1502,12 +1480,58 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1502
1480
  ? ctx.modelRegistry.getProviderAuthMode(ctx.model.provider)
1503
1481
  : undefined,
1504
1482
  baseUrl: s.currentUnitModel?.baseUrl ?? ctx.model?.baseUrl,
1483
+ activeTools: typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [],
1505
1484
  });
1506
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
+ }
1507
1502
  ctx.ui.notify(compatibilityError, "error");
1508
1503
  await deps.stopAuto(ctx, pi, compatibilityError);
1509
1504
  return { action: "break", reason: "workflow-capability" };
1510
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
+ });
1511
1535
  // Progress widget + preconditions — deferred to after model selection so the
1512
1536
  // widget's first render tick shows the correct model (#2899).
1513
1537
  deps.updateProgressWidget(ctx, unitType, unitId, state);
@@ -1,3 +1,5 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: Mutable auto-mode session state container.
1
3
  /**
2
4
  * AutoSession — encapsulates all mutable auto-mode state into a single instance.
3
5
  *
@@ -26,6 +28,7 @@ export class AutoSession {
26
28
  active = false;
27
29
  paused = false;
28
30
  completionStopInProgress = false;
31
+ preserveStepSurfaceAfterLoopExit = false;
29
32
  stepMode = false;
30
33
  verbose = false;
31
34
  activeEngineId = null;
@@ -210,6 +213,7 @@ export class AutoSession {
210
213
  this.active = false;
211
214
  this.paused = false;
212
215
  this.completionStopInProgress = false;
216
+ this.preserveStepSurfaceAfterLoopExit = false;
213
217
  this.stepMode = false;
214
218
  this.verbose = false;
215
219
  this.activeEngineId = null;
@@ -66,6 +66,9 @@ export function decideEngineDispatch(input) {
66
66
  export function decideFinalizeResult(input) {
67
67
  if (input.action === "break") {
68
68
  const reason = input.reason ?? "unknown";
69
+ if (reason === "step-wizard") {
70
+ return { action: "complete-and-break" };
71
+ }
69
72
  return {
70
73
  action: "stop",
71
74
  failureClass: reason === "git-closeout-failure" ? "git" : "closeout",
@@ -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");
@@ -616,7 +614,7 @@ export const DISPATCH_RULES = [
616
614
  },
617
615
  },
618
616
  {
619
- name: "planning (require_slice_discussion) → pause for discussion (#3454)",
617
+ name: "planning (require_slice_discussion) → pause for discussion",
620
618
  match: async ({ state, mid, basePath, prefs }) => {
621
619
  if (state.phase !== "planning")
622
620
  return null;
@@ -1023,7 +1021,7 @@ export const DISPATCH_RULES = [
1023
1021
  mkdirSync(mDir, { recursive: true });
1024
1022
  const validationPath = join(mDir, buildMilestoneFileName(mid, "VALIDATION"));
1025
1023
  const skipSource = trivialVariant
1026
- ? "trivial-scope pipeline variant (#4781)"
1024
+ ? "trivial-scope pipeline variant"
1027
1025
  : "`skip_milestone_validation` preference";
1028
1026
  const skipValidationReason = trivialVariant ? "trivial-scope" : "preference";
1029
1027
  const content = [
@@ -1098,19 +1096,19 @@ 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
- reason: `Cannot complete milestone ${mid}: VALIDATION verdict is "${verdict}". Address the validation findings and re-run validation, or update the verdict manually.`,
1111
+ reason: `Cannot complete milestone ${mid}: VALIDATION verdict is "${verdict}". Address the validation findings and re-run validation, or run \`/gsd verdict pass --rationale "..."\` to override.`,
1114
1112
  level: "warning",
1115
1113
  };
1116
1114
  }
@@ -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)`);
@@ -28,7 +28,7 @@ import { regenerateIfMissing } from "./workflow-projections.js";
28
28
  import { WorktreeStateProjection } from "./worktree-state-projection.js";
29
29
  import { createWorkspace, scopeMilestone } from "./workspace.js";
30
30
  import { normalizeWorktreePathForCompare } from "./worktree-root.js";
31
- import { isDbAvailable, getTask, getSlice, getMilestone, updateTaskStatus, _getAdapter, getVerificationEvidence } from "./gsd-db.js";
31
+ import { isDbAvailable, getDbPath, refreshOpenDatabaseFromDisk, getTask, getSlice, getMilestone, updateTaskStatus, _getAdapter, getVerificationEvidence } from "./gsd-db.js";
32
32
  import { renderPlanCheckboxes } from "./markdown-renderer.js";
33
33
  import { consumeSignal } from "./session-status-io.js";
34
34
  import { checkPostUnitHooks, isRetryPending, consumeRetryTrigger, persistHookState, resolveHookArtifactPath, } from "./post-unit-hooks.js";
@@ -294,14 +294,14 @@ export function detectRogueFileWrites(unitType, unitId, basePath) {
294
294
  * looping indefinitely (#2007).
295
295
  */
296
296
  export const MAX_ARTIFACT_VERIFICATION_RETRIES = 3;
297
- export const STEP_COMPLETE_FALLBACK_MESSAGE = "Step complete. Run /clear, then /gsd to continue (or /gsd auto to run continuously).";
297
+ export const STEP_COMPLETE_FALLBACK_MESSAGE = "Step complete. Run /clear if you want a clean view, then /gsd next to continue one step (or /gsd auto to run continuously).";
298
298
  export function buildStepCompleteMessage(nextState) {
299
299
  if (nextState.phase === "complete") {
300
300
  return "Step complete — milestone finished. Run /gsd status to review, or start the next milestone.";
301
301
  }
302
302
  const next = describeNextUnit(nextState);
303
303
  return `Step complete. Next: ${next.label}\n`
304
- + `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`;
304
+ + `Run /clear if you want a clean view, then /gsd next to continue one step (or /gsd auto to run continuously).`;
305
305
  }
306
306
  /**
307
307
  * Decide whether step mode should stop at the step wizard after a unit finishes.
@@ -553,6 +553,13 @@ export async function postUnitPreVerification(pctx, opts) {
553
553
  if (!opts?.skipSettleDelay) {
554
554
  await new Promise(r => setTimeout(r, 100));
555
555
  }
556
+ const dbPath = getDbPath();
557
+ if (isDbAvailable() && dbPath && dbPath !== ":memory:") {
558
+ const refreshed = refreshOpenDatabaseFromDisk();
559
+ if (!refreshed) {
560
+ logWarning("db", "post-unit database refresh failed; derived state may be stale");
561
+ }
562
+ }
556
563
  // Turn-level git action (commit | snapshot | status-only)
557
564
  if (s.currentUnit) {
558
565
  const unit = s.currentUnit;
@@ -999,7 +1006,7 @@ export async function postUnitPreVerification(pctx, opts) {
999
1006
  s.verificationRetryCount.delete(retryKey);
1000
1007
  s.verificationRetryFailureHashes.delete(retryKey);
1001
1008
  writeBlockerPlaceholder(s.currentUnit.type, s.currentUnit.id, s.basePath, reason);
1002
- ctx.ui.notify(`${s.currentUnit.type} ${s.currentUnit.id} — deterministic policy rejection, wrote blocker placeholder (no retries) (#4973)`, "warning");
1009
+ ctx.ui.notify(`${s.currentUnit.type} ${s.currentUnit.id} — deterministic policy rejection, wrote blocker placeholder (no retries)`, "warning");
1003
1010
  // Fall through to "continue" — do NOT enter the retry or db-unavailable paths.
1004
1011
  }
1005
1012
  else if (!triggerArtifactVerified && diagnoseWorktreeIntegrityFailure(s.basePath)) {
@@ -1458,8 +1465,8 @@ export async function postUnitPostVerification(pctx) {
1458
1465
  }
1459
1466
  }
1460
1467
  // Step mode → show wizard instead of dispatch.
1461
- // Without this notify(), /gsd in step mode finishes a unit and silently
1462
- // exits the loop, leaving the user with no hint to /clear and /gsd again.
1468
+ // Without this notify(), /gsd next finishes a unit and silently exits the
1469
+ // loop, leaving the user with no next-step command.
1463
1470
  if (s.stepMode) {
1464
1471
  let phaseAfterUnit = null;
1465
1472
  try {
@@ -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