gsd-pi 2.51.0 → 2.52.0-dev.655ad8a

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 (419) hide show
  1. package/README.md +59 -36
  2. package/dist/headless-events.d.ts +18 -0
  3. package/dist/headless-events.js +36 -0
  4. package/dist/headless-query.js +1 -1
  5. package/dist/headless-types.d.ts +28 -0
  6. package/dist/headless-types.js +7 -0
  7. package/dist/headless.d.ts +8 -3
  8. package/dist/headless.js +47 -16
  9. package/dist/help-text.js +16 -5
  10. package/dist/onboarding.js +5 -4
  11. package/dist/remote-questions-config.js +1 -1
  12. package/dist/resources/extensions/async-jobs/async-bash-tool.js +29 -17
  13. package/dist/resources/extensions/async-jobs/job-manager.js +4 -1
  14. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +18 -19
  15. package/dist/resources/extensions/get-secrets-from-user.js +7 -0
  16. package/dist/resources/extensions/gsd/auto/phases.js +34 -8
  17. package/dist/resources/extensions/gsd/auto-dispatch.js +23 -1
  18. package/dist/resources/extensions/gsd/auto-start.js +2 -0
  19. package/dist/resources/extensions/gsd/auto-timers.js +24 -2
  20. package/dist/resources/extensions/gsd/auto-tool-tracking.js +25 -7
  21. package/dist/resources/extensions/gsd/auto-worktree.js +91 -14
  22. package/dist/resources/extensions/gsd/auto.js +30 -4
  23. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +99 -70
  24. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +12 -2
  25. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +1 -1
  26. package/dist/resources/extensions/gsd/claude-import.js +60 -9
  27. package/dist/resources/extensions/gsd/commands/handlers/auto.js +69 -6
  28. package/dist/resources/extensions/gsd/commands-config.js +10 -5
  29. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +4 -4
  30. package/dist/resources/extensions/gsd/detection.js +6 -6
  31. package/dist/resources/extensions/gsd/docs/preferences-reference.md +5 -5
  32. package/dist/resources/extensions/gsd/error-classifier.js +105 -0
  33. package/dist/resources/extensions/gsd/git-service.js +4 -3
  34. package/dist/resources/extensions/gsd/gitignore.js +7 -7
  35. package/dist/resources/extensions/gsd/gsd-db.js +298 -45
  36. package/dist/resources/extensions/gsd/guided-flow.js +4 -3
  37. package/dist/resources/extensions/gsd/init-wizard.js +2 -2
  38. package/dist/resources/extensions/gsd/key-manager.js +7 -16
  39. package/dist/resources/extensions/gsd/markdown-renderer.js +5 -4
  40. package/dist/resources/extensions/gsd/memory-store.js +28 -13
  41. package/dist/resources/extensions/gsd/milestone-actions.js +19 -0
  42. package/dist/resources/extensions/gsd/parallel-orchestrator.js +18 -2
  43. package/dist/resources/extensions/gsd/preferences-models.js +1 -13
  44. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  45. package/dist/resources/extensions/gsd/preferences.js +13 -13
  46. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  47. package/dist/resources/extensions/gsd/provider-error-pause.js +0 -44
  48. package/dist/resources/extensions/gsd/rule-registry.js +1 -1
  49. package/dist/resources/extensions/gsd/service-tier.js +13 -2
  50. package/dist/resources/extensions/gsd/state.js +38 -30
  51. package/dist/resources/extensions/gsd/status-guards.js +12 -0
  52. package/dist/resources/extensions/gsd/tools/complete-milestone.js +7 -13
  53. package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -20
  54. package/dist/resources/extensions/gsd/tools/complete-task.js +11 -21
  55. package/dist/resources/extensions/gsd/tools/plan-milestone.js +28 -29
  56. package/dist/resources/extensions/gsd/tools/plan-slice.js +27 -26
  57. package/dist/resources/extensions/gsd/tools/plan-task.js +23 -23
  58. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +50 -41
  59. package/dist/resources/extensions/gsd/tools/reopen-slice.js +4 -3
  60. package/dist/resources/extensions/gsd/tools/reopen-task.js +5 -4
  61. package/dist/resources/extensions/gsd/tools/replan-slice.js +51 -41
  62. package/dist/resources/extensions/gsd/tools/validate-milestone.js +23 -16
  63. package/dist/resources/extensions/gsd/validation.js +21 -0
  64. package/dist/resources/extensions/gsd/workflow-logger.js +0 -1
  65. package/dist/resources/extensions/remote-questions/config.js +1 -1
  66. package/dist/resources/extensions/remote-questions/remote-command.js +1 -1
  67. package/dist/resources/extensions/search-the-web/native-search.js +1 -1
  68. package/dist/resources/extensions/search-the-web/provider.js +1 -1
  69. package/dist/resources/extensions/shared/rtk.js +14 -4
  70. package/dist/rtk.js +3 -1
  71. package/dist/web/standalone/.next/BUILD_ID +1 -1
  72. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  73. package/dist/web/standalone/.next/build-manifest.json +4 -4
  74. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  75. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  76. package/dist/web/standalone/.next/required-server-files.json +3 -3
  77. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  78. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  80. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  88. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  90. package/dist/web/standalone/.next/server/app/_not-found.rsc +4 -4
  91. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +4 -4
  92. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +4 -4
  94. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  97. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  104. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  116. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  130. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  131. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  132. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  133. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  134. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  135. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  136. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  137. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  138. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  139. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  140. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  141. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  142. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  143. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  144. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  145. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  146. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  147. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  148. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  149. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  150. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  151. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  152. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  153. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  154. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  155. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  156. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  157. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  158. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  159. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  160. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  161. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  162. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  163. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  164. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  165. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  166. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  167. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  168. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  169. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  170. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  171. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  172. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  173. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  174. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  175. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  176. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  177. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  178. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  179. package/dist/web/standalone/.next/server/app/index.html +1 -1
  180. package/dist/web/standalone/.next/server/app/index.rsc +5 -5
  181. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  182. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +5 -5
  183. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  184. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +4 -4
  185. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  186. package/dist/web/standalone/.next/server/app/page.js +2 -2
  187. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  188. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  189. package/dist/web/standalone/.next/server/chunks/2229.js +3 -3
  190. package/dist/web/standalone/.next/server/chunks/7471.js +3 -3
  191. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  192. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  193. package/dist/web/standalone/.next/server/middleware.js +2 -2
  194. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  195. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  196. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  197. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  198. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  199. package/dist/web/standalone/.next/static/chunks/4024.87fd909ae0110f50.js +9 -0
  200. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  201. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  202. package/dist/web/standalone/.next/static/chunks/app/page-b950e4e384cc62b3.js +1 -0
  203. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  204. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  205. package/dist/web/standalone/.next/static/chunks/{webpack-cfc9a116e6450a6b.js → webpack-bca0e732db0dcec3.js} +1 -1
  206. package/dist/web/standalone/.next/static/css/a58ef8a151aa0493.css +1 -0
  207. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  208. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  209. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  210. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  211. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  212. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  213. package/dist/web/standalone/server.js +1 -1
  214. package/dist/wizard.js +4 -1
  215. package/package.json +2 -2
  216. package/packages/mcp-server/README.md +202 -0
  217. package/packages/mcp-server/package.json +36 -0
  218. package/packages/mcp-server/src/cli.ts +68 -0
  219. package/packages/mcp-server/src/index.ts +14 -0
  220. package/packages/mcp-server/src/mcp-server.test.ts +628 -0
  221. package/packages/mcp-server/src/server.ts +278 -0
  222. package/packages/mcp-server/src/session-manager.ts +328 -0
  223. package/packages/mcp-server/src/types.ts +107 -0
  224. package/packages/mcp-server/tsconfig.json +24 -0
  225. package/packages/pi-ai/dist/models.d.ts +14 -3
  226. package/packages/pi-ai/dist/models.d.ts.map +1 -1
  227. package/packages/pi-ai/dist/models.js +53 -10
  228. package/packages/pi-ai/dist/models.js.map +1 -1
  229. package/packages/pi-ai/dist/models.test.js +102 -1
  230. package/packages/pi-ai/dist/models.test.js.map +1 -1
  231. package/packages/pi-ai/dist/types.d.ts +30 -0
  232. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  233. package/packages/pi-ai/dist/types.js.map +1 -1
  234. package/packages/pi-ai/src/models.test.ts +114 -1
  235. package/packages/pi-ai/src/models.ts +70 -13
  236. package/packages/pi-ai/src/types.ts +31 -0
  237. package/packages/pi-coding-agent/dist/cli/args.d.ts +2 -0
  238. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  239. package/packages/pi-coding-agent/dist/cli/args.js +3 -0
  240. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  241. package/packages/pi-coding-agent/dist/core/bash-executor.d.ts.map +1 -1
  242. package/packages/pi-coding-agent/dist/core/bash-executor.js +5 -1
  243. package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  244. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  245. package/packages/pi-coding-agent/dist/core/model-registry.js +9 -4
  246. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  247. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.d.ts +19 -0
  248. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.d.ts.map +1 -0
  249. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.js +83 -0
  250. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.js.map +1 -0
  251. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  252. package/packages/pi-coding-agent/dist/core/tools/bash.js +5 -1
  253. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  254. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  255. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  256. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  257. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  258. package/packages/pi-coding-agent/dist/main.js +5 -3
  259. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  260. package/packages/pi-coding-agent/dist/modes/index.d.ts +1 -1
  261. package/packages/pi-coding-agent/dist/modes/index.d.ts.map +1 -1
  262. package/packages/pi-coding-agent/dist/modes/index.js.map +1 -1
  263. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  264. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +0 -2
  265. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  266. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts +28 -1
  267. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  268. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js +49 -0
  269. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js.map +1 -1
  270. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts +1 -1
  271. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  272. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +114 -6
  273. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  274. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.d.ts +9 -0
  275. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.d.ts.map +1 -0
  276. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.js +831 -0
  277. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.js.map +1 -0
  278. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +66 -0
  279. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  280. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
  281. package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  282. package/packages/pi-coding-agent/dist/utils/shell.js +0 -1
  283. package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
  284. package/packages/pi-coding-agent/package.json +1 -1
  285. package/packages/pi-coding-agent/src/cli/args.ts +4 -0
  286. package/packages/pi-coding-agent/src/core/bash-executor.ts +5 -1
  287. package/packages/pi-coding-agent/src/core/model-registry.ts +10 -3
  288. package/packages/pi-coding-agent/src/core/tools/bash-spawn-windows.test.ts +101 -0
  289. package/packages/pi-coding-agent/src/core/tools/bash.ts +5 -1
  290. package/packages/pi-coding-agent/src/index.ts +3 -0
  291. package/packages/pi-coding-agent/src/main.ts +5 -3
  292. package/packages/pi-coding-agent/src/modes/index.ts +8 -1
  293. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +0 -2
  294. package/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts +54 -1
  295. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +124 -6
  296. package/packages/pi-coding-agent/src/modes/rpc/rpc-protocol-v2.test.ts +971 -0
  297. package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +61 -4
  298. package/packages/pi-coding-agent/src/utils/shell.ts +0 -1
  299. package/packages/rpc-client/package.json +20 -0
  300. package/pkg/package.json +1 -1
  301. package/scripts/ensure-workspace-builds.cjs +36 -8
  302. package/src/resources/extensions/async-jobs/async-bash-tool.ts +22 -11
  303. package/src/resources/extensions/async-jobs/job-manager.ts +4 -1
  304. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +19 -20
  305. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +21 -0
  306. package/src/resources/extensions/get-secrets-from-user.ts +8 -0
  307. package/src/resources/extensions/gsd/auto/phases.ts +44 -7
  308. package/src/resources/extensions/gsd/auto-dispatch.ts +25 -1
  309. package/src/resources/extensions/gsd/auto-start.ts +2 -0
  310. package/src/resources/extensions/gsd/auto-timers.ts +25 -1
  311. package/src/resources/extensions/gsd/auto-tool-tracking.ts +30 -6
  312. package/src/resources/extensions/gsd/auto-worktree.ts +94 -14
  313. package/src/resources/extensions/gsd/auto.ts +31 -4
  314. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +118 -73
  315. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +11 -2
  316. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +1 -1
  317. package/src/resources/extensions/gsd/claude-import.ts +58 -9
  318. package/src/resources/extensions/gsd/commands/handlers/auto.ts +73 -6
  319. package/src/resources/extensions/gsd/commands-config.ts +11 -5
  320. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +4 -4
  321. package/src/resources/extensions/gsd/detection.ts +6 -6
  322. package/src/resources/extensions/gsd/docs/preferences-reference.md +5 -5
  323. package/src/resources/extensions/gsd/error-classifier.ts +139 -0
  324. package/src/resources/extensions/gsd/git-service.ts +4 -3
  325. package/src/resources/extensions/gsd/gitignore.ts +7 -7
  326. package/src/resources/extensions/gsd/gsd-db.ts +355 -63
  327. package/src/resources/extensions/gsd/guided-flow.ts +4 -3
  328. package/src/resources/extensions/gsd/init-wizard.ts +2 -2
  329. package/src/resources/extensions/gsd/key-manager.ts +7 -16
  330. package/src/resources/extensions/gsd/markdown-renderer.ts +5 -4
  331. package/src/resources/extensions/gsd/memory-store.ts +29 -18
  332. package/src/resources/extensions/gsd/milestone-actions.ts +17 -0
  333. package/src/resources/extensions/gsd/parallel-orchestrator.ts +23 -1
  334. package/src/resources/extensions/gsd/preferences-models.ts +1 -13
  335. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  336. package/src/resources/extensions/gsd/preferences.ts +12 -13
  337. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  338. package/src/resources/extensions/gsd/provider-error-pause.ts +0 -57
  339. package/src/resources/extensions/gsd/rule-registry.ts +1 -1
  340. package/src/resources/extensions/gsd/service-tier.ts +14 -2
  341. package/src/resources/extensions/gsd/state.ts +39 -30
  342. package/src/resources/extensions/gsd/status-guards.ts +13 -0
  343. package/src/resources/extensions/gsd/tests/active-milestone-id-guard.test.ts +91 -0
  344. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +1 -1
  345. package/src/resources/extensions/gsd/tests/auto-milestone-target.test.ts +61 -0
  346. package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +87 -0
  347. package/src/resources/extensions/gsd/tests/auto-worktree-auto-resolve.test.ts +80 -0
  348. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +1 -1
  349. package/src/resources/extensions/gsd/tests/claude-import-marketplace-discovery.test.ts +191 -0
  350. package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +1 -1
  351. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +39 -0
  352. package/src/resources/extensions/gsd/tests/commands-config.test.ts +24 -0
  353. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  354. package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +106 -0
  355. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  356. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +35 -7
  357. package/src/resources/extensions/gsd/tests/detection.test.ts +1 -1
  358. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +4 -4
  359. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +1 -1
  360. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +2 -2
  361. package/src/resources/extensions/gsd/tests/empty-db-reconciliation.test.ts +79 -0
  362. package/src/resources/extensions/gsd/tests/git-service.test.ts +65 -31
  363. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
  364. package/src/resources/extensions/gsd/tests/idle-watchdog-stall-override.test.ts +125 -0
  365. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +1 -1
  366. package/src/resources/extensions/gsd/tests/interactive-tool-idle-exemption.test.ts +119 -0
  367. package/src/resources/extensions/gsd/tests/key-manager.test.ts +16 -1
  368. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  369. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  370. package/src/resources/extensions/gsd/tests/milestone-report-path.test.ts +51 -0
  371. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +7 -7
  372. package/src/resources/extensions/gsd/tests/parallel-orchestrator-zombie-cleanup.test.ts +277 -0
  373. package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +85 -0
  374. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +103 -0
  375. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +91 -0
  376. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -2
  377. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +77 -70
  378. package/src/resources/extensions/gsd/tests/rate-limit-model-fallback.test.ts +90 -0
  379. package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +110 -0
  380. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +29 -0
  381. package/src/resources/extensions/gsd/tests/session-lock-transient-read.test.ts +9 -8
  382. package/src/resources/extensions/gsd/tests/stash-pop-gsd-conflict.test.ts +125 -0
  383. package/src/resources/extensions/gsd/tests/status-guards.test.ts +30 -0
  384. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +42 -31
  385. package/src/resources/extensions/gsd/tests/token-cost-display.test.ts +2 -2
  386. package/src/resources/extensions/gsd/tests/vacuous-truth-slices.test.ts +115 -0
  387. package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +90 -0
  388. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +12 -2
  389. package/src/resources/extensions/gsd/tests/validation-gate-patterns.test.ts +124 -0
  390. package/src/resources/extensions/gsd/tests/validation.test.ts +72 -0
  391. package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +81 -1
  392. package/src/resources/extensions/gsd/tests/worktree-preferences-sync.test.ts +130 -0
  393. package/src/resources/extensions/gsd/tools/complete-milestone.ts +7 -17
  394. package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -24
  395. package/src/resources/extensions/gsd/tools/complete-task.ts +13 -25
  396. package/src/resources/extensions/gsd/tools/plan-milestone.ts +30 -32
  397. package/src/resources/extensions/gsd/tools/plan-slice.ts +30 -30
  398. package/src/resources/extensions/gsd/tools/plan-task.ts +26 -26
  399. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +57 -46
  400. package/src/resources/extensions/gsd/tools/reopen-slice.ts +4 -3
  401. package/src/resources/extensions/gsd/tools/reopen-task.ts +5 -4
  402. package/src/resources/extensions/gsd/tools/replan-slice.ts +55 -44
  403. package/src/resources/extensions/gsd/tools/validate-milestone.ts +26 -20
  404. package/src/resources/extensions/gsd/validation.ts +23 -0
  405. package/src/resources/extensions/gsd/workflow-logger.ts +0 -1
  406. package/src/resources/extensions/remote-questions/config.ts +1 -1
  407. package/src/resources/extensions/remote-questions/remote-command.ts +1 -1
  408. package/src/resources/extensions/search-the-web/native-search.ts +1 -1
  409. package/src/resources/extensions/search-the-web/provider.ts +1 -1
  410. package/src/resources/extensions/shared/rtk.ts +22 -4
  411. package/dist/web/standalone/.next/static/chunks/4024.9ad5def014d90ce4.js +0 -9
  412. package/dist/web/standalone/.next/static/chunks/app/page-fbecd1237e2d6d1f.js +0 -1
  413. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  414. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  415. package/dist/web/standalone/.next/static/css/de141508b083f922.css +0 -1
  416. /package/dist/resources/extensions/gsd/templates/{preferences.md → PREFERENCES.md} +0 -0
  417. /package/dist/web/standalone/.next/static/{vkr67v-utm1dgZnbrBWQh → zpvUPKoW5jRAMB_fWHlPi}/_buildManifest.js +0 -0
  418. /package/dist/web/standalone/.next/static/{vkr67v-utm1dgZnbrBWQh → zpvUPKoW5jRAMB_fWHlPi}/_ssgManifest.js +0 -0
  419. /package/src/resources/extensions/gsd/templates/{preferences.md → PREFERENCES.md} +0 -0
