gsd-pi 2.82.0-dev.9d5798940 → 2.82.0-dev.dfbc5f58f

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 (185) hide show
  1. package/README.md +2 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/GSD-WORKFLOW.md +7 -0
  4. package/dist/resources/extensions/gsd/auto/infra-errors.js +9 -3
  5. package/dist/resources/extensions/gsd/auto/loop.js +5 -5
  6. package/dist/resources/extensions/gsd/auto/orchestrator.js +11 -0
  7. package/dist/resources/extensions/gsd/auto/phases.js +8 -1
  8. package/dist/resources/extensions/gsd/auto/workflow-memory-pressure.js +12 -0
  9. package/dist/resources/extensions/gsd/auto-model-selection.js +2 -0
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
  11. package/dist/resources/extensions/gsd/auto-start.js +78 -9
  12. package/dist/resources/extensions/gsd/auto-worktree.js +15 -1
  13. package/dist/resources/extensions/gsd/auto.js +30 -3
  14. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +9 -8
  15. package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +5 -2
  16. package/dist/resources/extensions/gsd/crash-recovery.js +31 -5
  17. package/dist/resources/extensions/gsd/db/unit-dispatches.js +1 -0
  18. package/dist/resources/extensions/gsd/dispatch-guard.js +2 -2
  19. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +28 -11
  20. package/dist/resources/extensions/gsd/doctor.js +2 -28
  21. package/dist/resources/extensions/gsd/git-service.js +39 -1
  22. package/dist/resources/extensions/gsd/gsd-db.js +1 -0
  23. package/dist/resources/extensions/gsd/guided-flow.js +6 -0
  24. package/dist/resources/extensions/gsd/migrate/parsers.js +10 -0
  25. package/dist/resources/extensions/gsd/native-git-bridge.js +40 -9
  26. package/dist/resources/extensions/gsd/post-execution-checks.js +73 -2
  27. package/dist/resources/extensions/gsd/pre-execution-checks.js +28 -1
  28. package/dist/resources/extensions/gsd/prompt-loader.js +1 -1
  29. package/dist/resources/extensions/gsd/prompts/plan-slice.md +3 -3
  30. package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  31. package/dist/resources/extensions/gsd/status-guards.js +4 -0
  32. package/dist/resources/extensions/gsd/templates/plan.md +8 -5
  33. package/dist/resources/extensions/gsd/templates/task-plan.md +4 -2
  34. package/dist/resources/extensions/gsd/tools/complete-milestone.js +6 -8
  35. package/dist/resources/extensions/gsd/tools/complete-slice.js +6 -8
  36. package/dist/resources/extensions/gsd/tools/plan-milestone.js +7 -1
  37. package/dist/resources/extensions/gsd/tools/plan-slice.js +88 -14
  38. package/dist/resources/extensions/gsd/validation.js +23 -1
  39. package/dist/resources/extensions/gsd/verification-gate.js +68 -7
  40. package/dist/resources/extensions/gsd/workflow-projections.js +6 -8
  41. package/dist/resources/extensions/gsd/worktree-lifecycle.js +5 -1
  42. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  43. package/dist/web/standalone/.next/BUILD_ID +1 -1
  44. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  45. package/dist/web/standalone/.next/build-manifest.json +2 -2
  46. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  47. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/index.html +1 -1
  65. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  72. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/package.json +2 -2
  77. package/packages/mcp-server/src/workflow-tools.test.ts +1 -1
  78. package/packages/native/tsconfig.json +2 -1
  79. package/packages/native/tsconfig.tsbuildinfo +1 -1
  80. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  81. package/packages/pi-ai/dist/providers/openai-codex-responses.js +82 -1
  82. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  83. package/packages/pi-ai/dist/providers/openai-codex-responses.test.d.ts +2 -0
  84. package/packages/pi-ai/dist/providers/openai-codex-responses.test.d.ts.map +1 -0
  85. package/packages/pi-ai/dist/providers/openai-codex-responses.test.js +52 -0
  86. package/packages/pi-ai/dist/providers/openai-codex-responses.test.js.map +1 -0
  87. package/packages/pi-ai/dist/providers/simple-options.d.ts +2 -4
  88. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  89. package/packages/pi-ai/dist/providers/simple-options.js +5 -6
  90. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  91. package/packages/pi-ai/dist/providers/simple-options.test.d.ts +2 -0
  92. package/packages/pi-ai/dist/providers/simple-options.test.d.ts.map +1 -0
  93. package/packages/pi-ai/dist/providers/simple-options.test.js +50 -0
  94. package/packages/pi-ai/dist/providers/simple-options.test.js.map +1 -0
  95. package/packages/pi-ai/src/providers/openai-codex-responses.test.ts +63 -0
  96. package/packages/pi-ai/src/providers/openai-codex-responses.ts +91 -1
  97. package/packages/pi-ai/src/providers/simple-options.test.ts +60 -0
  98. package/packages/pi-ai/src/providers/simple-options.ts +5 -6
  99. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  100. package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.d.ts +2 -0
  101. package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.d.ts.map +1 -0
  102. package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.js +66 -0
  103. package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.js.map +1 -0
  104. package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
  105. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  106. package/packages/pi-coding-agent/src/core/agent-session-thinking-level.test.ts +79 -0
  107. package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
  108. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  109. package/src/resources/GSD-WORKFLOW.md +7 -0
  110. package/src/resources/extensions/gsd/auto/contracts.ts +14 -6
  111. package/src/resources/extensions/gsd/auto/infra-errors.ts +9 -3
  112. package/src/resources/extensions/gsd/auto/loop.ts +8 -5
  113. package/src/resources/extensions/gsd/auto/orchestrator.ts +11 -0
  114. package/src/resources/extensions/gsd/auto/phases.ts +7 -1
  115. package/src/resources/extensions/gsd/auto/workflow-memory-pressure.ts +13 -0
  116. package/src/resources/extensions/gsd/auto-model-selection.ts +2 -1
  117. package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
  118. package/src/resources/extensions/gsd/auto-start.ts +85 -6
  119. package/src/resources/extensions/gsd/auto-worktree.ts +15 -1
  120. package/src/resources/extensions/gsd/auto.ts +32 -3
  121. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +9 -8
  122. package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +3 -1
  123. package/src/resources/extensions/gsd/crash-recovery.ts +30 -4
  124. package/src/resources/extensions/gsd/db/unit-dispatches.ts +1 -0
  125. package/src/resources/extensions/gsd/dispatch-guard.ts +2 -2
  126. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +25 -13
  127. package/src/resources/extensions/gsd/doctor.ts +2 -27
  128. package/src/resources/extensions/gsd/git-service.ts +45 -1
  129. package/src/resources/extensions/gsd/gsd-db.ts +3 -0
  130. package/src/resources/extensions/gsd/guided-flow.ts +6 -0
  131. package/src/resources/extensions/gsd/migrate/parsers.ts +11 -0
  132. package/src/resources/extensions/gsd/native-git-bridge.ts +46 -9
  133. package/src/resources/extensions/gsd/post-execution-checks.ts +87 -2
  134. package/src/resources/extensions/gsd/pre-execution-checks.ts +32 -1
  135. package/src/resources/extensions/gsd/prompt-loader.ts +1 -1
  136. package/src/resources/extensions/gsd/prompts/plan-slice.md +3 -3
  137. package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  138. package/src/resources/extensions/gsd/status-guards.ts +5 -0
  139. package/src/resources/extensions/gsd/templates/plan.md +8 -5
  140. package/src/resources/extensions/gsd/templates/task-plan.md +4 -2
  141. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +54 -0
  142. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +80 -1
  143. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +6 -6
  144. package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +1 -0
  145. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +4 -1
  146. package/src/resources/extensions/gsd/tests/complete-task.test.ts +3 -1
  147. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -2
  148. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +2 -0
  149. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +27 -0
  150. package/src/resources/extensions/gsd/tests/guided-flow.test.ts +21 -0
  151. package/src/resources/extensions/gsd/tests/hook-model-resolution.test.ts +5 -0
  152. package/src/resources/extensions/gsd/tests/infra-error.test.ts +2 -2
  153. package/src/resources/extensions/gsd/tests/infra-errors-cooldown.test.ts +9 -0
  154. package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +20 -0
  155. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +103 -1
  156. package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +6 -1
  157. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +24 -1
  158. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +63 -2
  159. package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +121 -1
  160. package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +26 -0
  161. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +200 -1
  162. package/src/resources/extensions/gsd/tests/plan-task.test.ts +17 -0
  163. package/src/resources/extensions/gsd/tests/post-execution-checks.test.ts +86 -0
  164. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +53 -0
  165. package/src/resources/extensions/gsd/tests/prompt-loader.test.ts +23 -0
  166. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +31 -1
  167. package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +26 -2
  168. package/src/resources/extensions/gsd/tests/summary-render-parity.test.ts +7 -3
  169. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +110 -1
  170. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +1 -1
  171. package/src/resources/extensions/gsd/tests/workflow-memory-pressure.test.ts +21 -1
  172. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +1 -1
  173. package/src/resources/extensions/gsd/tests/worktree-git-pathspec.test.ts +39 -0
  174. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +7 -0
  175. package/src/resources/extensions/gsd/tools/complete-milestone.ts +8 -10
  176. package/src/resources/extensions/gsd/tools/complete-slice.ts +6 -8
  177. package/src/resources/extensions/gsd/tools/plan-milestone.ts +5 -1
  178. package/src/resources/extensions/gsd/tools/plan-slice.ts +96 -12
  179. package/src/resources/extensions/gsd/types.ts +1 -1
  180. package/src/resources/extensions/gsd/validation.ts +23 -1
  181. package/src/resources/extensions/gsd/verification-gate.ts +78 -6
  182. package/src/resources/extensions/gsd/workflow-projections.ts +6 -8
  183. package/src/resources/extensions/gsd/worktree-lifecycle.ts +7 -1
  184. /package/dist/web/standalone/.next/static/{BdZQhe8yKl6bdKLiXVEzh → q0WYuDVbHeFFYbdd-fei2}/_buildManifest.js +0 -0
  185. /package/dist/web/standalone/.next/static/{BdZQhe8yKl6bdKLiXVEzh → q0WYuDVbHeFFYbdd-fei2}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -376,7 +376,7 @@ The database is authoritative for milestones, slices, tasks, requirements, summa
