gsd-pi 2.51.0 → 2.52.0-dev.655ad8a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/README.md +59 -36
  2. package/dist/headless-events.d.ts +18 -0
  3. package/dist/headless-events.js +36 -0
  4. package/dist/headless-query.js +1 -1
  5. package/dist/headless-types.d.ts +28 -0
  6. package/dist/headless-types.js +7 -0
  7. package/dist/headless.d.ts +8 -3
  8. package/dist/headless.js +47 -16
  9. package/dist/help-text.js +16 -5
  10. package/dist/onboarding.js +5 -4
  11. package/dist/remote-questions-config.js +1 -1
  12. package/dist/resources/extensions/async-jobs/async-bash-tool.js +29 -17
  13. package/dist/resources/extensions/async-jobs/job-manager.js +4 -1
  14. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +18 -19
  15. package/dist/resources/extensions/get-secrets-from-user.js +7 -0
  16. package/dist/resources/extensions/gsd/auto/phases.js +34 -8
  17. package/dist/resources/extensions/gsd/auto-dispatch.js +23 -1
  18. package/dist/resources/extensions/gsd/auto-start.js +2 -0
  19. package/dist/resources/extensions/gsd/auto-timers.js +24 -2
  20. package/dist/resources/extensions/gsd/auto-tool-tracking.js +25 -7
  21. package/dist/resources/extensions/gsd/auto-worktree.js +91 -14
  22. package/dist/resources/extensions/gsd/auto.js +30 -4
  23. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +99 -70
  24. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +12 -2
  25. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +1 -1
  26. package/dist/resources/extensions/gsd/claude-import.js +60 -9
  27. package/dist/resources/extensions/gsd/commands/handlers/auto.js +69 -6
  28. package/dist/resources/extensions/gsd/commands-config.js +10 -5
  29. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +4 -4
  30. package/dist/resources/extensions/gsd/detection.js +6 -6
  31. package/dist/resources/extensions/gsd/docs/preferences-reference.md +5 -5
  32. package/dist/resources/extensions/gsd/error-classifier.js +105 -0
  33. package/dist/resources/extensions/gsd/git-service.js +4 -3
  34. package/dist/resources/extensions/gsd/gitignore.js +7 -7
  35. package/dist/resources/extensions/gsd/gsd-db.js +298 -45
  36. package/dist/resources/extensions/gsd/guided-flow.js +4 -3
  37. package/dist/resources/extensions/gsd/init-wizard.js +2 -2
  38. package/dist/resources/extensions/gsd/key-manager.js +7 -16
  39. package/dist/resources/extensions/gsd/markdown-renderer.js +5 -4
  40. package/dist/resources/extensions/gsd/memory-store.js +28 -13
  41. package/dist/resources/extensions/gsd/milestone-actions.js +19 -0
  42. package/dist/resources/extensions/gsd/parallel-orchestrator.js +18 -2
  43. package/dist/resources/extensions/gsd/preferences-models.js +1 -13
  44. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  45. package/dist/resources/extensions/gsd/preferences.js +13 -13
  46. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  47. package/dist/resources/extensions/gsd/provider-error-pause.js +0 -44
  48. package/dist/resources/extensions/gsd/rule-registry.js +1 -1
  49. package/dist/resources/extensions/gsd/service-tier.js +13 -2
  50. package/dist/resources/extensions/gsd/state.js +38 -30
  51. package/dist/resources/extensions/gsd/status-guards.js +12 -0
  52. package/dist/resources/extensions/gsd/tools/complete-milestone.js +7 -13
  53. package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -20
  54. package/dist/resources/extensions/gsd/tools/complete-task.js +11 -21
  55. package/dist/resources/extensions/gsd/tools/plan-milestone.js +28 -29
  56. package/dist/resources/extensions/gsd/tools/plan-slice.js +27 -26
  57. package/dist/resources/extensions/gsd/tools/plan-task.js +23 -23
  58. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +50 -41
  59. package/dist/resources/extensions/gsd/tools/reopen-slice.js +4 -3
  60. package/dist/resources/extensions/gsd/tools/reopen-task.js +5 -4
  61. package/dist/resources/extensions/gsd/tools/replan-slice.js +51 -41
  62. package/dist/resources/extensions/gsd/tools/validate-milestone.js +23 -16
  63. package/dist/resources/extensions/gsd/validation.js +21 -0
  64. package/dist/resources/extensions/gsd/workflow-logger.js +0 -1
  65. package/dist/resources/extensions/remote-questions/config.js +1 -1
  66. package/dist/resources/extensions/remote-questions/remote-command.js +1 -1
  67. package/dist/resources/extensions/search-the-web/native-search.js +1 -1
  68. package/dist/resources/extensions/search-the-web/provider.js +1 -1
  69. package/dist/resources/extensions/shared/rtk.js +14 -4
  70. package/dist/rtk.js +3 -1
  71. package/dist/web/standalone/.next/BUILD_ID +1 -1
  72. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  73. package/dist/web/standalone/.next/build-manifest.json +4 -4
  74. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  75. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  76. package/dist/web/standalone/.next/required-server-files.json +3 -3
  77. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  78. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  80. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  88. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  90. package/dist/web/standalone/.next/server/app/_not-found.rsc +4 -4
  91. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +4 -4
  92. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +4 -4
  94. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  97. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  104. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  116. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  130. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  131. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  132. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  133. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  134. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  135. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  136. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  137. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  138. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  139. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  140. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  141. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  142. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  143. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  144. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  145. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  146. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  147. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  148. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  149. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  150. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  151. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  152. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  153. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  154. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  155. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  156. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  157. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  158. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  159. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  160. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  161. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  162. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  163. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  164. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  165. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  166. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  167. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  168. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  169. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  170. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  171. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  172. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  173. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  174. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  175. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  176. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  177. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  178. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  179. package/dist/web/standalone/.next/server/app/index.html +1 -1
  180. package/dist/web/standalone/.next/server/app/index.rsc +5 -5
  181. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  182. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +5 -5
  183. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  184. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +4 -4
  185. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  186. package/dist/web/standalone/.next/server/app/page.js +2 -2
  187. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  188. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  189. package/dist/web/standalone/.next/server/chunks/2229.js +3 -3
  190. package/dist/web/standalone/.next/server/chunks/7471.js +3 -3
  191. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  192. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  193. package/dist/web/standalone/.next/server/middleware.js +2 -2
  194. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  195. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  196. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  197. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  198. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  199. package/dist/web/standalone/.next/static/chunks/4024.87fd909ae0110f50.js +9 -0
  200. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  201. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  202. package/dist/web/standalone/.next/static/chunks/app/page-b950e4e384cc62b3.js +1 -0
  203. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  204. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  205. package/dist/web/standalone/.next/static/chunks/{webpack-cfc9a116e6450a6b.js → webpack-bca0e732db0dcec3.js} +1 -1
  206. package/dist/web/standalone/.next/static/css/a58ef8a151aa0493.css +1 -0
  207. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  208. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  209. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  210. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  211. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  212. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  213. package/dist/web/standalone/server.js +1 -1
  214. package/dist/wizard.js +4 -1
  215. package/package.json +2 -2
  216. package/packages/mcp-server/README.md +202 -0
  217. package/packages/mcp-server/package.json +36 -0
  218. package/packages/mcp-server/src/cli.ts +68 -0
  219. package/packages/mcp-server/src/index.ts +14 -0
  220. package/packages/mcp-server/src/mcp-server.test.ts +628 -0
  221. package/packages/mcp-server/src/server.ts +278 -0
  222. package/packages/mcp-server/src/session-manager.ts +328 -0
  223. package/packages/mcp-server/src/types.ts +107 -0
  224. package/packages/mcp-server/tsconfig.json +24 -0
  225. package/packages/pi-ai/dist/models.d.ts +14 -3
  226. package/packages/pi-ai/dist/models.d.ts.map +1 -1
  227. package/packages/pi-ai/dist/models.js +53 -10
  228. package/packages/pi-ai/dist/models.js.map +1 -1
  229. package/packages/pi-ai/dist/models.test.js +102 -1
  230. package/packages/pi-ai/dist/models.test.js.map +1 -1
  231. package/packages/pi-ai/dist/types.d.ts +30 -0
  232. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  233. package/packages/pi-ai/dist/types.js.map +1 -1
  234. package/packages/pi-ai/src/models.test.ts +114 -1
  235. package/packages/pi-ai/src/models.ts +70 -13
  236. package/packages/pi-ai/src/types.ts +31 -0
  237. package/packages/pi-coding-agent/dist/cli/args.d.ts +2 -0
  238. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  239. package/packages/pi-coding-agent/dist/cli/args.js +3 -0
  240. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  241. package/packages/pi-coding-agent/dist/core/bash-executor.d.ts.map +1 -1
  242. package/packages/pi-coding-agent/dist/core/bash-executor.js +5 -1
  243. package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  244. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  245. package/packages/pi-coding-agent/dist/core/model-registry.js +9 -4
  246. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  247. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.d.ts +19 -0
  248. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.d.ts.map +1 -0
  249. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.js +83 -0
  250. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.js.map +1 -0
  251. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  252. package/packages/pi-coding-agent/dist/core/tools/bash.js +5 -1
  253. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  254. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  255. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  256. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  257. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  258. package/packages/pi-coding-agent/dist/main.js +5 -3
  259. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  260. package/packages/pi-coding-agent/dist/modes/index.d.ts +1 -1
  261. package/packages/pi-coding-agent/dist/modes/index.d.ts.map +1 -1
  262. package/packages/pi-coding-agent/dist/modes/index.js.map +1 -1
  263. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  264. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +0 -2
  265. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  266. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts +28 -1
  267. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  268. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js +49 -0
  269. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js.map +1 -1
  270. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts +1 -1
  271. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  272. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +114 -6
  273. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  274. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.d.ts +9 -0
  275. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.d.ts.map +1 -0
  276. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.js +831 -0
  277. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.js.map +1 -0
  278. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +66 -0
  279. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  280. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
  281. package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  282. package/packages/pi-coding-agent/dist/utils/shell.js +0 -1
  283. package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
  284. package/packages/pi-coding-agent/package.json +1 -1
  285. package/packages/pi-coding-agent/src/cli/args.ts +4 -0
  286. package/packages/pi-coding-agent/src/core/bash-executor.ts +5 -1
  287. package/packages/pi-coding-agent/src/core/model-registry.ts +10 -3
  288. package/packages/pi-coding-agent/src/core/tools/bash-spawn-windows.test.ts +101 -0
  289. package/packages/pi-coding-agent/src/core/tools/bash.ts +5 -1
  290. package/packages/pi-coding-agent/src/index.ts +3 -0
  291. package/packages/pi-coding-agent/src/main.ts +5 -3
  292. package/packages/pi-coding-agent/src/modes/index.ts +8 -1
  293. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +0 -2
  294. package/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts +54 -1
  295. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +124 -6
  296. package/packages/pi-coding-agent/src/modes/rpc/rpc-protocol-v2.test.ts +971 -0
  297. package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +61 -4
  298. package/packages/pi-coding-agent/src/utils/shell.ts +0 -1
  299. package/packages/rpc-client/package.json +20 -0
  300. package/pkg/package.json +1 -1
  301. package/scripts/ensure-workspace-builds.cjs +36 -8
  302. package/src/resources/extensions/async-jobs/async-bash-tool.ts +22 -11
  303. package/src/resources/extensions/async-jobs/job-manager.ts +4 -1
  304. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +19 -20
  305. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +21 -0
  306. package/src/resources/extensions/get-secrets-from-user.ts +8 -0
  307. package/src/resources/extensions/gsd/auto/phases.ts +44 -7
  308. package/src/resources/extensions/gsd/auto-dispatch.ts +25 -1
  309. package/src/resources/extensions/gsd/auto-start.ts +2 -0
  310. package/src/resources/extensions/gsd/auto-timers.ts +25 -1
  311. package/src/resources/extensions/gsd/auto-tool-tracking.ts +30 -6
  312. package/src/resources/extensions/gsd/auto-worktree.ts +94 -14
  313. package/src/resources/extensions/gsd/auto.ts +31 -4
  314. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +118 -73
  315. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +11 -2
  316. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +1 -1
  317. package/src/resources/extensions/gsd/claude-import.ts +58 -9
  318. package/src/resources/extensions/gsd/commands/handlers/auto.ts +73 -6
  319. package/src/resources/extensions/gsd/commands-config.ts +11 -5
  320. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +4 -4
  321. package/src/resources/extensions/gsd/detection.ts +6 -6
  322. package/src/resources/extensions/gsd/docs/preferences-reference.md +5 -5
  323. package/src/resources/extensions/gsd/error-classifier.ts +139 -0
  324. package/src/resources/extensions/gsd/git-service.ts +4 -3
  325. package/src/resources/extensions/gsd/gitignore.ts +7 -7
  326. package/src/resources/extensions/gsd/gsd-db.ts +355 -63
  327. package/src/resources/extensions/gsd/guided-flow.ts +4 -3
  328. package/src/resources/extensions/gsd/init-wizard.ts +2 -2
  329. package/src/resources/extensions/gsd/key-manager.ts +7 -16
  330. package/src/resources/extensions/gsd/markdown-renderer.ts +5 -4
  331. package/src/resources/extensions/gsd/memory-store.ts +29 -18
  332. package/src/resources/extensions/gsd/milestone-actions.ts +17 -0
  333. package/src/resources/extensions/gsd/parallel-orchestrator.ts +23 -1
  334. package/src/resources/extensions/gsd/preferences-models.ts +1 -13
  335. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  336. package/src/resources/extensions/gsd/preferences.ts +12 -13
  337. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  338. package/src/resources/extensions/gsd/provider-error-pause.ts +0 -57
  339. package/src/resources/extensions/gsd/rule-registry.ts +1 -1
  340. package/src/resources/extensions/gsd/service-tier.ts +14 -2
  341. package/src/resources/extensions/gsd/state.ts +39 -30
  342. package/src/resources/extensions/gsd/status-guards.ts +13 -0
  343. package/src/resources/extensions/gsd/tests/active-milestone-id-guard.test.ts +91 -0
  344. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +1 -1
  345. package/src/resources/extensions/gsd/tests/auto-milestone-target.test.ts +61 -0
  346. package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +87 -0
  347. package/src/resources/extensions/gsd/tests/auto-worktree-auto-resolve.test.ts +80 -0
  348. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +1 -1
  349. package/src/resources/extensions/gsd/tests/claude-import-marketplace-discovery.test.ts +191 -0
  350. package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +1 -1
  351. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +39 -0
  352. package/src/resources/extensions/gsd/tests/commands-config.test.ts +24 -0
  353. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  354. package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +106 -0
  355. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  356. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +35 -7
  357. package/src/resources/extensions/gsd/tests/detection.test.ts +1 -1
  358. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +4 -4
  359. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +1 -1
  360. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +2 -2
  361. package/src/resources/extensions/gsd/tests/empty-db-reconciliation.test.ts +79 -0
  362. package/src/resources/extensions/gsd/tests/git-service.test.ts +65 -31
  363. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
  364. package/src/resources/extensions/gsd/tests/idle-watchdog-stall-override.test.ts +125 -0
  365. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +1 -1
  366. package/src/resources/extensions/gsd/tests/interactive-tool-idle-exemption.test.ts +119 -0
  367. package/src/resources/extensions/gsd/tests/key-manager.test.ts +16 -1
  368. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  369. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  370. package/src/resources/extensions/gsd/tests/milestone-report-path.test.ts +51 -0
  371. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +7 -7
  372. package/src/resources/extensions/gsd/tests/parallel-orchestrator-zombie-cleanup.test.ts +277 -0
  373. package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +85 -0
  374. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +103 -0
  375. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +91 -0
  376. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -2
  377. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +77 -70
  378. package/src/resources/extensions/gsd/tests/rate-limit-model-fallback.test.ts +90 -0
  379. package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +110 -0
  380. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +29 -0
  381. package/src/resources/extensions/gsd/tests/session-lock-transient-read.test.ts +9 -8
  382. package/src/resources/extensions/gsd/tests/stash-pop-gsd-conflict.test.ts +125 -0
  383. package/src/resources/extensions/gsd/tests/status-guards.test.ts +30 -0
  384. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +42 -31
  385. package/src/resources/extensions/gsd/tests/token-cost-display.test.ts +2 -2
  386. package/src/resources/extensions/gsd/tests/vacuous-truth-slices.test.ts +115 -0
  387. package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +90 -0
  388. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +12 -2
  389. package/src/resources/extensions/gsd/tests/validation-gate-patterns.test.ts +124 -0
  390. package/src/resources/extensions/gsd/tests/validation.test.ts +72 -0
  391. package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +81 -1
  392. package/src/resources/extensions/gsd/tests/worktree-preferences-sync.test.ts +130 -0
  393. package/src/resources/extensions/gsd/tools/complete-milestone.ts +7 -17
  394. package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -24
  395. package/src/resources/extensions/gsd/tools/complete-task.ts +13 -25
  396. package/src/resources/extensions/gsd/tools/plan-milestone.ts +30 -32
  397. package/src/resources/extensions/gsd/tools/plan-slice.ts +30 -30
  398. package/src/resources/extensions/gsd/tools/plan-task.ts +26 -26
  399. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +57 -46
  400. package/src/resources/extensions/gsd/tools/reopen-slice.ts +4 -3
  401. package/src/resources/extensions/gsd/tools/reopen-task.ts +5 -4
  402. package/src/resources/extensions/gsd/tools/replan-slice.ts +55 -44
  403. package/src/resources/extensions/gsd/tools/validate-milestone.ts +26 -20
  404. package/src/resources/extensions/gsd/validation.ts +23 -0
  405. package/src/resources/extensions/gsd/workflow-logger.ts +0 -1
  406. package/src/resources/extensions/remote-questions/config.ts +1 -1
  407. package/src/resources/extensions/remote-questions/remote-command.ts +1 -1
  408. package/src/resources/extensions/search-the-web/native-search.ts +1 -1
  409. package/src/resources/extensions/search-the-web/provider.ts +1 -1
  410. package/src/resources/extensions/shared/rtk.ts +22 -4
  411. package/dist/web/standalone/.next/static/chunks/4024.9ad5def014d90ce4.js +0 -9
  412. package/dist/web/standalone/.next/static/chunks/app/page-fbecd1237e2d6d1f.js +0 -1
  413. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  414. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  415. package/dist/web/standalone/.next/static/css/de141508b083f922.css +0 -1
  416. /package/dist/resources/extensions/gsd/templates/{preferences.md → PREFERENCES.md} +0 -0
  417. /package/dist/web/standalone/.next/static/{vkr67v-utm1dgZnbrBWQh → zpvUPKoW5jRAMB_fWHlPi}/_buildManifest.js +0 -0
  418. /package/dist/web/standalone/.next/static/{vkr67v-utm1dgZnbrBWQh → zpvUPKoW5jRAMB_fWHlPi}/_ssgManifest.js +0 -0
  419. /package/src/resources/extensions/gsd/templates/{preferences.md → PREFERENCES.md} +0 -0
