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
@@ -15,6 +15,7 @@ import { join } from "node:path";
15
15
  import { resolveMilestonePath, resolveMilestoneFile, buildMilestoneFileName, } from "./paths.js";
16
16
  import { invalidateAllCaches } from "./cache.js";
17
17
  import { loadQueueOrder, saveQueueOrder } from "./queue-order.js";
18
+ import { isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
18
19
  // ─── Park ──────────────────────────────────────────────────────────────────
19
20
  /**
20
21
  * Park a milestone — creates a PARKED.md marker file with reason and timestamp.
@@ -44,6 +45,15 @@ export function parkMilestone(basePath, milestoneId, reason) {
44
45
  "",
45
46
  ].join("\n");
46
47
  writeFileSync(parkedPath, content, "utf-8");
48
+ // Sync DB status so deriveStateFromDb also skips this milestone (#2694)
49
+ if (isDbAvailable()) {
50
+ try {
51
+ updateMilestoneStatus(milestoneId, "parked");
52
+ }
53
+ catch (err) {
54
+ process.stderr.write(`gsd: parkMilestone DB sync failed for ${milestoneId}: ${err.message}\n`);
55
+ }
56
+ }
47
57
  invalidateAllCaches();
48
58
  return true;
49
59
  }
@@ -60,6 +70,15 @@ export function unparkMilestone(basePath, milestoneId) {
60
70
  if (!existsSync(parkedPath))
61
71
  return false; // not parked
62
72
  unlinkSync(parkedPath);
73
+ // Sync DB status so deriveStateFromDb picks up the unparked milestone (#2694)
74
+ if (isDbAvailable()) {
75
+ try {
76
+ updateMilestoneStatus(milestoneId, "active");
77
+ }
78
+ catch (err) {
79
+ process.stderr.write(`gsd: unparkMilestone DB sync failed for ${milestoneId}: ${err.message}\n`);
80
+ }
81
+ }
63
82
  invalidateAllCaches();
64
83
  return true;
65
84
  }
@@ -98,18 +98,6 @@ export function getNextFallbackModel(currentModelId, modelConfig) {
98
98
  return modelsToTry[0];
99
99
  }
100
100
  }
101
- /**
102
- * Detect whether an error message indicates a transient network error
103
- * (worth retrying the same model) vs a permanent provider error
104
- * (auth failure, quota exceeded, etc. -- should fall back immediately).
105
- */
106
- export function isTransientNetworkError(errorMsg) {
107
- if (!errorMsg)
108
- return false;
109
- const hasNetworkSignal = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fetch failed|connection.*reset|dns/i.test(errorMsg);
110
- const hasPermanentSignal = /auth|unauthorized|forbidden|invalid.*key|quota|billing/i.test(errorMsg);
111
- return hasNetworkSignal && !hasPermanentSignal;
112
- }
113
101
  /**
114
102
  * Validate a model ID string.
115
103
  * Returns true if the ID looks like a valid model identifier.
@@ -273,7 +261,7 @@ export function resolveContextSelection() {
273
261
  return profile === "budget" ? "smart" : "full";
274
262
  }
275
263
  /**
276
- * Resolve the search provider preference from preferences.md.
264
+ * Resolve the search provider preference from PREFERENCES.md.
277
265
  * Returns undefined if not configured (caller falls back to existing behavior).
278
266
  */
279
267
  export function resolveSearchProviderFromPreferences() {
@@ -24,27 +24,27 @@ export { validatePreferences } from "./preferences-validation.js";
24
24
  // ─── Re-exports: skills ─────────────────────────────────────────────────────
25
25
  export { resolveAllSkillReferences, resolveSkillDiscoveryMode, resolveSkillStalenessDays, } from "./preferences-skills.js";
26
26
  // ─── Re-exports: models ─────────────────────────────────────────────────────
27
- export { resolveModelForUnit, resolveModelWithFallbacksForUnit, getNextFallbackModel, isTransientNetworkError, validateModelId, updatePreferencesModels, resolveDynamicRoutingConfig, resolveAutoSupervisorConfig, resolveProfileDefaults, resolveEffectiveProfile, resolveInlineLevel, resolveContextSelection, resolveSearchProviderFromPreferences, } from "./preferences-models.js";
27
+ export { resolveModelForUnit, resolveModelWithFallbacksForUnit, getNextFallbackModel, validateModelId, updatePreferencesModels, resolveDynamicRoutingConfig, resolveAutoSupervisorConfig, resolveProfileDefaults, resolveEffectiveProfile, resolveInlineLevel, resolveContextSelection, resolveSearchProviderFromPreferences, } from "./preferences-models.js";
28
28
  // ─── Path Constants & Getters ───────────────────────────────────────────────
29
29
  function gsdHome() {
30
30
  return process.env.GSD_HOME || join(homedir(), ".gsd");
31
31
  }
32
32
  function globalPreferencesPath() {
33
- return join(gsdHome(), "preferences.md");
33
+ return join(gsdHome(), "PREFERENCES.md");
34
34
  }
35
35
  function legacyGlobalPreferencesPath() {
36
36
  return join(homedir(), ".pi", "agent", "gsd-preferences.md");
37
37
  }
38
38
  function projectPreferencesPath() {
39
- return join(gsdRoot(process.cwd()), "preferences.md");
39
+ return join(gsdRoot(process.cwd()), "PREFERENCES.md");
40
40
  }
41
- // Bootstrap in gitignore.ts historically created PREFERENCES.md (uppercase) by mistake.
42
- // Check uppercase as a fallback so those files aren't silently ignored.
43
- function globalPreferencesPathUppercase() {
44
- return join(gsdHome(), "PREFERENCES.md");
41
+ // Legacy: older versions used lowercase preferences.md.
42
+ // Check lowercase as a fallback so those files aren't silently ignored.
43
+ function globalPreferencesPathLegacy() {
44
+ return join(gsdHome(), "preferences.md");
45
45
  }
46
- function projectPreferencesPathUppercase() {
47
- return join(gsdRoot(process.cwd()), "PREFERENCES.md");
46
+ function projectPreferencesPathLegacy() {
47
+ return join(gsdRoot(process.cwd()), "preferences.md");
48
48
  }
49
49
  export function getGlobalGSDPreferencesPath() {
50
50
  return globalPreferencesPath();
@@ -58,12 +58,12 @@ export function getProjectGSDPreferencesPath() {
58
58
  // ─── Loading ────────────────────────────────────────────────────────────────
59
59
  export function loadGlobalGSDPreferences() {
60
60
  return loadPreferencesFile(globalPreferencesPath(), "global")
61
- ?? loadPreferencesFile(globalPreferencesPathUppercase(), "global")
61
+ ?? loadPreferencesFile(globalPreferencesPathLegacy(), "global")
62
62
  ?? loadPreferencesFile(legacyGlobalPreferencesPath(), "global");
63
63
  }
64
64
  export function loadProjectGSDPreferences() {
65
65
  return loadPreferencesFile(projectPreferencesPath(), "project")
66
- ?? loadPreferencesFile(projectPreferencesPathUppercase(), "project");
66
+ ?? loadPreferencesFile(projectPreferencesPathLegacy(), "project");
67
67
  }
68
68
  export function loadEffectiveGSDPreferences() {
69
69
  const globalPreferences = loadGlobalGSDPreferences();
@@ -149,7 +149,7 @@ export function parsePreferencesMarkdown(content) {
149
149
  }
150
150
  if (!_warnedUnrecognizedFormat) {
151
151
  _warnedUnrecognizedFormat = true;
152
- console.warn("[parsePreferencesMarkdown] preferences.md exists but uses an unrecognized format — skipping.");
152
+ console.warn("[parsePreferencesMarkdown] PREFERENCES.md exists but uses an unrecognized format — skipping.");
153
153
  }
154
154
  return null;
155
155
  }
@@ -398,7 +398,7 @@ export function resolvePreDispatchHooks() {
398
398
  * Resolve the effective git isolation mode from preferences.
399
399
  * Returns "none" (default), "worktree", or "branch".
400
400
  *
401
- * Default is "none" so GSD works out of the box without preferences.md.
401
+ * Default is "none" so GSD works out of the box without PREFERENCES.md.
402
402
  * Worktree isolation requires explicit opt-in because it depends on git
403
403
  * branch infrastructure that must be set up before use.
404
404
  */
@@ -92,7 +92,7 @@ Titles live inside file content (headings, frontmatter), not in file or director
92
92
 
93
93
  ### Isolation Model
94
94
 
95
- Auto-mode supports three isolation modes (configured in `.gsd/preferences.md` under `taskIsolation.mode`):
95
+ Auto-mode supports three isolation modes (configured in `.gsd/PREFERENCES.md` under `taskIsolation.mode`):
96
96
 
97
97
  - **worktree** (default): Work happens in `.gsd/worktrees/<MID>/`, a full git worktree on the `milestone/<MID>` branch. Each worktree has its own working copy and `.gsd/` directory. Squash-merged back to the integration branch on milestone completion.
98
98
  - **branch**: Work happens in the project root on a `milestone/<MID>` branch. No worktree directory — files are checked out in-place.
@@ -1,47 +1,3 @@
1
- /**
2
- * Classify a provider error as transient (auto-resume) or permanent (manual resume).
3
- *
4
- * Transient: rate limits, server errors (500/502/503), overloaded, internal errors.
5
- * These are expected to self-resolve and should auto-resume after a delay.
6
- *
7
- * Permanent: auth errors, invalid API key, billing issues.
8
- * These require user intervention and should pause indefinitely.
9
- */
10
- export function classifyProviderError(errorMsg) {
11
- const isRateLimit = /rate.?limit|too many requests|429/i.test(errorMsg);
12
- const isServerError = /internal server error|500|502|503|overloaded|server_error|api_error|service.?unavailable/i.test(errorMsg);
13
- // Connection/process errors — transient, auto-resume after brief backoff (#2309).
14
- // These indicate the process was killed, the connection was reset, or a network
15
- // blip occurred. They are NOT permanent failures.
16
- const isConnectionError = /terminated|connection.?reset|connection.?refused|other side closed|fetch failed|network.?(?:is\s+)?unavailable|ECONNREFUSED|ECONNRESET|EPIPE/i.test(errorMsg);
17
- // Permanent errors — never auto-resume
18
- const isPermanent = /auth|unauthorized|forbidden|invalid.*key|invalid.*api|billing|quota exceeded|account/i.test(errorMsg);
19
- if (isPermanent && !isRateLimit) {
20
- return { isTransient: false, isRateLimit: false, suggestedDelayMs: 0 };
21
- }
22
- if (isRateLimit) {
23
- // Try to extract retry-after from the message
24
- const resetMatch = errorMsg.match(/reset in (\d+)s/i);
25
- const delayMs = resetMatch ? Number(resetMatch[1]) * 1000 : 60_000; // default 60s for rate limits
26
- return { isTransient: true, isRateLimit: true, suggestedDelayMs: delayMs };
27
- }
28
- if (isServerError) {
29
- return { isTransient: true, isRateLimit: false, suggestedDelayMs: 30_000 }; // 30s for server errors
30
- }
31
- if (isConnectionError) {
32
- return { isTransient: true, isRateLimit: false, suggestedDelayMs: 15_000 }; // 15s for connection errors
33
- }
34
- // Stream-truncation JSON parse errors — transient (#2572).
35
- // When the API stream is cut mid-chunk, pi tries to reassemble the partial
36
- // tool-call JSON and gets a SyntaxError. This is the downstream symptom of
37
- // a connection drop — same root cause as ECONNRESET, one layer up.
38
- const isMalformedStream = /Unexpected end of JSON|Unexpected token.*JSON|Expected double-quoted property name|SyntaxError.*JSON/i.test(errorMsg);
39
- if (isMalformedStream) {
40
- return { isTransient: true, isRateLimit: false, suggestedDelayMs: 15_000 }; // 15s, same as connection errors
41
- }
42
- // Unknown error — treat as permanent (user reviews)
43
- return { isTransient: false, isRateLimit: false, suggestedDelayMs: 0 };
44
- }
45
1
  /**
46
2
  * Pause auto-mode due to a provider error.
47
3
  *
@@ -426,7 +426,7 @@ export class RuleRegistry {
426
426
  formatHookStatus() {
427
427
  const entries = this.getHookStatus();
428
428
  if (entries.length === 0) {
429
- return "No hooks configured. Add post_unit_hooks or pre_dispatch_hooks to .gsd/preferences.md";
429
+ return "No hooks configured. Add post_unit_hooks or pre_dispatch_hooks to .gsd/PREFERENCES.md";
430
430
  }
431
431
  const lines = ["Configured Hooks:", ""];
432
432
  const postHooks = entries.filter(e => e.type === "post");
@@ -13,16 +13,27 @@ import { getGlobalGSDPreferencesPath, loadEffectiveGSDPreferences, loadGlobalGSD
13
13
  import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
14
14
  const SERVICE_TIER_SCOPE_NOTE = "Only affects gpt-5.4 models, regardless of provider.";
15
15
  // ─── Gating ──────────────────────────────────────────────────────────────────
16
+ /**
17
+ * Model ID prefixes (bare, without provider) that support OpenAI service tiers.
18
+ *
19
+ * This list is the fallback for callers that only have a model ID string.
20
+ * The authoritative source of truth is `model.capabilities.supportsServiceTier`
21
+ * (set via CAPABILITY_PATCHES in packages/pi-ai/src/models.ts). When callers
22
+ * have access to the full Model object, prefer reading capabilities directly.
23
+ *
24
+ * See: https://github.com/gsd-build/gsd-2/issues/2546
25
+ */
26
+ const SERVICE_TIER_MODEL_PREFIXES = ["gpt-5.4"];
16
27
  /**
17
28
  * Returns true when the given model ID supports OpenAI service tiers.
18
- * Currently only gpt-5.4 variants qualify.
29
+ * Reads from SERVICE_TIER_MODEL_PREFIXES update that list, not this function.
19
30
  */
20
31
  export function supportsServiceTier(modelId) {
21
32
  if (!modelId)
22
33
  return false;
23
34
  // Strip provider prefix if present (e.g. "openai/gpt-5.4" → "gpt-5.4")
24
35
  const bare = modelId.includes("/") ? modelId.split("/").pop() : modelId;
25
- return bare.startsWith("gpt-5.4");
36
+ return SERVICE_TIER_MODEL_PREFIXES.some((prefix) => bare.startsWith(prefix));
26
37
  }
27
38
  // ─── Status Formatting ───────────────────────────────────────────────────────
28
39
  /**
@@ -144,7 +144,23 @@ export async function deriveState(basePath) {
144
144
  let result;
145
145
  // Dual-path: try DB-backed derivation first when hierarchy tables are populated
146
146
  if (isDbAvailable()) {
147
- const dbMilestones = getAllMilestones();
147
+ let dbMilestones = getAllMilestones();
148
+ // Disk→DB reconciliation (#2631): when the milestones table is empty
149
+ // (e.g. failed initial migration per #2529), the reconciliation code
150
+ // inside deriveStateFromDb is unreachable. Populate from disk here so
151
+ // the DB path activates correctly.
152
+ if (dbMilestones.length === 0) {
153
+ const diskIds = findMilestoneIds(basePath);
154
+ let synced = false;
155
+ for (const diskId of diskIds) {
156
+ if (!isGhostMilestone(basePath, diskId)) {
157
+ insertMilestone({ id: diskId, status: 'active' });
158
+ synced = true;
159
+ }
160
+ }
161
+ if (synced)
162
+ dbMilestones = getAllMilestones();
163
+ }
148
164
  if (dbMilestones.length > 0) {
149
165
  const stopDbTimer = debugTime("derive-state-db");
150
166
  result = await deriveStateFromDb(basePath);
@@ -465,7 +481,10 @@ export async function deriveStateFromDb(basePath) {
465
481
  };
466
482
  }
467
483
  // ── All slices done → validating/completing ─────────────────────────
468
- const allSlicesDone = activeMilestoneSlices.every(s => isStatusDone(s.status));
484
+ // Guard: [].every() === true (vacuous truth). Without the length check,
485
+ // an empty slice array causes a premature phase transition to
486
+ // validating-milestone. See: https://github.com/gsd-build/gsd-2/issues/2667
487
+ const allSlicesDone = activeMilestoneSlices.length > 0 && activeMilestoneSlices.every(s => isStatusDone(s.status));
469
488
  if (allSlicesDone) {
470
489
  const validationFile = resolveMilestoneFile(basePath, activeMilestone.id, "VALIDATION");
471
490
  const validationContent = validationFile ? await loadFile(validationFile) : null;
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { join } from "node:path";
9
9
  import { mkdirSync } from "node:fs";
10
- import { transaction, getMilestone, getMilestoneSlices, getSliceTasks, _getAdapter, } from "../gsd-db.js";
10
+ import { transaction, getMilestone, getMilestoneSlices, getSliceTasks, updateMilestoneStatus, } from "../gsd-db.js";
11
11
  import { resolveMilestonePath, clearPathCache } from "../paths.js";
12
12
  import { saveFile, clearParseCache } from "../files.js";
13
13
  import { invalidateStateCache } from "../state.js";
@@ -116,11 +116,7 @@ export async function handleCompleteMilestone(params, basePath) {
116
116
  }
117
117
  }
118
118
  // All guards passed — perform write
119
- const adapter = _getAdapter();
120
- adapter.prepare(`UPDATE milestones SET status = 'complete', completed_at = :completed_at WHERE id = :mid`).run({
121
- ":completed_at": completedAt,
122
- ":mid": params.milestoneId,
123
- });
119
+ updateMilestoneStatus(params.milestoneId, 'complete', completedAt);
124
120
  });
125
121
  if (guardError) {
126
122
  return { error: guardError };
@@ -144,10 +140,7 @@ export async function handleCompleteMilestone(params, basePath) {
144
140
  catch (renderErr) {
145
141
  // Disk render failed — roll back DB status so state stays consistent
146
142
  process.stderr.write(`gsd-db: complete_milestone — disk render failed, rolling back DB status: ${renderErr.message}\n`);
147
- const rollbackAdapter = _getAdapter();
148
- if (rollbackAdapter) {
149
- rollbackAdapter.prepare(`UPDATE milestones SET status = 'active', completed_at = NULL WHERE id = :mid`).run({ ":mid": params.milestoneId });
150
- }
143
+ updateMilestoneStatus(params.milestoneId, 'active', null);
151
144
  invalidateStateCache();
152
145
  return { error: `disk render failed: ${renderErr.message}` };
153
146
  }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { join } from "node:path";
10
10
  import { mkdirSync } from "node:fs";
11
- import { transaction, insertMilestone, insertSlice, getSlice, getSliceTasks, getMilestone, updateSliceStatus, _getAdapter, } from "../gsd-db.js";
11
+ import { transaction, insertMilestone, insertSlice, getSlice, getSliceTasks, getMilestone, updateSliceStatus, setSliceSummaryMd, } from "../gsd-db.js";
12
12
  import { resolveSlicePath, clearPathCache } from "../paths.js";
13
13
  import { checkOwnership, sliceUnitKey } from "../unit-ownership.js";
14
14
  import { saveFile, clearParseCache } from "../files.js";
@@ -240,26 +240,12 @@ export async function handleCompleteSlice(params, basePath) {
240
240
  catch (renderErr) {
241
241
  // Disk render failed — roll back DB status so state stays consistent
242
242
  process.stderr.write(`gsd-db: complete_slice — disk render failed, rolling back DB status: ${renderErr.message}\n`);
243
- const rollbackAdapter = _getAdapter();
244
- if (rollbackAdapter) {
245
- rollbackAdapter.prepare(`UPDATE slices SET status = 'pending' WHERE milestone_id = :mid AND id = :sid`).run({
246
- ":mid": params.milestoneId,
247
- ":sid": params.sliceId,
248
- });
249
- }
243
+ updateSliceStatus(params.milestoneId, params.sliceId, 'pending');
250
244
  invalidateStateCache();
251
245
  return { error: `disk render failed: ${renderErr.message}` };
252
246
  }
253
247
  // Store rendered markdown in DB for D004 recovery
254
- const adapter = _getAdapter();
255
- if (adapter) {
256
- adapter.prepare(`UPDATE slices SET full_summary_md = :summary_md, full_uat_md = :uat_md WHERE milestone_id = :mid AND id = :sid`).run({
257
- ":summary_md": summaryMd,
258
- ":uat_md": uatMd,
259
- ":mid": params.milestoneId,
260
- ":sid": params.sliceId,
261
- });
262
- }
248
+ setSliceSummaryMd(params.milestoneId, params.sliceId, summaryMd, uatMd);
263
249
  // Invalidate all caches
264
250
  invalidateStateCache();
265
251
  clearPathCache();
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { join } from "node:path";
10
10
  import { mkdirSync } from "node:fs";
11
- import { transaction, insertMilestone, insertSlice, insertTask, insertVerificationEvidence, getMilestone, getSlice, getTask, _getAdapter, } from "../gsd-db.js";
11
+ import { transaction, insertMilestone, insertSlice, insertTask, insertVerificationEvidence, getMilestone, getSlice, getTask, updateTaskStatus, setTaskSummaryMd, deleteVerificationEvidence, } from "../gsd-db.js";
12
12
  import { resolveSliceFile, resolveTasksDir, clearPathCache } from "../paths.js";
13
13
  import { checkOwnership, taskUnitKey } from "../unit-ownership.js";
14
14
  import { saveFile, clearParseCache } from "../files.js";
@@ -202,27 +202,16 @@ export async function handleCompleteTask(params, basePath) {
202
202
  catch (renderErr) {
203
203
  // Disk render failed — roll back DB status so state stays consistent
204
204
  process.stderr.write(`gsd-db: complete_task — disk render failed, rolling back DB status: ${renderErr.message}\n`);
205
- const rollbackAdapter = _getAdapter();
206
- if (rollbackAdapter) {
207
- rollbackAdapter.prepare(`UPDATE tasks SET status = 'pending' WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`).run({
208
- ":mid": params.milestoneId,
209
- ":sid": params.sliceId,
210
- ":tid": params.taskId,
211
- });
212
- }
205
+ // Delete orphaned verification_evidence rows first (FK constraint
206
+ // references tasks, so evidence must go before status change).
207
+ // Without this, retries accumulate duplicate evidence rows (#2724).
208
+ deleteVerificationEvidence(params.milestoneId, params.sliceId, params.taskId);
209
+ updateTaskStatus(params.milestoneId, params.sliceId, params.taskId, 'pending');
213
210
  invalidateStateCache();
214
211
  return { error: `disk render failed: ${renderErr.message}` };
215
212
  }
216
213
  // Store rendered markdown in DB for D004 recovery
217
- const adapter = _getAdapter();
218
- if (adapter) {
219
- adapter.prepare(`UPDATE tasks SET full_summary_md = :md WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`).run({
220
- ":md": summaryMd,
221
- ":mid": params.milestoneId,
222
- ":sid": params.sliceId,
223
- ":tid": params.taskId,
224
- });
225
- }
214
+ setTaskSummaryMd(params.milestoneId, params.sliceId, params.taskId, summaryMd);
226
215
  // Invalidate all caches
227
216
  invalidateStateCache();
228
217
  clearPathCache();
@@ -145,25 +145,31 @@ export async function handlePlanMilestone(rawParams, basePath) {
145
145
  catch (err) {
146
146
  return { error: `validation failed: ${err.message}` };
147
147
  }
148
- // ── State machine preconditions ─────────────────────────────────────────
149
- const existingMilestone = getMilestone(params.milestoneId);
150
- if (existingMilestone && (existingMilestone.status === "complete" || existingMilestone.status === "done")) {
151
- return { error: `cannot re-plan milestone ${params.milestoneId}: it is already complete` };
152
- }
153
- // Validate depends_on: all dependencies must exist and be complete
154
- if (params.dependsOn && params.dependsOn.length > 0) {
155
- for (const depId of params.dependsOn) {
156
- const dep = getMilestone(depId);
157
- if (!dep) {
158
- return { error: `depends_on references unknown milestone: ${depId}` };
159
- }
160
- if (dep.status !== "complete" && dep.status !== "done") {
161
- return { error: `depends_on milestone ${depId} is not yet complete (status: ${dep.status})` };
162
- }
163
- }
164
- }
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 = null;
165
152
  try {
166
153
  transaction(() => {
154
+ const existingMilestone = getMilestone(params.milestoneId);
155
+ if (existingMilestone && (existingMilestone.status === "complete" || existingMilestone.status === "done")) {
156
+ guardError = `cannot re-plan milestone ${params.milestoneId}: it is already complete`;
157
+ return;
158
+ }
159
+ // Validate depends_on: all dependencies must exist and be complete
160
+ if (params.dependsOn && params.dependsOn.length > 0) {
161
+ for (const depId of params.dependsOn) {
162
+ const dep = getMilestone(depId);
163
+ if (!dep) {
164
+ guardError = `depends_on references unknown milestone: ${depId}`;
165
+ return;
166
+ }
167
+ if (dep.status !== "complete" && dep.status !== "done") {
168
+ guardError = `depends_on milestone ${depId} is not yet complete (status: ${dep.status})`;
169
+ return;
170
+ }
171
+ }
172
+ }
167
173
  insertMilestone({
168
174
  id: params.milestoneId,
169
175
  title: params.title,
@@ -206,6 +212,9 @@ export async function handlePlanMilestone(rawParams, basePath) {
206
212
  catch (err) {
207
213
  return { error: `db write failed: ${err.message}` };
208
214
  }
215
+ if (guardError) {
216
+ return { error: guardError };
217
+ }
209
218
  let roadmapPath;
210
219
  try {
211
220
  const renderResult = await renderRoadmapFromDb(basePath, params.milestoneId);
@@ -102,22 +102,30 @@ export async function handlePlanSlice(rawParams, basePath) {
102
102
  catch (err) {
103
103
  return { error: `validation failed: ${err.message}` };
104
104
  }
105
- const parentMilestone = getMilestone(params.milestoneId);
106
- if (!parentMilestone) {
107
- return { error: `milestone not found: ${params.milestoneId}` };
108
- }
109
- if (parentMilestone.status === "complete" || parentMilestone.status === "done") {
110
- return { error: `cannot plan slice in a closed milestone: ${params.milestoneId} (status: ${parentMilestone.status})` };
111
- }
112
- const parentSlice = getSlice(params.milestoneId, params.sliceId);
113
- if (!parentSlice) {
114
- return { error: `missing parent slice: ${params.milestoneId}/${params.sliceId}` };
115
- }
116
- if (parentSlice.status === "complete" || parentSlice.status === "done") {
117
- return { error: `cannot re-plan slice ${params.sliceId}: it is already complete — use gsd_slice_reopen first` };
118
- }
105
+ // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
106
+ // Guards must be inside the transaction so the state they check cannot
107
+ // change between the read and the write (#2723).
108
+ let guardError = null;
119
109
  try {
120
110
  transaction(() => {
111
+ const parentMilestone = getMilestone(params.milestoneId);
112
+ if (!parentMilestone) {
113
+ guardError = `milestone not found: ${params.milestoneId}`;
114
+ return;
115
+ }
116
+ if (parentMilestone.status === "complete" || parentMilestone.status === "done") {
117
+ guardError = `cannot plan slice in a closed milestone: ${params.milestoneId} (status: ${parentMilestone.status})`;
118
+ return;
119
+ }
120
+ const parentSlice = getSlice(params.milestoneId, params.sliceId);
121
+ if (!parentSlice) {
122
+ guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
123
+ return;
124
+ }
125
+ if (parentSlice.status === "complete" || parentSlice.status === "done") {
126
+ guardError = `cannot re-plan slice ${params.sliceId}: it is already complete — use gsd_slice_reopen first`;
127
+ return;
128
+ }
121
129
  upsertSlicePlanning(params.milestoneId, params.sliceId, {
122
130
  goal: params.goal,
123
131
  successCriteria: params.successCriteria,
@@ -163,6 +171,9 @@ export async function handlePlanSlice(rawParams, basePath) {
163
171
  catch (err) {
164
172
  return { error: `db write failed: ${err.message}` };
165
173
  }
174
+ if (guardError) {
175
+ return { error: guardError };
176
+ }
166
177
  try {
167
178
  const renderResult = await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);
168
179
  invalidateStateCache();
@@ -50,19 +50,26 @@ export async function handlePlanTask(rawParams, basePath) {
50
50
  catch (err) {
51
51
  return { error: `validation failed: ${err.message}` };
52
52
  }
53
- const parentSlice = getSlice(params.milestoneId, params.sliceId);
54
- if (!parentSlice) {
55
- return { error: `missing parent slice: ${params.milestoneId}/${params.sliceId}` };
56
- }
57
- if (parentSlice.status === "complete" || parentSlice.status === "done") {
58
- return { error: `cannot plan task in a closed slice: ${params.sliceId} (status: ${parentSlice.status})` };
59
- }
60
- const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
61
- if (existingTask && (existingTask.status === "complete" || existingTask.status === "done")) {
62
- return { error: `cannot re-plan task ${params.taskId}: it is already complete — use gsd_task_reopen first` };
63
- }
53
+ // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
54
+ // Guards must be inside the transaction so the state they check cannot
55
+ // change between the read and the write (#2723).
56
+ let guardError = null;
64
57
  try {
65
58
  transaction(() => {
59
+ const parentSlice = getSlice(params.milestoneId, params.sliceId);
60
+ if (!parentSlice) {
61
+ guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
62
+ return;
63
+ }
64
+ if (parentSlice.status === "complete" || parentSlice.status === "done") {
65
+ guardError = `cannot plan task in a closed slice: ${params.sliceId} (status: ${parentSlice.status})`;
66
+ return;
67
+ }
68
+ const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
69
+ if (existingTask && (existingTask.status === "complete" || existingTask.status === "done")) {
70
+ guardError = `cannot re-plan task ${params.taskId}: it is already complete — use gsd_task_reopen first`;
71
+ return;
72
+ }
66
73
  if (!existingTask) {
67
74
  insertTask({
68
75
  id: params.taskId,
@@ -88,6 +95,9 @@ export async function handlePlanTask(rawParams, basePath) {
88
95
  catch (err) {
89
96
  return { error: `db write failed: ${err.message}` };
90
97
  }
98
+ if (guardError) {
99
+ return { error: guardError };
100
+ }
91
101
  try {
92
102
  const renderResult = await renderTaskPlanFromDb(basePath, params.milestoneId, params.sliceId, params.taskId);
93
103
  invalidateStateCache();