376
376
 
377
377
  10. **Adaptive replanning** — After each slice completes, the roadmap is reassessed. If the work revealed new information that changes the plan, slices are reordered, added, or removed before continuing.
378
378
 
379
- 11. **Verification enforcement** — Configure shell commands (`npm run lint`, `npm run test`, etc.) that run automatically after task execution. Failures trigger auto-fix retries before advancing. Execute-task commits and snapshots are deferred until verification passes; failed or incomplete verification blocks closeout instead of publishing changes. Auto-discovered checks from `package.json` run in advisory mode — they log warnings but don't block on pre-existing errors. Configurable via `verification_commands`, `verification_auto_fix`, and `verification_max_retries` preferences.
379
+ 11. **Verification enforcement** — Configure simple executable commands (`npm run lint`, `npm run test`, etc.) that run automatically after task execution. Verification commands must not use shell composition or control syntax such as pipes, redirects, semicolons, backticks, or command substitution. Failures trigger auto-fix retries before advancing. Execute-task commits and snapshots are deferred until verification passes; failed or incomplete verification blocks closeout instead of publishing changes. Auto-discovered checks from `package.json` and Python pytest project markers (`python-project`) run in advisory mode — they log warnings but don't block on pre-existing errors. Configurable via `verification_commands`, `verification_auto_fix`, and `verification_max_retries` preferences.
380
380
 
