gsd-pi 2.50.0-dev.d210a87 → 2.51.0-dev.7d435fe

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 (302) 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/claude-code-cli/stream-adapter.js +18 -19
  13. package/dist/resources/extensions/gsd/auto-dispatch.js +18 -0
  14. package/dist/resources/extensions/gsd/auto-start.js +2 -0
  15. package/dist/resources/extensions/gsd/auto-timers.js +24 -2
  16. package/dist/resources/extensions/gsd/auto-tool-tracking.js +25 -7
  17. package/dist/resources/extensions/gsd/auto-worktree.js +21 -0
  18. package/dist/resources/extensions/gsd/auto.js +4 -2
  19. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +95 -69
  20. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +12 -2
  21. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +1 -1
  22. package/dist/resources/extensions/gsd/claude-import.js +60 -9
  23. package/dist/resources/extensions/gsd/commands/handlers/auto.js +69 -6
  24. package/dist/resources/extensions/gsd/commands-config.js +10 -5
  25. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  26. package/dist/resources/extensions/gsd/detection.js +6 -6
  27. package/dist/resources/extensions/gsd/docs/preferences-reference.md +3 -3
  28. package/dist/resources/extensions/gsd/error-classifier.js +105 -0
  29. package/dist/resources/extensions/gsd/gitignore.js +7 -7
  30. package/dist/resources/extensions/gsd/gsd-db.js +298 -45
  31. package/dist/resources/extensions/gsd/init-wizard.js +2 -2
  32. package/dist/resources/extensions/gsd/key-manager.js +7 -16
  33. package/dist/resources/extensions/gsd/memory-store.js +28 -13
  34. package/dist/resources/extensions/gsd/milestone-actions.js +19 -0
  35. package/dist/resources/extensions/gsd/preferences-models.js +1 -13
  36. package/dist/resources/extensions/gsd/preferences.js +13 -13
  37. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  38. package/dist/resources/extensions/gsd/provider-error-pause.js +0 -44
  39. package/dist/resources/extensions/gsd/rule-registry.js +1 -1
  40. package/dist/resources/extensions/gsd/service-tier.js +13 -2
  41. package/dist/resources/extensions/gsd/state.js +21 -2
  42. package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -10
  43. package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -17
  44. package/dist/resources/extensions/gsd/tools/complete-task.js +7 -18
  45. package/dist/resources/extensions/gsd/tools/plan-milestone.js +26 -17
  46. package/dist/resources/extensions/gsd/tools/plan-slice.js +25 -14
  47. package/dist/resources/extensions/gsd/tools/plan-task.js +21 -11
  48. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +47 -37
  49. package/dist/resources/extensions/gsd/tools/replan-slice.js +49 -38
  50. package/dist/resources/extensions/gsd/tools/validate-milestone.js +23 -16
  51. package/dist/resources/extensions/gsd/workflow-logger.js +0 -1
  52. package/dist/resources/extensions/remote-questions/config.js +1 -1
  53. package/dist/resources/extensions/remote-questions/remote-command.js +1 -1
  54. package/dist/resources/extensions/search-the-web/native-search.js +1 -1
  55. package/dist/resources/extensions/search-the-web/provider.js +1 -1
  56. package/dist/web/standalone/.next/BUILD_ID +1 -1
  57. package/dist/web/standalone/.next/app-path-routes-manifest.json +21 -21
  58. package/dist/web/standalone/.next/build-manifest.json +3 -3
  59. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  60. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  63. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.rsc +2 -2
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  76. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  79. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/experimental/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  122. package/dist/web/standalone/.next/server/app/index.html +1 -1
  123. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  124. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  125. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  126. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  127. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  128. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  129. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  130. package/dist/web/standalone/.next/server/app-paths-manifest.json +21 -21
  131. package/dist/web/standalone/.next/server/chunks/2229.js +2 -2
  132. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  133. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  134. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  135. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  136. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  137. package/dist/web/standalone/.next/static/chunks/4024.21054f459af5cc78.js +9 -0
  138. package/dist/web/standalone/.next/static/chunks/{webpack-cfc9a116e6450a6b.js → webpack-024d82be84800e52.js} +1 -1
  139. package/dist/web/standalone/.next/static/css/a58ef8a151aa0493.css +1 -0
  140. package/dist/wizard.js +4 -1
  141. package/package.json +2 -2
  142. package/packages/pi-ai/dist/models.d.ts +14 -3
  143. package/packages/pi-ai/dist/models.d.ts.map +1 -1
  144. package/packages/pi-ai/dist/models.js +53 -10
  145. package/packages/pi-ai/dist/models.js.map +1 -1
  146. package/packages/pi-ai/dist/models.test.js +102 -1
  147. package/packages/pi-ai/dist/models.test.js.map +1 -1
  148. package/packages/pi-ai/dist/types.d.ts +30 -0
  149. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  150. package/packages/pi-ai/dist/types.js.map +1 -1
  151. package/packages/pi-ai/src/models.test.ts +114 -1
  152. package/packages/pi-ai/src/models.ts +70 -13
  153. package/packages/pi-ai/src/types.ts +31 -0
  154. package/packages/pi-coding-agent/dist/cli/args.d.ts +2 -0
  155. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  156. package/packages/pi-coding-agent/dist/cli/args.js +3 -0
  157. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  158. package/packages/pi-coding-agent/dist/core/bash-executor.d.ts.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/bash-executor.js +5 -1
  160. package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  161. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  162. package/packages/pi-coding-agent/dist/core/model-registry.js +9 -4
  163. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  164. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.d.ts +19 -0
  165. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.d.ts.map +1 -0
  166. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.js +83 -0
  167. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.js.map +1 -0
  168. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  169. package/packages/pi-coding-agent/dist/core/tools/bash.js +5 -1
  170. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  171. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  172. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  173. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  174. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  175. package/packages/pi-coding-agent/dist/main.js +5 -3
  176. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  177. package/packages/pi-coding-agent/dist/modes/index.d.ts +1 -1
  178. package/packages/pi-coding-agent/dist/modes/index.d.ts.map +1 -1
  179. package/packages/pi-coding-agent/dist/modes/index.js.map +1 -1
  180. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  181. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +0 -2
  182. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  183. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts +28 -1
  184. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  185. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js +49 -0
  186. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js.map +1 -1
  187. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts +1 -1
  188. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  189. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +114 -6
  190. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  191. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.d.ts +9 -0
  192. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.d.ts.map +1 -0
  193. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.js +831 -0
  194. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.js.map +1 -0
  195. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +66 -0
  196. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  197. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
  198. package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  199. package/packages/pi-coding-agent/dist/utils/shell.js +0 -1
  200. package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
  201. package/packages/pi-coding-agent/package.json +1 -1
  202. package/packages/pi-coding-agent/src/cli/args.ts +4 -0
  203. package/packages/pi-coding-agent/src/core/bash-executor.ts +5 -1
  204. package/packages/pi-coding-agent/src/core/model-registry.ts +10 -3
  205. package/packages/pi-coding-agent/src/core/tools/bash-spawn-windows.test.ts +101 -0
  206. package/packages/pi-coding-agent/src/core/tools/bash.ts +5 -1
  207. package/packages/pi-coding-agent/src/index.ts +3 -0
  208. package/packages/pi-coding-agent/src/main.ts +5 -3
  209. package/packages/pi-coding-agent/src/modes/index.ts +8 -1
  210. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +0 -2
  211. package/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts +54 -1
  212. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +124 -6
  213. package/packages/pi-coding-agent/src/modes/rpc/rpc-protocol-v2.test.ts +971 -0
  214. package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +61 -4
  215. package/packages/pi-coding-agent/src/utils/shell.ts +0 -1
  216. package/pkg/package.json +1 -1
  217. package/src/resources/extensions/async-jobs/async-bash-tool.ts +22 -11
  218. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +19 -20
  219. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +21 -0
  220. package/src/resources/extensions/gsd/auto-dispatch.ts +19 -0
  221. package/src/resources/extensions/gsd/auto-start.ts +2 -0
  222. package/src/resources/extensions/gsd/auto-timers.ts +25 -1
  223. package/src/resources/extensions/gsd/auto-tool-tracking.ts +30 -6
  224. package/src/resources/extensions/gsd/auto-worktree.ts +21 -0
  225. package/src/resources/extensions/gsd/auto.ts +5 -2
  226. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +115 -72
  227. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +11 -2
  228. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +1 -1
  229. package/src/resources/extensions/gsd/claude-import.ts +58 -9
  230. package/src/resources/extensions/gsd/commands/handlers/auto.ts +73 -6
  231. package/src/resources/extensions/gsd/commands-config.ts +11 -5
  232. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  233. package/src/resources/extensions/gsd/detection.ts +6 -6
  234. package/src/resources/extensions/gsd/docs/preferences-reference.md +3 -3
  235. package/src/resources/extensions/gsd/error-classifier.ts +139 -0
  236. package/src/resources/extensions/gsd/gitignore.ts +7 -7
  237. package/src/resources/extensions/gsd/gsd-db.ts +355 -63
  238. package/src/resources/extensions/gsd/init-wizard.ts +2 -2
  239. package/src/resources/extensions/gsd/key-manager.ts +7 -16
  240. package/src/resources/extensions/gsd/memory-store.ts +29 -18
  241. package/src/resources/extensions/gsd/milestone-actions.ts +17 -0
  242. package/src/resources/extensions/gsd/preferences-models.ts +1 -13
  243. package/src/resources/extensions/gsd/preferences.ts +12 -13
  244. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  245. package/src/resources/extensions/gsd/provider-error-pause.ts +0 -57
  246. package/src/resources/extensions/gsd/rule-registry.ts +1 -1
  247. package/src/resources/extensions/gsd/service-tier.ts +14 -2
  248. package/src/resources/extensions/gsd/state.ts +22 -2
  249. package/src/resources/extensions/gsd/tests/auto-milestone-target.test.ts +61 -0
  250. package/src/resources/extensions/gsd/tests/claude-import-marketplace-discovery.test.ts +191 -0
  251. package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +1 -1
  252. package/src/resources/extensions/gsd/tests/commands-config.test.ts +24 -0
  253. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  254. package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +106 -0
  255. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  256. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +35 -7
  257. package/src/resources/extensions/gsd/tests/detection.test.ts +1 -1
  258. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +4 -4
  259. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +1 -1
  260. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +2 -2
  261. package/src/resources/extensions/gsd/tests/empty-db-reconciliation.test.ts +79 -0
  262. package/src/resources/extensions/gsd/tests/git-service.test.ts +1 -1
  263. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
  264. package/src/resources/extensions/gsd/tests/idle-watchdog-stall-override.test.ts +125 -0
  265. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +1 -1
  266. package/src/resources/extensions/gsd/tests/interactive-tool-idle-exemption.test.ts +119 -0
  267. package/src/resources/extensions/gsd/tests/key-manager.test.ts +16 -1
  268. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  269. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  270. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +7 -7
  271. package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +85 -0
  272. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +91 -0
  273. package/src/resources/extensions/gsd/tests/preferences.test.ts +1 -1
  274. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +77 -70
  275. package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +110 -0
  276. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +29 -0
  277. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +42 -31
  278. package/src/resources/extensions/gsd/tests/token-cost-display.test.ts +2 -2
  279. package/src/resources/extensions/gsd/tests/vacuous-truth-slices.test.ts +115 -0
  280. package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +90 -0
  281. package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +81 -1
  282. package/src/resources/extensions/gsd/tests/worktree-preferences-sync.test.ts +130 -0
  283. package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -14
  284. package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -21
  285. package/src/resources/extensions/gsd/tools/complete-task.ts +9 -22
  286. package/src/resources/extensions/gsd/tools/plan-milestone.ts +28 -18
  287. package/src/resources/extensions/gsd/tools/plan-slice.ts +28 -16
  288. package/src/resources/extensions/gsd/tools/plan-task.ts +24 -12
  289. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +54 -42
  290. package/src/resources/extensions/gsd/tools/replan-slice.ts +53 -40
  291. package/src/resources/extensions/gsd/tools/validate-milestone.ts +26 -20
  292. package/src/resources/extensions/gsd/workflow-logger.ts +0 -1
  293. package/src/resources/extensions/remote-questions/config.ts +1 -1
  294. package/src/resources/extensions/remote-questions/remote-command.ts +1 -1
  295. package/src/resources/extensions/search-the-web/native-search.ts +1 -1
  296. package/src/resources/extensions/search-the-web/provider.ts +1 -1
  297. package/dist/web/standalone/.next/static/chunks/4024.9ad5def014d90ce4.js +0 -9
  298. package/dist/web/standalone/.next/static/css/de141508b083f922.css +0 -1
  299. /package/dist/resources/extensions/gsd/templates/{preferences.md → PREFERENCES.md} +0 -0
  300. /package/dist/web/standalone/.next/static/{yJIyd5cXPNpmXTv18ZlyC → RqOU-jOv9uZ1Q03P6L6nn}/_buildManifest.js +0 -0
  301. /package/dist/web/standalone/.next/static/{yJIyd5cXPNpmXTv18ZlyC → RqOU-jOv9uZ1Q03P6L6nn}/_ssgManifest.js +0 -0
  302. /package/src/resources/extensions/gsd/templates/{preferences.md → PREFERENCES.md} +0 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * worktree-preferences-sync.test.ts — Regression test for #2684.
