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
@@ -125,9 +125,9 @@ console.log('\n=== complete-slice: schema v6 migration ===');
125
125
 
126
126
  const adapter = _getAdapter()!;
127
127
 
128
- // Verify schema version is current (v12 after quality gates table)
128
+ // Verify schema version is current (v14 after indexes + slice_dependencies)
129
129
  const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
130
- assertEq(versionRow?.['v'], 12, 'schema version should be 12');
130
+ assertEq(versionRow?.['v'], 14, 'schema version should be 14');
131
131
 
132
132
  // Verify slices table has full_summary_md and full_uat_md columns
133
133
  const cols = adapter.prepare("PRAGMA table_info(slices)").all();
@@ -0,0 +1,106 @@
1
+ import { describe, it, afterEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { randomUUID } from "node:crypto";
7
+
8
+ import { handleCompleteTask } from "../tools/complete-task.js";
9
+ import {
10
+ openDatabase,
11
+ closeDatabase,
12
+ _getAdapter,
13
+ insertMilestone,
14
+ insertSlice,
15
+ } from "../gsd-db.js";
16
+ import { clearPathCache } from "../paths.js";
17
+ import { clearParseCache } from "../files.js";
18
+
19
+ function makeTmpBase(): string {
20
+ const base = join(tmpdir(), `gsd-ct-rollback-${randomUUID()}`);
21
+ // Create the full tasks directory so the success path works
22
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
23
+ return base;
24
+ }
25
+
26
+ const VALID_PARAMS = {
27
+ milestoneId: "M001",
28
+ sliceId: "S01",
29
+ taskId: "T01",
30
+ oneLiner: "Test task",
31
+ narrative: "Did the thing",
32
+ verification: "Checked it",
33
+ deviations: "None.",
34
+ knownIssues: "None.",
35
+ keyFiles: ["src/foo.ts"],
36
+ keyDecisions: ["Used approach A"],
37
+ blockerDiscovered: false,
38
+ verificationEvidence: [
39
+ { command: "npm test", exitCode: 0, verdict: "✅ pass", durationMs: 1000 },
40
+ { command: "npm run lint", exitCode: 0, verdict: "✅ pass", durationMs: 500 },
41
+ ],
42
+ };
43
+
44
+ describe("complete-task rollback cleans up verification_evidence (#2724)", () => {
45
+ let base: string;
46
+
47
+ afterEach(() => {
48
+ clearPathCache();
49
+ clearParseCache();
50
+ try { closeDatabase(); } catch { /* */ }
51
+ if (base) {
52
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
53
+ }
54
+ });
55
+
56
+ it("inserts verification_evidence rows on success", async () => {
57
+ base = makeTmpBase();
58
+ openDatabase(join(base, ".gsd", "gsd.db"));
59
+ insertMilestone({ id: "M001" });
60
+ insertSlice({ id: "S01", milestoneId: "M001" });
61
+
62
+ // Write a minimal slice plan so renderPlanCheckboxes doesn't error
63
+ writeFileSync(
64
+ join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md"),
65
+ "# S01 Plan\n\n## Tasks\n\n- [ ] **T01: Test task**\n",
66
+ );
67
+
68
+ const result = await handleCompleteTask(VALID_PARAMS, base);
69
+ assert.ok(!("error" in result), `unexpected error: ${"error" in result ? result.error : ""}`);
70
+
71
+ const adapter = _getAdapter()!;
72
+ const rows = adapter.prepare(
73
+ `SELECT * FROM verification_evidence WHERE task_id = 'T01' AND slice_id = 'S01' AND milestone_id = 'M001'`,
74
+ ).all();
75
+ assert.equal(rows.length, 2, "should have 2 evidence rows after success");
76
+ });
77
+
78
+ it("deletes verification_evidence rows on disk-render rollback", async () => {
79
+ base = makeTmpBase();
80
+ openDatabase(join(base, ".gsd", "gsd.db"));
81
+ insertMilestone({ id: "M001" });
82
+ insertSlice({ id: "S01", milestoneId: "M001" });
83
+
84
+ // Replace the tasks directory with a file so disk write fails (cross-platform)
85
+ const tasksDir = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks");
86
+ rmSync(tasksDir, { recursive: true, force: true });
87
+ writeFileSync(tasksDir, "not-a-directory");
88
+
89
+ const result = await handleCompleteTask(VALID_PARAMS, base);
90
+ assert.ok("error" in result, "should return error when disk write fails");
91
+
92
+ // Task should be rolled back to pending
93
+ const adapter = _getAdapter()!;
94
+ const task = adapter.prepare(
95
+ `SELECT status FROM tasks WHERE milestone_id = 'M001' AND slice_id = 'S01' AND id = 'T01'`,
96
+ ).get() as { status: string } | undefined;
97
+ assert.ok(task, "task row should still exist");
98
+ assert.equal(task!.status, "pending", "task status should be rolled back to pending");
99
+
100
+ // Verification evidence should be cleaned up — no orphaned rows
101
+ const evidenceRows = adapter.prepare(
102
+ `SELECT * FROM verification_evidence WHERE task_id = 'T01' AND slice_id = 'S01' AND milestone_id = 'M001'`,
103
+ ).all();
104
+ assert.equal(evidenceRows.length, 0, "verification_evidence should be empty after rollback");
105
+ });
106
+ });
@@ -109,9 +109,9 @@ console.log('\n=== complete-task: schema v5 migration ===');
109
109
 
110
110
  const adapter = _getAdapter()!;
111
111
 
112
- // Verify schema version is current (v12 after quality gates table)
112
+ // Verify schema version is current (v14 after indexes + slice_dependencies)
113
113
  const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
114
- assertEq(versionRow?.['v'], 12, 'schema version should be 12');
114
+ assertEq(versionRow?.['v'], 14, 'schema version should be 14');
115
115
 
116
116
  // Verify all 4 new tables exist
117
117
  const tables = adapter.prepare(
@@ -14,6 +14,7 @@ import {
14
14
  getAllMilestones,
15
15
  insertSlice,
16
16
  insertTask,
17
+ updateTaskStatus,
17
18
  } from '../gsd-db.ts';
18
19
  // ─── Fixture Helpers ───────────────────────────────────────────────────────
19
20
 
@@ -116,10 +117,17 @@ describe('derive-state-db', async () => {
116
117
  invalidateStateCache();
117
118
  const fileState = await deriveState(base);
118
119
 
119
- // Now open DB, insert matching artifacts
120
+ // Now open DB, insert matching artifacts + milestone hierarchy
120
121
  openDatabase(':memory:');
121
122
  assert.ok(isDbAvailable(), 'db-match: DB is available after open');
122
123
 
124
+ // Insert milestone hierarchy so deriveState takes the DB path (#2631 fix)
125
+ insertMilestone({ id: 'M001', title: 'Test Milestone', status: 'active' });
126
+ insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First Slice', status: 'active', risk: 'low', depends: [] });
127
+ insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second Slice', status: 'pending', risk: 'low', depends: ['S01'] });
128
+ insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'First Task', status: 'pending' });
129
+ insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Done Task', status: 'complete' });
130
+
123
131
  insertArtifactRow('milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT, {
124
132
  artifact_type: 'roadmap',
125
133
  milestone_id: 'M001',
@@ -197,18 +205,21 @@ describe('derive-state-db', async () => {
197
205
  writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', '');
198
206
  writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan');
199
207
 
200
- // Open DB but insert nothing — empty artifacts table
208
+ // Open DB but insert nothing — empty tables.
209
+ // With #2631 fix, deriveState will sync disk milestones into DB
210
+ // and then take the DB path. The result should still reflect the
211
+ // disk milestone correctly.
201
212
  openDatabase(':memory:');
202
213
  assert.ok(isDbAvailable(), 'empty-db: DB is available');
203
214
 
204
215
  invalidateStateCache();
205
216
  const state = await deriveState(base);
206
217
 
207
- // Should still work via cachedLoadFile loadFile disk fallback
208
- assert.deepStrictEqual(state.phase, 'executing', 'empty-db: phase is executing');
218
+ // Milestone should be detected (synced from disk)
209
219
  assert.deepStrictEqual(state.activeMilestone?.id, 'M001', 'empty-db: activeMilestone is M001');
210
- assert.deepStrictEqual(state.activeSlice?.id, 'S01', 'empty-db: activeSlice is S01');
211
- assert.deepStrictEqual(state.activeTask?.id, 'T01', 'empty-db: activeTask is T01');
220
+ // The DB path without explicit slice/task rows may derive a different
221
+ // phase than the filesystem path, but the milestone must be found.
222
+ assert.ok(state.activeMilestone !== null, 'empty-db: activeMilestone is not null');
212
223
 
213
224
  closeDatabase();
214
225
  } finally {
@@ -228,8 +239,12 @@ describe('derive-state-db', async () => {
228
239
  writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan');
229
240
  writeFile(base, 'REQUIREMENTS.md', REQUIREMENTS_CONTENT);
230
241
 
231
- // Open DB but only insert the roadmap plan and requirements missing from DB
242
+ // Open DB insert milestone hierarchy + partial artifacts (#2631 fix)
232
243
  openDatabase(':memory:');
244
+ insertMilestone({ id: 'M001', title: 'Test Milestone', status: 'active' });
245
+ insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First Slice', status: 'active', risk: 'low', depends: [] });
246
+ insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'First Task', status: 'pending' });
247
+ // Only insert the roadmap artifact — plan and requirements missing from DB
233
248
  insertArtifactRow('milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT, {
234
249
  artifact_type: 'roadmap',
235
250
  milestone_id: 'M001',
@@ -314,6 +329,13 @@ describe('derive-state-db', async () => {
314
329
 
315
330
  // Put roadmap content in DB only
316
331
  openDatabase(':memory:');
332
+ // Insert milestone rows so deriveState takes the DB path (#2631 fix:
333
+ // empty milestones table now triggers disk→DB sync, which would create
334
+ // rows without slices — insert explicitly to get the full DB path).
335
+ insertMilestone({ id: 'M001', title: 'First Milestone', status: 'complete' });
336
+ insertMilestone({ id: 'M002', title: 'Second Milestone', status: 'active' });
337
+ insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Done', status: 'complete', risk: 'low', depends: [] });
338
+ insertSlice({ id: 'S01', milestoneId: 'M002', title: 'In Progress', status: 'active', risk: 'low', depends: [] });
317
339
  insertArtifactRow('milestones/M001/M001-ROADMAP.md', completedRoadmap, {
318
340
  artifact_type: 'roadmap',
319
341
  milestone_id: 'M001',
@@ -355,6 +377,10 @@ describe('derive-state-db', async () => {
355
377
  writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan');
356
378
 
357
379
  openDatabase(':memory:');
380
+ // Insert milestone/slice/task rows so deriveState takes the DB path (#2631 fix)
381
+ insertMilestone({ id: 'M001', title: 'Test Milestone', status: 'active' });
382
+ insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First Slice', status: 'active', risk: 'low', depends: [] });
383
+ insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'First Task', status: 'pending' });
358
384
  insertArtifactRow('milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT, {
359
385
  artifact_type: 'roadmap',
360
386
  milestone_id: 'M001',
@@ -378,6 +404,8 @@ describe('derive-state-db', async () => {
378
404
  });
379
405
  // Also update file on disk (cachedLoadFile may read from disk for some paths)
380
406
  writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', updatedPlan);
407
+ // Update task status in DB so DB-path also sees completion (#2631 fix)
408
+ updateTaskStatus('M001', 'S01', 'T01', 'complete');
381
409
 
382
410
  // Without invalidation, should return cached result (T01 still active)
383
411
  const state2 = await deriveState(base);
@@ -99,7 +99,7 @@ test("detectProjectState: detects preferences in .gsd/", (t) => {
99
99
  t.after(() => cleanup(dir));
100
100
 
101
101
  mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
102
- writeFileSync(join(dir, ".gsd", "preferences.md"), "---\nversion: 1\n---\n", "utf-8");
102
+ writeFileSync(join(dir, ".gsd", "PREFERENCES.md"), "---\nversion: 1\n---\n", "utf-8");
103
103
  const result = detectProjectState(dir);
104
104
  assert.ok(result.v2);
105
105
  assert.equal(result.v2!.hasPreferences, true);
@@ -64,11 +64,11 @@ _None_
64
64
  return dir;
65
65
  }
66
66
 
67
- /** Write a .gsd/preferences.md with the given git isolation mode. */
67
+ /** Write a .gsd/PREFERENCES.md with the given git isolation mode. */
68
68
  function writePreferencesFile(dir: string, isolation: "none" | "worktree" | "branch"): void {
69
69
  const gsdDir = join(dir, ".gsd");
70
70
  mkdirSync(gsdDir, { recursive: true });
71
- writeFileSync(join(gsdDir, "preferences.md"), `---\ngit:\n isolation: "${isolation}"\n---\n`);
71
+ writeFileSync(join(gsdDir, "PREFERENCES.md"), `---\ngit:\n isolation: "${isolation}"\n---\n`);
72
72
  }
73
73
 
74
74
  /** Create a repo with an in-progress milestone. */
@@ -302,7 +302,7 @@ describe('doctor-git', async () => {
302
302
  // ─── Test 7: none-mode skips orphaned worktree check ───────────────
303
303
  // NOTE: loadEffectiveGSDPreferences() resolves PROJECT_PREFERENCES_PATH
304
304
  // at module load time from process.cwd(). We write the prefs file to
305
- // the test runner's cwd .gsd/preferences.md and clean up afterwards.
305
+ // the test runner's cwd .gsd/PREFERENCES.md and clean up afterwards.
306
306
  if (process.platform !== "win32") {
307
307
  test('none-mode skips orphaned worktree', async () => {
308
308
  const dir = createRepoWithCompletedMilestone();
@@ -409,7 +409,7 @@ describe('doctor-git', async () => {
409
409
  cleanups.push(dir);
410
410
 
411
411
  run("git branch trunk", dir);
412
- writeFileSync(join(dir, ".gsd", "preferences.md"), `---\ngit:\n isolation: "worktree"\n main_branch: "trunk"\n---\n`);
412
+ writeFileSync(join(dir, ".gsd", "PREFERENCES.md"), `---\ngit:\n isolation: "worktree"\n main_branch: "trunk"\n---\n`);
413
413
 
414
414
  const metaPath = join(dir, ".gsd", "milestones", "M001", "M001-META.json");
415
415
  writeFileSync(metaPath, JSON.stringify({ integrationBranch: "feat/does-not-exist" }, null, 2));
@@ -297,7 +297,7 @@ describe('doctor-proactive', async () => {
297
297
  cleanups.push(dir);
298
298
 
299
299
  run("git branch trunk", dir);
300
- writeFileSync(join(dir, ".gsd", "preferences.md"), `---\ngit:\n main_branch: "trunk"\n---\n`);
300
+ writeFileSync(join(dir, ".gsd", "PREFERENCES.md"), `---\ngit:\n main_branch: "trunk"\n---\n`);
301
301
  const metaPath = join(dir, ".gsd", "milestones", "M001", "M001-META.json");
302
302
  writeFileSync(metaPath, JSON.stringify({ integrationBranch: "feature/missing" }, null, 2));
303
303
 
@@ -419,7 +419,7 @@ test("runProviderChecks uses provider-qualified anthropic-vertex model IDs", ()
419
419
  const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-prefix-repo-")));
420
420
  mkdirSync(join(repo, ".gsd"), { recursive: true });
421
421
  writeFileSync(
422
- join(repo, ".gsd", "preferences.md"),
422
+ join(repo, ".gsd", "PREFERENCES.md"),
423
423
  [
424
424
  "---",
425
425
  "models:",
@@ -454,7 +454,7 @@ test("runProviderChecks uses object provider field for anthropic-vertex models",
454
454
  const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-provider-repo-")));
455
455
  mkdirSync(join(repo, ".gsd"), { recursive: true });
456
456
  writeFileSync(
457
- join(repo, ".gsd", "preferences.md"),
457
+ join(repo, ".gsd", "PREFERENCES.md"),
458
458
  [
459
459
  "---",
460
460
  "models:",
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Regression test for #2631: deriveState disk→DB reconciliation must
3
+ * run even when the milestones table starts empty.
4
+ *
5
+ * When getAllMilestones() returns [] (e.g. after a failed initial migration),
6
+ * the reconciliation code inside deriveStateFromDb was unreachable because
7
+ * deriveState only called it when dbMilestones.length > 0. The fix moves
8
+ * disk→DB sync into deriveState itself, before the length check.
9
+ */
10
+ import { test } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+
16
+ import { deriveState, invalidateStateCache } from "../state.ts";
17
+ import {
18
+ openDatabase,
19
+ closeDatabase,
20
+ getAllMilestones,
21
+ } from "../gsd-db.ts";
22
+
23
+ test("deriveState populates empty DB from disk milestones (#2631)", async () => {
24
+ const base = mkdtempSync(join(tmpdir(), "gsd-empty-db-"));
25
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
26
+
27
+ try {
28
+ // Create a milestone on disk with a CONTEXT file (not a ghost)
29
+ writeFileSync(
30
+ join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"),
31
+ "# M001: Test Milestone\n\nSome context about this milestone.",
32
+ );
33
+
34
+ // Open DB — milestones table is empty (simulating failed migration)
35
+ openDatabase(":memory:");
36
+ const before = getAllMilestones();
37
+ assert.equal(before.length, 0, "DB should start with 0 milestones");
38
+
39
+ // deriveState should reconcile disk → DB
40
+ invalidateStateCache();
41
+ const state = await deriveState(base);
42
+
43
+ // After deriveState, the DB should now have the disk milestone
44
+ const after = getAllMilestones();
45
+ assert.ok(after.length > 0, "DB should have milestones after reconciliation");
46
+ assert.equal(after[0]!.id, "M001", "reconciled milestone should be M001");
47
+
48
+ // State should reflect the milestone (not "No milestones found")
49
+ assert.ok(
50
+ state.activeMilestone !== null,
51
+ "activeMilestone should not be null after reconciliation",
52
+ );
53
+
54
+ closeDatabase();
55
+ } finally {
56
+ closeDatabase();
57
+ rmSync(base, { recursive: true, force: true });
58
+ }
59
+ });
60
+
61
+ test("deriveState does NOT insert ghost milestones into DB (#2631 guard)", async () => {
62
+ const base = mkdtempSync(join(tmpdir(), "gsd-empty-db-"));
63
+ // Create a ghost milestone directory (empty — no CONTEXT, no ROADMAP)
64
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
65
+
66
+ try {
67
+ openDatabase(":memory:");
68
+ invalidateStateCache();
69
+ await deriveState(base);
70
+
71
+ const milestones = getAllMilestones();
72
+ assert.equal(milestones.length, 0, "ghost milestone should NOT be inserted");
73
+
74
+ closeDatabase();
75
+ } finally {
76
+ closeDatabase();
77
+ rmSync(base, { recursive: true, force: true });
78
+ }
79
+ });
@@ -1142,7 +1142,7 @@ describe('git-service', async () => {
1142
1142
  mkdirSync(join(repo, ".gsd", "runtime"), { recursive: true });
1143
1143
  mkdirSync(join(repo, ".gsd", "activity"), { recursive: true });
1144
1144
  writeFileSync(join(repo, ".gsd", "milestones", "M001", "ROADMAP.md"), "# Roadmap");
1145
- writeFileSync(join(repo, ".gsd", "preferences.md"), "---\nversion: 1\n---");
1145
+ writeFileSync(join(repo, ".gsd", "PREFERENCES.md"), "---\nversion: 1\n---");
1146
1146
  writeFileSync(join(repo, ".gsd", "STATE.md"), "# State");
1147
1147
  writeFileSync(join(repo, ".gsd", "runtime", "units.json"), "{}");
1148
1148
  writeFileSync(join(repo, ".gsd", "activity", "log.jsonl"), "{}");
@@ -64,7 +64,7 @@ describe('gsd-db', () => {
64
64
  // Check schema_version table
65
65
  const adapter = _getAdapter()!;
66
66
  const version = adapter.prepare('SELECT MAX(version) as version FROM schema_version').get();
67
- assert.deepStrictEqual(version?.['version'], 12, 'schema version should be 12');
67
+ assert.deepStrictEqual(version?.['version'], 14, 'schema version should be 14');
68
68
 
69
69
  // Check tables exist by querying them
70
70
  const dRows = adapter.prepare('SELECT count(*) as cnt FROM decisions').get();
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Regression tests for #2527: idle watchdog stalled-tool detection.
3
+ *
4
+ * Bug 1: When a tool is stalled longer than idle_timeout, the watchdog
5
+ * notifies but falls through to detectWorkingTreeActivity(), which
6
+ * resets lastProgressAt if files were modified earlier. Recovery is
7
+ * never called — the session burns tokens indefinitely.
8
+ *
9
+ * Bug 2: After async recoverTimedOutUnit(), pauseAuto/stopAuto may set
10
+ * s.currentUnit = null, but the next line accesses .startedAt — crash.
11
+ *
12
+ * These tests verify the auto-timers.ts source contains the structural
13
+ * fixes: the stalledToolDetected flag, clearInFlightTools() call, the
14
+ * filesystem-check guard, and the null guard after recovery.
15
+ */
16
+
17
+ import { readFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { test, describe } from "node:test";
20
+ import assert from "node:assert/strict";
21
+
22
+ const TIMERS_SRC = readFileSync(
23
+ join(import.meta.dirname, "..", "auto-timers.ts"),
24
+ "utf-8",
25
+ );
26
+
27
+ // ═══ Bug 1: stalledToolDetected flag prevents filesystem-activity override ═══
28
+
29
+ describe("#2527 Bug 1: stalled tool should not be overridden by filesystem activity", () => {
30
+ test("auto-timers.ts imports clearInFlightTools", () => {
31
+ assert.ok(
32
+ TIMERS_SRC.includes("clearInFlightTools"),
33
+ "clearInFlightTools must be imported from auto-tool-tracking",
34
+ );
35
+ });
36
+
37
+ test("auto-timers.ts declares stalledToolDetected flag", () => {
38
+ assert.ok(
39
+ TIMERS_SRC.includes("stalledToolDetected"),
40
+ "stalledToolDetected flag must exist in idle watchdog",
41
+ );
42
+ });
43
+
44
+ test("stalled tool sets flag to true", () => {
45
+ // The flag must be set before the filesystem check
46
+ const flagSet = TIMERS_SRC.indexOf("stalledToolDetected = true");
47
+ assert.ok(flagSet > -1, "stalledToolDetected must be set to true when tool is stalled");
48
+
49
+ const notify = TIMERS_SRC.indexOf("Stalled tool detected:");
50
+ assert.ok(flagSet < notify, "flag must be set before the stall notification");
51
+ });
52
+
53
+ test("stalled tool calls clearInFlightTools", () => {
54
+ // clearInFlightTools() must be called when tool is stalled, so subsequent
55
+ // watchdog ticks don't re-detect the same stale entries
56
+ const clearCall = TIMERS_SRC.indexOf("clearInFlightTools()");
57
+ assert.ok(clearCall > -1, "clearInFlightTools() must be called when tool is stalled");
58
+
59
+ const flagSet = TIMERS_SRC.indexOf("stalledToolDetected = true");
60
+ assert.ok(
61
+ Math.abs(clearCall - flagSet) < 200,
62
+ "clearInFlightTools() should be near stalledToolDetected = true",
63
+ );
64
+ });
65
+
66
+ test("filesystem-activity check is guarded by stalledToolDetected", () => {
67
+ // The detectWorkingTreeActivity check must be skipped when stalledToolDetected is true
68
+ assert.ok(
69
+ TIMERS_SRC.includes("!stalledToolDetected && detectWorkingTreeActivity"),
70
+ "detectWorkingTreeActivity must be guarded by !stalledToolDetected",
71
+ );
72
+ });
73
+
74
+ test("control flow: stalled tool → skip filesystem check → reach recovery", () => {
75
+ // Verify the structural ordering: flag declaration → stall block → guarded fs check → recovery
76
+ const flagDecl = TIMERS_SRC.indexOf("let stalledToolDetected = false");
77
+ const stallBlock = TIMERS_SRC.indexOf("stalledToolDetected = true");
78
+ const fsGuard = TIMERS_SRC.indexOf("!stalledToolDetected && detectWorkingTreeActivity");
79
+ const recovery = TIMERS_SRC.indexOf("recoverTimedOutUnit(ctx, pi, unitType, unitId, \"idle\"");
80
+
81
+ assert.ok(flagDecl > -1, "flag declaration must exist");
82
+ assert.ok(flagDecl < stallBlock, "flag declared before stall block");
83
+ assert.ok(stallBlock < fsGuard, "stall block before filesystem guard");
84
+ assert.ok(fsGuard < recovery, "filesystem guard before recovery call");
85
+ });
86
+ });
87
+
88
+ // ═══ Bug 2: null guard after async recoverTimedOutUnit ═══════════════════════
89
+
90
+ describe("#2527 Bug 2: null guard after async recovery prevents crash", () => {
91
+ test("idle watchdog has null guard after recoverTimedOutUnit", () => {
92
+ // Find the idle recovery call
93
+ const idleRecovery = TIMERS_SRC.indexOf(
94
+ 'recoverTimedOutUnit(ctx, pi, unitType, unitId, "idle"',
95
+ );
96
+ assert.ok(idleRecovery > -1, "idle recovery call must exist");
97
+
98
+ // The null guard must appear between the recovery call and the next
99
+ // writeUnitRuntimeRecord that accesses s.currentUnit.startedAt
100
+ const afterRecovery = TIMERS_SRC.slice(idleRecovery, idleRecovery + 400);
101
+ assert.ok(
102
+ afterRecovery.includes("if (!s.currentUnit) return"),
103
+ "null guard for s.currentUnit must exist after idle recoverTimedOutUnit",
104
+ );
105
+ });
106
+
107
+ test("null guard is between recovery and writeUnitRuntimeRecord", () => {
108
+ const idleRecovery = TIMERS_SRC.indexOf(
109
+ 'recoverTimedOutUnit(ctx, pi, unitType, unitId, "idle"',
110
+ );
111
+ const afterRecovery = TIMERS_SRC.slice(idleRecovery);
112
+
113
+ const recoveredReturn = afterRecovery.indexOf('if (recovery === "recovered") return');
114
+ const nullGuard = afterRecovery.indexOf("if (!s.currentUnit) return");
115
+ const writeRecord = afterRecovery.indexOf("writeUnitRuntimeRecord(s.basePath");
116
+
117
+ assert.ok(recoveredReturn > -1, "recovered return must exist");
118
+ assert.ok(nullGuard > -1, "null guard must exist");
119
+ assert.ok(writeRecord > -1, "writeUnitRuntimeRecord must exist after recovery");
120
+ assert.ok(
121
+ recoveredReturn < nullGuard && nullGuard < writeRecord,
122
+ "order must be: recovered-return → null-guard → writeUnitRuntimeRecord",
123
+ );
124
+ });
125
+ });
@@ -123,7 +123,7 @@ test("init-wizard: v2 .gsd/ preferences detected", (t) => {
123
123
  const dir = makeTempDir("prefs-detect");
124
124
  try {
125
125
  mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
126
- writeFileSync(join(dir, ".gsd", "preferences.md"), "---\nversion: 1\nmode: solo\n---\n", "utf-8");
126
+ writeFileSync(join(dir, ".gsd", "PREFERENCES.md"), "---\nversion: 1\nmode: solo\n---\n", "utf-8");
127
127
 
128
128
  const detection = detectProjectState(dir);
129
129
  assert.ok(detection.v2);