@@ -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;
@@ -144,7 +139,23 @@ export async function deriveState(basePath) {
144
139
  let result;
145
140
  // Dual-path: try DB-backed derivation first when hierarchy tables are populated
146
141
  if (isDbAvailable()) {
147
- const dbMilestones = getAllMilestones();
142
+ let dbMilestones = getAllMilestones();
143
+ // Disk→DB reconciliation (#2631): when the milestones table is empty
144
+ // (e.g. failed initial migration per #2529), the reconciliation code
145
+ // inside deriveStateFromDb is unreachable. Populate from disk here so
146
+ // the DB path activates correctly.
147
+ if (dbMilestones.length === 0) {
148
+ const diskIds = findMilestoneIds(basePath);
149
+ let synced = false;
150
+ for (const diskId of diskIds) {
151
+ if (!isGhostMilestone(basePath, diskId)) {
152
+ insertMilestone({ id: diskId, status: 'active' });
153
+ synced = true;
154
+ }
155
+ }
156
+ if (synced)
157
+ dbMilestones = getAllMilestones();
158
+ }
148
159
  if (dbMilestones.length > 0) {
149
160
  const stopDbTimer = debugTime("derive-state-db");
150
161
  result = await deriveStateFromDb(basePath);
@@ -187,12 +198,6 @@ function extractContextTitle(content, fallback) {
187
198
  return stripMilestonePrefix(h1.slice(2).trim()) || fallback;
188
199
  }
189
200
  // ─── DB-backed State Derivation ────────────────────────────────────────────
190
- /**
191
- * Helper: check if a DB status counts as "done" (handles K002 ambiguity).
192
- */
193
- function isStatusDone(status) {
194
- return status === 'complete' || status === 'done';
195
- }
196
201
  /**
197
202
  * Derive GSD state from the milestones/slices/tasks DB tables.
198
203
  * Flag files (PARKED, VALIDATION, CONTINUE, REPLAN, REPLAN-TRIGGER, CONTEXT-DRAFT)
@@ -276,7 +281,7 @@ export async function deriveStateFromDb(basePath) {
276
281
  parkedMilestoneIds.add(m.id);
277
282
  continue;
278
283
  }
279
- if (isStatusDone(m.status)) {
284
+ if (isClosedStatus(m.status)) {
280
285
  completeMilestoneIds.add(m.id);
281
286
  continue;
282
287
  }
@@ -288,7 +293,7 @@ export async function deriveStateFromDb(basePath) {
288
293
  }
289
294
  // Check roadmap: all slices done means milestone is complete
290
295
  const slices = getMilestoneSlices(m.id);
291
- if (slices.length > 0 && slices.every(s => isStatusDone(s.status))) {
296
+ if (slices.length > 0 && slices.every(s => isClosedStatus(s.status))) {
292
297
  // All slices done but no summary — still counts as complete for dep resolution
293
298
  // if a summary file exists
294
299
  // Note: without summary file, the milestone is in validating/completing state, not complete
@@ -307,7 +312,7 @@ export async function deriveStateFromDb(basePath) {
307
312
  }
308
313
  // Ghost milestone check: no slices in DB AND no substantive files on disk
309
314
  const slices = getMilestoneSlices(m.id);
310
- if (slices.length === 0 && !isStatusDone(m.status)) {
315
+ if (slices.length === 0 && !isClosedStatus(m.status)) {
311
316
  // Check disk for ghost detection
312
317
  if (isGhostMilestone(basePath, m.id))
313
318
  continue;
@@ -328,7 +333,7 @@ export async function deriveStateFromDb(basePath) {
328
333
  continue;
329
334
  }
330
335
  // Not complete — determine if it should be active
331
- const allSlicesDone = slices.length > 0 && slices.every(s => isStatusDone(s.status));
336
+ const allSlicesDone = slices.length > 0 && slices.every(s => isClosedStatus(s.status));
332
337
  // Get title — prefer DB, fall back to context file extraction
333
338
  let title = stripMilestonePrefix(m.title) || m.id;
334
339
  if (title === m.id) {
@@ -465,7 +470,10 @@ export async function deriveStateFromDb(basePath) {
465
470
  };
466
471
  }
467
472
  // ── All slices done → validating/completing ─────────────────────────
468
- const allSlicesDone = activeMilestoneSlices.every(s => isStatusDone(s.status));
473
+ // Guard: [].every() === true (vacuous truth). Without the length check,
474
+ // an empty slice array causes a premature phase transition to
475
+ // validating-milestone. See: https://github.com/gsd-build/gsd-2/issues/2667
476
+ const allSlicesDone = activeMilestoneSlices.length > 0 && activeMilestoneSlices.every(s => isClosedStatus(s.status));
469
477
  if (allSlicesDone) {
470
478
  const validationFile = resolveMilestoneFile(basePath, activeMilestone.id, "VALIDATION");
471
479
  const validationContent = validationFile ? await loadFile(validationFile) : null;
@@ -495,14 +503,14 @@ export async function deriveStateFromDb(basePath) {
495
503
  }
496
504
  // ── Find active slice (first incomplete with deps satisfied) ─────────
497
505
  const sliceProgress = {
498
- done: activeMilestoneSlices.filter(s => isStatusDone(s.status)).length,
506
+ done: activeMilestoneSlices.filter(s => isClosedStatus(s.status)).length,
499
507
  total: activeMilestoneSlices.length,
500
508
  };
501
- 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));
502
510
  let activeSlice = null;
503
511
  let activeSliceRow = null;
504
512
  for (const s of activeMilestoneSlices) {
505
- if (isStatusDone(s.status))
513
+ if (isClosedStatus(s.status))
506
514
  continue;
507
515
  if (s.depends.every(dep => doneSliceIds.has(dep))) {
508
516
  activeSlice = { id: s.id, title: s.title };
@@ -542,7 +550,7 @@ export async function deriveStateFromDb(basePath) {
542
550
  // causing the dispatcher to re-dispatch the same completed task forever.
543
551
  let reconciled = false;
544
552
  for (const t of tasks) {
545
- if (isStatusDone(t.status))
553
+ if (isClosedStatus(t.status))
546
554
  continue;
547
555
  const summaryPath = resolveTaskFile(basePath, activeMilestone.id, activeSlice.id, t.id, "SUMMARY");
548
556
  if (summaryPath && existsSync(summaryPath)) {
@@ -562,10 +570,10 @@ export async function deriveStateFromDb(basePath) {
562
570
  tasks = getSliceTasks(activeMilestone.id, activeSlice.id);
563
571
  }
564
572
  const taskProgress = {
565
- done: tasks.filter(t => isStatusDone(t.status)).length,
573
+ done: tasks.filter(t => isClosedStatus(t.status)).length,
566
574
  total: tasks.length,
567
575
  };
568
- const activeTaskRow = tasks.find(t => !isStatusDone(t.status));
576
+ const activeTaskRow = tasks.find(t => !isClosedStatus(t.status));
569
577
  if (!activeTaskRow && tasks.length > 0) {
570
578
  // All tasks done but slice not marked complete → summarizing
571
579
  return {
@@ -620,7 +628,7 @@ export async function deriveStateFromDb(basePath) {
620
628
  };
621
629
  }
622
630
  // ── Blocker detection: check completed tasks for blocker_discovered ──
623
- const completedTasks = tasks.filter(t => isStatusDone(t.status));
631
+ const completedTasks = tasks.filter(t => isClosedStatus(t.status));
624
632
  let blockerTaskId = null;
625
633
  for (const ct of completedTasks) {
626
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
+ }
@@ -7,8 +7,9 @@
7
7
  */
8
8
  import { join } from "node:path";
9
9
  import { mkdirSync } from "node:fs";
10
- import { transaction, getMilestone, getMilestoneSlices, getSliceTasks, _getAdapter, } from "../gsd-db.js";
10
+ import { transaction, getMilestone, getMilestoneSlices, getSliceTasks, updateMilestoneStatus, } from "../gsd-db.js";
11
11
  import { resolveMilestonePath, clearPathCache } from "../paths.js";
12
+ import { isClosedStatus } from "../status-guards.js";
12
13
  import { saveFile, clearParseCache } from "../files.js";
13
14
  import { invalidateStateCache } from "../state.js";
14
15
  import { renderAllProjections } from "../workflow-projections.js";
@@ -89,7 +90,7 @@ export async function handleCompleteMilestone(params, basePath) {
89
90
  guardError = `milestone not found: ${params.milestoneId}`;
90
91
  return;
91
92
  }
92
- if (milestone.status === "complete" || milestone.status === "done") {
93
+ if (isClosedStatus(milestone.status)) {
93
94
  guardError = `milestone ${params.milestoneId} is already complete`;
94
95
  return;
95
96
  }
@@ -99,7 +100,7 @@ export async function handleCompleteMilestone(params, basePath) {
99
100
  guardError = `no slices found for milestone ${params.milestoneId}`;
100
101
  return;
101
102
  }
102
- const incompleteSlices = slices.filter(s => s.status !== "complete" && s.status !== "done");
103
+ const incompleteSlices = slices.filter(s => !isClosedStatus(s.status));
103
104
  if (incompleteSlices.length > 0) {
104
105
  const incompleteIds = incompleteSlices.map(s => `${s.id} (status: ${s.status})`).join(", ");
105
106
  guardError = `incomplete slices: ${incompleteIds}`;
@@ -108,7 +109,7 @@ export async function handleCompleteMilestone(params, basePath) {
108
109
  // Deep check: verify all tasks in all slices are complete
109
110
  for (const slice of slices) {
110
111
  const tasks = getSliceTasks(params.milestoneId, slice.id);
111
- const incompleteTasks = tasks.filter(t => t.status !== "complete" && t.status !== "done");
112
+ const incompleteTasks = tasks.filter(t => !isClosedStatus(t.status));
112
113
  if (incompleteTasks.length > 0) {
113
114
  const ids = incompleteTasks.map(t => `${t.id} (status: ${t.status})`).join(", ");
114
115
  guardError = `slice ${slice.id} has incomplete tasks: ${ids}`;
@@ -116,11 +117,7 @@ export async function handleCompleteMilestone(params, basePath) {
116
117
  }
117
118
  }
118
119
  // All guards passed — perform write
119
- const adapter = _getAdapter();
120
- adapter.prepare(`UPDATE milestones SET status = 'complete', completed_at = :completed_at WHERE id = :mid`).run({
121
- ":completed_at": completedAt,
122
- ":mid": params.milestoneId,
123
- });
120
+ updateMilestoneStatus(params.milestoneId, 'complete', completedAt);
124
121
  });
125
122
  if (guardError) {
126
123
  return { error: guardError };
@@ -144,10 +141,7 @@ export async function handleCompleteMilestone(params, basePath) {
144
141
  catch (renderErr) {
145
142
  // Disk render failed — roll back DB status so state stays consistent
146
143
  process.stderr.write(`gsd-db: complete_milestone — disk render failed, rolling back DB status: ${renderErr.message}\n`);
147
- const rollbackAdapter = _getAdapter();
148
- if (rollbackAdapter) {
149
- rollbackAdapter.prepare(`UPDATE milestones SET status = 'active', completed_at = NULL WHERE id = :mid`).run({ ":mid": params.milestoneId });
150
- }
144
+ updateMilestoneStatus(params.milestoneId, 'active', null);
151
145
  invalidateStateCache();
152
146
  return { error: `disk render failed: ${renderErr.message}` };
153
147
  }
@@ -8,7 +8,8 @@
8
8
  */
9
9
  import { join } from "node:path";
10
10
  import { mkdirSync } from "node:fs";
11
- import { transaction, insertMilestone, insertSlice, getSlice, getSliceTasks, getMilestone, updateSliceStatus, _getAdapter, } from "../gsd-db.js";
11
+ import { isClosedStatus } from "../status-guards.js";
12
+ import { transaction, insertMilestone, insertSlice, getSlice, getSliceTasks, getMilestone, updateSliceStatus, setSliceSummaryMd, } from "../gsd-db.js";
12
13
  import { resolveSlicePath, clearPathCache } from "../paths.js";
13
14
  import { checkOwnership, sliceUnitKey } from "../unit-ownership.js";
14
15
  import { saveFile, clearParseCache } from "../files.js";
@@ -179,12 +180,12 @@ export async function handleCompleteSlice(params, basePath) {
179
180
  // Milestone/slice not existing is OK — insertMilestone/insertSlice below will auto-create.
180
181
  // Only block if they exist and are closed.
181
182
  const milestone = getMilestone(params.milestoneId);
182
- if (milestone && (milestone.status === "complete" || milestone.status === "done")) {
183
+ if (milestone && isClosedStatus(milestone.status)) {
183
184
  guardError = `cannot complete slice in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
184
185
  return;
185
186
  }
186
187
  const slice = getSlice(params.milestoneId, params.sliceId);
187
- if (slice && (slice.status === "complete" || slice.status === "done")) {
188
+ if (slice && isClosedStatus(slice.status)) {
188
189
  guardError = `slice ${params.sliceId} is already complete — use gsd_slice_reopen first if you need to redo it`;
189
190
  return;
190
191
  }
@@ -194,7 +195,7 @@ export async function handleCompleteSlice(params, basePath) {
194
195
  guardError = `no tasks found for slice ${params.sliceId} in milestone ${params.milestoneId}`;
195
196
  return;
196
197
  }
197
- const incompleteTasks = tasks.filter(t => t.status !== "complete" && t.status !== "done");
198
+ const incompleteTasks = tasks.filter(t => !isClosedStatus(t.status));
198
199
  if (incompleteTasks.length > 0) {
199
200
  const incompleteIds = incompleteTasks.map(t => `${t.id} (status: ${t.status})`).join(", ");
200
201
  guardError = `incomplete tasks: ${incompleteIds}`;
@@ -240,26 +241,12 @@ export async function handleCompleteSlice(params, basePath) {
240
241
  catch (renderErr) {
241
242
  // Disk render failed — roll back DB status so state stays consistent
242
243
  process.stderr.write(`gsd-db: complete_slice — disk render failed, rolling back DB status: ${renderErr.message}\n`);
243
- const rollbackAdapter = _getAdapter();
244
- if (rollbackAdapter) {
245
- rollbackAdapter.prepare(`UPDATE slices SET status = 'pending' WHERE milestone_id = :mid AND id = :sid`).run({
246
- ":mid": params.milestoneId,
247
- ":sid": params.sliceId,
248
- });
249
- }
244
+ updateSliceStatus(params.milestoneId, params.sliceId, 'pending');
250
245
  invalidateStateCache();
251
246
  return { error: `disk render failed: ${renderErr.message}` };
252
247
  }
253
248
  // Store rendered markdown in DB for D004 recovery
254
- const adapter = _getAdapter();
255
- if (adapter) {
256
- adapter.prepare(`UPDATE slices SET full_summary_md = :summary_md, full_uat_md = :uat_md WHERE milestone_id = :mid AND id = :sid`).run({
257
- ":summary_md": summaryMd,
258
- ":uat_md": uatMd,
259
- ":mid": params.milestoneId,
260
- ":sid": params.sliceId,
261
- });
262
- }
249
+ setSliceSummaryMd(params.milestoneId, params.sliceId, summaryMd, uatMd);
263
250
  // Invalidate all caches
264
251
  invalidateStateCache();
265
252
  clearPathCache();
@@ -8,7 +8,8 @@
8
8
  */
9
9
  import { join } from "node:path";
10
10
  import { mkdirSync } from "node:fs";
11
- import { transaction, insertMilestone, insertSlice, insertTask, insertVerificationEvidence, getMilestone, getSlice, getTask, _getAdapter, } from "../gsd-db.js";
11
+ import { isClosedStatus } from "../status-guards.js";
12
+ import { transaction, insertMilestone, insertSlice, insertTask, insertVerificationEvidence, getMilestone, getSlice, getTask, updateTaskStatus, setTaskSummaryMd, deleteVerificationEvidence, } from "../gsd-db.js";
12
13
  import { resolveSliceFile, resolveTasksDir, clearPathCache } from "../paths.js";
13
14
  import { checkOwnership, taskUnitKey } from "../unit-ownership.js";
14
15
  import { saveFile, clearParseCache } from "../files.js";
@@ -122,17 +123,17 @@ export async function handleCompleteTask(params, basePath) {
122
123
  // Milestone/slice not existing is OK — insertMilestone/insertSlice below will auto-create.
123
124
  // Only block if they exist and are closed.
124
125
  const milestone = getMilestone(params.milestoneId);
125
- if (milestone && (milestone.status === "complete" || milestone.status === "done")) {
126
+ if (milestone && isClosedStatus(milestone.status)) {
126
127
  guardError = `cannot complete task in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
127
128
  return;
128
129
  }
129
130
  const slice = getSlice(params.milestoneId, params.sliceId);
130
- if (slice && (slice.status === "complete" || slice.status === "done")) {
131
+ if (slice && isClosedStatus(slice.status)) {
131
132
  guardError = `cannot complete task in a closed slice: ${params.sliceId} (status: ${slice.status})`;
132
133
  return;
133
134
  }
134
135
  const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
135
- if (existingTask && (existingTask.status === "complete" || existingTask.status === "done")) {
136
+ if (existingTask && isClosedStatus(existingTask.status)) {
136
137
  guardError = `task ${params.taskId} is already complete — use gsd_task_reopen first if you need to redo it`;
137
138
  return;
138
139
  }
@@ -202,27 +203,16 @@ export async function handleCompleteTask(params, basePath) {
202
203
  catch (renderErr) {
203
204
  // Disk render failed — roll back DB status so state stays consistent
204
205
  process.stderr.write(`gsd-db: complete_task — disk render failed, rolling back DB status: ${renderErr.message}\n`);
205
- const rollbackAdapter = _getAdapter();
206
- if (rollbackAdapter) {
207
- rollbackAdapter.prepare(`UPDATE tasks SET status = 'pending' WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`).run({
208
- ":mid": params.milestoneId,
209
- ":sid": params.sliceId,
210
- ":tid": params.taskId,
211
- });
212
- }
206
+ // Delete orphaned verification_evidence rows first (FK constraint
207
+ // references tasks, so evidence must go before status change).
208
+ // Without this, retries accumulate duplicate evidence rows (#2724).
209
+ deleteVerificationEvidence(params.milestoneId, params.sliceId, params.taskId);
210
+ updateTaskStatus(params.milestoneId, params.sliceId, params.taskId, 'pending');
213
211
  invalidateStateCache();
214
212
  return { error: `disk render failed: ${renderErr.message}` };
215
213
  }
216
214
  // Store rendered markdown in DB for D004 recovery
217
- const adapter = _getAdapter();
218
- if (adapter) {
219
- adapter.prepare(`UPDATE tasks SET full_summary_md = :md WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`).run({
220
- ":md": summaryMd,
221
- ":mid": params.milestoneId,
222
- ":sid": params.sliceId,
223
- ":tid": params.taskId,
224
- });
225
- }
215
+ setTaskSummaryMd(params.milestoneId, params.sliceId, params.taskId, summaryMd);
226
216
  // Invalidate all caches
227
217
  invalidateStateCache();
228
218
  clearPathCache();
@@ -1,22 +1,12 @@
1
1
  import { clearParseCache } from "../files.js";
2
+ import { isClosedStatus } from "../status-guards.js";
3
+ import { isNonEmptyString, validateStringArray } from "../validation.js";
2
4
  import { transaction, getMilestone, insertMilestone, insertSlice, upsertMilestonePlanning, upsertSlicePlanning, } from "../gsd-db.js";
3
5
  import { invalidateStateCache } from "../state.js";
4
6
  import { renderRoadmapFromDb } from "../markdown-renderer.js";
5
7
  import { renderAllProjections } from "../workflow-projections.js";
6
8
  import { writeManifest } from "../workflow-manifest.js";
7
9
  import { appendEvent } from "../workflow-events.js";
8
- function isNonEmptyString(value) {
9
- return typeof value === "string" && value.trim().length > 0;
10
- }
11
- function validateStringArray(value, field) {
12
- if (!Array.isArray(value)) {
13
- throw new Error(`${field} must be an array`);
14
- }
15
- if (value.some((item) => !isNonEmptyString(item))) {
16
- throw new Error(`${field} must contain only non-empty strings`);
17
- }
18
- return value;
19
- }
20
10
  function validateRiskEntries(value) {
21
11
  if (!Array.isArray(value)) {
22
12
  throw new Error("keyRisks must be an array");
@@ -145,25 +135,31 @@ export async function handlePlanMilestone(rawParams, basePath) {
145
135
  catch (err) {
146
136
  return { error: `validation failed: ${err.message}` };
147
137
  }
148
- // ── State machine preconditions ─────────────────────────────────────────
149
- const existingMilestone = getMilestone(params.milestoneId);
150
- if (existingMilestone && (existingMilestone.status === "complete" || existingMilestone.status === "done")) {
151
- return { error: `cannot re-plan milestone ${params.milestoneId}: it is already complete` };
152
- }
153
- // Validate depends_on: all dependencies must exist and be complete
154
- if (params.dependsOn && params.dependsOn.length > 0) {
155
- for (const depId of params.dependsOn) {
156
- const dep = getMilestone(depId);
157
- if (!dep) {
158
- return { error: `depends_on references unknown milestone: ${depId}` };
159
- }
160
- if (dep.status !== "complete" && dep.status !== "done") {
161
- return { error: `depends_on milestone ${depId} is not yet complete (status: ${dep.status})` };
162
- }
163
- }
164
- }
138
+ // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
139
+ // Guards must be inside the transaction so the state they check cannot
140
+ // change between the read and the write (#2723).
141
+ let guardError = null;
165
142
  try {
166
143
  transaction(() => {
144
+ const existingMilestone = getMilestone(params.milestoneId);
145
+ if (existingMilestone && isClosedStatus(existingMilestone.status)) {
146
+ guardError = `cannot re-plan milestone ${params.milestoneId}: it is already complete`;
147
+ return;
148
+ }
149
+ // Validate depends_on: all dependencies must exist and be complete
150
+ if (params.dependsOn && params.dependsOn.length > 0) {
151
+ for (const depId of params.dependsOn) {
152
+ const dep = getMilestone(depId);
153
+ if (!dep) {
154
+ guardError = `depends_on references unknown milestone: ${depId}`;
155
+ return;
156
+ }
157
+ if (!isClosedStatus(dep.status)) {
158
+ guardError = `depends_on milestone ${depId} is not yet complete (status: ${dep.status})`;
159
+ return;
160
+ }
161
+ }
162
+ }
167
163
  insertMilestone({
168
164
  id: params.milestoneId,
169
165
  title: params.title,
@@ -206,6 +202,9 @@ export async function handlePlanMilestone(rawParams, basePath) {
206
202
  catch (err) {
207
203
  return { error: `db write failed: ${err.message}` };
208
204
  }
205
+ if (guardError) {
206
+ return { error: guardError };
207
+ }
209
208
  let roadmapPath;
210
209
  try {
211
210
  const renderResult = await renderRoadmapFromDb(basePath, params.milestoneId);
@@ -1,22 +1,12 @@
1
1
  import { clearParseCache } from "../files.js";
2
+ import { isClosedStatus } from "../status-guards.js";
3
+ import { isNonEmptyString } from "../validation.js";
2
4
  import { transaction, getMilestone, getSlice, insertTask, upsertSlicePlanning, upsertTaskPlanning, insertGateRow, } from "../gsd-db.js";
3
5
  import { invalidateStateCache } from "../state.js";
4
6
  import { renderPlanFromDb } from "../markdown-renderer.js";
5
7
  import { renderAllProjections } from "../workflow-projections.js";
6
8
  import { writeManifest } from "../workflow-manifest.js";
7
9
  import { appendEvent } from "../workflow-events.js";
8
- function isNonEmptyString(value) {
9
- return typeof value === "string" && value.trim().length > 0;
10
- }
11
- function validateStringArray(value, field) {
12
- if (!Array.isArray(value)) {
13
- throw new Error(`${field} must be an array`);
14
- }
15
- if (value.some((item) => !isNonEmptyString(item))) {
16
- throw new Error(`${field} must contain only non-empty strings`);
17
- }
18
- return value;
19
- }
20
10
  function validateTasks(value) {
21
11
  if (!Array.isArray(value) || value.length === 0) {
22
12
  throw new Error("tasks must be a non-empty array");
@@ -102,22 +92,30 @@ export async function handlePlanSlice(rawParams, basePath) {
102
92
  catch (err) {
103
93
  return { error: `validation failed: ${err.message}` };
104
94
  }
105
- const parentMilestone = getMilestone(params.milestoneId);
106
- if (!parentMilestone) {
107
- return { error: `milestone not found: ${params.milestoneId}` };
108
- }
109
- if (parentMilestone.status === "complete" || parentMilestone.status === "done") {
110
- return { error: `cannot plan slice in a closed milestone: ${params.milestoneId} (status: ${parentMilestone.status})` };
111
- }
112
- const parentSlice = getSlice(params.milestoneId, params.sliceId);
113
- if (!parentSlice) {
114
- return { error: `missing parent slice: ${params.milestoneId}/${params.sliceId}` };
115
- }
116
- if (parentSlice.status === "complete" || parentSlice.status === "done") {
117
- return { error: `cannot re-plan slice ${params.sliceId}: it is already complete — use gsd_slice_reopen first` };
118
- }
95
+ // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
96
+ // Guards must be inside the transaction so the state they check cannot
97
+ // change between the read and the write (#2723).
98
+ let guardError = null;
119
99
  try {
120
100
  transaction(() => {
101
+ const parentMilestone = getMilestone(params.milestoneId);
102
+ if (!parentMilestone) {
103
+ guardError = `milestone not found: ${params.milestoneId}`;
104
+ return;
105
+ }
106
+ if (isClosedStatus(parentMilestone.status)) {
107
+ guardError = `cannot plan slice in a closed milestone: ${params.milestoneId} (status: ${parentMilestone.status})`;
108
+ return;
109
+ }
110
+ const parentSlice = getSlice(params.milestoneId, params.sliceId);
111
+ if (!parentSlice) {
112
+ guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
113
+ return;
114
+ }
115
+ if (isClosedStatus(parentSlice.status)) {
116
+ guardError = `cannot re-plan slice ${params.sliceId}: it is already complete — use gsd_slice_reopen first`;
117
+ return;
118
+ }
121
119
  upsertSlicePlanning(params.milestoneId, params.sliceId, {
122
120
  goal: params.goal,
123
121
  successCriteria: params.successCriteria,
@@ -163,6 +161,9 @@ export async function handlePlanSlice(rawParams, basePath) {
163
161
  catch (err) {
164
162
  return { error: `db write failed: ${err.message}` };
165
163
  }
164
+ if (guardError) {
165
+ return { error: guardError };
166
+ }
166
167
  try {
167
168
  const renderResult = await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);
168
169
  invalidateStateCache();
@@ -1,22 +1,12 @@
1
1
  import { clearParseCache } from "../files.js";
2
+ import { isClosedStatus } from "../status-guards.js";
3
+ import { isNonEmptyString, validateStringArray } from "../validation.js";
2
4
  import { transaction, getSlice, getTask, insertTask, upsertTaskPlanning } from "../gsd-db.js";
3
5
  import { invalidateStateCache } from "../state.js";
4
6
  import { renderTaskPlanFromDb } from "../markdown-renderer.js";
5
7
  import { renderAllProjections } from "../workflow-projections.js";
6
8
  import { writeManifest } from "../workflow-manifest.js";
7
9
  import { appendEvent } from "../workflow-events.js";
8
- function isNonEmptyString(value) {
9
- return typeof value === "string" && value.trim().length > 0;
10
- }
11
- function validateStringArray(value, field) {
12
- if (!Array.isArray(value)) {
13
- throw new Error(`${field} must be an array`);
14
- }
15
- if (value.some((item) => !isNonEmptyString(item))) {
16
- throw new Error(`${field} must contain only non-empty strings`);
17
- }
18
- return value;
19
- }
20
10
  function validateParams(params) {
21
11
  if (!isNonEmptyString(params?.milestoneId))
22
12
  throw new Error("milestoneId is required");
@@ -50,19 +40,26 @@ export async function handlePlanTask(rawParams, basePath) {
50
40
  catch (err) {
51
41
  return { error: `validation failed: ${err.message}` };
52
42
  }
53
- const parentSlice = getSlice(params.milestoneId, params.sliceId);
54
- if (!parentSlice) {
55
- return { error: `missing parent slice: ${params.milestoneId}/${params.sliceId}` };
56
- }
57
- if (parentSlice.status === "complete" || parentSlice.status === "done") {
58
- return { error: `cannot plan task in a closed slice: ${params.sliceId} (status: ${parentSlice.status})` };
59
- }
60
- const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
61
- if (existingTask && (existingTask.status === "complete" || existingTask.status === "done")) {
62
- return { error: `cannot re-plan task ${params.taskId}: it is already complete — use gsd_task_reopen first` };
63
- }
43
+ // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
44
+ // Guards must be inside the transaction so the state they check cannot
45
+ // change between the read and the write (#2723).
46
+ let guardError = null;
64
47
  try {
65
48
  transaction(() => {
49
+ const parentSlice = getSlice(params.milestoneId, params.sliceId);
50
+ if (!parentSlice) {
51
+ guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
52
+ return;
53
+ }
54
+ if (isClosedStatus(parentSlice.status)) {
55
+ guardError = `cannot plan task in a closed slice: ${params.sliceId} (status: ${parentSlice.status})`;
56
+ return;
57
+ }
58
+ const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
59
+ if (existingTask && isClosedStatus(existingTask.status)) {
60
+ guardError = `cannot re-plan task ${params.taskId}: it is already complete — use gsd_task_reopen first`;
61
+ return;
62
+ }
66
63
  if (!existingTask) {
67
64
  insertTask({
68
65
  id: params.taskId,
@@ -88,6 +85,9 @@ export async function handlePlanTask(rawParams, basePath) {
88
85
  catch (err) {
89
86
  return { error: `db write failed: ${err.message}` };
90
87
  }
88
+ if (guardError) {
89
+ return { error: guardError };
90
+ }
91
91
  try {
92
92
  const renderResult = await renderTaskPlanFromDb(basePath, params.milestoneId, params.sliceId, params.taskId);
93
93
  invalidateStateCache();