gsd-pi 2.51.0-dev.ae8f7cb → 2.52.0-dev.585e355

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 (337) hide show
  1. package/README.md +4 -4
  2. package/dist/headless-events.d.ts +18 -0
  3. package/dist/headless-events.js +36 -0
  4. package/dist/headless-types.d.ts +28 -0
  5. package/dist/headless-types.js +7 -0
  6. package/dist/headless.d.ts +8 -3
  7. package/dist/headless.js +47 -16
  8. package/dist/help-text.js +16 -5
  9. package/dist/onboarding.js +5 -4
  10. package/dist/remote-questions-config.js +1 -1
  11. package/dist/resources/extensions/async-jobs/async-bash-tool.js +29 -17
  12. package/dist/resources/extensions/async-jobs/job-manager.js +4 -1
  13. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +18 -19
  14. package/dist/resources/extensions/gsd/auto/phases.js +6 -0
  15. package/dist/resources/extensions/gsd/auto-dispatch.js +18 -0
  16. package/dist/resources/extensions/gsd/auto-start.js +2 -0
  17. package/dist/resources/extensions/gsd/auto-timers.js +24 -2
  18. package/dist/resources/extensions/gsd/auto-tool-tracking.js +25 -7
  19. package/dist/resources/extensions/gsd/auto-worktree.js +21 -0
  20. package/dist/resources/extensions/gsd/auto.js +8 -4
  21. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +105 -70
  22. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +12 -2
  23. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +1 -1
  24. package/dist/resources/extensions/gsd/claude-import.js +60 -9
  25. package/dist/resources/extensions/gsd/commands/handlers/auto.js +69 -6
  26. package/dist/resources/extensions/gsd/commands-config.js +10 -5
  27. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +4 -4
  28. package/dist/resources/extensions/gsd/detection.js +6 -6
  29. package/dist/resources/extensions/gsd/docs/preferences-reference.md +5 -5
  30. package/dist/resources/extensions/gsd/error-classifier.js +105 -0
  31. package/dist/resources/extensions/gsd/git-service.js +4 -3
  32. package/dist/resources/extensions/gsd/gitignore.js +7 -7
  33. package/dist/resources/extensions/gsd/gsd-db.js +298 -45
  34. package/dist/resources/extensions/gsd/init-wizard.js +2 -2
  35. package/dist/resources/extensions/gsd/key-manager.js +7 -16
  36. package/dist/resources/extensions/gsd/markdown-renderer.js +5 -4
  37. package/dist/resources/extensions/gsd/memory-store.js +28 -13
  38. package/dist/resources/extensions/gsd/milestone-actions.js +19 -0
  39. package/dist/resources/extensions/gsd/preferences-models.js +1 -13
  40. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  41. package/dist/resources/extensions/gsd/preferences.js +13 -13
  42. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  43. package/dist/resources/extensions/gsd/provider-error-pause.js +0 -44
  44. package/dist/resources/extensions/gsd/rule-registry.js +1 -1
  45. package/dist/resources/extensions/gsd/service-tier.js +13 -2
  46. package/dist/resources/extensions/gsd/state.js +33 -19
  47. package/dist/resources/extensions/gsd/status-guards.js +12 -0
  48. package/dist/resources/extensions/gsd/tools/complete-milestone.js +7 -13
  49. package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -20
  50. package/dist/resources/extensions/gsd/tools/complete-task.js +11 -21
  51. package/dist/resources/extensions/gsd/tools/plan-milestone.js +28 -29
  52. package/dist/resources/extensions/gsd/tools/plan-slice.js +27 -26
  53. package/dist/resources/extensions/gsd/tools/plan-task.js +23 -23
  54. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +50 -41
  55. package/dist/resources/extensions/gsd/tools/reopen-slice.js +4 -3
  56. package/dist/resources/extensions/gsd/tools/reopen-task.js +5 -4
  57. package/dist/resources/extensions/gsd/tools/replan-slice.js +51 -41
  58. package/dist/resources/extensions/gsd/tools/validate-milestone.js +23 -16
  59. package/dist/resources/extensions/gsd/validation.js +21 -0
  60. package/dist/resources/extensions/gsd/workflow-logger.js +0 -1
  61. package/dist/resources/extensions/remote-questions/config.js +1 -1
  62. package/dist/resources/extensions/remote-questions/remote-command.js +1 -1
  63. package/dist/resources/extensions/search-the-web/native-search.js +1 -1
  64. package/dist/resources/extensions/search-the-web/provider.js +1 -1
  65. package/dist/resources/extensions/shared/rtk.js +5 -3
  66. package/dist/rtk.js +3 -1
  67. package/dist/web/standalone/.next/BUILD_ID +1 -1
  68. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  69. package/dist/web/standalone/.next/build-manifest.json +3 -3
  70. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  71. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  74. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.rsc +2 -2
  84. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  85. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  87. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  90. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/experimental/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.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_client-reference-manifest.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  130. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  131. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  132. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  133. package/dist/web/standalone/.next/server/app/index.html +1 -1
  134. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  135. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  136. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  137. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  138. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  139. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  140. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  141. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  142. package/dist/web/standalone/.next/server/chunks/2229.js +2 -2
  143. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  144. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  145. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  146. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  147. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  148. package/dist/web/standalone/.next/static/chunks/4024.21054f459af5cc78.js +9 -0
  149. package/dist/web/standalone/.next/static/chunks/{webpack-cfc9a116e6450a6b.js → webpack-024d82be84800e52.js} +1 -1
  150. package/dist/web/standalone/.next/static/css/a58ef8a151aa0493.css +1 -0
  151. package/dist/wizard.js +4 -1
  152. package/package.json +2 -2
  153. package/packages/mcp-server/README.md +202 -0
  154. package/packages/mcp-server/package.json +36 -0
  155. package/packages/mcp-server/src/cli.ts +68 -0
  156. package/packages/mcp-server/src/index.ts +14 -0
  157. package/packages/mcp-server/src/mcp-server.test.ts +628 -0
  158. package/packages/mcp-server/src/server.ts +278 -0
  159. package/packages/mcp-server/src/session-manager.ts +328 -0
  160. package/packages/mcp-server/src/types.ts +107 -0
  161. package/packages/mcp-server/tsconfig.json +24 -0
  162. package/packages/pi-ai/dist/models.d.ts +14 -3
  163. package/packages/pi-ai/dist/models.d.ts.map +1 -1
  164. package/packages/pi-ai/dist/models.js +53 -10
  165. package/packages/pi-ai/dist/models.js.map +1 -1
  166. package/packages/pi-ai/dist/models.test.js +102 -1
  167. package/packages/pi-ai/dist/models.test.js.map +1 -1
  168. package/packages/pi-ai/dist/types.d.ts +30 -0
  169. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  170. package/packages/pi-ai/dist/types.js.map +1 -1
  171. package/packages/pi-ai/src/models.test.ts +114 -1
  172. package/packages/pi-ai/src/models.ts +70 -13
  173. package/packages/pi-ai/src/types.ts +31 -0
  174. package/packages/pi-coding-agent/dist/cli/args.d.ts +2 -0
  175. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  176. package/packages/pi-coding-agent/dist/cli/args.js +3 -0
  177. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  178. package/packages/pi-coding-agent/dist/core/bash-executor.d.ts.map +1 -1
  179. package/packages/pi-coding-agent/dist/core/bash-executor.js +5 -1
  180. package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  181. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  182. package/packages/pi-coding-agent/dist/core/model-registry.js +9 -4
  183. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  184. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.d.ts +19 -0
  185. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.d.ts.map +1 -0
  186. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.js +83 -0
  187. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.js.map +1 -0
  188. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  189. package/packages/pi-coding-agent/dist/core/tools/bash.js +5 -1
  190. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  191. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  192. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  193. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  194. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  195. package/packages/pi-coding-agent/dist/main.js +5 -3
  196. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  197. package/packages/pi-coding-agent/dist/modes/index.d.ts +1 -1
  198. package/packages/pi-coding-agent/dist/modes/index.d.ts.map +1 -1
  199. package/packages/pi-coding-agent/dist/modes/index.js.map +1 -1
  200. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  201. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +0 -2
  202. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  203. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts +28 -1
  204. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  205. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js +49 -0
  206. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js.map +1 -1
  207. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts +1 -1
  208. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  209. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +114 -6
  210. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  211. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.d.ts +9 -0
  212. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.d.ts.map +1 -0
  213. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.js +831 -0
  214. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.js.map +1 -0
  215. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +66 -0
  216. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  217. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
  218. package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  219. package/packages/pi-coding-agent/dist/utils/shell.js +0 -1
  220. package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
  221. package/packages/pi-coding-agent/package.json +1 -1
  222. package/packages/pi-coding-agent/src/cli/args.ts +4 -0
  223. package/packages/pi-coding-agent/src/core/bash-executor.ts +5 -1
  224. package/packages/pi-coding-agent/src/core/model-registry.ts +10 -3
  225. package/packages/pi-coding-agent/src/core/tools/bash-spawn-windows.test.ts +101 -0
  226. package/packages/pi-coding-agent/src/core/tools/bash.ts +5 -1
  227. package/packages/pi-coding-agent/src/index.ts +3 -0
  228. package/packages/pi-coding-agent/src/main.ts +5 -3
  229. package/packages/pi-coding-agent/src/modes/index.ts +8 -1
  230. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +0 -2
  231. package/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts +54 -1
  232. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +124 -6
  233. package/packages/pi-coding-agent/src/modes/rpc/rpc-protocol-v2.test.ts +971 -0
  234. package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +61 -4
  235. package/packages/pi-coding-agent/src/utils/shell.ts +0 -1
  236. package/packages/rpc-client/package.json +20 -0
  237. package/pkg/package.json +1 -1
  238. package/scripts/ensure-workspace-builds.cjs +36 -8
  239. package/src/resources/extensions/async-jobs/async-bash-tool.ts +22 -11
  240. package/src/resources/extensions/async-jobs/job-manager.ts +4 -1
  241. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +19 -20
  242. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +21 -0
  243. package/src/resources/extensions/gsd/auto/phases.ts +6 -0
  244. package/src/resources/extensions/gsd/auto-dispatch.ts +19 -0
  245. package/src/resources/extensions/gsd/auto-start.ts +2 -0
  246. package/src/resources/extensions/gsd/auto-timers.ts +25 -1
  247. package/src/resources/extensions/gsd/auto-tool-tracking.ts +30 -6
  248. package/src/resources/extensions/gsd/auto-worktree.ts +21 -0
  249. package/src/resources/extensions/gsd/auto.ts +10 -4
  250. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +125 -73
  251. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +11 -2
  252. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +1 -1
  253. package/src/resources/extensions/gsd/claude-import.ts +58 -9
  254. package/src/resources/extensions/gsd/commands/handlers/auto.ts +73 -6
  255. package/src/resources/extensions/gsd/commands-config.ts +11 -5
  256. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +4 -4
  257. package/src/resources/extensions/gsd/detection.ts +6 -6
  258. package/src/resources/extensions/gsd/docs/preferences-reference.md +5 -5
  259. package/src/resources/extensions/gsd/error-classifier.ts +139 -0
  260. package/src/resources/extensions/gsd/git-service.ts +4 -3
  261. package/src/resources/extensions/gsd/gitignore.ts +7 -7
  262. package/src/resources/extensions/gsd/gsd-db.ts +355 -63
  263. package/src/resources/extensions/gsd/init-wizard.ts +2 -2
  264. package/src/resources/extensions/gsd/key-manager.ts +7 -16
  265. package/src/resources/extensions/gsd/markdown-renderer.ts +5 -4
  266. package/src/resources/extensions/gsd/memory-store.ts +29 -18
  267. package/src/resources/extensions/gsd/milestone-actions.ts +17 -0
  268. package/src/resources/extensions/gsd/preferences-models.ts +1 -13
  269. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  270. package/src/resources/extensions/gsd/preferences.ts +12 -13
  271. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  272. package/src/resources/extensions/gsd/provider-error-pause.ts +0 -57
  273. package/src/resources/extensions/gsd/rule-registry.ts +1 -1
  274. package/src/resources/extensions/gsd/service-tier.ts +14 -2
  275. package/src/resources/extensions/gsd/state.ts +34 -20
  276. package/src/resources/extensions/gsd/status-guards.ts +13 -0
  277. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +1 -1
  278. package/src/resources/extensions/gsd/tests/auto-milestone-target.test.ts +61 -0
  279. package/src/resources/extensions/gsd/tests/claude-import-marketplace-discovery.test.ts +191 -0
  280. package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +1 -1
  281. package/src/resources/extensions/gsd/tests/commands-config.test.ts +24 -0
  282. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  283. package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +106 -0
  284. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  285. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +35 -7
  286. package/src/resources/extensions/gsd/tests/detection.test.ts +1 -1
  287. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +4 -4
  288. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +1 -1
  289. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +2 -2
  290. package/src/resources/extensions/gsd/tests/empty-db-reconciliation.test.ts +79 -0
  291. package/src/resources/extensions/gsd/tests/git-service.test.ts +37 -4
  292. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
  293. package/src/resources/extensions/gsd/tests/idle-watchdog-stall-override.test.ts +125 -0
  294. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +1 -1
  295. package/src/resources/extensions/gsd/tests/interactive-tool-idle-exemption.test.ts +119 -0
  296. package/src/resources/extensions/gsd/tests/key-manager.test.ts +16 -1
  297. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  298. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  299. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +7 -7
  300. package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +85 -0
  301. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +91 -0
  302. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -2
  303. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +77 -70
  304. package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +110 -0
  305. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +29 -0
  306. package/src/resources/extensions/gsd/tests/status-guards.test.ts +30 -0
  307. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +42 -31
  308. package/src/resources/extensions/gsd/tests/token-cost-display.test.ts +2 -2
  309. package/src/resources/extensions/gsd/tests/vacuous-truth-slices.test.ts +115 -0
  310. package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +90 -0
  311. package/src/resources/extensions/gsd/tests/validation.test.ts +72 -0
  312. package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +81 -1
  313. package/src/resources/extensions/gsd/tests/worktree-preferences-sync.test.ts +130 -0
  314. package/src/resources/extensions/gsd/tools/complete-milestone.ts +7 -17
  315. package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -24
  316. package/src/resources/extensions/gsd/tools/complete-task.ts +13 -25
  317. package/src/resources/extensions/gsd/tools/plan-milestone.ts +30 -32
  318. package/src/resources/extensions/gsd/tools/plan-slice.ts +30 -30
  319. package/src/resources/extensions/gsd/tools/plan-task.ts +26 -26
  320. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +57 -46
  321. package/src/resources/extensions/gsd/tools/reopen-slice.ts +4 -3
  322. package/src/resources/extensions/gsd/tools/reopen-task.ts +5 -4
  323. package/src/resources/extensions/gsd/tools/replan-slice.ts +55 -44
  324. package/src/resources/extensions/gsd/tools/validate-milestone.ts +26 -20
  325. package/src/resources/extensions/gsd/validation.ts +23 -0
  326. package/src/resources/extensions/gsd/workflow-logger.ts +0 -1
  327. package/src/resources/extensions/remote-questions/config.ts +1 -1
  328. package/src/resources/extensions/remote-questions/remote-command.ts +1 -1
  329. package/src/resources/extensions/search-the-web/native-search.ts +1 -1
  330. package/src/resources/extensions/search-the-web/provider.ts +1 -1
  331. package/src/resources/extensions/shared/rtk.ts +12 -3
  332. package/dist/web/standalone/.next/static/chunks/4024.9ad5def014d90ce4.js +0 -9
  333. package/dist/web/standalone/.next/static/css/de141508b083f922.css +0 -1
  334. /package/dist/resources/extensions/gsd/templates/{preferences.md → PREFERENCES.md} +0 -0
  335. /package/dist/web/standalone/.next/static/{5_KeZz1X0tXJK-d_4OhjB → KTe1kB5nPLQFIIFz2OcmI}/_buildManifest.js +0 -0
  336. /package/dist/web/standalone/.next/static/{5_KeZz1X0tXJK-d_4OhjB → KTe1kB5nPLQFIIFz2OcmI}/_ssgManifest.js +0 -0
  337. /package/src/resources/extensions/gsd/templates/{preferences.md → PREFERENCES.md} +0 -0
