gsd-pi 2.52.0 → 2.53.0-dev.a67436f

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 (247) hide show
  1. package/README.md +55 -32
  2. package/dist/headless-query.js +1 -1
  3. package/dist/headless-ui.d.ts +2 -2
  4. package/dist/headless-ui.js +18 -15
  5. package/dist/headless.d.ts +11 -0
  6. package/dist/headless.js +178 -38
  7. package/dist/resources/extensions/get-secrets-from-user.js +7 -0
  8. package/dist/resources/extensions/gsd/auto/phases.js +28 -8
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +5 -1
  10. package/dist/resources/extensions/gsd/auto-worktree.js +70 -14
  11. package/dist/resources/extensions/gsd/auto.js +22 -0
  12. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +4 -10
  13. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +3 -3
  14. package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -2
  15. package/dist/resources/extensions/gsd/git-service.js +4 -3
  16. package/dist/resources/extensions/gsd/guided-flow.js +4 -3
  17. package/dist/resources/extensions/gsd/markdown-renderer.js +5 -4
  18. package/dist/resources/extensions/gsd/parallel-orchestrator.js +18 -2
  19. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  20. package/dist/resources/extensions/gsd/state.js +18 -29
  21. package/dist/resources/extensions/gsd/status-guards.js +12 -0
  22. package/dist/resources/extensions/gsd/tools/complete-milestone.js +4 -3
  23. package/dist/resources/extensions/gsd/tools/complete-slice.js +4 -3
  24. package/dist/resources/extensions/gsd/tools/complete-task.js +4 -3
  25. package/dist/resources/extensions/gsd/tools/plan-milestone.js +4 -14
  26. package/dist/resources/extensions/gsd/tools/plan-slice.js +4 -14
  27. package/dist/resources/extensions/gsd/tools/plan-task.js +4 -14
  28. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +6 -7
  29. package/dist/resources/extensions/gsd/tools/reopen-slice.js +4 -3
  30. package/dist/resources/extensions/gsd/tools/reopen-task.js +5 -4
  31. package/dist/resources/extensions/gsd/tools/replan-slice.js +5 -6
  32. package/dist/resources/extensions/gsd/validation.js +21 -0
  33. package/dist/resources/extensions/shared/rtk.js +14 -4
  34. package/dist/rtk.js +3 -1
  35. package/dist/web/standalone/.next/BUILD_ID +1 -1
  36. package/dist/web/standalone/.next/app-path-routes-manifest.json +20 -20
  37. package/dist/web/standalone/.next/build-manifest.json +4 -4
  38. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  39. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  40. package/dist/web/standalone/.next/required-server-files.json +3 -3
  41. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  42. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  44. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  52. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  68. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  80. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  108. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  114. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  128. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  130. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  131. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  132. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  133. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  134. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  135. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  136. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  137. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  138. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  139. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  140. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  141. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  142. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  143. package/dist/web/standalone/.next/server/app/index.html +1 -1
  144. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  145. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  146. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  147. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  148. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  149. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  150. package/dist/web/standalone/.next/server/app/page.js +2 -2
  151. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  152. package/dist/web/standalone/.next/server/app-paths-manifest.json +20 -20
  153. package/dist/web/standalone/.next/server/chunks/2229.js +1 -1
  154. package/dist/web/standalone/.next/server/chunks/7471.js +3 -3
  155. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  156. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  157. package/dist/web/standalone/.next/server/middleware.js +2 -2
  158. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  159. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  160. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  161. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  162. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  163. package/dist/web/standalone/.next/static/chunks/4024.87fd909ae0110f50.js +9 -0
  164. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  165. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  166. package/dist/web/standalone/.next/static/chunks/app/page-b950e4e384cc62b3.js +1 -0
  167. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  168. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  169. package/dist/web/standalone/.next/static/chunks/{webpack-024d82be84800e52.js → webpack-bca0e732db0dcec3.js} +1 -1
  170. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  171. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  172. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  173. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  174. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  175. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  176. package/dist/web/standalone/server.js +1 -1
  177. package/package.json +1 -1
  178. package/packages/mcp-server/README.md +6 -6
  179. package/packages/mcp-server/package.json +14 -4
  180. package/packages/mcp-server/src/cli.ts +1 -1
  181. package/packages/mcp-server/src/index.ts +1 -1
  182. package/packages/mcp-server/src/mcp-server.test.ts +2 -2
  183. package/packages/mcp-server/src/session-manager.ts +2 -2
  184. package/packages/mcp-server/src/types.ts +1 -1
  185. package/packages/pi-coding-agent/package.json +1 -1
  186. package/packages/rpc-client/README.md +125 -0
  187. package/packages/rpc-client/examples/basic-usage.ts +13 -0
  188. package/packages/rpc-client/package.json +17 -3
  189. package/packages/rpc-client/src/index.ts +10 -0
  190. package/packages/rpc-client/src/jsonl.ts +64 -0
  191. package/packages/rpc-client/src/rpc-client.test.ts +568 -0
  192. package/packages/rpc-client/src/rpc-client.ts +666 -0
  193. package/packages/rpc-client/src/rpc-types.ts +399 -0
  194. package/packages/rpc-client/tsconfig.examples.json +17 -0
  195. package/packages/rpc-client/tsconfig.json +24 -0
  196. package/pkg/package.json +1 -1
  197. package/scripts/ensure-workspace-builds.cjs +36 -8
  198. package/src/resources/extensions/get-secrets-from-user.ts +8 -0
  199. package/src/resources/extensions/gsd/auto/phases.ts +38 -7
  200. package/src/resources/extensions/gsd/auto-dispatch.ts +6 -1
  201. package/src/resources/extensions/gsd/auto-worktree.ts +73 -14
  202. package/src/resources/extensions/gsd/auto.ts +21 -0
  203. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +4 -11
  204. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +3 -3
  205. package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -2
  206. package/src/resources/extensions/gsd/git-service.ts +4 -3
  207. package/src/resources/extensions/gsd/guided-flow.ts +4 -3
  208. package/src/resources/extensions/gsd/markdown-renderer.ts +5 -4
  209. package/src/resources/extensions/gsd/parallel-orchestrator.ts +23 -1
  210. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  211. package/src/resources/extensions/gsd/state.ts +18 -29
  212. package/src/resources/extensions/gsd/status-guards.ts +13 -0
  213. package/src/resources/extensions/gsd/tests/active-milestone-id-guard.test.ts +91 -0
  214. package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +87 -0
  215. package/src/resources/extensions/gsd/tests/auto-worktree-auto-resolve.test.ts +80 -0
  216. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +1 -1
  217. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +39 -0
  218. package/src/resources/extensions/gsd/tests/git-service.test.ts +64 -30
  219. package/src/resources/extensions/gsd/tests/milestone-report-path.test.ts +51 -0
  220. package/src/resources/extensions/gsd/tests/parallel-orchestrator-zombie-cleanup.test.ts +277 -0
  221. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +103 -0
  222. package/src/resources/extensions/gsd/tests/preferences.test.ts +1 -1
  223. package/src/resources/extensions/gsd/tests/rate-limit-model-fallback.test.ts +90 -0
  224. package/src/resources/extensions/gsd/tests/session-lock-transient-read.test.ts +9 -8
  225. package/src/resources/extensions/gsd/tests/stash-pop-gsd-conflict.test.ts +125 -0
  226. package/src/resources/extensions/gsd/tests/status-guards.test.ts +30 -0
  227. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +12 -2
  228. package/src/resources/extensions/gsd/tests/validation-gate-patterns.test.ts +124 -0
  229. package/src/resources/extensions/gsd/tests/validation.test.ts +72 -0
  230. package/src/resources/extensions/gsd/tools/complete-milestone.ts +4 -3
  231. package/src/resources/extensions/gsd/tools/complete-slice.ts +4 -3
  232. package/src/resources/extensions/gsd/tools/complete-task.ts +4 -3
  233. package/src/resources/extensions/gsd/tools/plan-milestone.ts +4 -16
  234. package/src/resources/extensions/gsd/tools/plan-slice.ts +4 -16
  235. package/src/resources/extensions/gsd/tools/plan-task.ts +4 -16
  236. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +6 -7
  237. package/src/resources/extensions/gsd/tools/reopen-slice.ts +4 -3
  238. package/src/resources/extensions/gsd/tools/reopen-task.ts +5 -4
  239. package/src/resources/extensions/gsd/tools/replan-slice.ts +5 -7
  240. package/src/resources/extensions/gsd/validation.ts +23 -0
  241. package/src/resources/extensions/shared/rtk.ts +22 -4
  242. package/dist/web/standalone/.next/static/chunks/4024.21054f459af5cc78.js +0 -9
  243. package/dist/web/standalone/.next/static/chunks/app/page-fbecd1237e2d6d1f.js +0 -1
  244. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  245. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  246. /package/dist/web/standalone/.next/static/{vlgS2rkXjxeKhgXhdp4lh → YO-PWFRitlHM-L-dotlmm}/_buildManifest.js +0 -0
  247. /package/dist/web/standalone/.next/static/{vlgS2rkXjxeKhgXhdp4lh → YO-PWFRitlHM-L-dotlmm}/_ssgManifest.js +0 -0