@@ -1,4 +1,6 @@
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";
@@ -32,20 +34,6 @@ export interface PlanTaskResult {
32
34
  taskPlanPath: string;
33
35
  }
34
36
 
35
- function isNonEmptyString(value: unknown): value is string {
36
- return typeof value === "string" && value.trim().length > 0;
37
- }
38
-
39
- function validateStringArray(value: unknown, field: string): string[] {
40
- if (!Array.isArray(value)) {
41
- throw new Error(`${field} must be an array`);
42
- }
43
- if (value.some((item) => !isNonEmptyString(item))) {
44
- throw new Error(`${field} must contain only non-empty strings`);
45
- }
46
- return value;
47
- }
48
-
49
37
  function validateParams(params: PlanTaskParams): PlanTaskParams {
50
38
  if (!isNonEmptyString(params?.milestoneId)) throw new Error("milestoneId is required");
51
39
  if (!isNonEmptyString(params?.sliceId)) throw new Error("sliceId is required");
@@ -77,21 +65,29 @@ export async function handlePlanTask(
77
65
  return { error: `validation failed: ${(err as Error).message}` };
78
66
  }
79
67
 
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
- }
68
+ // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
69
+ // Guards must be inside the transaction so the state they check cannot
70
+ // change between the read and the write (#2723).
71
+ let guardError: string | null = null;
92
72
 