381
381
  12. **Milestone validation** — After all slices complete, a `validate-milestone` gate compares roadmap success criteria against actual results before sealing the milestone.
382
382
 
@@ -670,7 +670,7 @@ auto_report: true
670
670
  | `context_mode.exec_stdout_cap_bytes` | Persisted stdout cap for `gsd_exec` output (default: 1048576) |
671
671
  | `context_mode.exec_digest_chars` | Trailing stdout characters returned to the agent context (default: 300) |
672
672
  | `context_mode.exec_env_allowlist` | Environment variables forwarded to sandboxed `gsd_exec` runs in addition to `PATH` and `HOME` |
673
- | `verification_commands` | Array of shell commands to run after task execution (e.g., `["npm run lint", "npm run test"]`) |
673
+ | `verification_commands` | Array of simple executable commands to run after task execution (e.g., `["npm run lint", "npm run test"]`); avoid pipes, redirects, semicolons, backticks, and command substitution |
674
674
  | `verification_auto_fix` | Auto-retry on verification failures (default: true) |
675
675
  | `verification_max_retries` | Max retries for verification failures (default: 2) |
676
676
  | `phases.require_slice_discussion` | Pause auto-mode before each slice for human discussion review |
@@ -1 +1 @@
1
- ed49f911008c62ca
1
+ 5c0ebf59b3b9fce3
@@ -448,6 +448,13 @@ What differed from the plan and why (or "None").
448
448
 
449
449
  The one-liner must be substantive: "JWT auth with refresh rotation using jose" not "Authentication implemented."
450
450
 
451
+ When `key_files` or `key_decisions` are empty, render them as empty YAML lists:
452
+
453
+ ```yaml
454
+ key_files: []
455
+ key_decisions: []
456
+ ```
457
+
451
458
  **Slice summary:** Written when all tasks in a slice complete. Compresses all task summaries. Includes `drill_down_paths` to each task summary. During slice completion, review task summaries for `key_decisions` and ensure any significant ones are captured in `.gsd/DECISIONS.md`.
452
459
 
453
460
  **Milestone summary:** Updated each time a slice completes. Compresses all slice summaries. This is what gets injected into later slice planning instead of loading many individual summaries.
@@ -6,9 +6,14 @@
6
6
  * failures that merit retry.
7
7
  */
8
8
  /**
9
- * Error codes indicating infrastructure failures that cannot be recovered by
10
- * retrying. Each retry re-dispatches the unit at full LLM cost, so we bail
11
- * immediately rather than burning budget on guaranteed failures.
9
+ * Error codes indicating infrastructure-level failures from the OS,
10
+ * filesystem, or network. This set includes permanent resource failures
11
+ * (ENOSPC, ENOMEM, EROFS), transient resource exhaustion (EAGAIN, ENOBUFS),
12
+ * and network/offline errors (ECONNREFUSED, ENOTFOUND, ENETUNREACH).
13
+ *
14
+ * Transient git failures are retried separately through
15
+ * TRANSIENT_GIT_RETRY_CODES in native-git-bridge.ts before escalating to the
16
+ * auto-loop.
12
17
  */