@@ -15,12 +15,20 @@ import { PROJECT_FILES } from "../detection.js";
15
15
  import { MergeConflictError } from "../git-service.js";
16
16
  import { join } from "node:path";
17
17
  import { existsSync, cpSync } from "node:fs";
18
- import { logWarning } from "../workflow-logger.js";
18
+ import { logWarning, logError } from "../workflow-logger.js";
19
19
  import { gsdRoot } from "../paths.js";
20
20
  import { atomicWriteSync } from "../atomic-write.js";
21
21
  import { verifyExpectedArtifact } from "../auto-recovery.js";
22
22
  import { writeUnitRuntimeRecord } from "../unit-runtime.js";
23
23
  // ─── generateMilestoneReport ──────────────────────────────────────────────────
24
+ /**
25
+ * Resolve the base path for milestone reports.
26
+ * Prefers originalBasePath (project root) over basePath (which may be a worktree).
27
+ * Exported for testing as _resolveReportBasePath.
28
+ */
29
+ export function _resolveReportBasePath(s) {
30
+ return s.originalBasePath || s.basePath;
31
+ }
24
32
  /**
25
33
  * Generate and write an HTML milestone report snapshot.
26
34
  * Extracted from the milestone-transition block in autoLoop.
@@ -30,18 +38,19 @@ async function generateMilestoneReport(s, ctx, milestoneId) {
30
38
  const { generateHtmlReport } = await importExtensionModule(import.meta.url, "../export-html.js");
31
39
  const { writeReportSnapshot } = await importExtensionModule(import.meta.url, "../reports.js");
32
40
  const { basename } = await import("node:path");
33
- const snapData = await loadVisualizerData(s.basePath);
41
+ const reportBasePath = _resolveReportBasePath(s);
42
+ const snapData = await loadVisualizerData(reportBasePath);
34
43
  const completedMs = snapData.milestones.find((m) => m.id === milestoneId);
35
44
  const msTitle = completedMs?.title ?? milestoneId;
36
45
  const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
37
- const projName = basename(s.basePath);
46
+ const projName = basename(reportBasePath);
38
47
  const doneSlices = snapData.milestones.reduce((acc, m) => acc + m.slices.filter((sl) => sl.done).length, 0);
39
48
  const totalSlices = snapData.milestones.reduce((acc, m) => acc + m.slices.length, 0);
40
49
  const outPath = writeReportSnapshot({
41
- basePath: s.basePath,
50
+ basePath: reportBasePath,
42
51
  html: generateHtmlReport(snapData, {
43
52
  projectName: projName,
44
- projectPath: s.basePath,
53
+ projectPath: reportBasePath,
45
54
  gsdVersion,
46
55
  milestoneId,
47
56
  indexRelPath: "index.html",
@@ -50,7 +59,7 @@ async function generateMilestoneReport(s, ctx, milestoneId) {
50
59
  milestoneTitle: msTitle,
51
60
  kind: "milestone",
52
61
  projectName: projName,
53
- projectPath: s.basePath,
62
+ projectPath: reportBasePath,
54
63
  gsdVersion,
55
64
  totalCost: snapData.totals?.cost ?? 0,
56
65
  totalTokens: snapData.totals?.tokens.total ?? 0,
@@ -160,8 +169,11 @@ export async function runPreDispatch(ic, loopState) {
160
169
  await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
161
170
  return { action: "break", reason: "merge-conflict" };
162
171
  }
163
- // Non-conflict merge errors — log and continue
164
- logWarning("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
172
+ // Non-conflict merge errors — stop auto to avoid advancing with unmerged work
173
+ logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
174
+ ctx.ui.notify(`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`, "error");
175
+ await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
176
+ return { action: "break", reason: "merge-failed" };
165
177
  }
166
178
  // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
167
179
  deps.invalidateAllCaches();
@@ -228,6 +240,10 @@ export async function runPreDispatch(ic, loopState) {
228
240
  await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
229
241
  return { action: "break", reason: "merge-conflict" };
230
242
  }
243
+ logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
244
+ ctx.ui.notify(`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`, "error");
245
+ await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
246
+ return { action: "break", reason: "merge-failed" };
231
247
  }
232
248
  // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
233
249
  }
@@ -295,6 +311,10 @@ export async function runPreDispatch(ic, loopState) {
295
311
  await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
296
312
  return { action: "break", reason: "merge-conflict" };
297
313
  }
314
+ logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
315
+ ctx.ui.notify(`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`, "error");
316
+ await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
317
+ return { action: "break", reason: "merge-failed" };
298
318
  }
299
319
  // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
300
320
  }
@@ -531,8 +531,12 @@ export const DISPATCH_RULES = [
531
531
  if (validationPath) {
532
532
  const validationContent = await loadFile(validationPath);
533
533
  if (validationContent) {
534
- const hasOperationalCheck = validationContent.includes("Operational") &&
534
+ // Accept either the structured template format (table with MET/N/A)
535
+ // or prose evidence patterns the validation agent may emit.
536
+ const structuredMatch = validationContent.includes("Operational") &&
535
537
  (validationContent.includes("MET") || validationContent.includes("N/A"));
538
+ const proseMatch = /[Oo]perational[\s:][^\n]*(?:pass|verified|confirmed|met|complete|true|yes|addressed|covered|n\/a|not\s+applicable)/i.test(validationContent);
539
+ const hasOperationalCheck = structuredMatch || proseMatch;
536
540
  if (!hasOperationalCheck) {
537
541
  return {
538
542
  action: "stop",
@@ -104,6 +104,21 @@ function clearProjectRootStateFiles(basePath, milestoneId) {
104
104
  }
105
105
  }
106
106
  }
107
+ // ─── Build Artifact Auto-Resolve ─────────────────────────────────────────────
108
+ /** Patterns for machine-generated build artifacts that can be safely
109
+ * auto-resolved by accepting --theirs during merge. These files are
110
+ * regenerable and never contain meaningful manual edits. */
111
+ export const SAFE_AUTO_RESOLVE_PATTERNS = [
112
+ /\.tsbuildinfo$/,
113
+ /\.pyc$/,
114
+ /\/__pycache__\//,
115
+ /\.DS_Store$/,
116
+ /\.map$/,
117
+ ];
118
+ /** Returns true if the file path is safe to auto-resolve during merge.
119
+ * Covers `.gsd/` state files and common build artifacts. */
120
+ export const isSafeToAutoResolve = (filePath) => filePath.startsWith(".gsd/") ||
121
+ SAFE_AUTO_RESOLVE_PATTERNS.some((re) => re.test(filePath));
107
122
  // ─── Dispatch-Level Sync (project root ↔ worktree) ──────────────────────────
