gsd-pi 2.82.0-dev.3709f22a5 → 2.82.0-dev.57fd453e4

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 (183) 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 +12 -18
  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-prefs-wizard.js +7 -2
  20. package/dist/resources/extensions/gsd/crash-recovery.js +16 -4
  21. package/dist/resources/extensions/gsd/db/milestone-leases.js +24 -0
  22. package/dist/resources/extensions/gsd/forensics.js +3 -3
  23. package/dist/resources/extensions/gsd/git-service.js +6 -2
  24. package/dist/resources/extensions/gsd/gsd-db.js +20 -6
  25. package/dist/resources/extensions/gsd/guided-flow-queue.js +4 -3
  26. package/dist/resources/extensions/gsd/guided-flow.js +8 -5
  27. package/dist/resources/extensions/gsd/markdown-renderer.js +10 -8
  28. package/dist/resources/extensions/gsd/paths.js +4 -0
  29. package/dist/resources/extensions/gsd/queue-reorder-ui.js +30 -13
  30. package/dist/resources/extensions/gsd/state.js +1 -1
  31. package/dist/resources/extensions/gsd/status-guards.js +7 -0
  32. package/dist/resources/extensions/gsd/templates/plan.md +1 -0
  33. package/dist/resources/extensions/gsd/templates/task-plan.md +6 -0
  34. package/dist/resources/extensions/gsd/tools/plan-slice.js +3 -5
  35. package/dist/resources/extensions/gsd/workflow-mcp.js +17 -1
  36. package/dist/resources/extensions/gsd/worktree-manager.js +1 -1
  37. package/dist/resources/extensions/ttsr/ttsr-manager.js +3 -1
  38. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  39. package/dist/web/standalone/.next/BUILD_ID +1 -1
  40. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  41. package/dist/web/standalone/.next/build-manifest.json +3 -3
  42. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  43. package/dist/web/standalone/.next/react-loadable-manifest.json +3 -3
  44. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  61. package/dist/web/standalone/.next/server/app/index.html +1 -1
  62. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  69. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  72. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  73. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  74. package/dist/web/standalone/.next/static/chunks/8359.65b24fac92188a6b.js +10 -0
  75. package/dist/web/standalone/.next/static/chunks/9441.ff70bb53f6835771.js +1 -0
  76. package/dist/web/standalone/.next/static/chunks/{webpack-9a4db269f9ed63ad.js → webpack-855d616060cb6e59.js} +1 -1
  77. package/package.json +1 -1
  78. package/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
  79. package/packages/pi-ai/dist/providers/google-gemini-cli.js +5 -0
  80. package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
  81. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts +2 -0
  82. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts.map +1 -0
  83. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js +41 -0
  84. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js.map +1 -0
  85. package/packages/pi-ai/src/providers/google-gemini-cli.test.ts +49 -0
  86. package/packages/pi-ai/src/providers/google-gemini-cli.ts +7 -0
  87. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  88. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +44 -3
  89. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  90. package/packages/pi-coding-agent/dist/core/sdk.js +1 -1
  91. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +71 -97
  94. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js +12 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js.map +1 -1
  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 +19 -8
  99. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  100. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +53 -3
  101. package/packages/pi-coding-agent/src/core/sdk.ts +1 -1
  102. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +75 -102
  103. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-ordering.test.ts +14 -0
  104. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +23 -8
  105. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  106. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts +2 -0
  107. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts.map +1 -0
  108. package/packages/pi-tui/dist/__tests__/terminal.test.js +103 -0
  109. package/packages/pi-tui/dist/__tests__/terminal.test.js.map +1 -0
  110. package/packages/pi-tui/dist/terminal.d.ts +2 -0
  111. package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
  112. package/packages/pi-tui/dist/terminal.js +12 -0
  113. package/packages/pi-tui/dist/terminal.js.map +1 -1
  114. package/packages/pi-tui/src/__tests__/terminal.test.ts +121 -0
  115. package/packages/pi-tui/src/terminal.ts +11 -0
  116. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  117. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +1 -1
  118. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +9 -0
  119. package/src/resources/extensions/gsd/auto/loop.ts +14 -1
  120. package/src/resources/extensions/gsd/auto/phases.ts +60 -36
  121. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  122. package/src/resources/extensions/gsd/auto/workflow-kernel.ts +5 -1
  123. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -0
  124. package/src/resources/extensions/gsd/auto-dispatch.ts +12 -18
  125. package/src/resources/extensions/gsd/auto-post-unit.ts +14 -6
  126. package/src/resources/extensions/gsd/auto-recovery.ts +45 -11
  127. package/src/resources/extensions/gsd/auto-start.ts +2 -3
  128. package/src/resources/extensions/gsd/auto-verification.ts +22 -2
  129. package/src/resources/extensions/gsd/auto-worktree.ts +74 -9
  130. package/src/resources/extensions/gsd/auto.ts +13 -8
  131. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +9 -1
  132. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +1 -1
  133. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -1
  134. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +8 -3
  135. package/src/resources/extensions/gsd/crash-recovery.ts +16 -2
  136. package/src/resources/extensions/gsd/db/milestone-leases.ts +26 -0
  137. package/src/resources/extensions/gsd/forensics.ts +3 -3
  138. package/src/resources/extensions/gsd/git-service.ts +6 -3
  139. package/src/resources/extensions/gsd/gsd-db.ts +18 -6
  140. package/src/resources/extensions/gsd/guided-flow-queue.ts +4 -3
  141. package/src/resources/extensions/gsd/guided-flow.ts +8 -5
  142. package/src/resources/extensions/gsd/markdown-renderer.ts +10 -8
  143. package/src/resources/extensions/gsd/paths.ts +5 -0
  144. package/src/resources/extensions/gsd/queue-reorder-ui.ts +31 -13
  145. package/src/resources/extensions/gsd/state.ts +1 -1
  146. package/src/resources/extensions/gsd/status-guards.ts +8 -0
  147. package/src/resources/extensions/gsd/templates/plan.md +1 -0
  148. package/src/resources/extensions/gsd/templates/task-plan.md +6 -0
  149. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
  150. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +139 -1
  151. package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +6 -5
  152. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +76 -5
  153. package/src/resources/extensions/gsd/tests/checkout-branch-stash-guard.test.ts +87 -0
  154. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -0
  155. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +2 -0
  156. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +27 -0
  157. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +11 -0
  158. package/src/resources/extensions/gsd/tests/gsdroot-worktree-detection.test.ts +5 -2
  159. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +9 -0
  160. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +46 -0
  161. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +179 -0
  162. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +2 -1
  163. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +26 -1
  164. package/src/resources/extensions/gsd/tests/post-unit-state-rebuild.test.ts +84 -0
  165. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +59 -0
  166. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +18 -1
  167. package/src/resources/extensions/gsd/tests/quality-gates.test.ts +6 -0
  168. package/src/resources/extensions/gsd/tests/queue-reorder-ui.test.ts +54 -0
  169. package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +43 -0
  170. package/src/resources/extensions/gsd/tests/run-uat-replay-cap.test.ts +2 -3
  171. package/src/resources/extensions/gsd/tests/status-guards.test.ts +13 -1
  172. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +17 -0
  173. package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +29 -2
  174. package/src/resources/extensions/gsd/tests/workflow-kernel.test.ts +7 -0
  175. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +18 -0
  176. package/src/resources/extensions/gsd/tools/plan-slice.ts +3 -4
  177. package/src/resources/extensions/gsd/workflow-mcp.ts +18 -1
  178. package/src/resources/extensions/gsd/worktree-manager.ts +1 -1
  179. package/src/resources/extensions/ttsr/ttsr-manager.ts +5 -1
  180. package/dist/web/standalone/.next/static/chunks/8359.7eb3bb8f8ecf4c01.js +0 -10
  181. package/dist/web/standalone/.next/static/chunks/9441.1081da1125d1764f.js +0 -1
  182. /package/dist/web/standalone/.next/static/{kkGf3_VaPFkiDNV_D7Dtl → ky6ieNHfZXB_oHPklwTJb}/_buildManifest.js +0 -0
  183. /package/dist/web/standalone/.next/static/{kkGf3_VaPFkiDNV_D7Dtl → ky6ieNHfZXB_oHPklwTJb}/_ssgManifest.js +0 -0