3
+ *
4
+ * Verifies that preferences.md is seeded into auto-mode worktrees:
5
+ *
6
+ * 1. copyPlanningArtifacts() copies preferences.md on initial worktree creation
7
+ * 2. syncGsdStateToWorktree() forward-syncs preferences.md (additive only)
8
+ * 3. syncWorktreeStateBack() does NOT overwrite project root preferences.md
9
+ */
10
+
11
+ import test from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import {
14
+ existsSync,
15
+ mkdirSync,
16
+ mkdtempSync,
17
+ readFileSync,
18
+ rmSync,
19
+ writeFileSync,
20
+ } from "node:fs";
21
+ import { join } from "node:path";
22
+ import { tmpdir } from "node:os";
23
+
24
+ import {
25
+ syncGsdStateToWorktree,
26
+ syncWorktreeStateBack,
27
+ } from "../auto-worktree.ts";
28
+
29
+ // ─── Helpers ─────────────────────────────────────────────────────────
30
+
31
+ function makeTempDir(prefix: string): string {
32
+ return mkdtempSync(join(tmpdir(), `gsd-prefs-test-${prefix}-`));
33
+ }
34
+
35
+ function cleanup(...dirs: string[]): void {
36
+ for (const dir of dirs) {
37
+ rmSync(dir, { recursive: true, force: true });
38
+ }
39
+ }
40
+
41
+ function writeFile(dir: string, relativePath: string, content: string): void {
42
+ const fullPath = join(dir, relativePath);
43
+ mkdirSync(join(fullPath, ".."), { recursive: true });
44
+ writeFileSync(fullPath, content, "utf-8");
45
+ }
46
+
47
+ // ─── Tests ───────────────────────────────────────────────────────────
48
+
49
+ const PREFS_CONTENT = [
50
+ "# Preferences",
51
+ "",
52
+ "post_unit_hooks:",
53
+ " - npm run lint",
54
+ "",
55
+ "skill_rules:",
56
+ ' - use: "frontend-design"',
57
+ ].join("\n");
58
+
59
+ test("#2684: syncGsdStateToWorktree forward-syncs preferences.md when missing from worktree", (t) => {
60
+ const mainBase = makeTempDir("main");
61
+ const wtBase = makeTempDir("wt");
62
+ t.after(() => cleanup(mainBase, wtBase));
63
+
64
+ // Project root has preferences.md
65
+ writeFile(mainBase, ".gsd/preferences.md", PREFS_CONTENT);
66
+
67
+ // Worktree has .gsd/ but no preferences.md
68
+ mkdirSync(join(wtBase, ".gsd"), { recursive: true });
69
+
70
+ const result = syncGsdStateToWorktree(mainBase, wtBase);
71
+
72
+ assert.ok(
73
+ existsSync(join(wtBase, ".gsd", "preferences.md")),
74
+ "preferences.md should be copied to worktree",
75
+ );
76
+ assert.equal(
77
+ readFileSync(join(wtBase, ".gsd", "preferences.md"), "utf-8"),
78
+ PREFS_CONTENT,
79
+ "preferences.md content should match source",
80
+ );
81
+ assert.ok(
82
+ result.synced.includes("preferences.md"),
83
+ "preferences.md should appear in synced list",
84
+ );
85
+ });
86
+
87
+ test("#2684: syncGsdStateToWorktree does NOT overwrite existing worktree preferences.md", (t) => {
88
+ const mainBase = makeTempDir("main");
89
+ const wtBase = makeTempDir("wt");
90
+ t.after(() => cleanup(mainBase, wtBase));
91
+
92
+ const rootPrefs = "# Root preferences\nold: true";
93
+ const wtPrefs = "# Worktree preferences\nmodified: true";
94
+
95
+ writeFile(mainBase, ".gsd/preferences.md", rootPrefs);
96
+ writeFile(wtBase, ".gsd/preferences.md", wtPrefs);
97
+
98
+ syncGsdStateToWorktree(mainBase, wtBase);
99
+
100
+ assert.equal(
101
+ readFileSync(join(wtBase, ".gsd", "preferences.md"), "utf-8"),
102
+ wtPrefs,
103
+ "existing worktree preferences.md must not be overwritten",
104
+ );
105
+ });
106
+
107
+ test("#2684: syncWorktreeStateBack does NOT overwrite project root preferences.md", (t) => {
108
+ const mainBase = makeTempDir("main");
109
+ const wtBase = makeTempDir("wt");
110
+ const mid = "M001";
111
+ t.after(() => cleanup(mainBase, wtBase));
112
+
113
+ const rootPrefs = "# Root preferences\nauthoritative: true";
114
+ const wtPrefs = "# Worktree preferences\nstale-copy: true";
115
+
116
+ writeFile(mainBase, ".gsd/preferences.md", rootPrefs);
117
+ writeFile(wtBase, ".gsd/preferences.md", wtPrefs);
118
+
119
+ // Worktree needs at least a milestone dir for the function to proceed
120
+ mkdirSync(join(wtBase, ".gsd", "milestones", mid), { recursive: true });
121
+ mkdirSync(join(mainBase, ".gsd", "milestones"), { recursive: true });
122
+
123
+ syncWorktreeStateBack(mainBase, wtBase, mid);
124
+
125
+ assert.equal(
126
+ readFileSync(join(mainBase, ".gsd", "preferences.md"), "utf-8"),
127
+ rootPrefs,
128
+ "project root preferences.md must NOT be overwritten by worktree copy",
129
+ );
130
+ });
@@ -14,7 +14,7 @@ import {
14
14
  getMilestone,
15
15
  getMilestoneSlices,
16
16
  getSliceTasks,
17
- _getAdapter,
17
+ updateMilestoneStatus,
18
18
  } from "../gsd-db.js";