93
73
  try {
94
74
  transaction(() => {
75
+ const parentSlice = getSlice(params.milestoneId, params.sliceId);
76
+ if (!parentSlice) {
77
+ guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
78
+ return;
79
+ }
80
+ if (isClosedStatus(parentSlice.status)) {
81
+ guardError = `cannot plan task in a closed slice: ${params.sliceId} (status: ${parentSlice.status})`;
82
+ return;
83
+ }
84
+
85
+ const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
86
+ if (existingTask && isClosedStatus(existingTask.status)) {
87
+ guardError = `cannot re-plan task ${params.taskId}: it is already complete — use gsd_task_reopen first`;
88
+ return;
89
+ }
90
+
95
91
  if (!existingTask) {
96
92
  insertTask({
97
93
  id: params.taskId,
@@ -117,6 +113,10 @@ export async function handlePlanTask(
117
113
  return { error: `db write failed: ${(err as Error).message}` };
118
114
  }
119
115
 
116
+ if (guardError) {
117
+ return { error: guardError };
118
+ }
119
+
120
120
  try {
121
121
  const renderResult = await renderTaskPlanFromDb(basePath, params.milestoneId, params.sliceId, params.taskId);
122
122
  invalidateStateCache();
@@ -1,4 +1,7 @@
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 {
3
6
  transaction,
4
7
  getMilestone,
@@ -14,7 +17,6 @@ import { renderRoadmapFromDb, renderAssessmentFromDb } from "../markdown-rendere
14
17
  import { renderAllProjections } from "../workflow-projections.js";
15
18
  import { writeManifest } from "../workflow-manifest.js";
16
19
  import { appendEvent } from "../workflow-events.js";
17
- import { join } from "node:path";
18
20
 
19
21
  export interface SliceChangeInput {
20
22
  sliceId: string;
@@ -47,9 +49,6 @@ export interface ReassessRoadmapResult {
47
49
  roadmapPath: string;
48
50
  }
49
51
 
50
- function isNonEmptyString(value: unknown): value is string {
51
- return typeof value === "string" && value.trim().length > 0;
52
- }
53
52
 
54
53
  function validateParams(params: ReassessRoadmapParams): ReassessRoadmapParams {
55
54
  if (!isNonEmptyString(params?.milestoneId)) throw new Error("milestoneId is required");
@@ -104,47 +103,6 @@ export async function handleReassessRoadmap(
104
103
  return { error: `validation failed: ${(err as Error).message}` };
105
104
  }
106
105
 
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
106
  // ── Compute assessment artifact path ──────────────────────────────
149
107
  // Assessment lives in the completed slice's directory
150
108
  const assessmentRelPath = join(
@@ -153,9 +111,58 @@ export async function handleReassessRoadmap(
153
111
  `${params.completedSliceId}-ASSESSMENT.md`,
154
112
  );
155
113
 
156
- // ── Transaction: DB mutations ─────────────────────────────────────
114
+ // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
115
+ // Guards must be inside the transaction so the state they check cannot
116
+ // change between the read and the write (#2723).
117
+ let guardError: string | null = null;
118
+
157
119
  try {
158
120
  transaction(() => {
121
+ // Verify milestone exists and is active
122
+ const milestone = getMilestone(params.milestoneId);
123
+ if (!milestone) {
124
+ guardError = `milestone not found: ${params.milestoneId}`;
125
+ return;
126
+ }
127
+ if (isClosedStatus(milestone.status)) {
128
+ guardError = `cannot reassess a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
129
+ return;
130
+ }
131
+
132
+ // Verify completedSliceId is actually complete
133
+ const completedSlice = getSlice(params.milestoneId, params.completedSliceId);
134
+ if (!completedSlice) {
135
+ guardError = `completedSliceId not found: ${params.milestoneId}/${params.completedSliceId}`;
136
+ return;
137
+ }
138
+ if (!isClosedStatus(completedSlice.status)) {
139
+ guardError = `completedSliceId ${params.completedSliceId} is not complete (status: ${completedSlice.status}) — reassess can only be called after a slice finishes`;
140
+ return;
141
+ }
142
+
143
+ // Structural enforcement — reject modifications/removal of completed slices
144
+ const existingSlices = getMilestoneSlices(params.milestoneId);
145
+ const completedSliceIds = new Set<string>();
146
+ for (const slice of existingSlices) {
147
+ if (isClosedStatus(slice.status)) {
148
+ completedSliceIds.add(slice.id);
149
+ }
150
+ }
151
+
152
+ for (const modifiedSlice of params.sliceChanges.modified) {
153
+ if (completedSliceIds.has(modifiedSlice.sliceId)) {
154
+ guardError = `cannot modify completed slice ${modifiedSlice.sliceId}`;
155
+ return;
156
+ }
157
+ }
158
+
159
+ for (const removedId of params.sliceChanges.removed) {
160
+ if (completedSliceIds.has(removedId)) {
161
+ guardError = `cannot remove completed slice ${removedId}`;
162
+ return;
163
+ }
164
+ }
165
+
159
166
  // Record assessment
160
167
  insertAssessment({
161
168
  path: assessmentRelPath,
@@ -198,6 +205,10 @@ export async function handleReassessRoadmap(
198
205
  return { error: `db write failed: ${(err as Error).message}` };
199
206
  }
200
207
 
208
+ if (guardError) {
209
+ return { error: guardError };
210
+ }
211
+
201
212
  // ── Render artifacts ──────────────────────────────────────────────
202
213
  try {
203
214
  const roadmapResult = await renderRoadmapFromDb(basePath, params.milestoneId);
@@ -20,6 +20,7 @@ import {
20
20
  transaction,
21
21
  } from "../gsd-db.js";
22
22
  import { invalidateStateCache } from "../state.js";
23
+ import { isClosedStatus } from "../status-guards.js";
23
24
  import { renderAllProjections } from "../workflow-projections.js";
24
25
  import { writeManifest } from "../workflow-manifest.js";
25
26
  import { appendEvent } from "../workflow-events.js";
@@ -62,8 +63,8 @@ export async function handleReopenSlice(
62
63
  guardError = `milestone not found: ${params.milestoneId}`;
63
64
  return;
64
65
  }
65
- if (milestone.status === "complete" || milestone.status === "done") {
66
- guardError = `cannot reopen slice inside a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
66
+ if (isClosedStatus(milestone.status)) {
67
+ guardError = `cannot reopen slice in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
67
68
  return;
68
69
  }
69
70
 
@@ -72,7 +73,7 @@ export async function handleReopenSlice(
72
73
  guardError = `slice not found: ${params.milestoneId}/${params.sliceId}`;
73
74
  return;
74
75
  }
75
- if (slice.status !== "complete" && slice.status !== "done") {
76
+ if (!isClosedStatus(slice.status)) {
76
77
  guardError = `slice ${params.sliceId} is not complete (status: ${slice.status}) — nothing to reopen`;
77
78
  return;
78
79
  }
@@ -18,6 +18,7 @@ import {
18
18
  transaction,
19
19
  } from "../gsd-db.js";
20
20
  import { invalidateStateCache } from "../state.js";
21
+ import { isClosedStatus } from "../status-guards.js";
21
22
  import { renderAllProjections } from "../workflow-projections.js";
22
23
  import { writeManifest } from "../workflow-manifest.js";
23
24
  import { appendEvent } from "../workflow-events.js";
@@ -63,7 +64,7 @@ export async function handleReopenTask(
63
64
  guardError = `milestone not found: ${params.milestoneId}`;
64
65
  return;
65
66
  }
66
- if (milestone.status === "complete" || milestone.status === "done") {
67
+ if (isClosedStatus(milestone.status)) {
67
68
  guardError = `cannot reopen task in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
68
69
  return;
69
70
  }
@@ -73,8 +74,8 @@ export async function handleReopenTask(
73
74
  guardError = `slice not found: ${params.milestoneId}/${params.sliceId}`;
74
75
  return;
75
76
  }
76
- if (slice.status === "complete" || slice.status === "done") {
77
- guardError = `cannot reopen task inside a closed slice: ${params.sliceId} (status: ${slice.status}) — use gsd_slice_reopen first`;
77
+ if (isClosedStatus(slice.status)) {
78
+ guardError = `cannot reopen task in a closed slice: ${params.sliceId} (status: ${slice.status}) — use gsd_slice_reopen first`;
78
79
  return;
79
80
  }
80
81
 
@@ -83,7 +84,7 @@ export async function handleReopenTask(
83
84
  guardError = `task not found: ${params.milestoneId}/${params.sliceId}/${params.taskId}`;
84
85
  return;
85
86
  }
86
- if (task.status !== "complete" && task.status !== "done") {
87
+ if (!isClosedStatus(task.status)) {
87
88
  guardError = `task ${params.taskId} is not complete (status: ${task.status}) — nothing to reopen`;
88
89
  return;
89
90
  }
@@ -10,6 +10,8 @@ import {
10
10
  deleteTask,
11
11
  } from "../gsd-db.js";
12
12
  import { invalidateStateCache } from "../state.js";
13
+ import { isClosedStatus } from "../status-guards.js";
14
+ import { isNonEmptyString } from "../validation.js";
13
15
  import { renderPlanFromDb, renderReplanFromDb } from "../markdown-renderer.js";
14
16
  import { renderAllProjections } from "../workflow-projections.js";
15
17
  import { writeManifest } from "../workflow-manifest.js";
@@ -48,10 +50,6 @@ export interface ReplanSliceResult {
48
50
  planPath: string;
49
51
  }
50
52
 
51
- function isNonEmptyString(value: unknown): value is string {
52
- return typeof value === "string" && value.trim().length > 0;
53
- }
54
-
55
53
  function validateParams(params: ReplanSliceParams): ReplanSliceParams {
56
54
  if (!isNonEmptyString(params?.milestoneId)) throw new Error("milestoneId is required");
57
55
  if (!isNonEmptyString(params?.sliceId)) throw new Error("sliceId is required");
@@ -90,52 +88,61 @@ export async function handleReplanSlice(
90
88
  return { error: `validation failed: ${(err as Error).message}` };
91
89
  }
92
90
 
93
- // ── Verify parent slice exists and is not closed ─────────────────
94
- const parentSlice = getSlice(params.milestoneId, params.sliceId);
95
- if (!parentSlice) {
96
- return { error: `missing parent slice: ${params.milestoneId}/${params.sliceId}` };
97
- }
98
- if (parentSlice.status === "complete" || parentSlice.status === "done") {
99
- return { error: `cannot replan a closed slice: ${params.sliceId} (status: ${parentSlice.status})` };
100
- }
91
+ // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
92
+ // Guards must be inside the transaction so the state they check cannot
93
+ // change between the read and the write (#2723).
94
+ let guardError: string | null = null;
95
+ let existingTaskIds: Set<string> = new Set();
101
96
 
102
- // ── Verify blocker task exists and is complete ────────────────────
103
- const blockerTask = getTask(params.milestoneId, params.sliceId, params.blockerTaskId);
104
- if (!blockerTask) {
105
- return { error: `blockerTaskId not found: ${params.milestoneId}/${params.sliceId}/${params.blockerTaskId}` };
106
- }
107
- if (blockerTask.status !== "complete" && blockerTask.status !== "done") {
108
- return { error: `blockerTaskId ${params.blockerTaskId} is not complete (status: ${blockerTask.status}) — the blocker task must be finished before a replan is triggered` };
109
- }
97
+ try {
98
+ transaction(() => {
99
+ // Verify parent slice exists and is not closed
100
+ const parentSlice = getSlice(params.milestoneId, params.sliceId);
101
+ if (!parentSlice) {
102
+ guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
103
+ return;
104
+ }
105
+ if (isClosedStatus(parentSlice.status)) {
106
+ guardError = `cannot replan a closed slice: ${params.sliceId} (status: ${parentSlice.status})`;
107
+ return;
108
+ }
110
109
 
111
- // ── Structural enforcement ────────────────────────────────────────
112
- const existingTasks = getSliceTasks(params.milestoneId, params.sliceId);
113
- const completedTaskIds = new Set<string>();
114
- for (const task of existingTasks) {
115
- if (task.status === "complete" || task.status === "done") {
116
- completedTaskIds.add(task.id);
117
- }
118
- }
110
+ // Verify blocker task exists and is complete
111
+ const blockerTask = getTask(params.milestoneId, params.sliceId, params.blockerTaskId);
112
+ if (!blockerTask) {
113
+ guardError = `blockerTaskId not found: ${params.milestoneId}/${params.sliceId}/${params.blockerTaskId}`;
114
+ return;
115
+ }
116
+ if (!isClosedStatus(blockerTask.status)) {
117
+ guardError = `blockerTaskId ${params.blockerTaskId} is not complete (status: ${blockerTask.status}) — the blocker task must be finished before a replan is triggered`;
118
+ return;
119
+ }
119
120
 
120
- // Reject updates to completed tasks
121
- for (const updatedTask of params.updatedTasks) {
122
- if (completedTaskIds.has(updatedTask.taskId)) {
123
- return { error: `cannot modify completed task ${updatedTask.taskId}` };
124
- }
125
- }
121
+ // Structural enforcement reject modifications/removal of completed tasks
122
+ const existingTasks = getSliceTasks(params.milestoneId, params.sliceId);
123
+ const completedTaskIds = new Set<string>();
124
+ for (const task of existingTasks) {
125
+ if (isClosedStatus(task.status)) {
126
+ completedTaskIds.add(task.id);
127
+ }
128
+ }
126
129
 
127
- // Reject removal of completed tasks
128
- for (const removedId of params.removedTaskIds) {
129
- if (completedTaskIds.has(removedId)) {
130
- return { error: `cannot remove completed task ${removedId}` };
131
- }
132
- }
130
+ for (const updatedTask of params.updatedTasks) {
131
+ if (completedTaskIds.has(updatedTask.taskId)) {
132
+ guardError = `cannot modify completed task ${updatedTask.taskId}`;
133
+ return;
134
+ }
135
+ }
133
136
 
134
- // ── Transaction: DB mutations ─────────────────────────────────────
135
- const existingTaskIds = new Set(existingTasks.map((t) => t.id));
137
+ for (const removedId of params.removedTaskIds) {
138
+ if (completedTaskIds.has(removedId)) {
139
+ guardError = `cannot remove completed task ${removedId}`;
140
+ return;
141
+ }
142
+ }
143
+
144
+ existingTaskIds = new Set(existingTasks.map((t) => t.id));
136
145
 
137
- try {
138
- transaction(() => {
139
146
  // Record replan history
140
147
  insertReplanHistory({
141
148
  milestoneId: params.milestoneId,
@@ -189,6 +196,10 @@ export async function handleReplanSlice(
189
196
  return { error: `db write failed: ${(err as Error).message}` };
190
197
  }
191
198
 
199
+ if (guardError) {
200
+ return { error: guardError };
201
+ }
202
+
192
203
  // ── Render artifacts ──────────────────────────────────────────────
193
204
  try {
194
205
  const renderResult = await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);
@@ -9,7 +9,8 @@ import { join } from "node:path";
9
9
 
10
10
  import {
11
11
  transaction,
12
- _getAdapter,
12
+ insertAssessment,
13
+ deleteAssessmentByScope,
13
14
  } from "../gsd-db.js";
14
15
  import { resolveMilestonePath, clearPathCache } from "../paths.js";
15
16
  import { saveFile, clearParseCache } from "../files.js";
@@ -76,7 +77,7 @@ export async function handleValidateMilestone(
76
77
  return { error: `verdict must be one of: ${VALIDATION_VERDICTS.join(", ")}` };
77
78
  }
78
79
 
79
- // ── Filesystem render ──────────────────────────────────────────────────
80
+ // ── Resolve paths and render markdown ────────────────────────────────
80
81
  const validationMd = renderValidationMarkdown(params);
81
82
 
82
83
  let validationPath: string;
@@ -89,32 +90,37 @@ export async function handleValidateMilestone(
89
90
  validationPath = join(manualDir, `${params.milestoneId}-VALIDATION.md`);
90
91
  }
91
92
 
93
+ // ── DB write first — matches complete-task/complete-slice pattern ───
94
+ // Write DB before disk so a crash between the two leaves a recoverable
95
+ // state: the DB row exists but the file is missing, which projection
96
+ // rendering can regenerate. The inverse (file exists, no DB row) is
97
+ // harder to detect and recover from (#2725).
98
+ const validatedAt = new Date().toISOString();
99
+
100
+ transaction(() => {
101
+ insertAssessment({
102
+ path: validationPath,
103
+ milestoneId: params.milestoneId,
104
+ sliceId: null,
105
+ taskId: null,
106
+ status: params.verdict,
107
+ scope: 'milestone-validation',
108
+ fullContent: validationMd,
109
+ });
110
+ });
111
+
112
+ // ── Filesystem render (outside transaction) ────────────────────────────
113
+ // If disk render fails, roll back the DB row so state stays consistent.
92
114
  try {
93
115
  await saveFile(validationPath, validationMd);
94
116
  } catch (renderErr) {
95
117
  process.stderr.write(
96
- `gsd-db: validate_milestone — disk render failed: ${(renderErr as Error).message}\n`,
118
+ `gsd-db: validate_milestone — disk render failed, rolling back DB row: ${(renderErr as Error).message}\n`,
97
119
  );
120
+ deleteAssessmentByScope(params.milestoneId, 'milestone-validation');
98
121
  return { error: `disk render failed: ${(renderErr as Error).message}` };
99
122
  }
100
123
 
101
- // ── DB write — store in assessments table ──────────────────────────────
102
- const validatedAt = new Date().toISOString();
103
-
104
- transaction(() => {
105
- const adapter = _getAdapter()!;
106
- adapter.prepare(
107
- `INSERT OR REPLACE INTO assessments (path, milestone_id, slice_id, task_id, status, scope, full_content, created_at)
108
- VALUES (:path, :mid, NULL, NULL, :verdict, 'milestone-validation', :content, :created_at)`,
109
- ).run({
110
- ":path": validationPath,
111
- ":mid": params.milestoneId,
112
- ":verdict": params.verdict,
113
- ":content": validationMd,
114
- ":created_at": validatedAt,
115
- });
116
- });
117
-
118
124
  invalidateStateCache();
119
125
  clearPathCache();
120
126
  clearParseCache();
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared input-validation primitives for GSD tool handlers.
3
+ */
4
+
5
+ /** Type guard: value is a string with at least one non-whitespace character. */
6
+ export function isNonEmptyString(value: unknown): value is string {
7
+ return typeof value === "string" && value.trim().length > 0;
8
+ }
9
+
10
+ /**
11
+ * Validate that `value` is an array of non-empty strings.
12
+ * Throws with a message referencing `field` on failure.
13
+ * Returns the validated array (narrowed to string[]).
14
+ */
15
+ export function validateStringArray(value: unknown, field: string): string[] {
16
+ if (!Array.isArray(value)) {
17
+ throw new Error(`${field} must be an array`);
18
+ }
19
+ if (value.some((item) => !isNonEmptyString(item))) {
20
+ throw new Error(`${field} must contain only non-empty strings`);
21
+ }
22
+ return value;
23
+ }
@@ -199,7 +199,6 @@ export function readAuditLog(basePath?: string): LogEntry[] {
199
199
  */
200
200
  export function _resetLogs(): void {
201
201
  _buffer = [];
202
- _auditBasePath = null;
203
202
  }
204
203
 
205
204
  // ─── Internal ───────────────────────────────────────────────────────────
@@ -59,7 +59,7 @@ function hydrateRemoteTokensFromAuth(): void {
59
59
  for (const [providerId, envVar] of needed) {
60
60
  try {
61
61
  const creds = auth.getCredentialsForProvider(providerId);
62
- const apiKeyCred = creds.find((c: { type: string }) => c.type === "api_key") as
62
+ const apiKeyCred = creds.find((c: { type: string; key?: string }) => c.type === "api_key" && !!c.key) as
63
63
  | { type: "api_key"; key: string }
64
64
  | undefined;
65
65
  if (apiKeyCred?.key) {
@@ -312,7 +312,7 @@ function saveProviderToken(provider: string, token: string): void {
312
312
 
313
313
  function removeProviderToken(provider: string): void {
314
314
  const auth = getAuthStorage();
315
- auth.set(provider, { type: "api_key", key: "" });
315
+ auth.remove(provider);
316
316
  }
317
317
 
318
318
  export function saveRemoteQuestionsConfig(channel: "slack" | "discord" | "telegram", channelId: string): void {
@@ -28,7 +28,7 @@ export const MAX_NATIVE_SEARCHES_PER_SESSION = 15;
28
28
 
29
29
  /** When true, skip native web search injection and keep Brave/custom tools active on Anthropic. */
30
30
  export function preferBraveSearch(): boolean {
31
- // preferences.md takes priority over env var
31
+ // PREFERENCES.md takes priority over env var
32
32
  const prefsPref = resolveSearchProviderFromPreferences();
33
33
  if (prefsPref === "brave" || prefsPref === "tavily" || prefsPref === "ollama") return true;
34
34
  if (prefsPref === "native") return false;
@@ -105,7 +105,7 @@ export function resolveSearchProvider(overridePreference?: string): SearchProvid
105
105
  if (overridePreference && VALID_PREFERENCES.has(overridePreference)) {
106
106
  pref = overridePreference as SearchProviderPreference
107
107
  } else {
108
- // preferences.md takes priority over auth.json
108
+ // PREFERENCES.md takes priority over auth.json
109
109
  const mdPref = resolveSearchProviderFromPreferences()
110
110
  if (mdPref && mdPref !== 'auto' && mdPref !== 'native') {
111
111
  pref = mdPref as SearchProviderPreference
@@ -5,6 +5,7 @@ import { delimiter, join } from "node:path";
5
5
 
6
6
  const GSD_RTK_PATH_ENV = "GSD_RTK_PATH";
7
7
  const GSD_RTK_DISABLED_ENV = "GSD_RTK_DISABLED";
8
+ const GSD_RTK_REWRITE_TIMEOUT_MS_ENV = "GSD_RTK_REWRITE_TIMEOUT_MS";
8
9
  const RTK_TELEMETRY_DISABLED_ENV = "RTK_TELEMETRY_DISABLED";
9
10
  const RTK_REWRITE_TIMEOUT_MS = 5_000;
10
11
 
@@ -14,6 +15,14 @@ function isTruthy(value: string | undefined): boolean {
14
15
  return normalized === "1" || normalized === "true" || normalized === "yes";
15
16
  }
16
17
 
18
+ function getRewriteTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
19
+ const configured = Number.parseInt(env[GSD_RTK_REWRITE_TIMEOUT_MS_ENV] ?? "", 10);
20
+ if (Number.isFinite(configured) && configured > 0) {
21
+ return configured;
22
+ }
23
+ return RTK_REWRITE_TIMEOUT_MS;
24
+ }
25
+
17
26
  export function isRtkEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
18
27
  return !isTruthy(env[GSD_RTK_DISABLED_ENV]);
19
28
  }
@@ -96,18 +105,27 @@ export function resolveRtkBinaryPath(options: ResolveRtkBinaryPathOptions = {}):
96
105
  return resolveSystemRtkPath(options.pathValue ?? getPathValue(env), platform);
97
106
  }
98
107
 
99
- export function rewriteCommandWithRtk(command: string, env: NodeJS.ProcessEnv = process.env): string {
108
+ interface RewriteCommandOptions {
109
+ binaryPath?: string;
110
+ env?: NodeJS.ProcessEnv;
111
+ spawnSyncImpl?: typeof spawnSync;
112
+ }
113
+
114
+ export function rewriteCommandWithRtk(command: string, options: RewriteCommandOptions = {}): string {
115
+ const env = options.env ?? process.env;
116
+
100
117
  if (!command.trim()) return command;
101
118
  if (!isRtkEnabled(env)) return command;
102
119
 
103
- const binaryPath = resolveRtkBinaryPath({ env });
120
+ const binaryPath = options.binaryPath ?? resolveRtkBinaryPath({ env });
104
121
  if (!binaryPath) return command;
105
122
 
106
- const result = spawnSync(binaryPath, ["rewrite", command], {
123
+ const run = options.spawnSyncImpl ?? spawnSync;
124
+ const result = run(binaryPath, ["rewrite", command], {
107
125
  encoding: "utf-8",
108
126
  env: buildRtkEnv(env),
109
127
  stdio: ["ignore", "pipe", "ignore"],
110
- timeout: RTK_REWRITE_TIMEOUT_MS,
128
+ timeout: getRewriteTimeoutMs(env),
111
129
  // .cmd/.bat wrappers (used by fake-rtk in tests) require shell:true on Windows
112
130
  shell: /\.(cmd|bat)$/i.test(binaryPath),
113
131
  });