13
18
  export const INFRA_ERROR_CODES = new Set([
14
19
  "ENOSPC", // disk full
@@ -18,6 +23,7 @@ export const INFRA_ERROR_CODES = new Set([
18
23
  "EMFILE", // too many open files (process)
19
24
  "ENFILE", // too many open files (system)
20
25
  "EAGAIN", // resource temporarily unavailable (resource exhaustion)
26
+ "ENOBUFS", // no buffer space available (transient pipe exhaustion)
21
27
  "ECONNREFUSED", // connection refused (offline / local server down)
22
28
  "ENOTFOUND", // DNS lookup failed (offline / no network)
23
29
  "ENETUNREACH", // network unreachable (offline / no route)
@@ -35,7 +35,7 @@ import { createWorkflowTurnReporter } from "./workflow-turn-reporter.js";
35
35
  import { validateWorkflowSessionLock } from "./workflow-session-lock.js";
36
36
  import { dequeueSidecarItem } from "./workflow-sidecar-queue.js";
37
37
  import { maintainWorkerHeartbeat } from "./workflow-worker-heartbeat.js";
38
- import { measureMemoryPressure } from "./workflow-memory-pressure.js";
38
+ import { measureMemoryPressure, shouldCheckMemoryPressure, } from "./workflow-memory-pressure.js";
39
39
  import { buildSidecarIterationData } from "./workflow-sidecar-iteration.js";
40
40
  import { createExecutionGraphUnitDispatchDeps, runUnitPhaseViaContract, } from "./workflow-unit-dispatch.js";
41
41
  import { handleCustomEngineDispatchOutcome } from "./workflow-custom-engine-dispatch-outcome.js";
@@ -130,9 +130,9 @@ function logCustomVerifyRetrySaveFailure(err) {
130
130
  });
131
131
  }
132
132
  // ── Memory pressure monitoring (#3331) ──────────────────────────────────
133
- // Check heap usage every N iterations and trigger graceful shutdown before
134
- // the OS OOM killer sends SIGKILL. The threshold is 90% of the V8 heap
135
- // limit (--max-old-space-size or default ~1.5-4GB depending on platform).
133
+ // Check heap usage on session startup, then every N iterations, and trigger
134
+ // graceful shutdown before the OS OOM killer sends SIGKILL. The threshold is
135
+ // 90% of the V8 heap limit (--max-old-space-size or default ~1.5-4GB depending on platform).
136
136
  const MEMORY_CHECK_INTERVAL = 5; // check every 5 iterations
137
137
  const MAX_CUSTOM_ENGINE_VERIFY_RETRIES = 3;
138
138
  async function enforceMinRequestInterval(s, prefs) {
@@ -262,7 +262,7 @@ export async function autoLoop(ctx, pi, s, deps, options) {
262
262
  }
263
263
  // ── Memory pressure check (#3331) ──
264
264
  // Graceful shutdown before OOM killer sends SIGKILL.
265
- if (iteration % MEMORY_CHECK_INTERVAL === 0) {
265
+ if (shouldCheckMemoryPressure(iteration, MEMORY_CHECK_INTERVAL)) {
266
266
  const mem = measureMemoryPressure();
267
267
  debugLog("autoLoop", { phase: "memory-check", ...mem });
268
268
  const memoryDecision = decideMemoryPressure({ ...mem, iteration });
@@ -117,6 +117,17 @@ export class AutoOrchestrator {
117
117
  await this.deps.health.postAdvanceRecord(stopped);
118
118
  return stopped;
119
119
  }
120
+ if (!("unitType" in decision)) {
121
+ const blocked = {
122
+ kind: "blocked",
123
+ reason: decision.reason,
124
+ action: decision.action,
125
+ stateSnapshot: reconciliation.stateSnapshot,
126
+ };
127
+ await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
128
+ await this.deps.health.postAdvanceRecord(blocked);
129
+ return blocked;
130
+ }
120
131
  const nextKey = `${decision.unitType}:${decision.unitId}`;
121
132
  // Record every dispatch decision in the ring buffer before pre-flight
122
133
  // checks so the stuck-loop detector observes the full decision history
@@ -968,7 +968,14 @@ export async function runDispatch(ic, preData, loopState) {
968
968
  prompt = preDispatchResult.prompt;
969
969
  }
970
970
  const guardBasePath = _resolveDispatchGuardBasePath(s);
971
- const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(guardBasePath, deps.getMainBranch(guardBasePath), unitType, unitId);
971
+ let mainBranch = "main";
972
+ try {
973
+ mainBranch = deps.getMainBranch(guardBasePath);
974
+ }
975
+ catch (err) {
976
+ debugLog("autoLoop", { phase: "getMainBranch-failed", error: String(err) });
977
+ }
978
+ const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(guardBasePath, mainBranch, unitType, unitId);
972
979
  if (priorSliceBlocker) {
973
980
  await deps.stopAuto(ctx, pi, priorSliceBlocker);
974
981
  debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
@@ -4,6 +4,18 @@ import { createRequire } from "node:module";
4
4
  const require = createRequire(import.meta.url);
5
5
  const DEFAULT_MEMORY_PRESSURE_THRESHOLD = 0.85;
6
6
  const DEFAULT_HEAP_LIMIT_MB = 4096;
7
+ /**
8
+ * Returns true on auto-mode startup, then every configured interval.
9
+ *
10
+ * Iteration 1 is checked explicitly so early session memory pressure cannot
11
+ * bypass the periodic interval guard.
12
+ */
13
+ export function shouldCheckMemoryPressure(iteration, interval) {
14
+ if (!Number.isInteger(interval) || interval <= 0) {
15
+ throw new Error("Memory pressure check interval must be a positive integer");
16
+ }
17
+ return iteration === 1 || iteration % interval === 0;
18
+ }
7
19
  function defaultHeapLimitBytes() {
8
20
  const v8 = require("node:v8");
9
21
  const limit = v8.getHeapStatistics?.().heap_size_limit;
@@ -494,6 +494,8 @@ autoModeStartThinkingLevel) {
494
494
  * Handles formats: "provider/model", "bare-id", "org/model-name" (OpenRouter).
495
495
  */
496
496
  export function resolveModelId(modelId, availableModels, currentProvider) {
497
+ if (!modelId)
498
+ return undefined;
497
499
  const slashIdx = modelId.indexOf("/");
498
500
  if (slashIdx !== -1) {
499
501
  const maybeProvider = modelId.substring(0, slashIdx);
@@ -125,7 +125,7 @@ async function buildTaskCommitContextForUnit(basePath, unitId) {
125
125
  sliceId: sid,
126
126
  sliceTitle: stripKnownIdPrefix(slice?.title, sid),
127
127
  oneLiner: summary?.oneLiner || task?.one_liner || undefined,
128
- keyFiles: summary?.frontmatter.key_files?.filter(f => !f.includes("{{")) ??
128
+ keyFiles: summary?.frontmatter.key_files?.filter(f => !f.includes("{{") && f.trim() !== "(none)") ??
129
129
  task?.key_files ??
130
130
  undefined,
131
131
  issueNumber: ghIssueNumber,
@@ -21,7 +21,7 @@ import { invalidateAllCaches } from "./cache.js";
21
21
  import { writeLock, clearLock } from "./crash-recovery.js";
22
22
  import { acquireSessionLock, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
23
23
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
24
- import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch, nativeCheckoutBranch, nativeBranchList, nativeBranchListMerged, nativeBranchDelete, nativeWorktreeRemove, nativeCommitCountBetween, } from "./native-git-bridge.js";
24
+ import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch, nativeCheckoutBranch, nativeBranchList, nativeBranchExists, nativeBranchListMerged, nativeBranchDelete, nativeWorktreeRemove, nativeCommitCountBetween, } from "./native-git-bridge.js";
25
25
  import { GitServiceImpl } from "./git-service.js";
26
26
  import { captureIntegrationBranch, detectWorktreeName, setActiveMilestoneId, } from "./worktree.js";
27
27
  import { getAutoWorktreePath } from "./auto-worktree.js";
@@ -33,7 +33,7 @@ import { initRoutingHistory } from "./routing-history.js";
33
33
  import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
34
34
  import { resetProactiveHealing, setLevelChangeCallback } from "./doctor-proactive.js";
35
35
  import { snapshotSkills } from "./skill-discovery.js";
36
- import { isDbAvailable, getMilestone, openDatabase, getDbStatus } from "./gsd-db.js";
36
+ import { isDbAvailable, getMilestone, getAllMilestones, openDatabase, getDbStatus } from "./gsd-db.js";
37
37
  import { isClosedStatus } from "./status-guards.js";
38
38
  import { classifyMilestoneSummaryContent } from "./milestone-summary-classifier.js";
39
39
  import { auditOrphanedPreflightStashes } from "./orphan-stash-audit.js";
@@ -88,9 +88,11 @@ export function decideSurvivorAction(hasSurvivorBranch, phase) {
88
88
  return "finalize";
89
89
  return "none";
90
90
  }
91
- export function auditOrphanedMilestoneBranches(basePath, isolationMode) {
91
+ export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps = {}) {
92
92
  const recovered = [];
93
93
  const warnings = [];
94
+ const branchList = gitDeps.branchList ?? nativeBranchList;
95
+ const branchExists = gitDeps.branchExists ?? nativeBranchExists;
94
96
  // Skip in none mode — no milestone branches are created
95
97
  if (isolationMode === "none")
96
98
  return { recovered, warnings };
@@ -98,15 +100,16 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode) {
98
100
  if (!isDbAvailable())
99
101
  return { recovered, warnings };
100
102
  let milestoneBranches;
103
+ let milestoneBranchListAvailable = true;
101
104
  try {
102
- milestoneBranches = nativeBranchList(basePath, "milestone/*");
105
+ milestoneBranches = branchList(basePath, "milestone/*");
103
106
  }
104
107
  catch {
105
- // git branch list failed — skip audit
106
- return { recovered, warnings };
108
+ milestoneBranchListAvailable = false;
109
+ // git branch list failed — fall through with an empty branch set so the
110
+ // branch-less orphan pass can still run after per-milestone verification.
111
+ milestoneBranches = [];
107
112
  }
108
- if (milestoneBranches.length === 0)
109
- return { recovered, warnings };
110
113
  // Detect main branch for merge-check
111
114
  let mainBranch;
112
115
  try {
@@ -236,6 +239,71 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode) {
236
239
  }
237
240
  }
238
241
  }
242
+ // Second pass (#5879): catch worktree directories stranded by a previous
243
+ // audit that deleted the milestone/* branch but failed to remove the
244
+ // directory (or the dir was orphaned by a separate path entirely, e.g.
245
+ // postflight-stash-restore-failed during closeout). The branch-keyed loop
246
+ // above is invisible to these cases — `nativeBranchList` returns nothing
247
+ // for the milestone, so the dir-cleanup block at line ~310 is never
248
+ // reached.
249
+ //
250
+ // Keyed on milestones whose DB status is `complete`. We do not iterate
251
+ // over arbitrary directories under .gsd/worktrees/ to avoid touching
252
+ // dirs that belong to an in-progress milestone whose branch was deleted
253
+ // separately — those are handled by the in-progress orphan path above
254
+ // when the branch is present, and by `/gsd doctor` when it is not.
255
+ const seenMilestoneIds = new Set(milestoneBranches.map((branch) => branch.replace(/^milestone\//, "")));
256
+ let completedMilestones = [];
257
+ try {
258
+ completedMilestones = getAllMilestones();
259
+ }
260
+ catch {
261
+ // DB read failure — skip the second pass; the first pass is still useful.
262
+ completedMilestones = [];
263
+ }
264
+ for (const m of completedMilestones) {
265
+ if (m.status !== "complete")
266
+ continue;
267
+ if (seenMilestoneIds.has(m.id))
268
+ continue; // already processed in the branch loop
269
+ if (!milestoneBranchListAvailable) {
270
+ try {
271
+ if (branchExists(basePath, `milestone/${m.id}`))
272
+ continue;
273
+ }
274
+ catch (err) {
275
+ warnings.push(`Could not verify whether milestone/${m.id} still exists; skipping branch-less worktree cleanup for safety: ${err instanceof Error ? err.message : String(err)}`);
276
+ continue;
277
+ }
278
+ }
279
+ const wtDir = getWorktreeDir(basePath, m.id);
280
+ if (!existsSync(wtDir))
281
+ continue;
282
+ if (!isInsideWorktreesDir(basePath, wtDir)) {
283
+ warnings.push(`Orphaned worktree directory for ${m.id} is outside .gsd/worktrees/ — skipping removal for safety.`);
284
+ continue;
285
+ }
286
+ // Try `git worktree remove` first in case the dir is still registered
287
+ // (defensive — usually it is not when we reach this branch-less pass).
288
+ try {
289
+ nativeWorktreeRemove(basePath, wtDir, true);
290
+ }
291
+ catch (e) {
292
+ logWarning("engine", `worktree remove failed (expected for branch-less orphans): ${e instanceof Error ? e.message : String(e)}`);
293
+ }
294
+ if (existsSync(wtDir)) {
295
+ try {
296
+ rmSync(wtDir, { recursive: true, force: true });
297
+ recovered.push(`Removed orphaned worktree directory for ${m.id} (branch already deleted).`);
298
+ }
299
+ catch (err) {
300
+ warnings.push(`Failed to remove orphaned worktree directory for ${m.id}: ${err instanceof Error ? err.message : String(err)}`);
301
+ }
302
+ }
303
+ else {
304
+ recovered.push(`Removed orphaned worktree directory for ${m.id} (branch already deleted).`);
305
+ }
306
+ }
239
307
  return { recovered, warnings };
240
308
  }
241
309
  /**
@@ -368,7 +436,7 @@ export function _mergeOrphanCompletedMilestone(lifecycle, orphanId, ui) {
368
436
  return { merged: false, error: err };
369
437
  }
370
438
  export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, deps, interrupted) {
371
- const { shouldUseWorktreeIsolation, registerSigtermHandler, lockBase, buildLifecycle, } = deps;
439
+ const { shouldUseWorktreeIsolation, registerSigtermHandler, registerAutoWorkerForSession, lockBase, buildLifecycle, } = deps;
372
440
  const dirCheck = validateDirectory(base);
373
441
  if (dirCheck.severity === "blocked") {
374
442
  ctx.ui.notify(dirCheck.reason, "error");
@@ -523,6 +591,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
523
591
  // consult DB status and avoid clearing runtime units for milestones that
524
592
  // only have a failure-path SUMMARY on disk (#4663).
525
593
  await openProjectDbIfPresent(base);
594
+ registerAutoWorkerForSession(base);
526
595
  // Clean stale runtime unit files for completed milestones (#887).
527
596
  // DB-authoritative: when DB is available, require DB status to be closed
528
597
  // before clearing runtime units. A SUMMARY file alone is no longer
@@ -173,7 +173,18 @@ function gitPathspecForWorktreePath(basePath, targetPath) {
173
173
  let base = basePath;
174
174
  let target = targetPath;
175
175
  try {
176
- base = realpathSync.native(basePath);
176
+ base = execFileSync("git", ["rev-parse", "--show-toplevel"], {
177
+ cwd: basePath,
178
+ stdio: ["ignore", "pipe", "ignore"],
179
+ encoding: "utf-8",
180
+ }).trim() || basePath;
181
+ }
182
+ catch {
183
+ /* keep original */
184
+ void base;
185
+ }
186
+ try {
187
+ base = realpathSync.native(base);
177
188
  }
178
189
  catch {
179
190
  /* keep original */
@@ -191,6 +202,9 @@ function gitPathspecForWorktreePath(basePath, targetPath) {
191
202
  return null;
192
203
  return rel.replaceAll("\\", "/");
193
204
  }
205
+ export function _gitPathspecForWorktreePath(basePath, targetPath) {
206
+ return gitPathspecForWorktreePath(basePath, targetPath);
207
+ }
194
208
  function gitRemoteExists(basePath, remote) {
195
209
  try {
196
210
  execFileSync("git", ["remote", "get-url", remote], {
@@ -22,7 +22,7 @@ import { gsdRoot, resolveMilestoneFile, resolveMilestonePath, resolveDir, milest
22
22
  import { invalidateAllCaches } from "./cache.js";
23
23
  import { clearActivityLogState } from "./activity-log.js";
24
24
  import { synthesizeCrashRecovery, getDeepDiagnostic, readActiveMilestoneId, } from "./session-forensics.js";
25
- import { writeLock, clearLock, readCrashLock, isLockProcessAlive, formatCrashInfo, emitCrashRecoveredUnitEnd, emitOpenUnitEndForUnit, } from "./crash-recovery.js";
25
+ import { writeLock, clearLock, clearStaleWorkerLock, readCrashLock, isLockProcessAlive, formatCrashInfo, emitCrashRecoveredUnitEnd, emitOpenUnitEndForUnit, } from "./crash-recovery.js";
26
26
  import { acquireSessionLock, getSessionLockStatus, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
27
27
  import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences, getIsolationMode, } from "./preferences.js";
28
28
  import { sendDesktopNotification } from "./notifications.js";
@@ -707,6 +707,7 @@ export async function cleanupAfterLoopExit(ctx) {
707
707
  // visible so the user still has a resumable auto-mode signal on screen.
708
708
  if (!s.paused) {
709
709
  ctx.ui.setStatus("gsd-auto", undefined);
710
+ ctx.ui.setWidget("gsd-progress", undefined);
710
711
  if (s.completionStopInProgress) {
711
712
  s.completionStopInProgress = false;
712
713
  }
@@ -1420,6 +1421,13 @@ export function createWiredDispatchAdapter(ctx, pi, dispatchBasePath) {
1420
1421
  sessionProvider,
1421
1422
  modelRegistry,
1422
1423
  });
1424
+ if (action.action === "stop") {
1425
+ return {
1426
+ kind: "blocked",
1427
+ reason: action.reason,
1428
+ action: action.level === "warning" ? "pause" : "stop",
1429
+ };
1430
+ }
1423
1431
  if (action.action !== "dispatch")
1424
1432
  return null;
1425
1433
  return {
@@ -1640,6 +1648,17 @@ export function createWiredAutoOrchestrationModule(ctx, pi, dispatchBasePath, ru
1640
1648
  };
1641
1649
  return createAutoOrchestrator(deps);
1642
1650
  }
1651
+ function notifyResumeBlocked(ctx, result) {
1652
+ const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";
1653
+ ctx.ui.notify(`Auto-mode blocked: ${result.reason}. Fix and run ${resumeCmd} to resume.`, "warning");
1654
+ setLifecycleOutcome(ctx, {
1655
+ status: "blocked",
1656
+ title: "Auto-mode blocked",
1657
+ detail: result.reason,
1658
+ nextAction: `Fix the blocker, then run ${resumeCmd} to resume.`,
1659
+ commands: ["/gsd status for overview", `${resumeCmd} to resume`, "/gsd doctor to diagnose"],
1660
+ });
1661
+ }
1643
1662
  function ensureOrchestrationModule(ctx, pi, basePath) {
1644
1663
  s.orchestration = createWiredAutoOrchestrationModule(ctx, pi, basePath, lockBase());
1645
1664
  }
@@ -1924,7 +1943,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1924
1943
  // This closes the journal gap reported in #3348 where the worker wrote side
1925
1944
  // effects (SUMMARY.md, DB updates) but died before emitting unit-end.
1926
1945
  emitCrashRecoveredUnitEnd(base, freshStartAssessment.lock);
1927
- clearLock(base);
1946
+ clearStaleWorkerLock(base);
1928
1947
  }
1929
1948
  if (!s.paused) {
1930
1949
  s.pendingCrashRecovery =
@@ -1987,6 +2006,8 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1987
2006
  initMetrics(base);
1988
2007
  if (s.currentMilestoneId)
1989
2008
  setActiveMilestoneId(base, s.currentMilestoneId);
2009
+ await openProjectDbIfPresent(base);
2010
+ registerAutoWorkerForSession(s, base);
1990
2011
  // Re-register health level notification callback lost across process restart
1991
2012
  setLevelChangeCallback((_from, to, summary) => {
1992
2013
  const level = to === "red" ? "error" : to === "yellow" ? "warning" : "info";
@@ -2062,7 +2083,12 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
2062
2083
  }
2063
2084
  pi.events.emit(CMUX_CHANNELS.LOG, { preferences: loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, message: s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", level: "progress" });
2064
2085
  try {
2065
- await s.orchestration?.resume();
2086
+ const resumeResult = await s.orchestration?.resume();
2087
+ if (resumeResult?.kind === "blocked") {
2088
+ notifyResumeBlocked(ctx, resumeResult);
2089
+ await cleanupAfterLoopExit(ctx);
2090
+ return;
2091
+ }
2066
2092
  }
2067
2093
  catch (err) {
2068
2094
  debugLog("resume-orchestration-resume", { error: err instanceof Error ? err.message : String(err) });
@@ -2083,6 +2109,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
2083
2109
  const bootstrapDeps = {
2084
2110
  shouldUseWorktreeIsolation,
2085
2111
  registerSigtermHandler,
2112
+ registerAutoWorkerForSession: (projectRoot) => registerAutoWorkerForSession(s, projectRoot),
2086
2113
  lockBase,
2087
2114
  buildLifecycle,
2088
2115
  };
@@ -468,17 +468,18 @@ export function registerDbTools(pi) {
468
468
  promptGuidelines: [
469
469
  "Use gsd_plan_milestone for milestone planning instead of writing ROADMAP.md directly.",
470
470
  "Keep parameters flat and provide the full milestone planning payload, including slices.",
471
+ "Milestone and slice titles must not contain forward slash (/), en dash, or em dash characters.",
471
472
  "The tool validates input, writes milestone and slice planning data transactionally, renders ROADMAP.md from DB, and clears both state and parse caches after success.",
472
473
  "Use the canonical name gsd_plan_milestone; gsd_milestone_plan is only an alias.",
473
474
  ],
474
475
  parameters: Type.Object({
475
476
  // ── Core identification + content (required) ──────────────────────
476
477
  milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
477
- title: Type.String({ description: "Milestone title" }),
478
+ title: Type.String({ description: "Milestone title; must not contain forward slash (/), en dash, or em dash characters" }),
478
479
  vision: Type.String({ description: "Milestone vision" }),
479
480
  slices: Type.Array(Type.Object({
480
481
  sliceId: Type.String({ description: "Slice ID (e.g. S01)" }),
481
- title: Type.String({ description: "Slice title" }),
482
+ title: Type.String({ description: "Slice title; must not contain forward slash (/), en dash, or em dash characters" }),
482
483
  risk: Type.String({ description: "Slice risk" }),
483
484
  depends: Type.Array(Type.String(), { description: "Slice dependency IDs" }),
484
485
  demo: Type.String({ description: "Roadmap demo text / After this" }),
@@ -546,10 +547,10 @@ export function registerDbTools(pi) {
546
547
  title: Type.String({ description: "Task title" }),
547
548
  description: Type.String({ description: "Task description / steps block" }),
548
549
  estimate: Type.String({ description: "Task estimate string" }),
549
- files: Type.Array(Type.String(), { description: "Files likely touched" }),
550
+ files: Type.Array(Type.String(), { description: "Array<string> of files likely touched; pass [\"path\"] or [], never a single string" }),
550
551
  verify: Type.String({ description: "Verification command or block" }),
551
- inputs: Type.Array(Type.String(), { description: "Input files or references" }),
552
- expectedOutput: Type.Array(Type.String(), { description: "Expected output files or artifacts" }),
552
+ inputs: Type.Array(Type.String(), { description: "Array<string> of input files or references; pass [\"path\"] or [], never a single string" }),
553
+ expectedOutput: Type.Array(Type.String(), { description: "Array<string> of expected output files or artifacts; pass [\"path\"] or [], never a single string" }),
553
554
  observabilityImpact: Type.Optional(Type.String({ description: "Task observability impact" })),
554
555
  }), { description: "Planned tasks for the slice" }),
555
556
  // ── Enrichment metadata (optional — defaults to empty) ────────────
@@ -622,10 +623,10 @@ export function registerDbTools(pi) {
622
623
  title: Type.String({ description: "Task title" }),
623
624
  description: Type.String({ description: "Task description / steps block" }),
624
625
  estimate: Type.String({ description: "Task estimate string" }),
625
- files: Type.Array(Type.String(), { description: "Files likely touched" }),
626
+ files: Type.Array(Type.String(), { description: "Array<string> of files likely touched; pass [\"path\"] or [], never a single string" }),
626
627
  verify: Type.String({ description: "Verification command or block" }),
627
- inputs: Type.Array(Type.String(), { description: "Input files or references" }),
628
- expectedOutput: Type.Array(Type.String(), { description: "Expected output files or artifacts" }),
628
+ inputs: Type.Array(Type.String(), { description: "Array<string> of input files or references; pass [\"path\"] or [], never a single string" }),
629
+ expectedOutput: Type.Array(Type.String(), { description: "Array<string> of expected output files or artifacts; pass [\"path\"] or [], never a single string" }),
629
630
  observabilityImpact: Type.Optional(Type.String({ description: "Task observability impact" })),
630
631
  // Single-writer v3 audit trail (Stream 2): caller-provided actor identity + causation.
631
632
  actorName: Type.Optional(Type.String({ description: "Caller-provided actor identity for the audit trail (e.g. 'executor-01', 'gsd-orchestrator')" })),
@@ -4,8 +4,11 @@ export function extractSubagentAgentClasses(input) {
4
4
  const agentClasses = [];
5
5
  const visited = new WeakSet();
6
6
  const addAgentClass = (value) => {
7
- if (typeof value === "string" && value.trim().length > 0)
8
- agentClasses.push(value.trim());
7
+ if (typeof value !== "string")
8
+ return;
9
+ const normalized = value.trim().replace(/\.md$/i, "");
10
+ if (normalized.length > 0)
11
+ agentClasses.push(normalized);
9
12
  };
10
13
  const visitItems = (value) => {
11
14
  if (!Array.isArray(value))
@@ -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, } from "./db/auto-workers.js";
26
+ import { findStaleWorkerForProject, getAllAutoWorkers, markWorkerCrashed, } from "./db/auto-workers.js";
27
+ import { markLatestActiveForWorkerCanceled } from "./db/unit-dispatches.js";
27
28
  import { getRuntimeKv, setRuntimeKv, deleteRuntimeKv } from "./db/runtime-kv.js";
28
29
  import { _getAdapter, isDbAvailable } from "./gsd-db.js";
29
30
  import { gsdRoot, normalizeRealPath } from "./paths.js";
@@ -34,6 +35,16 @@ const SESSION_FILE_KV_KEY = "session_file";
34
35
  function lockPath(basePath) {
35
36
  return join(gsdRoot(basePath), effectiveLockFile());
36
37
  }
38
+ function clearLegacyLockFile(basePath) {
39
+ try {
40
+ const p = lockPath(basePath);
41
+ if (existsSync(p))
42
+ unlinkSync(p);
43
+ }
44
+ catch {
45
+ // Best-effort.
46
+ }
47
+ }
37
48
  function readLegacyLock(basePath) {
38
49
  try {
39
50
  const p = lockPath(basePath);
@@ -166,21 +177,36 @@ export function writeLock(basePath, unitType, unitId, sessionFile) {
166
177
  * stale session-file pointer.
167
178
  */
168
179
  export function clearLock(basePath) {
180
+ clearLegacyLockFile(basePath);
181
+ if (!isDbAvailable())
182
+ return;
169
183
  try {
170
- const p = lockPath(basePath);
171
- if (existsSync(p))
172
- unlinkSync(p);
184
+ const projectRoot = normalizeRealPath(basePath);
185
+ const worker = findActiveWorkerForCurrentProcess(projectRoot);
186
+ if (!worker)
187
+ return;
188
+ deleteRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY);
173
189
  }
174
190
  catch {
175
191
  // Best-effort.
176
192
  }
193
+ }
194
+ /**
195
+ * Clear a stale DB-backed worker lock after readCrashLock/findStaleWorkerForProject
196
+ * has identified a dead worker. Unlike clearLock(), this targets the stale
197
+ * worker row instead of the current process's active worker.
198
+ */
199
+ export function clearStaleWorkerLock(basePath) {
200
+ clearLegacyLockFile(basePath);
177
201
  if (!isDbAvailable())
178
202
  return;
179
203
  try {
180
204
  const projectRoot = normalizeRealPath(basePath);
181
- const worker = findActiveWorkerForCurrentProcess(projectRoot);
205
+ const worker = findStaleWorkerForProject(projectRoot);
182
206
  if (!worker)
183
207
  return;
208
+ markLatestActiveForWorkerCanceled(worker.worker_id, "crash-recovered");
209
+ markWorkerCrashed(worker.worker_id);
184
210
  deleteRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY);
185
211
  }
186
212
  catch {
@@ -395,6 +395,7 @@ export function getRecentUnitKeysForProjectRoot(projectRootRealpath, limit = 20)
395
395
  FROM unit_dispatches ud
396
396
  INNER JOIN workers w ON w.worker_id = ud.worker_id
397
397
  WHERE w.project_root_realpath = :project_root_realpath
398
+ AND w.status != 'crashed'
398
399
  ORDER BY ud.started_at DESC, ud.id DESC
399
400
  LIMIT :limit`).all({
400
401
  ":project_root_realpath": projectRootRealpath,
@@ -4,7 +4,7 @@ import { findMilestoneIds } from "./guided-flow.js";
4
4
  import { parseUnitId } from "./unit-id.js";
5
5
  import { isDbAvailable, getMilestoneSlices, getMilestone } from "./gsd-db.js";
6
6
  import { parseRoadmap } from "./parsers-legacy.js";
7
- import { isClosedStatus } from "./status-guards.js";
7
+ import { isClosedStatus, isSkippedForDispatch } from "./status-guards.js";
8
8
  import { classifyMilestoneSummaryContent } from "./milestone-summary-classifier.js";
9
9
  import { readFileSync } from "node:fs";
10
10
  const SLICE_DISPATCH_TYPES = new Set([
@@ -49,7 +49,7 @@ export function getPriorSliceCompletionBlocker(base, _mainBranch, unitType, unit
49
49
  // DB-backed projects must not treat SUMMARY.md as authoritative.
50
50
  if (isDbAvailable()) {
51
51
  const milestoneRow = getMilestone(mid);
52
- if (milestoneRow && isClosedStatus(milestoneRow.status))
52
+ if (milestoneRow && isSkippedForDispatch(milestoneRow.status))
53
53
  continue;
54
54
  }
55
55
  else {