108
123
  /**
109
124
  * Sync milestone artifacts from project root INTO worktree before deriveState.
@@ -1191,27 +1206,27 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
1191
1206
  ? mergeResult.conflicts
1192
1207
  : nativeConflictFiles(originalBasePath_);
1193
1208
  if (conflictedFiles.length > 0) {
1194
- // Separate .gsd/ state file conflicts from real code conflicts.
1195
- // GSD state files (STATE.md, auto.lock, etc.)
1196
- // diverge between branches during normal operation always prefer the
1197
- // milestone branch version since it has the latest execution state.
1198
- const gsdConflicts = conflictedFiles.filter((f) => f.startsWith(".gsd/"));
1199
- const codeConflicts = conflictedFiles.filter((f) => !f.startsWith(".gsd/"));
1200
- // Auto-resolve .gsd/ conflicts by accepting the milestone branch version
1201
- if (gsdConflicts.length > 0) {
1202
- for (const gsdFile of gsdConflicts) {
1209
+ // Separate auto-resolvable conflicts (GSD state files + build artifacts)
1210
+ // from real code conflicts. GSD state files diverge between branches
1211
+ // during normal operation. Build artifacts are machine-generated and
1212
+ // regenerable. Both are safe to accept from the milestone branch.
1213
+ const autoResolvable = conflictedFiles.filter(isSafeToAutoResolve);
1214
+ const codeConflicts = conflictedFiles.filter((f) => !isSafeToAutoResolve(f));
1215
+ // Auto-resolve safe conflicts by accepting the milestone branch version
1216
+ if (autoResolvable.length > 0) {
1217
+ for (const safeFile of autoResolvable) {
1203
1218
  try {
1204
- nativeCheckoutTheirs(originalBasePath_, [gsdFile]);
1205
- nativeAddPaths(originalBasePath_, [gsdFile]);
1219
+ nativeCheckoutTheirs(originalBasePath_, [safeFile]);
1220
+ nativeAddPaths(originalBasePath_, [safeFile]);
1206
1221
  }
1207
1222
  catch {
1208
1223
  // If checkout --theirs fails, try removing the file from the merge
1209
1224
  // (it's a runtime file that shouldn't be committed anyway)
1210
- nativeRmForce(originalBasePath_, [gsdFile]);
1225
+ nativeRmForce(originalBasePath_, [safeFile]);
1211
1226
  }
1212
1227
  }
1213
1228
  }
1214
- // If there are still non-.gsd conflicts, escalate
1229
+ // If there are still real code conflicts, escalate
1215
1230
  if (codeConflicts.length > 0) {
1216
1231
  // Pop stash before throwing so local work is not lost (#2151).
1217
1232
  if (stashed) {
@@ -1256,7 +1271,48 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
1256
1271
  });
1257
1272
  }
1258
1273
  catch {
1259
- // Stash pop conflict is non-fatal stash entry persists for manual resolution.
1274
+ // Stash pop after squash merge can conflict on .gsd/ state files that
1275
+ // diverged between branches. Left unresolved, these UU entries block
1276
+ // every subsequent merge. Auto-resolve them the same way we handle
1277
+ // .gsd/ conflicts during the merge itself: accept HEAD (the just-committed
1278
+ // version) and drop the now-applied stash.
1279
+ const uu = nativeConflictFiles(originalBasePath_);
1280
+ const gsdUU = uu.filter((f) => f.startsWith(".gsd/"));
1281
+ const nonGsdUU = uu.filter((f) => !f.startsWith(".gsd/"));
1282
+ if (gsdUU.length > 0) {
1283
+ for (const f of gsdUU) {
1284
+ try {
1285
+ // Accept the committed (HEAD) version of the state file
1286
+ execFileSync("git", ["checkout", "HEAD", "--", f], {
1287
+ cwd: originalBasePath_,
1288
+ stdio: ["ignore", "pipe", "pipe"],
1289
+ encoding: "utf-8",
1290
+ });
1291
+ nativeAddPaths(originalBasePath_, [f]);
1292
+ }
1293
+ catch {
1294
+ // Last resort: remove the conflicted state file
1295
+ nativeRmForce(originalBasePath_, [f]);
1296
+ }
1297
+ }
1298
+ }
1299
+ if (nonGsdUU.length === 0) {
1300
+ // All conflicts were .gsd/ files — safe to drop the stash
1301
+ try {
1302
+ execFileSync("git", ["stash", "drop"], {
1303
+ cwd: originalBasePath_,
1304
+ stdio: ["ignore", "pipe", "pipe"],
1305
+ encoding: "utf-8",
1306
+ });
1307
+ }
1308
+ catch { /* stash may already be consumed */ }
1309
+ }
1310
+ else {
1311
+ // Non-.gsd conflicts remain — leave stash for manual resolution
1312
+ logWarning("reconcile", "Stash pop conflict on non-.gsd files after merge", {
1313
+ files: nonGsdUU.join(", "),
1314
+ });
1315
+ }
1260
1316
  }