@@ -47,7 +47,7 @@ import { regenerateIfMissing } from "./workflow-projections.js";
47
47
  import { WorktreeStateProjection } from "./worktree-state-projection.js";
48
48
  import { createWorkspace, scopeMilestone } from "./workspace.js";
49
49
  import { normalizeWorktreePathForCompare } from "./worktree-root.js";
50
- import { isDbAvailable, getTask, getSlice, getMilestone, updateTaskStatus, _getAdapter, getVerificationEvidence } from "./gsd-db.js";
50
+ import { isDbAvailable, getDbPath, refreshOpenDatabaseFromDisk, getTask, getSlice, getMilestone, updateTaskStatus, _getAdapter, getVerificationEvidence } from "./gsd-db.js";
51
51
  import { renderPlanCheckboxes } from "./markdown-renderer.js";
52
52
  import { consumeSignal } from "./session-status-io.js";
53
53
  import {
@@ -371,7 +371,7 @@ export function detectRogueFileWrites(
371
371
  export const MAX_ARTIFACT_VERIFICATION_RETRIES = 3;
372
372
 
373
373
  export const STEP_COMPLETE_FALLBACK_MESSAGE =
374
- "Step complete. Run /clear, then /gsd to continue (or /gsd auto to run continuously).";
374
+ "Step complete. Run /clear if you want a clean view, then /gsd next to continue one step (or /gsd auto to run continuously).";
375
375
 
376
376
  export function buildStepCompleteMessage(nextState: import("./types.js").GSDState): string {
377
377
  if (nextState.phase === "complete") {
@@ -379,7 +379,7 @@ export function buildStepCompleteMessage(nextState: import("./types.js").GSDStat
379
379
  }
380
380
  const next = describeNextUnit(nextState);
381
381
  return `Step complete. Next: ${next.label}\n`
382
- + `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`;
382
+ + `Run /clear if you want a clean view, then /gsd next to continue one step (or /gsd auto to run continuously).`;
383
383
  }
384
384
 
385
385
  /**
@@ -686,6 +686,14 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
686
686
  await new Promise(r => setTimeout(r, 100));
687
687
  }
688
688
 
689
+ const dbPath = getDbPath();
690
+ if (isDbAvailable() && dbPath && dbPath !== ":memory:") {
691
+ const refreshed = refreshOpenDatabaseFromDisk();
692
+ if (!refreshed) {
693
+ logWarning("db", "post-unit database refresh failed; derived state may be stale");
694
+ }
695
+ }
696
+
689
697
  // Turn-level git action (commit | snapshot | status-only)
690
698
  if (s.currentUnit) {
691
699
  const unit = s.currentUnit;
@@ -1177,7 +1185,7 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
1177
1185
  s.verificationRetryFailureHashes.delete(retryKey);
1178
1186
  writeBlockerPlaceholder(s.currentUnit.type, s.currentUnit.id, s.basePath, reason);
1179
1187
  ctx.ui.notify(
1180
- `${s.currentUnit.type} ${s.currentUnit.id} — deterministic policy rejection, wrote blocker placeholder (no retries) (#4973)`,
1188
+ `${s.currentUnit.type} ${s.currentUnit.id} — deterministic policy rejection, wrote blocker placeholder (no retries)`,
1181
1189
  "warning",
1182
1190
  );
1183
1191
  // Fall through to "continue" — do NOT enter the retry or db-unavailable paths.
@@ -1731,8 +1739,8 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
1731
1739
  }
1732
1740
 
1733
1741
  // Step mode → show wizard instead of dispatch.
1734
- // Without this notify(), /gsd in step mode finishes a unit and silently
1735
- // exits the loop, leaving the user with no hint to /clear and /gsd again.
1742
+ // Without this notify(), /gsd next finishes a unit and silently exits the
1743
+ // loop, leaving the user with no next-step command.
1736
1744
  if (s.stepMode) {
1737
1745
  let phaseAfterUnit: string | null = null;
1738
1746
  try {
@@ -201,9 +201,15 @@ export function hasImplementationArtifacts(basePath: string, milestoneId?: strin
201
201
  // Strategy: check `git diff --name-only` against the merge-base with the
202
202
  // main branch. This captures ALL files changed during the milestone's
203
203
  // lifetime while running on a milestone branch.
204
- const integrationBranch = milestoneId
205
- ? readIntegrationBranch(basePath, milestoneId) ?? detectMainBranch(basePath)
206
- : detectMainBranch(basePath);
204
+ const recordedIntegrationBranch = milestoneId
205
+ ? readIntegrationBranch(basePath, milestoneId)
206
+ : null;
207
+ let integrationBranch: string;
208
+ if (recordedIntegrationBranch?.startsWith("milestone/")) {
209
+ integrationBranch = detectMainBranch(basePath);
210
+ } else {
211
+ integrationBranch = recordedIntegrationBranch ?? detectMainBranch(basePath);
212
+ }
207
213
  const currentBranch = getCurrentBranch(basePath);
208
214
  const branchDiff = getChangedFilesSinceBranch(basePath, integrationBranch);
209
215
  if (!branchDiff.ok) return "unknown";
@@ -556,27 +562,55 @@ function commitMatchesMilestone(basePath: string, message: string, milestoneId:
556
562
  // rather than Mxx/Sxx/Tyy. Bind those commits back to the milestone when
557
563
  // either the commit touched this milestone's artifacts, or — for projects
558
564
  // where .gsd/ is gitignored/external (#5033) — the message explicitly
559
- // names the milestone or local GSD state proves the task belongs here.
565
+ // names the milestone, local GSD state proves the task belongs here, or the
566
+ // commit is implementation-bearing evidence itself (#5100).
560
567
  if (/^GSD-Task:\s*S[^/\s]+\/T\S+/m.test(message)) {
561
568
  if (files.some((file) => isMilestoneArtifactPath(file, milestoneId))) return true;
562
569
  if (commitMessageMentionsMilestone(message, milestoneId)) return true;
563
- if (commitTaskTrailerBelongsToMilestone(basePath, message, milestoneId)) return true;
570
+ const taskTrailerOwnership = getTaskOwnershipStatus(basePath, message, milestoneId);
571
+ if (taskTrailerOwnership === true) return true;
572
+ if (taskTrailerOwnership === false) return false;
573
+ // taskTrailerOwnership === null: unknown ownership. Apply fallback only
574
+ // in this case to avoid cross-milestone attribution.
575
+ if (MILESTONE_ID_RE.test(milestoneId) && classifyImplementationFiles(files) === "present") return true;
564
576
  }
565
577
 
566
578
  return false;
567
579
  }
568
580
 
569
- function commitTaskTrailerBelongsToMilestone(basePath: string, message: string, milestoneId: string): boolean {
581
+ /**
582
+ * Tri-state task ownership probe.
583
+ * true => DB or local files confirm this milestone owns the task.
584
+ * false => DB is available and this milestone is registered, but task is absent.
585
+ * null => ownership unknown (milestone not in DB yet, or no DB + no local files).
586
+ */
587
+ function getTaskOwnershipStatus(
588
+ basePath: string,
589
+ message: string,
590
+ milestoneId: string,
591
+ ): true | false | null {
570
592
  const match = message.match(/^GSD-Task:\s*(S[^/\s]+)\/(T[^\s]+)/m);
571
- if (!match) return false;
593
+ if (!match) return null;
572
594
  const [, sliceId, taskId] = match;
573
595
 
574
- if (getTask(milestoneId, sliceId, taskId)) return true;
596
+ if (isDbAvailable()) {
597
+ if (!getMilestone(milestoneId)) return null;
598
+ return getTask(milestoneId, sliceId, taskId) ? true : false;
599
+ }
575
600
 
601
+ // DB unavailable: fallback to local task-file presence.
576
602
  const tasksDir = resolveTasksDir(basePath, milestoneId, sliceId);
577
- if (!tasksDir) return false;
578
- return existsSync(join(tasksDir, `${taskId}-PLAN.md`))
579
- || existsSync(join(tasksDir, `${taskId}-SUMMARY.md`));
603
+ if (
604
+ tasksDir
605
+ && (
606
+ existsSync(join(tasksDir, `${taskId}-PLAN.md`))
607
+ || existsSync(join(tasksDir, `${taskId}-SUMMARY.md`))
608
+ )
609
+ ) {
610
+ return true;
611
+ }
612
+
613
+ return null;
580
614
  }
581
615
 
582
616
  function commitMessageMentionsMilestone(message: string, milestoneId: string): boolean {
@@ -42,7 +42,6 @@ import {
42
42
  nativeCommit,
43
43
  nativeGetCurrentBranch,
44
44
  nativeDetectMainBranch,
45
- nativeCheckoutBranch,
46
45
  nativeBranchList,
47
46
  nativeBranchExists,
48
47
  nativeBranchListMerged,
@@ -56,7 +55,7 @@ import {
56
55
  detectWorktreeName,
57
56
  setActiveMilestoneId,
58
57
  } from "./worktree.js";
59
- import { getAutoWorktreePath, isInAutoWorktree } from "./auto-worktree.js";
58
+ import { getAutoWorktreePath, isInAutoWorktree, checkoutBranchWithStashGuard } from "./auto-worktree.js";
60
59
  import { readResourceVersion, cleanStaleRuntimeUnits } from "./auto-worktree.js";
61
60
  import { worktreePath as getWorktreeDir, isInsideWorktreesDir } from "./worktree-manager.js";
62
61
  import { emitWorktreeOrphaned } from "./worktree-telemetry.js";
@@ -1170,7 +1169,7 @@ export async function bootstrapAutoSession(
1170
1169
  isRepo,
1171
1170
  );
1172
1171
  if (branchToCheckout) {
1173
- nativeCheckoutBranch(base, branchToCheckout);
1172
+ checkoutBranchWithStashGuard(base, branchToCheckout, "isolation-none-recovery");
1174
1173
  logWarning("bootstrap", `Returned to "${branchToCheckout}" — HEAD was on stale milestone branch "${currentBranch}" (isolation: none does not use milestone branches).`);
1175
1174
  }
1176
1175
  } catch (err) {
@@ -105,11 +105,31 @@ async function runValidateMilestonePostCheck(
105
105
  const { milestone: mid } = parseUnitId(s.currentUnit.id);
106
106
  if (!mid) return "continue";
107
107
 
108
+ const setToolFailureRetry = (message: string): VerificationResult => {
109
+ const retryKey = verificationRetryKey(s.currentUnit!.type, s.currentUnit!.id);
110
+ const attempt = (s.verificationRetryCount.get(retryKey) ?? 0) + 1;
111
+ s.verificationRetryCount.set(retryKey, attempt);
112
+ s.pendingVerificationRetry = {
113
+ unitId: s.currentUnit!.id,
114
+ failureContext: message,
115
+ attempt,
116
+ };
117
+ return "retry";
118
+ };
119
+
108
120
  const validationFile = resolveMilestoneFile(s.basePath, mid, "VALIDATION");
109
- if (!validationFile) return "continue";
121
+ if (!validationFile) {
122
+ return setToolFailureRetry(
123
+ "You must call gsd_validate_milestone to persist the validation results. No VALIDATION.md was created.",
124
+ );
125
+ }
110
126
 
111
127
  const validationContent = await loadFile(validationFile);
112
- if (!validationContent) return "continue";
128
+ if (!validationContent) {
129
+ return setToolFailureRetry(
130
+ "You must call gsd_validate_milestone to persist the validation results. VALIDATION.md exists but is empty.",
131
+ );
132
+ }
113
133
 
114
134
  const verdict = extractVerdict(validationContent);
115
135
  if (verdict !== "needs-remediation") {
@@ -1007,7 +1007,73 @@ export function enterBranchModeForMilestone(
1007
1007
  });
1008
1008
  }
1009
1009
 
1010
- nativeCheckoutBranch(basePath, branch);
1010
+ checkoutBranchWithStashGuard(basePath, branch, `enter-branch-mode:${milestoneId}`);
1011
+ }
1012
+
1013
+ export function checkoutBranchWithStashGuard(
1014
+ basePath: string,
1015
+ branch: string,
1016
+ reason: string,
1017
+ ): void {
1018
+ let stashMarker: string | null = null;
1019
+ let stashed = false;
1020
+
1021
+ const status = nativeWorkingTreeStatus(basePath).trim();
1022
+ if (status.length > 0) {
1023
+ stashMarker = `gsd-checkout-stash:${reason}:${process.pid}:${Date.now()}:${process.hrtime.bigint().toString(36)}`;
1024
+ const stashListBefore = execFileSync("git", ["stash", "list"], {
1025
+ cwd: basePath,
1026
+ stdio: ["ignore", "pipe", "pipe"],
1027
+ encoding: "utf-8",
1028
+ });
1029
+ execFileSync(
1030
+ "git",
1031
+ ["stash", "push", "--include-untracked", "-m", `gsd: checkout stash [${stashMarker}]`],
1032
+ {
1033
+ cwd: basePath,
1034
+ stdio: ["ignore", "pipe", "pipe"],
1035
+ encoding: "utf-8",
1036
+ },
1037
+ );
1038
+ const stashListAfter = execFileSync("git", ["stash", "list"], {
1039
+ cwd: basePath,
1040
+ stdio: ["ignore", "pipe", "pipe"],
1041
+ encoding: "utf-8",
1042
+ });
1043
+ stashed = stashListAfter !== stashListBefore;
1044
+ }
1045
+
1046
+ // Checkout and stash-restore are split so we can distinguish two failure
1047
+ // modes: (a) checkout failed → HEAD did not move, restore stash and rethrow;
1048
+ // (b) checkout succeeded but stash pop failed → HEAD moved to `branch` but
1049
+ // the working-tree changes remain in the stash list. We surface a distinct
1050
+ // error in case (b) so callers don't assume the branch switch was rolled back.
1051
+ try {
1052
+ nativeCheckoutBranch(basePath, branch);
1053
+ } catch (checkoutErr) {
1054
+ if (stashed) {
1055
+ try {
1056
+ popStashByRef(basePath, stashMarker);
1057
+ } catch (restoreErr) {
1058
+ logWarning("worktree", `git stash pop failed during checkout restore: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`);
1059
+ }
1060
+ }
1061
+ throw checkoutErr;
1062
+ }
1063
+
1064
+ if (stashed) {
1065
+ try {
1066
+ popStashByRef(basePath, stashMarker);
1067
+ } catch (popErr) {
1068
+ const msg = popErr instanceof Error ? popErr.message : String(popErr);
1069
+ const wrapped = new Error(
1070
+ `checkout to '${branch}' succeeded but stash restore failed; working tree changes remain in the stash list. Original error: ${msg}`,
1071
+ );
1072
+ const ref = (popErr as { stashRef?: string } | null)?.stashRef;
1073
+ if (ref) (wrapped as { stashRef?: string }).stashRef = ref;
1074
+ throw wrapped;
1075
+ }
1076
+ }
1011
1077
  }
1012
1078
 
1013
1079
  // ─── Public API ────────────────────────────────────────────────────────────
@@ -1992,14 +2058,6 @@ export function mergeMilestoneToMain(
1992
2058
  logWarning("worktree", `git stash failed: ${err instanceof Error ? err.message : String(err)}`);
1993
2059
  }
1994
2060
 
1995
- if (needsDbCycle && dbPathToReopen) {
1996
- try {
1997
- openDatabase(dbPathToReopen);
1998
- } catch (err) {
1999
- logWarning("worktree", `post-stash db reopen failed: ${err instanceof Error ? err.message : String(err)}`);
2000
- }
2001
- }
2002
-
2003
2061
  // 7b. Clean up stale merge state before attempting squash merge (#2912).
2004
2062
  // A leftover MERGE_HEAD (from a previous failed merge, libgit2 native path,
2005
2063
  // or interrupted operation) causes `git merge --squash` to refuse with
@@ -2009,6 +2067,13 @@ export function mergeMilestoneToMain(
2009
2067
 
2010
2068
  // 8. Squash merge — auto-resolve .gsd/ state file conflicts (#530)
2011
2069
  const mergeResult = nativeMergeSquash(originalBasePath_, milestoneBranch);
2070
+ if (needsDbCycle && dbPathToReopen) {
2071
+ try {
2072
+ openDatabase(dbPathToReopen);
2073
+ } catch (err) {
2074
+ logWarning("worktree", `post-merge db reopen failed: ${err instanceof Error ? err.message : String(err)}`);
2075
+ }
2076
+ }
2012
2077
 
2013
2078
  if (!mergeResult.success) {
2014
2079
  // Dirty working tree — the merge was rejected before it started (e.g.
@@ -1029,6 +1029,8 @@ export async function rerootCommandSession(
1029
1029
  }
1030
1030
 
1031
1031
  export async function cleanupAfterLoopExit(ctx: ExtensionContext): Promise<void> {
1032
+ const preserveStepSurface = s.preserveStepSurfaceAfterLoopExit;
1033
+ const preservePausedSurface = s.paused;
1032
1034
  s.currentUnit = null;
1033
1035
  s.active = false;
1034
1036
  deactivateGSD();
@@ -1051,12 +1053,16 @@ export async function cleanupAfterLoopExit(ctx: ExtensionContext): Promise<void>
1051
1053
  // A transient provider-error pause intentionally leaves the paused badge
1052
1054
  // visible so the user still has a resumable auto-mode signal on screen.
1053
1055
  if (!s.paused) {
1054
- ctx.ui.setStatus("gsd-auto", undefined);
1055
- ctx.ui.setWidget("gsd-progress", undefined);
1056
- if (s.completionStopInProgress) {
1057
- s.completionStopInProgress = false;
1056
+ if (preserveStepSurface) {
1057
+ s.preserveStepSurfaceAfterLoopExit = false;
1058
+ } else {
1059
+ ctx.ui.setStatus("gsd-auto", undefined);
1060
+ ctx.ui.setWidget("gsd-progress", undefined);
1061
+ if (s.completionStopInProgress) {
1062
+ s.completionStopInProgress = false;
1063
+ }
1064
+ initHealthWidget(ctx);
1058
1065
  }
1059
- initHealthWidget(ctx);
1060
1066
  }
1061
1067
 
1062
1068
  // ADR-016 phase 3 (#5693): the stop-path basePath restore + chdir routes
@@ -1064,7 +1070,7 @@ export async function cleanupAfterLoopExit(ctx: ExtensionContext): Promise<void>
1064
1070
  // `s.basePath` mutation and the paired `process.chdir` for auto-loop
1065
1071
  // transitions. The verb assigns `s.basePath` before any throwable work, so
1066
1072
  // a thrown error still leaves basePath restored.
1067
- if (s.originalBasePath) {
1073
+ if (s.originalBasePath && !preserveStepSurface && !preservePausedSurface) {
1068
1074
  try {
1069
1075
  buildLifecycle().restoreToProjectRoot();
1070
1076
  } catch (err) {
@@ -1076,7 +1082,7 @@ export async function cleanupAfterLoopExit(ctx: ExtensionContext): Promise<void>
1076
1082
  }
1077
1083
  }
1078
1084
 
1079
- if (s.originalBasePath && s.cmdCtx) {
1085
+ if (s.originalBasePath && s.cmdCtx && !preserveStepSurface && !preservePausedSurface) {
1080
1086
  const result = await rerootCommandSession(s.cmdCtx, s.originalBasePath);
1081
1087
  if (result.status === "cancelled") {
1082
1088
  logWarning("engine", "post-loop session re-root was cancelled", { file: "auto.ts", basePath: s.originalBasePath });
@@ -1731,7 +1737,6 @@ export async function pauseAuto(
1731
1737
  restoreProjectRootEnv();
1732
1738
  restoreMilestoneLockEnv();
1733
1739
  s.pendingVerificationRetry = null;
1734
- s.verificationRetryCount.clear();
1735
1740
  ctx?.ui.setStatus("gsd-auto", "paused");
1736
1741
  ctx?.ui.setWidget("gsd-progress", undefined);
1737
1742
  const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";
@@ -100,6 +100,14 @@ export function isUserInitiatedAbortMessage(message: string | undefined | null):
100
100
  return /\b(?:claude code process aborted by user|request aborted by user|process aborted by user)\b/i.test(message);
101
101
  }
102
102
 
103
+ export function shouldDeferTransientErrorToCoreRetry(
104
+ cls: ErrorClass,
105
+ rawErrorMsg: string,
106
+ ): boolean {
107
+ if (!isTransient(cls) || cls.kind === "rate-limit") return false;
108
+ return !/retry failed after \d+ attempts:/i.test(rawErrorMsg);
109
+ }
110
+
103
111
  function isBareClaudeCodeSessionSwitchAbortMarker(message: string | undefined | null): boolean {
104
112
  if (!message) return false;
105
113
  const normalized = message.trim().replace(/\s+/g, " ").toLowerCase();
@@ -488,7 +496,7 @@ export async function handleAgentEnd(
488
496
  // Core retries transient failures in-session after this handler.
489
497
  // Keep that behavior for non-rate-limit classes to avoid pause/retry races,
490
498
  // but let rate-limit continue into model fallback logic below (#4373).
491
- if (isTransient(cls) && cls.kind !== "rate-limit") {
499
+ if (shouldDeferTransientErrorToCoreRetry(cls, rawErrorMsg)) {
492
500
  return;
493
501
  }
494
502
 
@@ -913,7 +913,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
913
913
  label: "Skip Slice",
914
914
  description:
915
915
  "Mark a slice as skipped so auto-mode advances past it without executing. " +
916
- "Non-closed tasks within the slice are cascaded to skipped so milestone completion is not blocked by leftover pending tasks (#4375). " +
916
+ "Non-closed tasks within the slice are cascaded to skipped so milestone completion is not blocked by leftover pending tasks. " +
917
917
  "The slice data is preserved for reference. The state machine treats skipped slices like completed ones for dependency satisfaction.",
918
918
  promptSnippet: "Skip a GSD slice (mark as skipped, auto-mode will advance past it)",
919
919
  promptGuidelines: [
@@ -748,7 +748,7 @@ function matchesAllowedGlob(absPath: string, basePath: string, globs: readonly s
748
748
  function blockReason(unitType: string, mode: string, what: string): string {
749
749
  return [
750
750
  `HARD BLOCK: unit "${unitType}" runs under tools-policy "${mode}" — ${what}.`,
751
- `This is a mechanical gate enforced by manifest.tools (#4934). You MUST NOT proceed,`,
751
+ `This is a mechanical gate enforced by manifest.tools. You MUST NOT proceed,`,
752
752
  `retry the same call, or rationalize past this block. If you need to write user source,`,
753
753
  `the work belongs in execute-task, not in a planning unit.`,
754
754
  ].join(" ");
@@ -587,10 +587,15 @@ async function configureModels(ctx: ExtensionCommandContext, prefs: Record<strin
587
587
  const models: Record<string, unknown> = (prefs.models as Record<string, unknown>) ?? {};
588
588
 
589
589
  const availableModels = ctx.modelRegistry.getAvailable();
590
- if (availableModels.length > 0) {
590
+ const getAllWithDiscovered = (ctx.modelRegistry as { getAllWithDiscovered?: () => typeof availableModels }).getAllWithDiscovered;
591
+ const availableProviders = new Set(availableModels.map((m) => m.provider));
592
+ const selectableModels = typeof getAllWithDiscovered === "function"
593
+ ? getAllWithDiscovered().filter((m) => availableProviders.has(m.provider))
594
+ : availableModels;
595
+ if (selectableModels.length > 0) {
591
596
  // Group models by provider, sorted alphabetically
592
- const byProvider = new Map<string, typeof availableModels>();
593
- for (const m of availableModels) {
597
+ const byProvider = new Map<string, typeof selectableModels>();
598
+ for (const m of selectableModels) {
594
599
  let group = byProvider.get(m.provider);
595
600
  if (!group) {
596
601
  group = [];
@@ -31,8 +31,10 @@ import {
31
31
  findStaleWorkerForProject,
32
32
  getAllAutoWorkers,
33
33
  markWorkerCrashed,
34
+ markWorkerStopping,
34
35
  type AutoWorkerRow,
35
36
  } from "./db/auto-workers.js";
37
+ import { forceReleaseLeasesForWorker } from "./db/milestone-leases.js";
36
38
  import { markLatestActiveForWorkerCanceled, type DispatchStatus } from "./db/unit-dispatches.js";
37
39
  import { getRuntimeKv, setRuntimeKv, deleteRuntimeKv } from "./db/runtime-kv.js";
38
40
  import { _getAdapter, isDbAvailable } from "./gsd-db.js";
@@ -219,9 +221,21 @@ export function clearLock(basePath: string): void {
219
221
  if (!isDbAvailable()) return;
220
222
  try {
221
223
  const projectRoot = normalizeRealPath(basePath);
224
+ const staleWorker = findStaleWorkerForProject(projectRoot);
225
+ if (staleWorker) {
226
+ markWorkerCrashed(staleWorker.worker_id);
227
+ forceReleaseLeasesForWorker(staleWorker.worker_id);
228
+ deleteRuntimeKv("worker", staleWorker.worker_id, SESSION_FILE_KV_KEY);
229
+ return;
230
+ }
222
231
  const worker = findActiveWorkerForCurrentProcess(projectRoot);
223
- if (!worker) return;
224
- deleteRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY);
232
+ if (worker) deleteRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY);
233
+
234
+ const stale = findStaleWorkerForProject(projectRoot);
235
+ if (stale) {
236
+ markWorkerStopping(stale.worker_id);
237
+ deleteRuntimeKv("worker", stale.worker_id, SESSION_FILE_KV_KEY);
238
+ }
225
239
  } catch {
226
240
  // Best-effort.
227
241
  }
@@ -255,6 +255,32 @@ export function releaseMilestoneLease(
255
255
  });
256
256
  }
257
257
 
258
+ /**
259
+ * Force-release all held leases for a worker.
260
+ *
261
+ * Used by crash recovery once PID liveness has confirmed the worker is dead.
262
+ * No fencing token is required because this path is cleanup-only for a
263
+ * non-running process.
264
+ */
265
+ export function forceReleaseLeasesForWorker(workerId: string): number {
266
+ if (!isDbAvailable()) return 0;
267
+ const db = _getAdapter()!;
268
+ let changes = 0;
269
+ transaction(() => {
270
+ const result = db.prepare(
271
+ `UPDATE milestone_leases
272
+ SET status = 'released'
273
+ WHERE worker_id = :worker_id
274
+ AND status = 'held'`,
275
+ ).run({ ":worker_id": workerId });
276
+ changes =
277
+ typeof (result as { changes?: unknown }).changes === "number"
278
+ ? (result as { changes: number }).changes
279
+ : 0;
280
+ });
281
+ return changes;
282
+ }
283
+
258
284
  /**
259
285
  * Read current lease row for diagnostics. Returns null if no row exists.
260
286
  */
@@ -837,7 +837,7 @@ export function detectWorktreeOrphans(
837
837
  type: "worktree-unmerged-exit",
838
838
  severity: "warning",
839
839
  summary: `${summary.exitsWithUnmergedWork} auto-exit(s) left milestone work unmerged`,
840
- details: `Exit reasons: ${reasonBreakdown || "(none)"} · Producer-side signal for #4761-class orphans. Inspect .gsd/journal/*.jsonl with eventType:"auto-exit" for per-exit detail.`,
840
+ details: `Exit reasons: ${reasonBreakdown || "(none)"} · Producer-side signal for orphaned worktrees. Inspect .gsd/journal/*.jsonl with eventType:"auto-exit" for per-exit detail.`,
841
841
  });
842
842
  }
843
843
  }
@@ -1056,7 +1056,7 @@ function saveForensicReport(basePath: string, report: ForensicReport, problemDes
1056
1056
  .map(([r, n]) => `${r}=${n}`).join(", ");
1057
1057
  sections.push(` - Exit reasons: ${breakdown}`);
1058
1058
  }
1059
- sections.push(`- Canonical-root redirects (#4761 fix fired): ${t.canonicalRedirects}`);
1059
+ sections.push(`- Canonical-root redirects: ${t.canonicalRedirects}`);
1060
1060
  // #4765 slice-cadence counters
1061
1061
  if (t.slicesMerged + t.sliceMergeConflicts + t.milestoneResquashes > 0) {
1062
1062
  sections.push(`- Slices merged: ${t.slicesMerged} · Slice merge conflicts: ${t.sliceMergeConflicts}`);
@@ -1220,7 +1220,7 @@ function formatReportForPrompt(report: ForensicReport): string {
1220
1220
  if (hasSignal) {
1221
1221
  sections.push("### Worktree Telemetry");
1222
1222
  sections.push(`- Created: ${t.worktreesCreated} · Merged: ${t.worktreesMerged} · Conflicts: ${t.mergeConflicts}`);
1223
- sections.push(`- Orphans: ${t.orphansDetected} · Unmerged exits: ${t.exitsWithUnmergedWork} · Redirects (#4761): ${t.canonicalRedirects}`);
1223
+ sections.push(`- Orphans: ${t.orphansDetected} · Unmerged exits: ${t.exitsWithUnmergedWork} · Redirects: ${t.canonicalRedirects}`);
1224
1224
  if (t.orphansDetected > 0) {
1225
1225
  const breakdown = Object.entries(t.orphansByReason)
1226
1226
  .map(([r, n]) => `${r}=${n}`).join(", ");
@@ -388,6 +388,9 @@ export function readIntegrationBranch(basePath: string, milestoneId: string): st
388
388
  }
389
389
  }
390
390
 
391
+ /** Re-export for backward compatibility — canonical definitions in branch-patterns.ts */
392
+ export { QUICK_BRANCH_RE, WORKFLOW_BRANCH_RE } from "./branch-patterns.js";
393
+
391
394
  /**
392
395
  * Persist the integration branch for a milestone.
393
396
  *
@@ -398,14 +401,14 @@ export function readIntegrationBranch(basePath: string, milestoneId: string): st
398
401
  *
399
402
  * The file is committed immediately so the metadata is persisted in git.
400
403
  */
401
- /** Re-export for backward compatibility — canonical definitions in branch-patterns.ts */
402
- export { QUICK_BRANCH_RE, WORKFLOW_BRANCH_RE } from "./branch-patterns.js";
403
-
404
404
  export function writeIntegrationBranch(
405
405
  basePath: string,
406
406
  milestoneId: string,
407
407
  branch: string,
408
408
  ): void {
409
+ // Never persist milestone branches as integration targets.
410
+ // They are ephemeral execution branches and can cause self-diff corruption.
411
+ if (branch.startsWith("milestone/")) return;
409
412
  // Don't record slice branches as the integration target
410
413
  if (SLICE_BRANCH_RE.test(branch)) return;
411
414
  // Don't record quick-task branches — they are ephemeral and merge back
@@ -111,13 +111,14 @@ const providerLoader = createSqliteProviderLoader({
111
111
 
112
112
  export const SCHEMA_VERSION = 28;
113
113
 
114
- function initSchema(db: DbAdapter, fileBacked: boolean): void {
115
- if (fileBacked) db.exec("PRAGMA journal_mode=WAL");
114
+ function initSchema(db: DbAdapter, fileBacked: boolean, dbPath: string | null): void {
115
+ const conservativeFilePragmas = fileBacked && _isLikelyWslDrvFsPathForTest(dbPath);
116
+ if (fileBacked) db.exec(conservativeFilePragmas ? "PRAGMA journal_mode=DELETE" : "PRAGMA journal_mode=WAL");
116
117
  if (fileBacked) db.exec("PRAGMA busy_timeout = 5000");
117
- if (fileBacked) db.exec("PRAGMA synchronous = NORMAL");
118
+ if (fileBacked) db.exec(conservativeFilePragmas ? "PRAGMA synchronous = FULL" : "PRAGMA synchronous = NORMAL");
118
119
  if (fileBacked) db.exec("PRAGMA auto_vacuum = INCREMENTAL");
119
120
  if (fileBacked) db.exec("PRAGMA cache_size = -8000"); // 8 MB page cache
120
- if (fileBacked && process.platform !== "darwin") db.exec("PRAGMA mmap_size = 67108864"); // 64 MB mmap
121
+ if (fileBacked && !conservativeFilePragmas && process.platform !== "darwin") db.exec("PRAGMA mmap_size = 67108864"); // 64 MB mmap
121
122
  db.exec("PRAGMA temp_store = MEMORY");
122
123
  db.exec("PRAGMA foreign_keys = ON");
123
124
 
@@ -156,6 +157,17 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
156
157
  migrateSchema(db);
157
158
  }
158
159
 
160
+ export function _isLikelyWslDrvFsPathForTest(dbPath: string | null): boolean {
161
+ if (!dbPath || process.platform !== "linux") return false;
162
+ const drvFsPathPattern = /^\/mnt\/[a-z](?:\/|$)/i;
163
+ if (drvFsPathPattern.test(dbPath)) return true;
164
+ try {
165
+ return drvFsPathPattern.test(realpathSync(dbPath));
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
170
+
159
171
  /**
160
172
  * Create the FTS5 virtual table for memories plus the triggers that keep it
161
173
  * in sync with the base table. FTS5 may be unavailable on stripped-down
@@ -605,14 +617,14 @@ export function openDatabase(path: string): boolean {
605
617
  const adapter = createDbAdapter(rawDb);
606
618
  const fileBacked = path !== ":memory:";
607
619
  try {
608
- initSchema(adapter, fileBacked);
620
+ initSchema(adapter, fileBacked, path);
609
621
  } catch (err) {
610
622
  // Corrupt freelist: DDL fails with "malformed" but VACUUM can rebuild.
611
623
  // Attempt VACUUM recovery before giving up (see #2519).
612
624
  if (fileBacked && err instanceof Error && err.message?.includes("malformed")) {
613
625
  try {
614
626
  adapter.exec("VACUUM");
615
- initSchema(adapter, fileBacked);
627
+ initSchema(adapter, fileBacked, path);
616
628
  process.stderr.write("gsd-db: recovered corrupt database via VACUUM\n");
617
629
  } catch (retryErr) {
618
630
  _dbOpenState.recordError("vacuum-recovery", retryErr);
@@ -23,6 +23,7 @@ import { nativeAddPaths, nativeCommit } from "./native-git-bridge.js";
23
23
  import { loadEffectiveGSDPreferences } from "./preferences.js";
24
24
  import { loadQueueOrder, sortByQueueOrder, saveQueueOrder } from "./queue-order.js";
25
25
  import { findMilestoneIds, nextMilestoneId } from "./milestone-ids.js";
26
+ import { isFutureMilestoneStatus } from "./status-guards.js";
26
27
 
27
28
  // ─── Queue Entry Point ──────────────────────────────────────────────────────
28
29
 
@@ -63,7 +64,7 @@ export async function showQueue(
63
64
 
64
65
  // ── Count pending milestones ────────────────────────────────────────
65
66
  const pendingMilestones = state.registry.filter(
66
- m => m.status === "pending" || m.status === "active",
67
+ m => isFutureMilestoneStatus(m.status) || m.status === "active",
67
68
  );
68
69
  const completeCount = state.registry.filter(m => m.status === "complete").length;
69
70
  const parkedCount = state.registry.filter(m => m.status === "parked").length;
@@ -182,7 +183,7 @@ export async function showQueueAdd(
182
183
  ? `Currently executing: ${state.activeMilestone.id} — ${state.activeMilestone.title} (phase: ${state.phase}).`
183
184
  : "No milestone currently active.";
184
185
 
185
- const pendingCount = state.registry.filter(m => m.status === "pending").length;
186
+ const pendingCount = state.registry.filter(m => isFutureMilestoneStatus(m.status)).length;
186
187
  const completeCount = state.registry.filter(m => m.status === "complete").length;
187
188
 
188
189
  const preamble = [
@@ -284,7 +285,7 @@ export async function buildExistingMilestonesContext(
284
285
 
285
286
  // For active/pending/parked milestones, include the roadmap if it exists
286
287
  // (shows what's planned but not yet built)
287
- if (status === "active" || status === "pending" || status === "parked") {
288
+ if (status === "active" || isFutureMilestoneStatus(status) || status === "parked") {
288
289
  const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
289
290
  if (roadmapFile) {
290
291
  const content = await loadFile(roadmapFile);