@@ -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();
@@ -1,14 +1,13 @@
1
+ import { join } from "node:path";
1
2
  import { clearParseCache } from "../files.js";
3
+ import { isClosedStatus } from "../status-guards.js";
4
+ import { isNonEmptyString } from "../validation.js";
2
5
  import { transaction, getMilestone, getMilestoneSlices, getSlice, insertSlice, updateSliceFields, insertAssessment, deleteSlice, } from "../gsd-db.js";
3
6
  import { invalidateStateCache } from "../state.js";
4
7
  import { renderRoadmapFromDb, renderAssessmentFromDb } from "../markdown-renderer.js";
5
8
  import { renderAllProjections } from "../workflow-projections.js";
6
9
  import { writeManifest } from "../workflow-manifest.js";
7
10
  import { appendEvent } from "../workflow-events.js";
8
- import { join } from "node:path";
9
- function isNonEmptyString(value) {
10
- return typeof value === "string" && value.trim().length > 0;
11
- }
12
11
  function validateParams(params) {
13
12
  if (!isNonEmptyString(params?.milestoneId))
14
13
  throw new Error("milestoneId is required");
@@ -61,48 +60,55 @@ export async function handleReassessRoadmap(rawParams, basePath) {
61
60
  catch (err) {
62
61
  return { error: `validation failed: ${err.message}` };
63
62
  }
64
- // ── Verify milestone exists and is active ────────────────────────
65
- const milestone = getMilestone(params.milestoneId);
66
- if (!milestone) {
67
- return { error: `milestone not found: ${params.milestoneId}` };
68
- }
69
- if (milestone.status === "complete" || milestone.status === "done") {
70
- return { error: `cannot reassess a closed milestone: ${params.milestoneId} (status: ${milestone.status})` };
71
- }
72
- // ── Verify completedSliceId is actually complete ──────────────────
73
- const completedSlice = getSlice(params.milestoneId, params.completedSliceId);
74
- if (!completedSlice) {
75
- return { error: `completedSliceId not found: ${params.milestoneId}/${params.completedSliceId}` };
76
- }
77
- if (completedSlice.status !== "complete" && completedSlice.status !== "done") {
78
- return { error: `completedSliceId ${params.completedSliceId} is not complete (status: ${completedSlice.status}) — reassess can only be called after a slice finishes` };
79
- }
80
- // ── Structural enforcement ────────────────────────────────────────
81
- const existingSlices = getMilestoneSlices(params.milestoneId);
82
- const completedSliceIds = new Set();
83
- for (const slice of existingSlices) {
84
- if (slice.status === "complete" || slice.status === "done") {
85
- completedSliceIds.add(slice.id);
86
- }
87
- }
88
- // Reject modifications to completed slices
89
- for (const modifiedSlice of params.sliceChanges.modified) {
90
- if (completedSliceIds.has(modifiedSlice.sliceId)) {
91
- return { error: `cannot modify completed slice ${modifiedSlice.sliceId}` };
92
- }
93
- }
94
- // Reject removal of completed slices
95
- for (const removedId of params.sliceChanges.removed) {
96
- if (completedSliceIds.has(removedId)) {
97
- return { error: `cannot remove completed slice ${removedId}` };
98
- }
99
- }
100
63
  // ── Compute assessment artifact path ──────────────────────────────
101
64
  // Assessment lives in the completed slice's directory
102
65
  const assessmentRelPath = join(".gsd", "milestones", params.milestoneId, "slices", params.completedSliceId, `${params.completedSliceId}-ASSESSMENT.md`);
103
- // ── Transaction: DB mutations ─────────────────────────────────────
66
+ // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
67
+ // Guards must be inside the transaction so the state they check cannot
68
+ // change between the read and the write (#2723).
69
+ let guardError = null;
104
70
  try {
105
71
  transaction(() => {
72
+ // Verify milestone exists and is active
73
+ const milestone = getMilestone(params.milestoneId);
74
+ if (!milestone) {
75
+ guardError = `milestone not found: ${params.milestoneId}`;
76
+ return;
77
+ }
78
+ if (isClosedStatus(milestone.status)) {
79
+ guardError = `cannot reassess a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
80
+ return;
81
+ }
82
+ // Verify completedSliceId is actually complete
83
+ const completedSlice = getSlice(params.milestoneId, params.completedSliceId);
84
+ if (!completedSlice) {
85
+ guardError = `completedSliceId not found: ${params.milestoneId}/${params.completedSliceId}`;
86
+ return;
87
+ }
88
+ if (!isClosedStatus(completedSlice.status)) {
89
+ guardError = `completedSliceId ${params.completedSliceId} is not complete (status: ${completedSlice.status}) — reassess can only be called after a slice finishes`;
90
+ return;
91
+ }
92
+ // Structural enforcement — reject modifications/removal of completed slices
93
+ const existingSlices = getMilestoneSlices(params.milestoneId);
94
+ const completedSliceIds = new Set();
95
+ for (const slice of existingSlices) {
96
+ if (isClosedStatus(slice.status)) {
97
+ completedSliceIds.add(slice.id);
98
+ }
99
+ }
100
+ for (const modifiedSlice of params.sliceChanges.modified) {
101
+ if (completedSliceIds.has(modifiedSlice.sliceId)) {
102
+ guardError = `cannot modify completed slice ${modifiedSlice.sliceId}`;
103
+ return;
104
+ }
105
+ }
106
+ for (const removedId of params.sliceChanges.removed) {
107
+ if (completedSliceIds.has(removedId)) {
108
+ guardError = `cannot remove completed slice ${removedId}`;
109
+ return;
110
+ }
111
+ }
106
112
  // Record assessment
107
113
  insertAssessment({
108
114
  path: assessmentRelPath,
@@ -142,6 +148,9 @@ export async function handleReassessRoadmap(rawParams, basePath) {
142
148
  catch (err) {
143
149
  return { error: `db write failed: ${err.message}` };
144
150
  }
151
+ if (guardError) {
152
+ return { error: guardError };
153
+ }
145
154
  // ── Render artifacts ──────────────────────────────────────────────
146
155
  try {
147
156
  const roadmapResult = await renderRoadmapFromDb(basePath, params.milestoneId);
@@ -11,6 +11,7 @@
11
11
  // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
12
12
  import { getMilestone, getSlice, getSliceTasks, updateSliceStatus, updateTaskStatus, transaction, } from "../gsd-db.js";
13
13
  import { invalidateStateCache } from "../state.js";
14
+ import { isClosedStatus } from "../status-guards.js";
14
15
  import { renderAllProjections } from "../workflow-projections.js";
15
16
  import { writeManifest } from "../workflow-manifest.js";
16
17
  import { appendEvent } from "../workflow-events.js";
@@ -31,8 +32,8 @@ export async function handleReopenSlice(params, basePath) {
31
32
  guardError = `milestone not found: ${params.milestoneId}`;
32
33
  return;
33
34
  }
34
- if (milestone.status === "complete" || milestone.status === "done") {
35
- guardError = `cannot reopen slice inside a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
35
+ if (isClosedStatus(milestone.status)) {
36
+ guardError = `cannot reopen slice in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
36
37
  return;
37
38
  }
38
39
  const slice = getSlice(params.milestoneId, params.sliceId);
@@ -40,7 +41,7 @@ export async function handleReopenSlice(params, basePath) {
40
41
  guardError = `slice not found: ${params.milestoneId}/${params.sliceId}`;
41
42
  return;
42
43
  }
43
- if (slice.status !== "complete" && slice.status !== "done") {
44
+ if (!isClosedStatus(slice.status)) {
44
45
  guardError = `slice ${params.sliceId} is not complete (status: ${slice.status}) — nothing to reopen`;
45
46
  return;
46
47
  }
@@ -10,6 +10,7 @@
10
10
  // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
11
11
  import { getMilestone, getSlice, getTask, updateTaskStatus, transaction, } from "../gsd-db.js";
12
12
  import { invalidateStateCache } from "../state.js";
13
+ import { isClosedStatus } from "../status-guards.js";
13
14
  import { renderAllProjections } from "../workflow-projections.js";
14
15
  import { writeManifest } from "../workflow-manifest.js";
15
16
  import { appendEvent } from "../workflow-events.js";
@@ -32,7 +33,7 @@ export async function handleReopenTask(params, basePath) {
32
33
  guardError = `milestone not found: ${params.milestoneId}`;
33
34
  return;
34
35
  }
35
- if (milestone.status === "complete" || milestone.status === "done") {
36
+ if (isClosedStatus(milestone.status)) {
36
37
  guardError = `cannot reopen task in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
37
38
  return;
38
39
  }
@@ -41,8 +42,8 @@ export async function handleReopenTask(params, basePath) {
41
42
  guardError = `slice not found: ${params.milestoneId}/${params.sliceId}`;
42
43
  return;
43
44
  }
44
- if (slice.status === "complete" || slice.status === "done") {
45
- guardError = `cannot reopen task inside a closed slice: ${params.sliceId} (status: ${slice.status}) — use gsd_slice_reopen first`;
45
+ if (isClosedStatus(slice.status)) {
46
+ guardError = `cannot reopen task in a closed slice: ${params.sliceId} (status: ${slice.status}) — use gsd_slice_reopen first`;
46
47
  return;
47
48
  }
48
49
  const task = getTask(params.milestoneId, params.sliceId, params.taskId);
@@ -50,7 +51,7 @@ export async function handleReopenTask(params, basePath) {
50
51
  guardError = `task not found: ${params.milestoneId}/${params.sliceId}/${params.taskId}`;
51
52
  return;
52
53
  }
53
- if (task.status !== "complete" && task.status !== "done") {
54
+ if (!isClosedStatus(task.status)) {
54
55
  guardError = `task ${params.taskId} is not complete (status: ${task.status}) — nothing to reopen`;
55
56
  return;
56
57
  }
@@ -1,13 +1,12 @@
1
1
  import { clearParseCache } from "../files.js";
2
2
  import { transaction, getSlice, getSliceTasks, getTask, insertTask, upsertTaskPlanning, insertReplanHistory, deleteTask, } from "../gsd-db.js";
3
3
  import { invalidateStateCache } from "../state.js";
4
+ import { isClosedStatus } from "../status-guards.js";
5
+ import { isNonEmptyString } from "../validation.js";
4
6
  import { renderPlanFromDb, renderReplanFromDb } 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
10
  function validateParams(params) {
12
11
  if (!isNonEmptyString(params?.milestoneId))
13
12
  throw new Error("milestoneId is required");
@@ -46,46 +45,54 @@ export async function handleReplanSlice(rawParams, basePath) {
46
45
  catch (err) {
47
46
  return { error: `validation failed: ${err.message}` };
48
47
  }
49
- // ── Verify parent slice exists and is not closed ─────────────────
50
- const parentSlice = getSlice(params.milestoneId, params.sliceId);
51
- if (!parentSlice) {
52
- return { error: `missing parent slice: ${params.milestoneId}/${params.sliceId}` };
53
- }
54
- if (parentSlice.status === "complete" || parentSlice.status === "done") {
55
- return { error: `cannot replan a closed slice: ${params.sliceId} (status: ${parentSlice.status})` };
56
- }
57
- // ── Verify blocker task exists and is complete ────────────────────
58
- const blockerTask = getTask(params.milestoneId, params.sliceId, params.blockerTaskId);
59
- if (!blockerTask) {
60
- return { error: `blockerTaskId not found: ${params.milestoneId}/${params.sliceId}/${params.blockerTaskId}` };
61
- }
62
- if (blockerTask.status !== "complete" && blockerTask.status !== "done") {
63
- return { error: `blockerTaskId ${params.blockerTaskId} is not complete (status: ${blockerTask.status}) — the blocker task must be finished before a replan is triggered` };
64
- }
65
- // ── Structural enforcement ────────────────────────────────────────
66
- const existingTasks = getSliceTasks(params.milestoneId, params.sliceId);
67
- const completedTaskIds = new Set();
68
- for (const task of existingTasks) {
69
- if (task.status === "complete" || task.status === "done") {
70
- completedTaskIds.add(task.id);
71
- }
72
- }
73
- // Reject updates to completed tasks
74
- for (const updatedTask of params.updatedTasks) {
75
- if (completedTaskIds.has(updatedTask.taskId)) {
76
- return { error: `cannot modify completed task ${updatedTask.taskId}` };
77
- }
78
- }
79
- // Reject removal of completed tasks
80
- for (const removedId of params.removedTaskIds) {
81
- if (completedTaskIds.has(removedId)) {
82
- return { error: `cannot remove completed task ${removedId}` };
83
- }
84
- }
85
- // ── Transaction: DB mutations ─────────────────────────────────────
86
- const existingTaskIds = new Set(existingTasks.map((t) => t.id));
48
+ // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
49
+ // Guards must be inside the transaction so the state they check cannot
50
+ // change between the read and the write (#2723).
51
+ let guardError = null;
52
+ let existingTaskIds = new Set();
87
53
  try {
88
54
  transaction(() => {
55
+ // Verify parent slice exists and is not closed
56
+ const parentSlice = getSlice(params.milestoneId, params.sliceId);
57
+ if (!parentSlice) {
58
+ guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
59
+ return;
60
+ }
61
+ if (isClosedStatus(parentSlice.status)) {
62
+ guardError = `cannot replan a closed slice: ${params.sliceId} (status: ${parentSlice.status})`;
63
+ return;
64
+ }
65
+ // Verify blocker task exists and is complete
66
+ const blockerTask = getTask(params.milestoneId, params.sliceId, params.blockerTaskId);
67
+ if (!blockerTask) {
68
+ guardError = `blockerTaskId not found: ${params.milestoneId}/${params.sliceId}/${params.blockerTaskId}`;
69
+ return;
70
+ }
71
+ if (!isClosedStatus(blockerTask.status)) {
72
+ guardError = `blockerTaskId ${params.blockerTaskId} is not complete (status: ${blockerTask.status}) — the blocker task must be finished before a replan is triggered`;
73
+ return;
74
+ }
75
+ // Structural enforcement — reject modifications/removal of completed tasks
76
+ const existingTasks = getSliceTasks(params.milestoneId, params.sliceId);
77
+ const completedTaskIds = new Set();
78
+ for (const task of existingTasks) {
79
+ if (isClosedStatus(task.status)) {
80
+ completedTaskIds.add(task.id);
81
+ }
82
+ }
83
+ for (const updatedTask of params.updatedTasks) {
84
+ if (completedTaskIds.has(updatedTask.taskId)) {
85
+ guardError = `cannot modify completed task ${updatedTask.taskId}`;
86
+ return;
87
+ }
88
+ }
89
+ for (const removedId of params.removedTaskIds) {
90
+ if (completedTaskIds.has(removedId)) {
91
+ guardError = `cannot remove completed task ${removedId}`;
92
+ return;
93
+ }
94
+ }
95
+ existingTaskIds = new Set(existingTasks.map((t) => t.id));
89
96
  // Record replan history
90
97
  insertReplanHistory({
91
98
  milestoneId: params.milestoneId,
@@ -138,6 +145,9 @@ export async function handleReplanSlice(rawParams, basePath) {
138
145
  catch (err) {
139
146
  return { error: `db write failed: ${err.message}` };
140
147
  }
148
+ if (guardError) {
149
+ return { error: guardError };
150
+ }
141
151
  // ── Render artifacts ──────────────────────────────────────────────
142
152
  try {
143
153
  const renderResult = await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);