1261
1317
  }
1262
1318
  // 9b. Safety check (#1792): if nothing was committed, verify the milestone
@@ -215,6 +215,12 @@ export function stopAutoRemote(projectRoot) {
215
215
  const lock = readCrashLock(projectRoot);
216
216
  if (!lock)
217
217
  return { found: false };
218
+ // Never SIGTERM ourselves — a stale lock with our own PID is not a remote
219
+ // session, it is leftover from a prior loop exit in this process. (#2730)
220
+ if (lock.pid === process.pid) {
221
+ clearLock(projectRoot);
222
+ return { found: false };
223
+ }
218
224
  if (!isLockProcessAlive(lock)) {
219
225
  // Stale lock — clean it up
220
226
  clearLock(projectRoot);
@@ -239,6 +245,10 @@ export function checkRemoteAutoSession(projectRoot) {
239
245
  const lock = readCrashLock(projectRoot);
240
246
  if (!lock)
241
247
  return { running: false };
248
+ // Our own PID is not a "remote" session — it is a stale lock left by this
249
+ // process (e.g. after step-mode exit without full cleanup). (#2730)
250
+ if (lock.pid === process.pid)
251
+ return { running: false };
242
252
  if (!isLockProcessAlive(lock)) {
243
253
  // Stale lock from a dead process — not a live remote session
244
254
  return { running: false };
@@ -321,6 +331,18 @@ function cleanupAfterLoopExit(ctx) {
321
331
  s.currentUnit = null;
322
332
  s.active = false;
323
333
  clearUnitTimeout();
334
+ // Clear crash lock and release session lock so the next `/gsd next` does
335
+ // not see a stale lock with the current PID and treat it as a "remote"
336
+ // session (which would cause it to SIGTERM itself). (#2730)
337
+ try {
338
+ if (lockBase())
339
+ clearLock(lockBase());
340
+ if (lockBase())
341
+ releaseSessionLock(lockBase());
342
+ }
343
+ catch {
344
+ /* best-effort — mirror stopAuto cleanup */
345
+ }
324
346
  ctx.ui.setStatus("gsd-auto", undefined);
325
347
  ctx.ui.setWidget("gsd-progress", undefined);
326
348
  ctx.ui.setFooter(undefined);
@@ -77,15 +77,9 @@ export async function handleAgentEnd(pi, event, ctx) {
77
77
  retryState.currentRetryModelId = undefined;
78
78
  ctx.ui.notify(`Network retries exhausted for ${currentModelId}. Attempting model fallback.`, "warning");
79
79
  }
80
- // --- Rate limit: skip model fallback, go straight to pause ---
81
- // Rate-limiting is a provider issue, not a model issue.
82
- // Switching models won't help if the provider is throttling you.
83
- if (cls.kind === "rate-limit") {
84
- await pauseTransientWithBackoff(cls, pi, ctx, errorDetail, true);
85
- return;
86
- }
87
- // --- Server/connection/stream errors: try model fallback first ---
88
- if (cls.kind === "network" || cls.kind === "server" || cls.kind === "connection" || cls.kind === "stream") {
80
+ // --- Transient errors: try model fallback first, then pause ---
81
+ // Rate limits are often per-model, so switching models can bypass them.
82
+ if (cls.kind === "rate-limit" || cls.kind === "network" || cls.kind === "server" || cls.kind === "connection" || cls.kind === "stream") {
89
83
  // Try model fallback
90
84
  const dash = getAutoDashboardData();
91
85
  if (dash.currentUnit) {
@@ -128,7 +122,7 @@ export async function handleAgentEnd(pi, event, ctx) {
128
122
  }
129
123
  // --- Transient fallback: pause with auto-resume ---
130
124
  if (isTransient(cls)) {
131
- await pauseTransientWithBackoff(cls, pi, ctx, errorDetail, false);
125
+ await pauseTransientWithBackoff(cls, pi, ctx, errorDetail, cls.kind === "rate-limit");
132
126
  return;
133
127
  }
134
128
  // --- Permanent / unknown: pause indefinitely ---
@@ -337,7 +337,7 @@ async function configureGit(ctx, prefs) {
337
337
  const gitBooleanFields = [
338
338
  { key: "auto_push", label: "Auto-push commits after committing", defaultVal: false },
339
339
  { key: "push_branches", label: "Push milestone branches to remote", defaultVal: false },
340
- { key: "snapshots", label: "Create WIP snapshot commits during long tasks", defaultVal: false },
340
+ { key: "snapshots", label: "Create WIP snapshot commits during long tasks", defaultVal: true },
341
341
  ];
342
342
  for (const field of gitBooleanFields) {
343
343
  const current = git[field.key];
@@ -361,7 +361,7 @@ async function configureGit(ctx, prefs) {
361
361
  }
362
362
  // pre_merge_check
363
363
  const currentPreMerge = git.pre_merge_check !== undefined ? String(git.pre_merge_check) : "";
364
- const preMergeChoice = await ctx.ui.select(`Pre-merge check${currentPreMerge ? ` (current: ${currentPreMerge})` : " (default: false)"}:`, ["true", "false", "auto", "(keep current)"]);
364
+ const preMergeChoice = await ctx.ui.select(`Pre-merge check${currentPreMerge ? ` (current: ${currentPreMerge})` : " (default: auto)"}:`, ["true", "false", "auto", "(keep current)"]);
365
365
  if (preMergeChoice && preMergeChoice !== "(keep current)") {
366
366
  if (preMergeChoice === "auto") {
367
367
  git.pre_merge_check = "auto";
@@ -487,7 +487,7 @@ export async function configureMode(ctx, prefs) {
487
487
  if (modeStr && modeStr !== "(keep current)") {
488
488
  if (modeStr.startsWith("solo")) {
489
489
  prefs.mode = "solo";
490
- ctx.ui.notify("Mode: solo — defaults: auto_push=true, push_branches=false, pre_merge_check=false, merge_strategy=squash, isolation=worktree, unique_milestone_ids=false", "info");
490
+ ctx.ui.notify("Mode: solo — defaults: auto_push=true, push_branches=false, pre_merge_check=auto, merge_strategy=squash, isolation=worktree, unique_milestone_ids=false", "info");
491
491
  }
492
492
  else if (modeStr.startsWith("team")) {
493
493
  prefs.mode = "team";
@@ -126,8 +126,8 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
126
126
  - `auto_push`: boolean — automatically push commits to the remote after committing. Default: `false`.
127
127
  - `push_branches`: boolean — push the milestone branch to the remote after commits. Default: `false`.
128
128
  - `remote`: string — git remote name to push to. Default: `"origin"`.
129
- - `snapshots`: boolean — create snapshot commits (WIP saves) during long-running tasks. Default: `false`.
130
- - `pre_merge_check`: boolean or `"auto"` — run pre-merge checks before merging a worktree back to the integration branch. `true` always runs, `false` never runs, `"auto"` runs when CI is detected. Default: `false`.
129
+ - `snapshots`: boolean — create snapshot commits (WIP saves) during long-running tasks. Default: `true`.
130
+ - `pre_merge_check`: boolean or `"auto"` — run pre-merge checks before merging a worktree back to the integration branch. `true` always runs, `false` never runs, `"auto"` runs when CI is detected. Default: `"auto"`.
131
131
  - `commit_type`: string — override the conventional commit type prefix. Must be one of: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `ci`, `build`, `style`. Default: inferred from diff content.
132
132
  - `main_branch`: string — the primary branch name for new git repos (e.g., `"main"`, `"master"`, `"trunk"`). Also used by `getMainBranch()` as the preferred branch when auto-detection is ambiguous. Default: `"main"`.
133
133
  - `merge_strategy`: `"squash"` or `"merge"` — controls how worktree branches are merged back. `"squash"` combines all commits into one; `"merge"` preserves individual commits. Default: `"squash"`.
@@ -446,11 +446,12 @@ export class GitServiceImpl {
446
446
  }
447
447
  /**
448
448
  * Create a snapshot ref for the given label (typically a slice branch name).
449
- * Gated on prefs.snapshots === true. Ref path: refs/gsd/snapshots/<label>/<timestamp>
449
+ * Enabled by default; opt out with prefs.snapshots === false.
450
+ * Ref path: refs/gsd/snapshots/<label>/<timestamp>
450
451
  * The ref points at HEAD, capturing the current commit before destructive operations.
451
452
  */
452
453
  createSnapshot(label) {
453
- if (this.prefs.snapshots !== true)
454
+ if (this.prefs.snapshots === false)
454
455
  return;
455
456
  const now = new Date();
456
457
  const ts = now.getFullYear().toString()
@@ -470,7 +471,7 @@ export class GitServiceImpl {
470
471
  * Stub: to be implemented in T03.
471
472
  */
472
473
  runPreMergeCheck() {
473
- if (this.prefs.pre_merge_check === false || this.prefs.pre_merge_check === undefined) {
474
+ if (this.prefs.pre_merge_check === false) {
474
475
  return { passed: true, skipped: true };
475
476
  }
476
477
  // Determine command: explicit string or auto-detect from package.json
@@ -408,8 +408,9 @@ export async function showDiscuss(ctx, pi, basePath) {
408
408
  // Invalidate caches to pick up artifacts written by a just-completed discuss/plan
409
409
  invalidateAllCaches();
410
410
  const state = await deriveState(basePath);
411
- // No active milestone check for pending milestones to discuss instead
412
- if (!state.activeMilestone) {
411
+ // No active milestone (or corrupted milestone with undefined id)
412
+ // check for pending milestones to discuss instead
413
+ if (!state.activeMilestone?.id) {
413
414
  const pendingMilestones = state.registry.filter(m => m.status === "pending");
414
415
  if (pendingMilestones.length === 0) {
415
416
  ctx.ui.notify("No active milestone. Run /gsd to create one first.", "warning");
@@ -864,7 +865,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
864
865
  }
865
866
  }
866
867
  const state = await deriveState(basePath);
867
- if (!state.activeMilestone) {
868
+ if (!state.activeMilestone?.id) {
868
869
  // Guard: if a discuss session is already in flight, don't re-inject the prompt.
869
870
  // Both /gsd and /gsd auto reach this branch when no milestone exists yet.
870
871
  // Without this guard, every subsequent /gsd call overwrites pendingAutoStart
@@ -8,6 +8,7 @@
8
8
  // Critical invariant: rendered markdown must round-trip through
9
9
  // parseRoadmap(), parsePlan(), parseSummary() in files.ts.
10
10
  import { readFileSync, existsSync, mkdirSync } from "node:fs";
11
+ import { isClosedStatus } from "./status-guards.js";
11
12
  import { join, relative } from "node:path";
12
13
  import { createRequire } from "node:module";
13
14
  import { getAllMilestones, getMilestone, getMilestoneSlices, getSliceTasks, getTask, getSlice, getArtifact, insertArtifact, getGateResults, } from "./gsd-db.js";
@@ -262,7 +263,7 @@ function renderSlicePlanMarkdown(slice, tasks, gates = []) {
262
263
  lines.push("## Tasks");
263
264
  lines.push("");
264
265
  for (const task of tasks) {
265
- const done = task.status === "done" || task.status === "complete" ? "x" : " ";
266
+ const done = isClosedStatus(task.status) ? "x" : " ";
266
267
  const estimate = task.estimate.trim() ? ` \`est:${task.estimate.trim()}\`` : "";
267
268
  lines.push(`- [${done}] **${task.id}: ${task.title || task.id}**${estimate}`);
268
269
  if (task.description.trim()) {
@@ -435,7 +436,7 @@ export async function renderPlanCheckboxes(basePath, milestoneId, sliceId) {
435
436
  // Apply checkbox patches for each task
436
437
  let updated = content;
437
438
  for (const task of tasks) {
438
- const isDone = task.status === "done" || task.status === "complete";
439
+ const isDone = isClosedStatus(task.status);
439
440
  const tid = task.id;
440
441
  if (isDone) {
441
442
  // Set [x]
@@ -660,7 +661,7 @@ export function detectStaleRenders(basePath) {
660
661
  const content = readFileSync(planPath, "utf-8");
661
662
  const parsed = parsePlan(content);
662
663
  for (const task of tasks) {
663
- const isDoneInDb = task.status === "done" || task.status === "complete";
664
+ const isDoneInDb = isClosedStatus(task.status);
664
665
  const planTask = parsed.tasks.find((t) => t.id === task.id);
665
666
  if (!planTask)
666
667
  continue;
@@ -684,7 +685,7 @@ export function detectStaleRenders(basePath) {
684
685
  }
685
686
  // Check missing task summary files
686
687
  for (const task of tasks) {
687
- if ((task.status === "done" || task.status === "complete") && task.full_summary_md) {
688
+ if (isClosedStatus(task.status) && task.full_summary_md) {
688
689
  const slicePath = resolveSlicePath(basePath, milestone.id, slice.id);
689
690
  if (slicePath) {
690
691
  const tasksDir = join(slicePath, "tasks");
@@ -131,8 +131,15 @@ function appendWorkerLog(basePath, milestoneId, chunk) {
131
131
  }
132
132
  }
133
133
  function restoreRuntimeState(basePath) {
134
- if (state?.active)
135
- return true;
134
+ if (state?.active) {
135
+ // Verify at least one worker is alive — if all are in terminal states,
136
+ // the cached state is stale and we should fall through to cleanup.
137
+ const hasLiveWorker = [...state.workers.values()].some((w) => w.state !== "error" && w.state !== "stopped");
138
+ if (hasLiveWorker)
139
+ return true;
140
+ // All workers dead — clear stale state so restoreState() can clean up.
141
+ state = null;
142
+ }
136
143
  const restored = restoreState(basePath);
137
144
  if (restored && restored.workers.length > 0) {
138
145
  const config = resolveParallelConfig(undefined);
@@ -778,6 +785,15 @@ export function refreshWorkerStatuses(basePath, options = {}) {
778
785
  for (const worker of state.workers.values()) {
779
786
  state.totalCost += worker.cost;
780
787
  }
788
+ // If all workers are in a terminal state (error/stopped), the orchestration
789
+ // is finished — deactivate and clean up so zombie workers don't persist.
790
+ const allDead = [...state.workers.values()].every((w) => w.state === "error" || w.state === "stopped");
791
+ if (allDead) {
792
+ state.active = false;
793
+ removeStateFile(basePath);
794
+ state = null;
795
+ return;
796
+ }
781
797
  // Persist updated state for crash recovery
782
798
  persistState(basePath);
783
799
  }
@@ -11,7 +11,7 @@ export const MODE_DEFAULTS = {
11
11
  git: {
12
12
  auto_push: true,
13
13
  push_branches: false,
14
- pre_merge_check: false,
14
+ pre_merge_check: "auto",
15
15
  merge_strategy: "squash",
16
16
  isolation: "none",
17
17
  },
@@ -6,6 +6,7 @@ import { parseSummary, loadFile, parseRequirementCounts, parseContextDependsOn,
6
6
  import { resolveMilestoneFile, resolveSlicePath, resolveSliceFile, resolveTaskFile, resolveTasksDir, resolveGsdRootFile, gsdRoot, } from './paths.js';
7
7
  import { findMilestoneIds } from './milestone-ids.js';
8
8
  import { loadQueueOrder, sortByQueueOrder } from './queue-order.js';
9
+ import { isClosedStatus } from './status-guards.js';
9
10
  import { nativeBatchParseGsdFiles } from './native-parser-bridge.js';
10
11
  import { join, resolve } from 'path';
11
12
  import { existsSync, readdirSync } from 'node:fs';
@@ -39,19 +40,13 @@ export function isMilestoneComplete(roadmap) {
39
40
  return roadmap.slices.length > 0 && roadmap.slices.every(s => s.done);
40
41
  }
41
42
  /**
42
- * Check whether a VALIDATION file's verdict is terminal (pass or needs-attention).
43
- * A non-terminal verdict (needs-remediation) means validation must re-run
44
- * after remediation slices are executed.
43
+ * Check whether a VALIDATION file's verdict is terminal.
44
+ * Any successfully extracted verdict (pass, needs-attention, needs-remediation,
45
+ * fail, etc.) means validation completed. Only return false when no verdict
46
+ * could be parsed — i.e. extractVerdict() returns undefined (#2769).
45
47
  */
46
48
  export function isValidationTerminal(validationContent) {
47
- const v = extractVerdict(validationContent);
48
- if (!v)
49
- return false;
50
- // 'pass' and 'needs-attention' are always terminal.
51
- // 'needs-remediation' is treated as terminal to prevent infinite loops
52
- // when no remediation slices exist in the roadmap (#832). The validation
53
- // report is preserved on disk for manual review.
54
- return v === 'pass' || v === 'needs-attention' || v === 'needs-remediation';
49
+ return extractVerdict(validationContent) != null;
55
50
  }
56
51
  const CACHE_TTL_MS = 100;
57
52
  let _stateCache = null;
@@ -203,12 +198,6 @@ function extractContextTitle(content, fallback) {
203
198
  return stripMilestonePrefix(h1.slice(2).trim()) || fallback;
204
199
  }
205
200
  // ─── DB-backed State Derivation ────────────────────────────────────────────
206
- /**
207
- * Helper: check if a DB status counts as "done" (handles K002 ambiguity).
208
- */
209
- function isStatusDone(status) {
210
- return status === 'complete' || status === 'done';
211
- }
212
201
  /**
213
202
  * Derive GSD state from the milestones/slices/tasks DB tables.
214
203
  * Flag files (PARKED, VALIDATION, CONTINUE, REPLAN, REPLAN-TRIGGER, CONTEXT-DRAFT)
@@ -292,7 +281,7 @@ export async function deriveStateFromDb(basePath) {
292
281
  parkedMilestoneIds.add(m.id);
293
282
  continue;
294
283
  }
295
- if (isStatusDone(m.status)) {
284
+ if (isClosedStatus(m.status)) {
296
285
  completeMilestoneIds.add(m.id);
297
286
  continue;
298
287
  }
@@ -304,7 +293,7 @@ export async function deriveStateFromDb(basePath) {
304
293
  }
305
294
  // Check roadmap: all slices done means milestone is complete
306
295
  const slices = getMilestoneSlices(m.id);
307
- if (slices.length > 0 && slices.every(s => isStatusDone(s.status))) {
296
+ if (slices.length > 0 && slices.every(s => isClosedStatus(s.status))) {
308
297
  // All slices done but no summary — still counts as complete for dep resolution
309
298
  // if a summary file exists
310
299
  // Note: without summary file, the milestone is in validating/completing state, not complete
@@ -323,7 +312,7 @@ export async function deriveStateFromDb(basePath) {
323
312
  }
324
313
  // Ghost milestone check: no slices in DB AND no substantive files on disk
325
314
  const slices = getMilestoneSlices(m.id);
326
- if (slices.length === 0 && !isStatusDone(m.status)) {
315
+ if (slices.length === 0 && !isClosedStatus(m.status)) {
327
316
  // Check disk for ghost detection
328
317
  if (isGhostMilestone(basePath, m.id))
329
318
  continue;
@@ -344,7 +333,7 @@ export async function deriveStateFromDb(basePath) {
344
333
  continue;
345
334
  }
346
335
  // Not complete — determine if it should be active
347
- const allSlicesDone = slices.length > 0 && slices.every(s => isStatusDone(s.status));
336
+ const allSlicesDone = slices.length > 0 && slices.every(s => isClosedStatus(s.status));
348
337
  // Get title — prefer DB, fall back to context file extraction
349
338
  let title = stripMilestonePrefix(m.title) || m.id;
350
339
  if (title === m.id) {
@@ -484,7 +473,7 @@ export async function deriveStateFromDb(basePath) {
484
473
  // Guard: [].every() === true (vacuous truth). Without the length check,
485
474
  // an empty slice array causes a premature phase transition to
486
475
  // validating-milestone. See: https://github.com/gsd-build/gsd-2/issues/2667
487
- const allSlicesDone = activeMilestoneSlices.length > 0 && activeMilestoneSlices.every(s => isStatusDone(s.status));
476
+ const allSlicesDone = activeMilestoneSlices.length > 0 && activeMilestoneSlices.every(s => isClosedStatus(s.status));
488
477
  if (allSlicesDone) {
489
478
  const validationFile = resolveMilestoneFile(basePath, activeMilestone.id, "VALIDATION");
490
479
  const validationContent = validationFile ? await loadFile(validationFile) : null;
@@ -514,14 +503,14 @@ export async function deriveStateFromDb(basePath) {
514
503
  }
515
504
  // ── Find active slice (first incomplete with deps satisfied) ─────────
516
505
  const sliceProgress = {
517
- done: activeMilestoneSlices.filter(s => isStatusDone(s.status)).length,
506
+ done: activeMilestoneSlices.filter(s => isClosedStatus(s.status)).length,
518
507
  total: activeMilestoneSlices.length,
519
508
  };
520
- const doneSliceIds = new Set(activeMilestoneSlices.filter(s => isStatusDone(s.status)).map(s => s.id));
509
+ const doneSliceIds = new Set(activeMilestoneSlices.filter(s => isClosedStatus(s.status)).map(s => s.id));
521
510
  let activeSlice = null;
522
511
  let activeSliceRow = null;
523
512
  for (const s of activeMilestoneSlices) {
524
- if (isStatusDone(s.status))
513
+ if (isClosedStatus(s.status))
525
514
  continue;
526
515
  if (s.depends.every(dep => doneSliceIds.has(dep))) {
527
516
  activeSlice = { id: s.id, title: s.title };
@@ -561,7 +550,7 @@ export async function deriveStateFromDb(basePath) {
561
550
  // causing the dispatcher to re-dispatch the same completed task forever.
562
551
  let reconciled = false;
563
552
  for (const t of tasks) {
564
- if (isStatusDone(t.status))
553
+ if (isClosedStatus(t.status))
565
554
  continue;
566
555
  const summaryPath = resolveTaskFile(basePath, activeMilestone.id, activeSlice.id, t.id, "SUMMARY");
567
556
  if (summaryPath && existsSync(summaryPath)) {
@@ -581,10 +570,10 @@ export async function deriveStateFromDb(basePath) {
581
570
  tasks = getSliceTasks(activeMilestone.id, activeSlice.id);
582
571
  }
583
572
  const taskProgress = {
584
- done: tasks.filter(t => isStatusDone(t.status)).length,
573
+ done: tasks.filter(t => isClosedStatus(t.status)).length,
585
574
  total: tasks.length,
586
575
  };
587
- const activeTaskRow = tasks.find(t => !isStatusDone(t.status));
576
+ const activeTaskRow = tasks.find(t => !isClosedStatus(t.status));
588
577
  if (!activeTaskRow && tasks.length > 0) {
589
578
  // All tasks done but slice not marked complete → summarizing
590
579
  return {
@@ -639,7 +628,7 @@ export async function deriveStateFromDb(basePath) {
639
628
  };
640
629
  }
641
630
  // ── Blocker detection: check completed tasks for blocker_discovered ──
642
- const completedTasks = tasks.filter(t => isStatusDone(t.status));
631
+ const completedTasks = tasks.filter(t => isClosedStatus(t.status));
643
632
  let blockerTaskId = null;
644
633
  for (const ct of completedTasks) {
645
634
  if (ct.blocker_discovered) {
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Status predicates for GSD state-machine guards.
3
+ *
4
+ * The DB stores status as free-form strings. Two values indicate
5
+ * "closed": "complete" (canonical) and "done" (legacy / alias).
6
+ * Every inline `status === "complete" || status === "done"` should
7
+ * use isClosedStatus() instead.
8
+ */
9
+ /** Returns true when a milestone, slice, or task status indicates closure. */
10
+ export function isClosedStatus(status) {
11
+ return status === "complete" || status === "done";
12
+ }