19
19
  import { resolveMilestonePath, clearPathCache } from "../paths.js";
20
20
  import { saveFile, clearParseCache } from "../files.js";
@@ -165,13 +165,7 @@ export async function handleCompleteMilestone(
165
165
  }
166
166
 
167
167
  // All guards passed — perform write
168
- const adapter = _getAdapter()!;
169
- adapter.prepare(
170
- `UPDATE milestones SET status = 'complete', completed_at = :completed_at WHERE id = :mid`,
171
- ).run({
172
- ":completed_at": completedAt,
173
- ":mid": params.milestoneId,
174
- });
168
+ updateMilestoneStatus(params.milestoneId, 'complete', completedAt);
175
169
  });
176
170
 
177
171
  if (guardError) {
@@ -199,12 +193,7 @@ export async function handleCompleteMilestone(
199
193
  process.stderr.write(
200
194
  `gsd-db: complete_milestone — disk render failed, rolling back DB status: ${(renderErr as Error).message}\n`,
201
195
  );
202
- const rollbackAdapter = _getAdapter();
203
- if (rollbackAdapter) {
204
- rollbackAdapter.prepare(
205
- `UPDATE milestones SET status = 'active', completed_at = NULL WHERE id = :mid`,
206
- ).run({ ":mid": params.milestoneId });
207
- }
196
+ updateMilestoneStatus(params.milestoneId, 'active', null);
208
197
  invalidateStateCache();
209
198
  return { error: `disk render failed: ${(renderErr as Error).message}` };
210
199
  }
@@ -19,7 +19,7 @@ import {
19
19
  getSliceTasks,
20
20
  getMilestone,
21
21
  updateSliceStatus,
22
- _getAdapter,
22
+ setSliceSummaryMd,
23
23
  } from "../gsd-db.js";
24
24
  import { resolveSliceFile, resolveSlicePath, clearPathCache } from "../paths.js";
25
25
  import { checkOwnership, sliceUnitKey } from "../unit-ownership.js";
@@ -299,31 +299,13 @@ export async function handleCompleteSlice(
299
299
  process.stderr.write(
300
300
  `gsd-db: complete_slice — disk render failed, rolling back DB status: ${(renderErr as Error).message}\n`,
301
301
  );
302
- const rollbackAdapter = _getAdapter();
303
- if (rollbackAdapter) {
304
- rollbackAdapter.prepare(
305
- `UPDATE slices SET status = 'pending' WHERE milestone_id = :mid AND id = :sid`,
306
- ).run({
307
- ":mid": params.milestoneId,
308
- ":sid": params.sliceId,
309
- });
310
- }
302
+ updateSliceStatus(params.milestoneId, params.sliceId, 'pending');
311
303
  invalidateStateCache();
312
304
  return { error: `disk render failed: ${(renderErr as Error).message}` };
313
305
  }
314
306
 
315
307
  // Store rendered markdown in DB for D004 recovery
316
- const adapter = _getAdapter();
317
- if (adapter) {
318
- adapter.prepare(
319
- `UPDATE slices SET full_summary_md = :summary_md, full_uat_md = :uat_md WHERE milestone_id = :mid AND id = :sid`,
320
- ).run({
321
- ":summary_md": summaryMd,
322
- ":uat_md": uatMd,
323
- ":mid": params.milestoneId,
324
- ":sid": params.sliceId,
325
- });
326
- }
308
+ setSliceSummaryMd(params.milestoneId, params.sliceId, summaryMd, uatMd);
327
309
 
328
310
  // Invalidate all caches
329
311
  invalidateStateCache();
@@ -20,7 +20,9 @@ import {
20
20
  getMilestone,
21
21
  getSlice,
22
22
  getTask,
23
- _getAdapter,
23
+ updateTaskStatus,
24
+ setTaskSummaryMd,
25
+ deleteVerificationEvidence,
24
26
  } from "../gsd-db.js";
25
27
  import { resolveSliceFile, resolveTasksDir, clearPathCache } from "../paths.js";
26
28
  import { checkOwnership, taskUnitKey } from "../unit-ownership.js";
@@ -248,32 +250,17 @@ export async function handleCompleteTask(
248
250
  process.stderr.write(
249
251
  `gsd-db: complete_task — disk render failed, rolling back DB status: ${(renderErr as Error).message}\n`,
250
252
  );
251
- const rollbackAdapter = _getAdapter();
252
- if (rollbackAdapter) {
253
- rollbackAdapter.prepare(
254
- `UPDATE tasks SET status = 'pending' WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`,
255
- ).run({
256
- ":mid": params.milestoneId,
257
- ":sid": params.sliceId,
258
- ":tid": params.taskId,
259
- });
260
- }
253
+ // Delete orphaned verification_evidence rows first (FK constraint
254
+ // references tasks, so evidence must go before status change).
255
+ // Without this, retries accumulate duplicate evidence rows (#2724).
256
+ deleteVerificationEvidence(params.milestoneId, params.sliceId, params.taskId);
257
+ updateTaskStatus(params.milestoneId, params.sliceId, params.taskId, 'pending');
261
258
  invalidateStateCache();
262
259
  return { error: `disk render failed: ${(renderErr as Error).message}` };
263
260
  }
264
261
 
265
262
  // Store rendered markdown in DB for D004 recovery
266
- const adapter = _getAdapter();
267
- if (adapter) {
268
- adapter.prepare(
269
- `UPDATE tasks SET full_summary_md = :md WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`,
270
- ).run({
271
- ":md": summaryMd,
272
- ":mid": params.milestoneId,
273
- ":sid": params.sliceId,
274
- ":tid": params.taskId,
275
- });
276
- }
263
+ setTaskSummaryMd(params.milestoneId, params.sliceId, params.taskId, summaryMd);
277
264
 
278
265
  // Invalidate all caches
279
266
  invalidateStateCache();
@@ -6,7 +6,6 @@ import {
6
6
  insertSlice,
7
7
  upsertMilestonePlanning,
8
8
  upsertSlicePlanning,
9
- _getAdapter,
10
9
  } from "../gsd-db.js";
11
10
  import { invalidateStateCache } from "../state.js";
12
11
  import { renderRoadmapFromDb } from "../markdown-renderer.js";
@@ -189,27 +188,34 @@ export async function handlePlanMilestone(
189
188
  return { error: `validation failed: ${(err as Error).message}` };
190
189
  }
191
190
 
192
- // ── State machine preconditions ─────────────────────────────────────────
193
- const existingMilestone = getMilestone(params.milestoneId);
194
- if (existingMilestone && (existingMilestone.status === "complete" || existingMilestone.status === "done")) {
195
- return { error: `cannot re-plan milestone ${params.milestoneId}: it is already complete` };
196
- }
191
+ // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
192
+ // Guards must be inside the transaction so the state they check cannot
193
+ // change between the read and the write (#2723).
194
+ let guardError: string | null = null;
197
195
 
198
- // Validate depends_on: all dependencies must exist and be complete
199
- if (params.dependsOn && params.dependsOn.length > 0) {
200
- for (const depId of params.dependsOn) {
201
- const dep = getMilestone(depId);
202
- if (!dep) {
203
- return { error: `depends_on references unknown milestone: ${depId}` };
196
+ try {
197
+ transaction(() => {
198
+ const existingMilestone = getMilestone(params.milestoneId);
199
+ if (existingMilestone && (existingMilestone.status === "complete" || existingMilestone.status === "done")) {
200
+ guardError = `cannot re-plan milestone ${params.milestoneId}: it is already complete`;
201
+ return;
204
202
  }
205
- if (dep.status !== "complete" && dep.status !== "done") {
206
- return { error: `depends_on milestone ${depId} is not yet complete (status: ${dep.status})` };
203
+
204
+ // Validate depends_on: all dependencies must exist and be complete
205
+ if (params.dependsOn && params.dependsOn.length > 0) {
206
+ for (const depId of params.dependsOn) {
207
+ const dep = getMilestone(depId);
208
+ if (!dep) {
209
+ guardError = `depends_on references unknown milestone: ${depId}`;
210
+ return;
211
+ }
212
+ if (dep.status !== "complete" && dep.status !== "done") {
213
+ guardError = `depends_on milestone ${depId} is not yet complete (status: ${dep.status})`;
214
+ return;
215
+ }
216
+ }
207
217
  }
208
- }
209
- }
210
218
 
211
- try {
212
- transaction(() => {
213
219
  insertMilestone({
214
220
  id: params.milestoneId,
215
221
  title: params.title,
@@ -254,6 +260,10 @@ export async function handlePlanMilestone(
254
260
  return { error: `db write failed: ${(err as Error).message}` };
255
261
  }
256
262
 
263
+ if (guardError) {
264
+ return { error: guardError };
265
+ }
266
+
257
267
  let roadmapPath: string;
258
268
  try {
259
269
  const renderResult = await renderRoadmapFromDb(basePath, params.milestoneId);
@@ -7,7 +7,6 @@ import {
7
7
  upsertSlicePlanning,
8
8
  upsertTaskPlanning,
9
9
  insertGateRow,
10
- _getAdapter,
11
10
  } from "../gsd-db.js";
12
11
  import type { GateId } from "../types.js";
13
12
  import { invalidateStateCache } from "../state.js";
@@ -146,24 +145,33 @@ export async function handlePlanSlice(
146
145
  return { error: `validation failed: ${(err as Error).message}` };
147
146
  }
148
147
 
149
- const parentMilestone = getMilestone(params.milestoneId);
150
- if (!parentMilestone) {
151
- return { error: `milestone not found: ${params.milestoneId}` };
152
- }
153
- if (parentMilestone.status === "complete" || parentMilestone.status === "done") {
154
- return { error: `cannot plan slice in a closed milestone: ${params.milestoneId} (status: ${parentMilestone.status})` };
155
- }
156
-
157
- const parentSlice = getSlice(params.milestoneId, params.sliceId);
158
- if (!parentSlice) {
159
- return { error: `missing parent slice: ${params.milestoneId}/${params.sliceId}` };
160
- }
161
- if (parentSlice.status === "complete" || parentSlice.status === "done") {
162
- return { error: `cannot re-plan slice ${params.sliceId}: it is already complete — use gsd_slice_reopen first` };
163
- }
148
+ // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
149
+ // Guards must be inside the transaction so the state they check cannot
150
+ // change between the read and the write (#2723).
151
+ let guardError: string | null = null;
164
152
 
165
153
  try {
166
154
  transaction(() => {
155
+ const parentMilestone = getMilestone(params.milestoneId);
156
+ if (!parentMilestone) {
157
+ guardError = `milestone not found: ${params.milestoneId}`;
158
+ return;
159
+ }
160
+ if (parentMilestone.status === "complete" || parentMilestone.status === "done") {
161
+ guardError = `cannot plan slice in a closed milestone: ${params.milestoneId} (status: ${parentMilestone.status})`;
162
+ return;
163
+ }
164
+
165
+ const parentSlice = getSlice(params.milestoneId, params.sliceId);
166
+ if (!parentSlice) {
167
+ guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
168
+ return;
169
+ }
170
+ if (parentSlice.status === "complete" || parentSlice.status === "done") {
171
+ guardError = `cannot re-plan slice ${params.sliceId}: it is already complete — use gsd_slice_reopen first`;
172
+ return;
173
+ }
174
+
167
175
  upsertSlicePlanning(params.milestoneId, params.sliceId, {
168
176
  goal: params.goal,
169
177
  successCriteria: params.successCriteria,
@@ -211,6 +219,10 @@ export async function handlePlanSlice(
211
219
  return { error: `db write failed: ${(err as Error).message}` };
212
220
  }
213
221
 
222
+ if (guardError) {
223
+ return { error: guardError };
224
+ }
225
+
214
226
  try {
215
227
  const renderResult = await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);
216
228
  invalidateStateCache();
@@ -77,21 +77,29 @@ export async function handlePlanTask(
77
77
  return { error: `validation failed: ${(err as Error).message}` };
78
78
  }
79
79
 
80
- const parentSlice = getSlice(params.milestoneId, params.sliceId);
81
- if (!parentSlice) {
82
- return { error: `missing parent slice: ${params.milestoneId}/${params.sliceId}` };
83
- }
84
- if (parentSlice.status === "complete" || parentSlice.status === "done") {
85
- return { error: `cannot plan task in a closed slice: ${params.sliceId} (status: ${parentSlice.status})` };
86
- }
87
-
88
- const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
89
- if (existingTask && (existingTask.status === "complete" || existingTask.status === "done")) {
90
- return { error: `cannot re-plan task ${params.taskId}: it is already complete — use gsd_task_reopen first` };
91
- }
80
+ // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
81
+ // Guards must be inside the transaction so the state they check cannot
82
+ // change between the read and the write (#2723).
83
+ let guardError: string | null = null;
92
84
 
93
85
  try {
94
86
  transaction(() => {
87
+ const parentSlice = getSlice(params.milestoneId, params.sliceId);
88
+ if (!parentSlice) {
89
+ guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
90
+ return;
91
+ }
92
+ if (parentSlice.status === "complete" || parentSlice.status === "done") {
93
+ guardError = `cannot plan task in a closed slice: ${params.sliceId} (status: ${parentSlice.status})`;
94
+ return;
95
+ }
96
+
97
+ const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
98
+ if (existingTask && (existingTask.status === "complete" || existingTask.status === "done")) {
99
+ guardError = `cannot re-plan task ${params.taskId}: it is already complete — use gsd_task_reopen first`;
100
+ return;
101
+ }
102
+
95
103
  if (!existingTask) {
96
104
  insertTask({
97
105
  id: params.taskId,
@@ -117,6 +125,10 @@ export async function handlePlanTask(
117
125
  return { error: `db write failed: ${(err as Error).message}` };
118
126
  }
119
127
 
128
+ if (guardError) {
129
+ return { error: guardError };
130
+ }
131
+
120
132
  try {
121
133
  const renderResult = await renderTaskPlanFromDb(basePath, params.milestoneId, params.sliceId, params.taskId);
122
134
  invalidateStateCache();
@@ -104,47 +104,6 @@ export async function handleReassessRoadmap(
104
104
  return { error: `validation failed: ${(err as Error).message}` };
105
105
  }
106
106
 
107
- // ── Verify milestone exists and is active ────────────────────────
108
- const milestone = getMilestone(params.milestoneId);
109
- if (!milestone) {
110
- return { error: `milestone not found: ${params.milestoneId}` };
111
- }
112
- if (milestone.status === "complete" || milestone.status === "done") {
113
- return { error: `cannot reassess a closed milestone: ${params.milestoneId} (status: ${milestone.status})` };
114
- }
115
-
116
- // ── Verify completedSliceId is actually complete ──────────────────
117
- const completedSlice = getSlice(params.milestoneId, params.completedSliceId);
118
- if (!completedSlice) {
119
- return { error: `completedSliceId not found: ${params.milestoneId}/${params.completedSliceId}` };
120
- }
121
- if (completedSlice.status !== "complete" && completedSlice.status !== "done") {
122
- return { error: `completedSliceId ${params.completedSliceId} is not complete (status: ${completedSlice.status}) — reassess can only be called after a slice finishes` };
123
- }
124
-
125
- // ── Structural enforcement ────────────────────────────────────────
126
- const existingSlices = getMilestoneSlices(params.milestoneId);
127
- const completedSliceIds = new Set<string>();
128
- for (const slice of existingSlices) {
129
- if (slice.status === "complete" || slice.status === "done") {
130
- completedSliceIds.add(slice.id);
131
- }
132
- }
133
-
134
- // Reject modifications to completed slices
135
- for (const modifiedSlice of params.sliceChanges.modified) {
136
- if (completedSliceIds.has(modifiedSlice.sliceId)) {
137
- return { error: `cannot modify completed slice ${modifiedSlice.sliceId}` };
138
- }
139
- }
140
-
141
- // Reject removal of completed slices
142
- for (const removedId of params.sliceChanges.removed) {
143
- if (completedSliceIds.has(removedId)) {
144
- return { error: `cannot remove completed slice ${removedId}` };
145
- }
146
- }
147
-
148
107
  // ── Compute assessment artifact path ──────────────────────────────
149
108
  // Assessment lives in the completed slice's directory
150
109
  const assessmentRelPath = join(
@@ -153,9 +112,58 @@ export async function handleReassessRoadmap(
153
112
  `${params.completedSliceId}-ASSESSMENT.md`,
154
113
  );
155
114
 
156
- // ── Transaction: DB mutations ─────────────────────────────────────
115
+ // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
116
+ // Guards must be inside the transaction so the state they check cannot
117
+ // change between the read and the write (#2723).
118
+ let guardError: string | null = null;
119
+
157
120
  try {
158
121
  transaction(() => {
122
+ // Verify milestone exists and is active
123
+ const milestone = getMilestone(params.milestoneId);
124
+ if (!milestone) {
125
+ guardError = `milestone not found: ${params.milestoneId}`;
126
+ return;
127
+ }
128
+ if (milestone.status === "complete" || milestone.status === "done") {
129
+ guardError = `cannot reassess a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
130
+ return;
131
+ }
132
+
133
+ // Verify completedSliceId is actually complete
134
+ const completedSlice = getSlice(params.milestoneId, params.completedSliceId);
135
+ if (!completedSlice) {
136
+ guardError = `completedSliceId not found: ${params.milestoneId}/${params.completedSliceId}`;
137
+ return;
138
+ }
139
+ if (completedSlice.status !== "complete" && completedSlice.status !== "done") {
140
+ guardError = `completedSliceId ${params.completedSliceId} is not complete (status: ${completedSlice.status}) — reassess can only be called after a slice finishes`;
141
+ return;
142
+ }
143
+
144
+ // Structural enforcement — reject modifications/removal of completed slices
145
+ const existingSlices = getMilestoneSlices(params.milestoneId);
146
+ const completedSliceIds = new Set<string>();
147
+ for (const slice of existingSlices) {
148
+ if (slice.status === "complete" || slice.status === "done") {
149
+ completedSliceIds.add(slice.id);
150
+ }
151
+ }
152
+
153
+ for (const modifiedSlice of params.sliceChanges.modified) {
154
+ if (completedSliceIds.has(modifiedSlice.sliceId)) {
155
+ guardError = `cannot modify completed slice ${modifiedSlice.sliceId}`;
156
+ return;
157
+ }
158
+ }
159
+
160
+ for (const removedId of params.sliceChanges.removed) {
161
+ if (completedSliceIds.has(removedId)) {
162
+ guardError = `cannot remove completed slice ${removedId}`;
163
+ return;
164
+ }
165
+ }
166
+
159
167
  // Record assessment
160
168
  insertAssessment({
161
169
  path: assessmentRelPath,
@@ -198,6 +206,10 @@ export async function handleReassessRoadmap(
198
206
  return { error: `db write failed: ${(err as Error).message}` };
199
207
  }
200
208
 
209
+ if (guardError) {
210
+ return { error: guardError };
211
+ }
212
+
201
213
  // ── Render artifacts ──────────────────────────────────────────────
202
214
  try {
203
215
  const roadmapResult = await renderRoadmapFromDb(basePath, params.milestoneId);