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
@@ -689,6 +689,8 @@ export async function rerootCommandSession(cmdCtx, workspaceRoot) {
689
689
  }
690
690
  }
691
691
  export async function cleanupAfterLoopExit(ctx) {
692
+ const preserveStepSurface = s.preserveStepSurfaceAfterLoopExit;
693
+ const preservePausedSurface = s.paused;
692
694
  s.currentUnit = null;
693
695
  s.active = false;
694
696
  deactivateGSD();
@@ -712,19 +714,24 @@ export async function cleanupAfterLoopExit(ctx) {
712
714
  // A transient provider-error pause intentionally leaves the paused badge
713
715
  // visible so the user still has a resumable auto-mode signal on screen.
714
716
  if (!s.paused) {
715
- ctx.ui.setStatus("gsd-auto", undefined);
716
- ctx.ui.setWidget("gsd-progress", undefined);
717
- if (s.completionStopInProgress) {
718
- s.completionStopInProgress = false;
717
+ if (preserveStepSurface) {
718
+ s.preserveStepSurfaceAfterLoopExit = false;
719
+ }
720
+ else {
721
+ ctx.ui.setStatus("gsd-auto", undefined);
722
+ ctx.ui.setWidget("gsd-progress", undefined);
723
+ if (s.completionStopInProgress) {
724
+ s.completionStopInProgress = false;
725
+ }
726
+ initHealthWidget(ctx);
719
727
  }
720
- initHealthWidget(ctx);
721
728
  }
722
729
  // ADR-016 phase 3 (#5693): the stop-path basePath restore + chdir routes
723
730
  // through `Lifecycle.restoreToProjectRoot()`, the sole owner of both
724
731
  // `s.basePath` mutation and the paired `process.chdir` for auto-loop
725
732
  // transitions. The verb assigns `s.basePath` before any throwable work, so
726
733
  // a thrown error still leaves basePath restored.
727
- if (s.originalBasePath) {
734
+ if (s.originalBasePath && !preserveStepSurface && !preservePausedSurface) {
728
735
  try {
729
736
  buildLifecycle().restoreToProjectRoot();
730
737
  }
@@ -732,7 +739,7 @@ export async function cleanupAfterLoopExit(ctx) {
732
739
  logWarning("engine", `restore project root failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
733
740
  }
734
741
  }
735
- if (s.originalBasePath && s.cmdCtx) {
742
+ if (s.originalBasePath && s.cmdCtx && !preserveStepSurface && !preservePausedSurface) {
736
743
  const result = await rerootCommandSession(s.cmdCtx, s.originalBasePath);
737
744
  if (result.status === "cancelled") {
738
745
  logWarning("engine", "post-loop session re-root was cancelled", { file: "auto.ts", basePath: s.originalBasePath });
@@ -1325,7 +1332,6 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
1325
1332
  restoreProjectRootEnv();
1326
1333
  restoreMilestoneLockEnv();
1327
1334
  s.pendingVerificationRetry = null;
1328
- s.verificationRetryCount.clear();
1329
1335
  ctx?.ui.setStatus("gsd-auto", "paused");
1330
1336
  ctx?.ui.setWidget("gsd-progress", undefined);
1331
1337
  const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";
@@ -61,6 +61,11 @@ export function isUserInitiatedAbortMessage(message) {
61
61
  return false;
62
62
  return /\b(?:claude code process aborted by user|request aborted by user|process aborted by user)\b/i.test(message);
63
63
  }
64
+ export function shouldDeferTransientErrorToCoreRetry(cls, rawErrorMsg) {
65
+ if (!isTransient(cls) || cls.kind === "rate-limit")
66
+ return false;
67
+ return !/retry failed after \d+ attempts:/i.test(rawErrorMsg);
68
+ }
64
69
  function isBareClaudeCodeSessionSwitchAbortMarker(message) {
65
70
  if (!message)
66
71
  return false;
@@ -394,7 +399,7 @@ export async function handleAgentEnd(pi, event, ctx) {
394
399
  // Core retries transient failures in-session after this handler.
395
400
  // Keep that behavior for non-rate-limit classes to avoid pause/retry races,
396
401
  // but let rate-limit continue into model fallback logic below (#4373).
397
- if (isTransient(cls) && cls.kind !== "rate-limit") {
402
+ if (shouldDeferTransientErrorToCoreRetry(cls, rawErrorMsg)) {
398
403
  return;
399
404
  }
400
405
  // Cap rate-limit backoff for CLI-style providers (openai-codex, google-gemini-cli)
@@ -848,7 +848,7 @@ export function registerDbTools(pi) {
848
848
  name: "gsd_skip_slice",
849
849
  label: "Skip Slice",
850
850
  description: "Mark a slice as skipped so auto-mode advances past it without executing. " +
851
- "Non-closed tasks within the slice are cascaded to skipped so milestone completion is not blocked by leftover pending tasks (#4375). " +
851
+ "Non-closed tasks within the slice are cascaded to skipped so milestone completion is not blocked by leftover pending tasks. " +
852
852
  "The slice data is preserved for reference. The state machine treats skipped slices like completed ones for dependency satisfaction.",
853
853
  promptSnippet: "Skip a GSD slice (mark as skipped, auto-mode will advance past it)",
854
854
  promptGuidelines: [
@@ -625,7 +625,7 @@ function matchesAllowedGlob(absPath, basePath, globs) {
625
625
  function blockReason(unitType, mode, what) {
626
626
  return [
627
627
  `HARD BLOCK: unit "${unitType}" runs under tools-policy "${mode}" — ${what}.`,
628
- `This is a mechanical gate enforced by manifest.tools (#4934). You MUST NOT proceed,`,
628
+ `This is a mechanical gate enforced by manifest.tools. You MUST NOT proceed,`,
629
629
  `retry the same call, or rationalize past this block. If you need to write user source,`,
630
630
  `the work belongs in execute-task, not in a planning unit.`,
631
631
  ].join(" ");
@@ -571,10 +571,15 @@ async function configureModels(ctx, prefs) {
571
571
  ];
572
572
  const models = prefs.models ?? {};
573
573
  const availableModels = ctx.modelRegistry.getAvailable();
574
- if (availableModels.length > 0) {
574
+ const getAllWithDiscovered = ctx.modelRegistry.getAllWithDiscovered;
575
+ const availableProviders = new Set(availableModels.map((m) => m.provider));
576
+ const selectableModels = typeof getAllWithDiscovered === "function"
577
+ ? getAllWithDiscovered().filter((m) => availableProviders.has(m.provider))
578
+ : availableModels;
579
+ if (selectableModels.length > 0) {
575
580
  // Group models by provider, sorted alphabetically
576
581
  const byProvider = new Map();
577
- for (const m of availableModels) {
582
+ for (const m of selectableModels) {
578
583
  let group = byProvider.get(m.provider);
579
584
  if (!group) {
580
585
  group = [];
@@ -23,7 +23,8 @@
23
23
  import { emitJournalEvent, queryJournal, } from "./journal.js";
24
24
  import { readFileSync, unlinkSync, existsSync } from "node:fs";
25
25
  import { join } from "node:path";
26
- import { findStaleWorkerForProject, getAllAutoWorkers, markWorkerCrashed, } from "./db/auto-workers.js";
26
+ import { findStaleWorkerForProject, getAllAutoWorkers, markWorkerCrashed, markWorkerStopping, } from "./db/auto-workers.js";
27
+ import { forceReleaseLeasesForWorker } from "./db/milestone-leases.js";
27
28
  import { markLatestActiveForWorkerCanceled } from "./db/unit-dispatches.js";
28
29
  import { getRuntimeKv, setRuntimeKv, deleteRuntimeKv } from "./db/runtime-kv.js";
29
30
  import { _getAdapter, isDbAvailable } from "./gsd-db.js";
@@ -182,10 +183,21 @@ export function clearLock(basePath) {
182
183
  return;
183
184
  try {
184
185
  const projectRoot = normalizeRealPath(basePath);
185
- const worker = findActiveWorkerForCurrentProcess(projectRoot);
186
- if (!worker)
186
+ const staleWorker = findStaleWorkerForProject(projectRoot);
187
+ if (staleWorker) {
188
+ markWorkerCrashed(staleWorker.worker_id);
189
+ forceReleaseLeasesForWorker(staleWorker.worker_id);
190
+ deleteRuntimeKv("worker", staleWorker.worker_id, SESSION_FILE_KV_KEY);
187
191
  return;
188
- deleteRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY);
192
+ }
193
+ const worker = findActiveWorkerForCurrentProcess(projectRoot);
194
+ if (worker)
195
+ deleteRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY);
196
+ const stale = findStaleWorkerForProject(projectRoot);
197
+ if (stale) {
198
+ markWorkerStopping(stale.worker_id);
199
+ deleteRuntimeKv("worker", stale.worker_id, SESSION_FILE_KV_KEY);
200
+ }
189
201
  }
190
202
  catch {
191
203
  // Best-effort.
@@ -193,6 +193,30 @@ export function releaseMilestoneLease(workerId, milestoneId, fencingToken) {
193
193
  return changes === 1;
194
194
  });
195
195
  }
196
+ /**
197
+ * Force-release all held leases for a worker.
198
+ *
199
+ * Used by crash recovery once PID liveness has confirmed the worker is dead.
200
+ * No fencing token is required because this path is cleanup-only for a
201
+ * non-running process.
202
+ */
203
+ export function forceReleaseLeasesForWorker(workerId) {
204
+ if (!isDbAvailable())
205
+ return 0;
206
+ const db = _getAdapter();
207
+ let changes = 0;
208
+ transaction(() => {
209
+ const result = db.prepare(`UPDATE milestone_leases
210
+ SET status = 'released'
211
+ WHERE worker_id = :worker_id
212
+ AND status = 'held'`).run({ ":worker_id": workerId });
213
+ changes =
214
+ typeof result.changes === "number"
215
+ ? result.changes
216
+ : 0;
217
+ });
218
+ return changes;
219
+ }
196
220
  /**
197
221
  * Read current lease row for diagnostics. Returns null if no row exists.
198
222
  */
@@ -677,7 +677,7 @@ export function detectWorktreeOrphans(summary, anomalies) {
677
677
  type: "worktree-unmerged-exit",
678
678
  severity: "warning",
679
679
  summary: `${summary.exitsWithUnmergedWork} auto-exit(s) left milestone work unmerged`,
680
- details: `Exit reasons: ${reasonBreakdown || "(none)"} · Producer-side signal for #4761-class orphans. Inspect .gsd/journal/*.jsonl with eventType:"auto-exit" for per-exit detail.`,
680
+ details: `Exit reasons: ${reasonBreakdown || "(none)"} · Producer-side signal for orphaned worktrees. Inspect .gsd/journal/*.jsonl with eventType:"auto-exit" for per-exit detail.`,
681
681
  });
682
682
  }
683
683
  }
@@ -884,7 +884,7 @@ function saveForensicReport(basePath, report, problemDescription) {
884
884
  .map(([r, n]) => `${r}=${n}`).join(", ");
885
885
  sections.push(` - Exit reasons: ${breakdown}`);
886
886
  }
887
- sections.push(`- Canonical-root redirects (#4761 fix fired): ${t.canonicalRedirects}`);
887
+ sections.push(`- Canonical-root redirects: ${t.canonicalRedirects}`);
888
888
  // #4765 slice-cadence counters
889
889
  if (t.slicesMerged + t.sliceMergeConflicts + t.milestoneResquashes > 0) {
890
890
  sections.push(`- Slices merged: ${t.slicesMerged} · Slice merge conflicts: ${t.sliceMergeConflicts}`);
@@ -1033,7 +1033,7 @@ function formatReportForPrompt(report) {
1033
1033
  if (hasSignal) {
1034
1034
  sections.push("### Worktree Telemetry");
1035
1035
  sections.push(`- Created: ${t.worktreesCreated} · Merged: ${t.worktreesMerged} · Conflicts: ${t.mergeConflicts}`);
1036
- sections.push(`- Orphans: ${t.orphansDetected} · Unmerged exits: ${t.exitsWithUnmergedWork} · Redirects (#4761): ${t.canonicalRedirects}`);
1036
+ sections.push(`- Orphans: ${t.orphansDetected} · Unmerged exits: ${t.exitsWithUnmergedWork} · Redirects: ${t.canonicalRedirects}`);
1037
1037
  if (t.orphansDetected > 0) {
1038
1038
  const breakdown = Object.entries(t.orphansByReason)
1039
1039
  .map(([r, n]) => `${r}=${n}`).join(", ");
@@ -237,6 +237,8 @@ export function readIntegrationBranch(basePath, milestoneId) {
237
237
  return null;
238
238
  }
239
239
  }
240
+ /** Re-export for backward compatibility — canonical definitions in branch-patterns.ts */
241
+ export { QUICK_BRANCH_RE, WORKFLOW_BRANCH_RE } from "./branch-patterns.js";
240
242
  /**
241
243
  * Persist the integration branch for a milestone.
242
244
  *
@@ -247,9 +249,11 @@ export function readIntegrationBranch(basePath, milestoneId) {
247
249
  *
248
250
  * The file is committed immediately so the metadata is persisted in git.
249
251
  */
250
- /** Re-export for backward compatibility — canonical definitions in branch-patterns.ts */
251
- export { QUICK_BRANCH_RE, WORKFLOW_BRANCH_RE } from "./branch-patterns.js";
252
252
  export function writeIntegrationBranch(basePath, milestoneId, branch) {
253
+ // Never persist milestone branches as integration targets.
254
+ // They are ephemeral execution branches and can cause self-diff corruption.
255
+ if (branch.startsWith("milestone/"))
256
+ return;
253
257
  // Don't record slice branches as the integration target
254
258
  if (SLICE_BRANCH_RE.test(branch))
255
259
  return;
@@ -54,18 +54,19 @@ const providerLoader = createSqliteProviderLoader({
54
54
  writeStderr: (message) => process.stderr.write(message),
55
55
  });
56
56
  export const SCHEMA_VERSION = 28;
57
- function initSchema(db, fileBacked) {
57
+ function initSchema(db, fileBacked, dbPath) {
58
+ const conservativeFilePragmas = fileBacked && _isLikelyWslDrvFsPathForTest(dbPath);
58
59
  if (fileBacked)
59
- db.exec("PRAGMA journal_mode=WAL");
60
+ db.exec(conservativeFilePragmas ? "PRAGMA journal_mode=DELETE" : "PRAGMA journal_mode=WAL");
60
61
  if (fileBacked)
61
62
  db.exec("PRAGMA busy_timeout = 5000");
62
63
  if (fileBacked)
63
- db.exec("PRAGMA synchronous = NORMAL");
64
+ db.exec(conservativeFilePragmas ? "PRAGMA synchronous = FULL" : "PRAGMA synchronous = NORMAL");
64
65
  if (fileBacked)
65
66
  db.exec("PRAGMA auto_vacuum = INCREMENTAL");
66
67
  if (fileBacked)
67
68
  db.exec("PRAGMA cache_size = -8000"); // 8 MB page cache
68
- if (fileBacked && process.platform !== "darwin")
69
+ if (fileBacked && !conservativeFilePragmas && process.platform !== "darwin")
69
70
  db.exec("PRAGMA mmap_size = 67108864"); // 64 MB mmap
70
71
  db.exec("PRAGMA temp_store = MEMORY");
71
72
  db.exec("PRAGMA foreign_keys = ON");
@@ -99,6 +100,19 @@ function initSchema(db, fileBacked) {
99
100
  }
100
101
  migrateSchema(db);
101
102
  }
103
+ export function _isLikelyWslDrvFsPathForTest(dbPath) {
104
+ if (!dbPath || process.platform !== "linux")
105
+ return false;
106
+ const drvFsPathPattern = /^\/mnt\/[a-z](?:\/|$)/i;
107
+ if (drvFsPathPattern.test(dbPath))
108
+ return true;
109
+ try {
110
+ return drvFsPathPattern.test(realpathSync(dbPath));
111
+ }
112
+ catch {
113
+ return false;
114
+ }
115
+ }
102
116
  /**
103
117
  * Create the FTS5 virtual table for memories plus the triggers that keep it
104
118
  * in sync with the base table. FTS5 may be unavailable on stripped-down
@@ -504,7 +518,7 @@ export function openDatabase(path) {
504
518
  const adapter = createDbAdapter(rawDb);
505
519
  const fileBacked = path !== ":memory:";
506
520
  try {
507
- initSchema(adapter, fileBacked);
521
+ initSchema(adapter, fileBacked, path);
508
522
  }
509
523
  catch (err) {
510
524
  // Corrupt freelist: DDL fails with "malformed" but VACUUM can rebuild.
@@ -512,7 +526,7 @@ export function openDatabase(path) {
512
526
  if (fileBacked && err instanceof Error && err.message?.includes("malformed")) {
513
527
  try {
514
528
  adapter.exec("VACUUM");
515
- initSchema(adapter, fileBacked);
529
+ initSchema(adapter, fileBacked, path);
516
530
  process.stderr.write("gsd-db: recovered corrupt database via VACUUM\n");
517
531
  }
518
532
  catch (retryErr) {
@@ -18,6 +18,7 @@ import { nativeAddPaths, nativeCommit } from "./native-git-bridge.js";
18
18
  import { loadEffectiveGSDPreferences } from "./preferences.js";
19
19
  import { saveQueueOrder } from "./queue-order.js";
20
20
  import { findMilestoneIds, nextMilestoneId } from "./milestone-ids.js";
21
+ import { isFutureMilestoneStatus } from "./status-guards.js";
21
22
  // ─── Queue Entry Point ──────────────────────────────────────────────────────
22
23
  /**
23
24
  * Queue future milestones via conversational intake.
@@ -48,7 +49,7 @@ export async function showQueue(ctx, pi, basePath) {
48
49
  return;
49
50
  }
50
51
  // ── Count pending milestones ────────────────────────────────────────
51
- const pendingMilestones = state.registry.filter(m => m.status === "pending" || m.status === "active");
52
+ const pendingMilestones = state.registry.filter(m => isFutureMilestoneStatus(m.status) || m.status === "active");
52
53
  const completeCount = state.registry.filter(m => m.status === "complete").length;
53
54
  const parkedCount = state.registry.filter(m => m.status === "parked").length;
54
55
  // ── If multiple pending milestones, show queue management hub ──────
@@ -140,7 +141,7 @@ export async function showQueueAdd(ctx, pi, basePath, state) {
140
141
  const activePart = state.activeMilestone
141
142
  ? `Currently executing: ${state.activeMilestone.id} — ${state.activeMilestone.title} (phase: ${state.phase}).`
142
143
  : "No milestone currently active.";
143
- const pendingCount = state.registry.filter(m => m.status === "pending").length;
144
+ const pendingCount = state.registry.filter(m => isFutureMilestoneStatus(m.status)).length;
144
145
  const completeCount = state.registry.filter(m => m.status === "complete").length;
145
146
  const preamble = [
146
147
  `Queuing new work onto an existing GSD project.`,
@@ -223,7 +224,7 @@ export async function buildExistingMilestonesContext(basePath, milestoneIds, sta
223
224
  }
224
225
  // For active/pending/parked milestones, include the roadmap if it exists
225
226
  // (shows what's planned but not yet built)
226
- if (status === "active" || status === "pending" || status === "parked") {
227
+ if (status === "active" || isFutureMilestoneStatus(status) || status === "parked") {
227
228
  const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
228
229
  if (roadmapFile) {
229
230
  const content = await loadFile(roadmapFile);
@@ -30,6 +30,7 @@ import { getIsolationMode, loadEffectiveGSDPreferences } from "./preferences.js"
30
30
  import { resolveUokFlags } from "./uok/flags.js";
31
31
  import { ensurePlanV2Graph, isMissingFinalizedContextResult } from "./uok/plan-v2.js";
32
32
  import { detectProjectState, hasGsdBootstrapArtifacts } from "./detection.js";
33
+ import { isFutureMilestoneStatus } from "./status-guards.js";
33
34
  import { showProjectInit, offerMigration } from "./init-wizard.js";
34
35
  import { validateDirectory } from "./validate-directory.js";
35
36
  import { showConfirm } from "../shared/tui.js";
@@ -811,6 +812,7 @@ async function dispatchWorkflow(pi, note, customType = "gsd-run", ctx, unitType,
811
812
  ? ctx.modelRegistry.getProviderAuthMode(ctx.model.provider)
812
813
  : undefined,
813
814
  baseUrl: result.appliedModel?.baseUrl ?? ctx.model?.baseUrl,
815
+ activeTools: typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [],
814
816
  });
815
817
  if (compatibilityError) {
816
818
  ctx.ui.notify(compatibilityError, "error");
@@ -1164,7 +1166,7 @@ export async function showDiscuss(ctx, pi, basePath) {
1164
1166
  // No active milestone (or corrupted milestone with undefined id) —
1165
1167
  // check for pending milestones to discuss instead
1166
1168
  if (!state.activeMilestone?.id) {
1167
- const pendingMilestones = state.registry.filter(m => m.status === "pending");
1169
+ const pendingMilestones = state.registry.filter(m => isFutureMilestoneStatus(m.status));
1168
1170
  if (pendingMilestones.length === 0) {
1169
1171
  ctx.ui.notify("No active milestone. Run /gsd to create one first.", "warning");
1170
1172
  return;
@@ -1269,7 +1271,7 @@ export async function showDiscuss(ctx, pi, basePath) {
1269
1271
  const pendingSlices = normSlices.filter(s => !s.done);
1270
1272
  if (pendingSlices.length === 0) {
1271
1273
  // All slices complete — but queued milestones may still need discussion (#3150)
1272
- const pendingMilestones = state.registry.filter(m => m.status === "pending");
1274
+ const pendingMilestones = state.registry.filter(m => isFutureMilestoneStatus(m.status));
1273
1275
  if (pendingMilestones.length > 0) {
1274
1276
  await showDiscussQueuedMilestone(ctx, pi, basePath, pendingMilestones);
1275
1277
  return;
@@ -1290,7 +1292,7 @@ export async function showDiscuss(ctx, pi, basePath) {
1290
1292
  // If all pending slices are discussed, check for queued milestones before exiting (#3150)
1291
1293
  const allDiscussed = pendingSlices.every(s => discussedMap.get(s.id));
1292
1294
  if (allDiscussed) {
1293
- const pendingMilestones = state.registry.filter(m => m.status === "pending");
1295
+ const pendingMilestones = state.registry.filter(m => isFutureMilestoneStatus(m.status));
1294
1296
  if (pendingMilestones.length > 0) {
1295
1297
  await showDiscussQueuedMilestone(ctx, pi, basePath, pendingMilestones);
1296
1298
  return;
@@ -1321,7 +1323,7 @@ export async function showDiscuss(ctx, pi, basePath) {
1321
1323
  };
1322
1324
  });
1323
1325
  // Offer access to queued milestones when any exist
1324
- const pendingMilestones = state.registry.filter(m => m.status === "pending");
1326
+ const pendingMilestones = state.registry.filter(m => isFutureMilestoneStatus(m.status));
1325
1327
  if (pendingMilestones.length > 0) {
1326
1328
  actions.push({
1327
1329
  id: "discuss_queued_milestone",
@@ -1385,10 +1387,11 @@ async function showDiscussQueuedMilestone(ctx, pi, basePath, pendingMilestones)
1385
1387
  const hasContext = !!resolveMilestoneFile(basePath, m.id, "CONTEXT");
1386
1388
  const hasDraft = !hasContext && !!resolveMilestoneFile(basePath, m.id, "CONTEXT-DRAFT");
1387
1389
  const contextStatus = hasContext ? "context ✓" : hasDraft ? "draft context" : "no context yet";
1390
+ const statusLabel = m.status === "planned" ? "planned" : "queued";
1388
1391
  return {
1389
1392
  id: m.id,
1390
1393
  label: `${m.id}: ${m.title}`,
1391
- description: `[queued] · ${contextStatus}`,
1394
+ description: `[${statusLabel}] · ${contextStatus}`,
1392
1395
  recommended: i === 0,
1393
1396
  };
1394
1397
  });
@@ -14,7 +14,7 @@ import { isClosedStatus } from "./status-guards.js";
14
14
  import { join, relative } from "node:path";
15
15
  import { createRequire } from "node:module";
16
16
  import { getAllMilestones, getMilestone, getMilestoneSlices, getSliceTasks, getTask, getSlice, getArtifact, insertArtifact, getGateResults, } from "./gsd-db.js";
17
- import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksDir, gsdRoot, buildTaskFileName, buildSliceFileName, } from "./paths.js";
17
+ import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, gsdProjectionRoot, gsdRoot, buildTaskFileName, buildSliceFileName, } from "./paths.js";
18
18
  import { saveFile, clearParseCache } from "./files.js";
19
19
  import { invalidateStateCache } from "./state.js";
20
20
  import { clearPathCache } from "./paths.js";
@@ -24,7 +24,11 @@ import { clearPathCache } from "./paths.js";
24
24
  * E.g. "/project/.gsd/milestones/M001/M001-ROADMAP.md" → "milestones/M001/M001-ROADMAP.md"
25
25
  */
26
26
  function toArtifactPath(absPath, basePath) {
27
- const root = gsdRoot(basePath);
27
+ const projectionRoot = gsdProjectionRoot(basePath);
28
+ const projectionRel = relative(projectionRoot, absPath);
29
+ const root = projectionRel && !projectionRel.startsWith("..") && !projectionRel.startsWith("/")
30
+ ? projectionRoot
31
+ : gsdRoot(basePath);
28
32
  const rel = relative(root, absPath);
29
33
  // Normalize to forward slashes for consistent DB keys
30
34
  return rel.replace(/\\/g, "/");
@@ -305,10 +309,9 @@ export async function renderPlanFromDb(basePath, milestoneId, sliceId) {
305
309
  if (tasks.length === 0) {
306
310
  throw new Error(`no tasks found for ${milestoneId}/${sliceId}`);
307
311
  }
308
- const slicePath = resolveSlicePath(basePath, milestoneId, sliceId)
309
- ?? join(gsdRoot(basePath), "milestones", milestoneId, "slices", sliceId);
310
- const absPath = resolveSliceFile(basePath, milestoneId, sliceId, "PLAN")
311
- ?? join(slicePath, `${sliceId}-PLAN.md`);
312
+ const slicePath = join(gsdProjectionRoot(basePath), "milestones", milestoneId, "slices", sliceId);
313
+ mkdirSync(slicePath, { recursive: true });
314
+ const absPath = join(slicePath, `${sliceId}-PLAN.md`);
312
315
  const artifactPath = toArtifactPath(absPath, basePath);
313
316
  const sliceGates = getGateResults(milestoneId, sliceId, "slice");
314
317
  const content = renderSlicePlanMarkdown(slice, tasks, sliceGates);
@@ -329,8 +332,7 @@ export async function renderTaskPlanFromDb(basePath, milestoneId, sliceId, taskI
329
332
  if (!task) {
330
333
  throw new Error(`task ${milestoneId}/${sliceId}/${taskId} not found`);
331
334
  }
332
- const tasksDir = resolveTasksDir(basePath, milestoneId, sliceId)
333
- ?? join(gsdRoot(basePath), "milestones", milestoneId, "slices", sliceId, "tasks");
335
+ const tasksDir = join(gsdProjectionRoot(basePath), "milestones", milestoneId, "slices", sliceId, "tasks");
334
336
  mkdirSync(tasksDir, { recursive: true });
335
337
  const absPath = join(tasksDir, buildTaskFileName(taskId, "PLAN"));
336
338
  const artifactPath = toArtifactPath(absPath, basePath);
@@ -320,6 +320,10 @@ export function resolveGsdPathContract(workRoot, originalProjectRoot) {
320
320
  isWorktree,
321
321
  };
322
322
  }
323
+ export function gsdProjectionRoot(basePath) {
324
+ const contract = resolveGsdPathContract(basePath);
325
+ return normalizeRealPath(contract.worktreeGsd ?? contract.projectGsd);
326
+ }
323
327
  /**
324
328
  * Invalidate the gsdRoot cache.
325
329
  * Use ONLY at session-reset boundaries: workspace switch, process exit, or
@@ -24,6 +24,7 @@ export async function showQueueReorder(ctx, completed, pending) {
24
24
  const items = [...pending];
25
25
  let cursor = 0;
26
26
  let grabbed = false;
27
+ let scrollOffset = 0;
27
28
  let cachedLines;
28
29
  let validation;
29
30
  // Mutable deps map — tracks removals during this session
@@ -128,9 +129,11 @@ export async function showQueueReorder(ctx, completed, pending) {
128
129
  return cachedLines;
129
130
  const ui = makeUI(theme, width);
130
131
  const lines = [];
132
+ const queueRows = [];
131
133
  const push = (...rows) => { for (const r of rows)
132
134
  lines.push(...r); };
133
135
  const add = (s) => truncateToWidth(s, width);
136
+ let cursorQueueRow = 0;
134
137
  const headerText = grabbed ? " Queue Reorder — Moving Item" : " Queue Reorder";
135
138
  push(ui.bar(), ui.blank(), ui.header(headerText), ui.blank());
136
139
  // Completed milestones (dimmed)
@@ -153,13 +156,15 @@ export async function showQueueReorder(ctx, completed, pending) {
153
156
  const num = i + 1;
154
157
  const label = item.title && item.title !== item.id ? `${item.id} ${item.title}` : item.id;
155
158
  if (isCursor && grabbed) {
156
- lines.push(add(` ${theme.fg("warning", `▸▸ ${num}. ${label}`)}`));
159
+ cursorQueueRow = queueRows.length;
160
+ queueRows.push(add(` ${theme.fg("warning", `▸▸ ${num}. ${label}`)}`));
157
161
  }
158
162
  else if (isCursor) {
159
- lines.push(add(` ${theme.fg("accent", `${GLYPH.cursor} ${num}. ${label}`)}`));
163
+ cursorQueueRow = queueRows.length;
164
+ queueRows.push(add(` ${theme.fg("accent", `${GLYPH.cursor} ${num}. ${label}`)}`));
160
165
  }
161
166
  else {
162
- lines.push(add(` ${theme.fg("text", `${num}. ${label}`)}`));
167
+ queueRows.push(add(` ${theme.fg("text", `${num}. ${label}`)}`));
163
168
  }
164
169
  // depends_on annotations
165
170
  const deps = liveDeps.get(item.id) ?? [];
@@ -168,34 +173,35 @@ export async function showQueueReorder(ctx, completed, pending) {
168
173
  continue;
169
174
  const pairKey = `${item.id}:${dep}`;
170
175
  if (violatedPairs.has(pairKey)) {
171
- lines.push(add(` ${theme.fg("warning", `${GLYPH.statusWarning} depends_on: ${dep} — auto-removed on confirm`)}`));
176
+ queueRows.push(add(` ${theme.fg("warning", `${GLYPH.statusWarning} depends_on: ${dep} — auto-removed on confirm`)}`));
172
177
  }
173
178
  else if (redundantPairs.has(pairKey)) {
174
- lines.push(add(` ${theme.fg("dim", `↳ depends_on: ${dep} (redundant)`)}`));
179
+ queueRows.push(add(` ${theme.fg("dim", `↳ depends_on: ${dep} (redundant)`)}`));
175
180
  }
176
181
  else {
177
- lines.push(add(` ${theme.fg("dim", `↳ depends_on: ${dep}`)}`));
182
+ queueRows.push(add(` ${theme.fg("dim", `↳ depends_on: ${dep}`)}`));
178
183
  }
179
184
  }
180
185
  // Missing deps
181
186
  for (const v of validation.violations.filter(v => v.milestone === item.id && v.type === 'missing_dep')) {
182
- lines.push(add(` ${theme.fg("error", `${GLYPH.statusWarning} depends_on: ${v.dependsOn} (does not exist)`)}`));
187
+ queueRows.push(add(` ${theme.fg("error", `${GLYPH.statusWarning} depends_on: ${v.dependsOn} (does not exist)`)}`));
183
188
  }
184
189
  }
185
190
  // Removed deps feedback
191
+ const trailingLines = [];
186
192
  if (removedDeps.length > 0) {
187
- push(ui.blank());
193
+ trailingLines.push(...ui.blank());
188
194
  for (const r of removedDeps) {
189
- lines.push(add(` ${theme.fg("success", `${GLYPH.statusDone} Removed: ${r.milestone} depends_on ${r.dep}`)}`));
195
+ trailingLines.push(add(` ${theme.fg("success", `${GLYPH.statusDone} Removed: ${r.milestone} depends_on ${r.dep}`)}`));
190
196
  }
191
197
  }
192
198
  // Circular warning
193
199
  const circ = validation.violations.find(v => v.type === 'circular');
194
200
  if (circ) {
195
- push(ui.blank());
196
- lines.push(add(` ${theme.fg("error", `${GLYPH.statusWarning} ${circ.message}`)}`));
201
+ trailingLines.push(...ui.blank());
202
+ trailingLines.push(add(` ${theme.fg("error", `${GLYPH.statusWarning} ${circ.message}`)}`));
197
203
  }
198
- push(ui.blank());
204
+ trailingLines.push(...ui.blank());
199
205
  // Hints — context-sensitive based on grab state
200
206
  const hints = [];
201
207
  if (grabbed) {
@@ -215,7 +221,18 @@ export async function showQueueReorder(ctx, completed, pending) {
215
221
  hints.push("enter ok");
216
222
  }
217
223
  hints.push("esc");
218
- push(ui.hints(hints), ui.bar());
224
+ trailingLines.push(...ui.hints(hints), ...ui.bar());
225
+ const maxOverlayRows = Math.max(10, process.stdout.rows ? Math.floor(process.stdout.rows * 0.8) : 24);
226
+ const availableQueueRows = Math.max(1, maxOverlayRows - lines.length - trailingLines.length);
227
+ const maxScroll = Math.max(0, queueRows.length - availableQueueRows);
228
+ if (cursorQueueRow < scrollOffset) {
229
+ scrollOffset = cursorQueueRow;
230
+ }
231
+ else if (cursorQueueRow >= scrollOffset + availableQueueRows) {
232
+ scrollOffset = cursorQueueRow - availableQueueRows + 1;
233
+ }
234
+ scrollOffset = Math.min(Math.max(scrollOffset, 0), maxScroll);
235
+ lines.push(...queueRows.slice(scrollOffset, scrollOffset + availableQueueRows), ...trailingLines);
219
236
  cachedLines = lines;
220
237
  return lines;
221
238
  }
@@ -1253,7 +1253,7 @@ export async function _deriveStateImpl(basePath, opts) {
1253
1253
  const summaryPath = resolveTaskFile(basePath, activeMilestone.id, activeSlice.id, t.id, "SUMMARY");
1254
1254
  if (summaryPath && existsSync(summaryPath)) {
1255
1255
  t.done = true;
1256
- logWarning("reconcile", `task ${activeMilestone.id}/${activeSlice.id}/${t.id} reconciled via SUMMARY on disk (#2514)`, { mid: activeMilestone.id, sid: activeSlice.id, tid: t.id });
1256
+ logWarning("reconcile", `task ${activeMilestone.id}/${activeSlice.id}/${t.id} reconciled via SUMMARY on disk`, { mid: activeMilestone.id, sid: activeSlice.id, tid: t.id });
1257
1257
  }
1258
1258
  }
1259
1259
  const taskProgress = {
@@ -26,3 +26,10 @@ export function isInactiveStatus(status) {
26
26
  export function isSkippedForDispatch(status) {
27
27
  return isClosedStatus(status) || status === "parked" || isDeferredStatus(status);
28
28
  }
29
+ /**
30
+ * Returns true when a milestone is future/backlog work (not currently executing).
31
+ * Includes legacy/project-specific alias "planned" for compatibility.
32
+ */
33
+ export function isFutureMilestoneStatus(status) {
34
+ return status === "pending" || status === "queued" || status === "planned";
35
+ }
@@ -132,6 +132,7 @@
132
132
  Verify field rules:
133
133
  - MUST be a mechanically executable command: `npm test`, `grep -q "pattern" file`, `test -f path`
134
134
  - MUST NOT use shell pipes, redirects, semicolons, backticks, command substitution, or output trimming
135
+ - MUST NOT use inline `node -e` assertions for verification; put assertions in a real test file and run it with `node --test` or a package test script
135
136
  - For content/document tasks: verify file existence, section count, YAML validity, or word count
136
137
  NOT exact phrasing, specific formulas, or "zero TBD" aspirational criteria
137
138
  - If no command can verify the output, write: "Manual review — file exists and is non-empty"
@@ -57,6 +57,12 @@ skills_used:
57
57
  - {{howToVerifyThisTaskIsActuallyDone}}
58
58
  - {{commandToRun_OR_behaviorToCheck}}
59
59
 
60
+ ## Verify Rules
61
+
62
+ - Use a real executable check, not prose.
63
+ - If the check needs file-content assertions, write a `node:test` file and run it with `node --test` or a package test script.
64
+ - Do not use inline `node -e` assertions for verification.
65
+
60
66
  ## Observability Impact
61
67
 
62
68
  <!-- OMIT THIS SECTION ENTIRELY for simple tasks that don't touch runtime boundaries,
@@ -12,7 +12,7 @@ import { appendEvent } from "../workflow-events.js";
12
12
  import { logWarning } from "../workflow-logger.js";
13
13
  import { validatePlanningPathScope } from "../planning-path-scope.js";
14
14
  import { checkFilePathConsistency, checkTaskOrdering } from "../pre-execution-checks.js";
15
- import { buildTaskFileName, gsdRoot, resolveTasksDir } from "../paths.js";
15
+ import { buildTaskFileName, gsdProjectionRoot } from "../paths.js";
16
16
  function validateTasks(value) {
17
17
  if (!Array.isArray(value) || value.length === 0) {
18
18
  throw new Error("tasks must be a non-empty array");
@@ -241,14 +241,12 @@ export async function handlePlanSlice(rawParams, basePath) {
241
241
  return { error: guardError };
242
242
  }
243
243
  try {
244
- const tasksDir = resolveTasksDir(basePath, params.milestoneId, params.sliceId);
244
+ const tasksDir = join(gsdProjectionRoot(basePath), "milestones", params.milestoneId, "slices", params.sliceId, "tasks");
245
245
  for (const taskId of omittedTaskIds) {
246
- if (!tasksDir)
247
- continue;
248
246
  const taskPlanPath = join(tasksDir, buildTaskFileName(taskId, "PLAN"));
249
247
  if (existsSync(taskPlanPath))
250
248
  rmSync(taskPlanPath, { force: true });
251
- const artifactPath = relative(gsdRoot(basePath), taskPlanPath).replace(/\\/g, "/");
249
+ const artifactPath = relative(gsdProjectionRoot(basePath), taskPlanPath).replace(/\\/g, "/");
252
250
  deleteArtifactByPath(artifactPath);
253
251
  }
254
252
  const renderResult = await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);