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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/README.md +4 -4
  2. package/dist/headless-events.d.ts +18 -0
  3. package/dist/headless-events.js +36 -0
  4. package/dist/headless-types.d.ts +28 -0
  5. package/dist/headless-types.js +7 -0
  6. package/dist/headless.d.ts +8 -3
  7. package/dist/headless.js +47 -16
  8. package/dist/help-text.js +16 -5
  9. package/dist/onboarding.js +5 -4
  10. package/dist/remote-questions-config.js +1 -1
  11. package/dist/resources/extensions/async-jobs/async-bash-tool.js +29 -17
  12. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +18 -19
  13. package/dist/resources/extensions/gsd/auto-dispatch.js +18 -0
  14. package/dist/resources/extensions/gsd/auto-start.js +2 -0
  15. package/dist/resources/extensions/gsd/auto-timers.js +24 -2
  16. package/dist/resources/extensions/gsd/auto-tool-tracking.js +25 -7
  17. package/dist/resources/extensions/gsd/auto-worktree.js +21 -0
  18. package/dist/resources/extensions/gsd/auto.js +4 -2
  19. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +95 -69
  20. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +12 -2
  21. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +1 -1
  22. package/dist/resources/extensions/gsd/claude-import.js +60 -9
  23. package/dist/resources/extensions/gsd/commands/handlers/auto.js +69 -6
  24. package/dist/resources/extensions/gsd/commands-config.js +10 -5
  25. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  26. package/dist/resources/extensions/gsd/detection.js +6 -6
  27. package/dist/resources/extensions/gsd/docs/preferences-reference.md +3 -3
  28. package/dist/resources/extensions/gsd/error-classifier.js +105 -0
  29. package/dist/resources/extensions/gsd/gitignore.js +7 -7
  30. package/dist/resources/extensions/gsd/gsd-db.js +298 -45
  31. package/dist/resources/extensions/gsd/init-wizard.js +2 -2
  32. package/dist/resources/extensions/gsd/key-manager.js +7 -16
  33. package/dist/resources/extensions/gsd/memory-store.js +28 -13
  34. package/dist/resources/extensions/gsd/milestone-actions.js +19 -0
  35. package/dist/resources/extensions/gsd/preferences-models.js +1 -13
  36. package/dist/resources/extensions/gsd/preferences.js +13 -13
  37. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  38. package/dist/resources/extensions/gsd/provider-error-pause.js +0 -44
  39. package/dist/resources/extensions/gsd/rule-registry.js +1 -1
  40. package/dist/resources/extensions/gsd/service-tier.js +13 -2
  41. package/dist/resources/extensions/gsd/state.js +21 -2
  42. package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -10
  43. package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -17
  44. package/dist/resources/extensions/gsd/tools/complete-task.js +7 -18
  45. package/dist/resources/extensions/gsd/tools/plan-milestone.js +26 -17
  46. package/dist/resources/extensions/gsd/tools/plan-slice.js +25 -14
  47. package/dist/resources/extensions/gsd/tools/plan-task.js +21 -11
  48. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +47 -37
  49. package/dist/resources/extensions/gsd/tools/replan-slice.js +49 -38
  50. package/dist/resources/extensions/gsd/tools/validate-milestone.js +23 -16
  51. package/dist/resources/extensions/gsd/workflow-logger.js +0 -1
  52. package/dist/resources/extensions/remote-questions/config.js +1 -1
  53. package/dist/resources/extensions/remote-questions/remote-command.js +1 -1
  54. package/dist/resources/extensions/search-the-web/native-search.js +1 -1
  55. package/dist/resources/extensions/search-the-web/provider.js +1 -1
  56. package/dist/web/standalone/.next/BUILD_ID +1 -1
  57. package/dist/web/standalone/.next/app-path-routes-manifest.json +21 -21
  58. package/dist/web/standalone/.next/build-manifest.json +3 -3
  59. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  60. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  63. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.rsc +2 -2
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  76. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  79. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/experimental/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  122. package/dist/web/standalone/.next/server/app/index.html +1 -1
  123. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  124. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  125. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  126. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  127. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  128. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  129. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  130. package/dist/web/standalone/.next/server/app-paths-manifest.json +21 -21
  131. package/dist/web/standalone/.next/server/chunks/2229.js +2 -2
  132. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  133. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  134. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  135. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  136. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  137. package/dist/web/standalone/.next/static/chunks/4024.21054f459af5cc78.js +9 -0
  138. package/dist/web/standalone/.next/static/chunks/{webpack-cfc9a116e6450a6b.js → webpack-024d82be84800e52.js} +1 -1
  139. package/dist/web/standalone/.next/static/css/a58ef8a151aa0493.css +1 -0
  140. package/dist/wizard.js +4 -1
  141. package/package.json +2 -2
  142. package/packages/pi-ai/dist/models.d.ts +14 -3
  143. package/packages/pi-ai/dist/models.d.ts.map +1 -1
  144. package/packages/pi-ai/dist/models.js +53 -10
  145. package/packages/pi-ai/dist/models.js.map +1 -1
  146. package/packages/pi-ai/dist/models.test.js +102 -1
  147. package/packages/pi-ai/dist/models.test.js.map +1 -1
  148. package/packages/pi-ai/dist/types.d.ts +30 -0
  149. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  150. package/packages/pi-ai/dist/types.js.map +1 -1
  151. package/packages/pi-ai/src/models.test.ts +114 -1
  152. package/packages/pi-ai/src/models.ts +70 -13
  153. package/packages/pi-ai/src/types.ts +31 -0
  154. package/packages/pi-coding-agent/dist/cli/args.d.ts +2 -0
  155. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  156. package/packages/pi-coding-agent/dist/cli/args.js +3 -0
  157. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  158. package/packages/pi-coding-agent/dist/core/bash-executor.d.ts.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/bash-executor.js +5 -1
  160. package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  161. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  162. package/packages/pi-coding-agent/dist/core/model-registry.js +9 -4
  163. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  164. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.d.ts +19 -0
  165. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.d.ts.map +1 -0
  166. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.js +83 -0
  167. package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.js.map +1 -0
  168. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  169. package/packages/pi-coding-agent/dist/core/tools/bash.js +5 -1
  170. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  171. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  172. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  173. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  174. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  175. package/packages/pi-coding-agent/dist/main.js +5 -3
  176. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  177. package/packages/pi-coding-agent/dist/modes/index.d.ts +1 -1
  178. package/packages/pi-coding-agent/dist/modes/index.d.ts.map +1 -1
  179. package/packages/pi-coding-agent/dist/modes/index.js.map +1 -1
  180. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  181. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +0 -2
  182. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  183. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts +28 -1
  184. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  185. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js +49 -0
  186. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js.map +1 -1
  187. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts +1 -1
  188. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  189. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +114 -6
  190. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  191. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.d.ts +9 -0
  192. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.d.ts.map +1 -0
  193. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.js +831 -0
  194. package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.js.map +1 -0
  195. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +66 -0
  196. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  197. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
  198. package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  199. package/packages/pi-coding-agent/dist/utils/shell.js +0 -1
  200. package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
  201. package/packages/pi-coding-agent/package.json +1 -1
  202. package/packages/pi-coding-agent/src/cli/args.ts +4 -0
  203. package/packages/pi-coding-agent/src/core/bash-executor.ts +5 -1
  204. package/packages/pi-coding-agent/src/core/model-registry.ts +10 -3
  205. package/packages/pi-coding-agent/src/core/tools/bash-spawn-windows.test.ts +101 -0
  206. package/packages/pi-coding-agent/src/core/tools/bash.ts +5 -1
  207. package/packages/pi-coding-agent/src/index.ts +3 -0
  208. package/packages/pi-coding-agent/src/main.ts +5 -3
  209. package/packages/pi-coding-agent/src/modes/index.ts +8 -1
  210. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +0 -2
  211. package/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts +54 -1
  212. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +124 -6
  213. package/packages/pi-coding-agent/src/modes/rpc/rpc-protocol-v2.test.ts +971 -0
  214. package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +61 -4
  215. package/packages/pi-coding-agent/src/utils/shell.ts +0 -1
  216. package/pkg/package.json +1 -1
  217. package/src/resources/extensions/async-jobs/async-bash-tool.ts +22 -11
  218. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +19 -20
  219. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +21 -0
  220. package/src/resources/extensions/gsd/auto-dispatch.ts +19 -0
  221. package/src/resources/extensions/gsd/auto-start.ts +2 -0
  222. package/src/resources/extensions/gsd/auto-timers.ts +25 -1
  223. package/src/resources/extensions/gsd/auto-tool-tracking.ts +30 -6
  224. package/src/resources/extensions/gsd/auto-worktree.ts +21 -0
  225. package/src/resources/extensions/gsd/auto.ts +5 -2
  226. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +115 -72
  227. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +11 -2
  228. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +1 -1
  229. package/src/resources/extensions/gsd/claude-import.ts +58 -9
  230. package/src/resources/extensions/gsd/commands/handlers/auto.ts +73 -6
  231. package/src/resources/extensions/gsd/commands-config.ts +11 -5
  232. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  233. package/src/resources/extensions/gsd/detection.ts +6 -6
  234. package/src/resources/extensions/gsd/docs/preferences-reference.md +3 -3
  235. package/src/resources/extensions/gsd/error-classifier.ts +139 -0
  236. package/src/resources/extensions/gsd/gitignore.ts +7 -7
  237. package/src/resources/extensions/gsd/gsd-db.ts +355 -63
  238. package/src/resources/extensions/gsd/init-wizard.ts +2 -2
  239. package/src/resources/extensions/gsd/key-manager.ts +7 -16
  240. package/src/resources/extensions/gsd/memory-store.ts +29 -18
  241. package/src/resources/extensions/gsd/milestone-actions.ts +17 -0
  242. package/src/resources/extensions/gsd/preferences-models.ts +1 -13
  243. package/src/resources/extensions/gsd/preferences.ts +12 -13
  244. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  245. package/src/resources/extensions/gsd/provider-error-pause.ts +0 -57
  246. package/src/resources/extensions/gsd/rule-registry.ts +1 -1
  247. package/src/resources/extensions/gsd/service-tier.ts +14 -2
  248. package/src/resources/extensions/gsd/state.ts +22 -2
  249. package/src/resources/extensions/gsd/tests/auto-milestone-target.test.ts +61 -0
  250. package/src/resources/extensions/gsd/tests/claude-import-marketplace-discovery.test.ts +191 -0
  251. package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +1 -1
  252. package/src/resources/extensions/gsd/tests/commands-config.test.ts +24 -0
  253. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  254. package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +106 -0
  255. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  256. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +35 -7
  257. package/src/resources/extensions/gsd/tests/detection.test.ts +1 -1
  258. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +4 -4
  259. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +1 -1
  260. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +2 -2
  261. package/src/resources/extensions/gsd/tests/empty-db-reconciliation.test.ts +79 -0
  262. package/src/resources/extensions/gsd/tests/git-service.test.ts +1 -1
  263. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
  264. package/src/resources/extensions/gsd/tests/idle-watchdog-stall-override.test.ts +125 -0
  265. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +1 -1
  266. package/src/resources/extensions/gsd/tests/interactive-tool-idle-exemption.test.ts +119 -0
  267. package/src/resources/extensions/gsd/tests/key-manager.test.ts +16 -1
  268. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  269. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  270. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +7 -7
  271. package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +85 -0
  272. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +91 -0
  273. package/src/resources/extensions/gsd/tests/preferences.test.ts +1 -1
  274. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +77 -70
  275. package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +110 -0
  276. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +29 -0
  277. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +42 -31
  278. package/src/resources/extensions/gsd/tests/token-cost-display.test.ts +2 -2
  279. package/src/resources/extensions/gsd/tests/vacuous-truth-slices.test.ts +115 -0
  280. package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +90 -0
  281. package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +81 -1
  282. package/src/resources/extensions/gsd/tests/worktree-preferences-sync.test.ts +130 -0
  283. package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -14
  284. package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -21
  285. package/src/resources/extensions/gsd/tools/complete-task.ts +9 -22
  286. package/src/resources/extensions/gsd/tools/plan-milestone.ts +28 -18
  287. package/src/resources/extensions/gsd/tools/plan-slice.ts +28 -16
  288. package/src/resources/extensions/gsd/tools/plan-task.ts +24 -12
  289. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +54 -42
  290. package/src/resources/extensions/gsd/tools/replan-slice.ts +53 -40
  291. package/src/resources/extensions/gsd/tools/validate-milestone.ts +26 -20
  292. package/src/resources/extensions/gsd/workflow-logger.ts +0 -1
  293. package/src/resources/extensions/remote-questions/config.ts +1 -1
  294. package/src/resources/extensions/remote-questions/remote-command.ts +1 -1
  295. package/src/resources/extensions/search-the-web/native-search.ts +1 -1
  296. package/src/resources/extensions/search-the-web/provider.ts +1 -1
  297. package/dist/web/standalone/.next/static/chunks/4024.9ad5def014d90ce4.js +0 -9
  298. package/dist/web/standalone/.next/static/css/de141508b083f922.css +0 -1
  299. /package/dist/resources/extensions/gsd/templates/{preferences.md → PREFERENCES.md} +0 -0
  300. /package/dist/web/standalone/.next/static/{yJIyd5cXPNpmXTv18ZlyC → RqOU-jOv9uZ1Q03P6L6nn}/_buildManifest.js +0 -0
  301. /package/dist/web/standalone/.next/static/{yJIyd5cXPNpmXTv18ZlyC → RqOU-jOv9uZ1Q03P6L6nn}/_ssgManifest.js +0 -0
  302. /package/src/resources/extensions/gsd/templates/{preferences.md → PREFERENCES.md} +0 -0
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Regression test for #2675: completing-milestone dispatch rule must
3
+ * block completion when VALIDATION verdict is "needs-remediation".
4
+ *
5
+ * Without this guard, needs-remediation + allSlicesDone causes a loop:
6
+ * complete-milestone dispatched → agent refuses (correct) → no SUMMARY
7
+ * → re-dispatch → repeat until stuck detection fires.
8
+ */
9
+ import { test } from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { tmpdir } from "node:os";
14
+
15
+ import { DISPATCH_RULES } from "../auto-dispatch.ts";
16
+
17
+ /** Find the completing-milestone dispatch rule */
18
+ const completingRule = DISPATCH_RULES.find(r => r.name === "completing-milestone → complete-milestone");
19
+
20
+ test("completing-milestone dispatch rule exists", () => {
21
+ assert.ok(completingRule, "rule should exist in DISPATCH_RULES");
22
+ });
23
+
24
+ test("completing-milestone blocks when VALIDATION verdict is needs-remediation (#2675)", async () => {
25
+ const base = mkdtempSync(join(tmpdir(), "gsd-remediation-"));
26
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
27
+
28
+ try {
29
+ // Write a VALIDATION file with needs-remediation verdict
30
+ writeFileSync(
31
+ join(base, ".gsd", "milestones", "M001", "M001-VALIDATION.md"),
32
+ [
33
+ "---",
34
+ "verdict: needs-remediation",
35
+ "remediation_round: 0",
36
+ "---",
37
+ "",
38
+ "# Validation Report",
39
+ "",
40
+ "3 success criteria failed. Remediation required.",
41
+ ].join("\n"),
42
+ );
43
+
44
+ const ctx = {
45
+ mid: "M001",
46
+ midTitle: "Test Milestone",
47
+ basePath: base,
48
+ state: { phase: "completing-milestone" } as any,
49
+ prefs: {} as any,
50
+ session: undefined,
51
+ };
52
+
53
+ const result = await completingRule!.match(ctx);
54
+
55
+ assert.ok(result !== null, "rule should match");
56
+ assert.equal(result!.action, "stop", "should return stop action");
57
+ if (result!.action === "stop") {
58
+ assert.equal(result!.level, "warning", "should be warning level (pausable)");
59
+ assert.ok(
60
+ result!.reason.includes("needs-remediation"),
61
+ "reason should mention needs-remediation",
62
+ );
63
+ }
64
+ } finally {
65
+ rmSync(base, { recursive: true, force: true });
66
+ }
67
+ });
68
+
69
+ test("completing-milestone proceeds normally when VALIDATION verdict is pass (#2675 guard)", async () => {
70
+ const base = mkdtempSync(join(tmpdir(), "gsd-remediation-"));
71
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
72
+
73
+ try {
74
+ // Write a VALIDATION file with pass verdict
75
+ writeFileSync(
76
+ join(base, ".gsd", "milestones", "M001", "M001-VALIDATION.md"),
77
+ [
78
+ "---",
79
+ "verdict: pass",
80
+ "---",
81
+ "",
82
+ "# Validation Report",
83
+ "",
84
+ "All criteria met.",
85
+ ].join("\n"),
86
+ );
87
+
88
+ const ctx = {
89
+ mid: "M001",
90
+ midTitle: "Test Milestone",
91
+ basePath: base,
92
+ state: { phase: "completing-milestone" } as any,
93
+ prefs: {} as any,
94
+ session: undefined,
95
+ };
96
+
97
+ const result = await completingRule!.match(ctx);
98
+
99
+ // Should NOT return a stop — should either dispatch or return stop for
100
+ // a different reason (e.g. missing SUMMARY files, no implementation)
101
+ if (result && result.action === "stop") {
102
+ assert.ok(
103
+ !result.reason.includes("needs-remediation"),
104
+ "pass verdict should NOT trigger the remediation guard",
105
+ );
106
+ }
107
+ } finally {
108
+ rmSync(base, { recursive: true, force: true });
109
+ }
110
+ });
@@ -724,3 +724,32 @@ test("resolveRemoteConfig returns null when preferences are absent (no env side-
724
724
  if (savedTelegram !== undefined) process.env.TELEGRAM_BOT_TOKEN = savedTelegram;
725
725
  }
726
726
  });
727
+
728
+ test("config source-level: hydration skips api_key entries with empty keys", () => {
729
+ const configSrc = readFileSync(
730
+ join(__dirname, "..", "..", "remote-questions", "config.ts"),
731
+ "utf-8",
732
+ );
733
+ // The find() call in hydrateRemoteTokensFromAuth must filter for non-empty keys,
734
+ // not just match on type === "api_key". This prevents stale empty-key entries
735
+ // (left by removeProviderToken) from shadowing valid tokens.
736
+ assert.ok(
737
+ configSrc.includes('c.type === "api_key" && !!c.key'),
738
+ "hydrateRemoteTokensFromAuth find() should require a non-empty key",
739
+ );
740
+ });
741
+
742
+ test("config source-level: removeProviderToken uses auth.remove not auth.set with empty key", () => {
743
+ const commandSrc = readFileSync(
744
+ join(__dirname, "..", "..", "remote-questions", "remote-command.ts"),
745
+ "utf-8",
746
+ );
747
+ // removeProviderToken should call auth.remove(provider), not auth.set(provider, { key: "" }).
748
+ // Setting an empty key pollutes the credentials array and shadows valid tokens.
749
+ const fnStart = commandSrc.indexOf("function removeProviderToken");
750
+ assert.ok(fnStart !== -1, "removeProviderToken should exist");
751
+ const fnEnd = commandSrc.indexOf("\n}", fnStart);
752
+ const fnBody = commandSrc.slice(fnStart, fnEnd);
753
+ assert.ok(fnBody.includes("auth.remove("), "removeProviderToken should call auth.remove()");
754
+ assert.ok(!fnBody.includes('key: ""'), "removeProviderToken should not set an empty key");
755
+ });
@@ -1,73 +1,84 @@
1
1
  /**
2
2
  * terminated-transient.test.ts — Regression test for #2309.
3
3
  *
4
- * classifyProviderError should treat 'terminated' errors (process killed,
4
+ * classifyError should treat 'terminated' errors (process killed,
5
5
  * connection reset) as transient with auto-resume, not permanent.
6
6
  */
7
7
 
8
8
  import test from "node:test";
9
9
  import assert from "node:assert/strict";
10
- import { classifyProviderError } from "../provider-error-pause.ts";
10
+ import { classifyError, isTransient } from "../error-classifier.ts";
11
11
 
12
12
  test("#2309: 'terminated' errors should be classified as transient", () => {
13
- const result = classifyProviderError("terminated");
14
- assert.equal(result.isTransient, true, "'terminated' should be transient");
15
- assert.equal(result.isRateLimit, false, "'terminated' is not a rate limit");
16
- assert.ok(result.suggestedDelayMs > 0, "'terminated' should have a retry delay");
13
+ const result = classifyError("terminated");
14
+ assert.equal(isTransient(result), true, "'terminated' should be transient");
15
+ assert.equal(result.kind, "connection", "'terminated' matches connection");
16
+ assert.ok("retryAfterMs" in result && result.retryAfterMs > 0, "'terminated' should have a retry delay");
17
+ assert.equal("retryAfterMs" in result && result.retryAfterMs, 15_000, "'terminated' should use 15s backoff");
17
18
  });
18
19
 
19
- test("#2309: 'connection reset' errors should be classified as transient", () => {
20
- const result = classifyProviderError("connection reset by peer");
21
- assert.equal(result.isTransient, true, "'connection reset' should be transient");
20
+ test("#2309: 'connection reset by peer' errors should be classified as transient (network)", () => {
21
+ const result = classifyError("connection reset by peer");
22
+ assert.equal(isTransient(result), true, "'connection reset by peer' should be transient");
23
+ assert.equal(result.kind, "network", "'connection reset by peer' matches NETWORK_RE (connection.*reset) before CONNECTION_RE");
24
+ assert.equal("retryAfterMs" in result && result.retryAfterMs, 3_000, "network errors use 3s backoff");
22
25
  });
23
26
 
24
27
  test("#2309: 'other side closed' errors should be classified as transient", () => {
25
- const result = classifyProviderError("other side closed the connection");
26
- assert.equal(result.isTransient, true, "'other side closed' should be transient");
28
+ const result = classifyError("other side closed the connection");
29
+ assert.equal(isTransient(result), true, "'other side closed' should be transient");
30
+ assert.equal(result.kind, "connection", "'other side closed' matches CONNECTION_RE");
27
31
  });
28
32
 
29
33
  test("#2309: 'fetch failed' errors should be classified as transient", () => {
30
- const result = classifyProviderError("fetch failed: network error");
31
- assert.equal(result.isTransient, true, "'fetch failed' should be transient");
34
+ const result = classifyError("fetch failed: network error");
35
+ assert.equal(isTransient(result), true, "'fetch failed' should be transient");
36
+ assert.equal(result.kind, "network", "'fetch failed' matches NETWORK_RE");
37
+ assert.equal("retryAfterMs" in result && result.retryAfterMs, 3_000, "network errors use 3s backoff");
32
38
  });
33
39
 
34
40
  test("#2309: 'connection refused' errors should be classified as transient", () => {
35
- const result = classifyProviderError("ECONNREFUSED: connection refused");
36
- assert.equal(result.isTransient, true, "'connection refused' should be transient");
41
+ const result = classifyError("ECONNREFUSED: connection refused");
42
+ assert.equal(isTransient(result), true, "'connection refused' should be transient");
43
+ assert.equal(result.kind, "network", "'ECONNREFUSED' matches NETWORK_RE (same-model retry)");
37
44
  });
38
45
 
39
46
  test("#2309: permanent errors are still permanent", () => {
40
- const authResult = classifyProviderError("unauthorized: invalid API key");
41
- assert.equal(authResult.isTransient, false, "auth errors should stay permanent");
42
- assert.equal(authResult.suggestedDelayMs, 0, "permanent errors have no delay");
47
+ const authResult = classifyError("unauthorized: invalid API key");
48
+ assert.equal(isTransient(authResult), false, "auth errors should stay permanent");
49
+ assert.equal(authResult.kind, "permanent", "auth errors are permanent");
50
+ assert.equal("retryAfterMs" in authResult, false, "permanent errors have no retryAfterMs");
43
51
  });
44
52
 
45
53
  test("#2309: rate limits are still transient", () => {
46
- const rlResult = classifyProviderError("rate limit exceeded (429)");
47
- assert.equal(rlResult.isTransient, true, "rate limits are still transient");
48
- assert.equal(rlResult.isRateLimit, true, "rate limits are flagged as rate limits");
54
+ const rlResult = classifyError("rate limit exceeded (429)");
55
+ assert.equal(isTransient(rlResult), true, "rate limits are still transient");
56
+ assert.equal(rlResult.kind, "rate-limit", "rate limits are flagged as rate-limit kind");
49
57
  });
50
58
 
51
59
  // --- #2572: stream-truncation JSON parse errors should be transient ---
52
60
 
53
61
  test("#2572: 'Expected double-quoted property name' (truncated stream) is transient", () => {
54
- const result = classifyProviderError("Expected double-quoted property name in JSON at position 23 (line 1 column 24)");
55
- assert.equal(result.isTransient, true, "truncated-stream JSON parse error should be transient");
56
- assert.equal(result.isRateLimit, false, "not a rate limit");
57
- assert.equal(result.suggestedDelayMs, 15_000, "should use 15s backoff like connection errors");
62
+ const result = classifyError("Expected double-quoted property name in JSON at position 23 (line 1 column 24)");
63
+ assert.equal(isTransient(result), true, "truncated-stream JSON parse error should be transient");
64
+ assert.equal(result.kind, "stream", "JSON parse errors are stream kind");
65
+ assert.equal("retryAfterMs" in result && result.retryAfterMs, 15_000, "should use 15s backoff");
58
66
  });
59
67
 
60
68
  test("#2572: 'Unexpected end of JSON input' (truncated stream) is transient", () => {
61
- const result = classifyProviderError("Unexpected end of JSON input");
62
- assert.equal(result.isTransient, true, "'Unexpected end of JSON input' should be transient");
69
+ const result = classifyError("Unexpected end of JSON input");
70
+ assert.equal(isTransient(result), true, "'Unexpected end of JSON input' should be transient");
71
+ assert.equal(result.kind, "stream", "JSON parse errors are stream kind");
63
72
  });
64
73
 
65
74
  test("#2572: 'Unexpected token' in JSON (truncated stream) is transient", () => {
66
- const result = classifyProviderError("Unexpected token < in JSON at position 0");
67
- assert.equal(result.isTransient, true, "'Unexpected token in JSON' should be transient");
75
+ const result = classifyError("Unexpected token < in JSON at position 0");
76
+ assert.equal(isTransient(result), true, "'Unexpected token in JSON' should be transient");
77
+ assert.equal(result.kind, "stream", "JSON parse errors are stream kind");
68
78
  });
69
79
 
70
80
  test("#2572: 'SyntaxError' with JSON context (truncated stream) is transient", () => {
71
- const result = classifyProviderError("SyntaxError: JSON.parse: unexpected character at line 1 column 1");
72
- assert.equal(result.isTransient, true, "'SyntaxError...JSON' should be transient");
81
+ const result = classifyError("SyntaxError: JSON.parse: unexpected character at line 1 column 1");
82
+ assert.equal(isTransient(result), true, "'SyntaxError...JSON' should be transient");
83
+ assert.equal(result.kind, "stream", "JSON parse errors are stream kind");
73
84
  });
@@ -63,13 +63,13 @@ test("show_token_cost defaults to undefined (disabled) when not set", () => {
63
63
  assert.equal(preferences.show_token_cost, undefined);
64
64
  });
65
65
 
66
- test("empty preferences.md does not enable show_token_cost", () => {
66
+ test("empty PREFERENCES.md does not enable show_token_cost", () => {
67
67
  const prefs = parsePreferencesMarkdown("---\nversion: 1\n---\n");
68
68
  assert.ok(prefs);
69
69
  assert.equal(prefs.show_token_cost, undefined);
70
70
  });
71
71
 
72
- test("preferences.md with show_token_cost: true enables the preference", () => {
72
+ test("PREFERENCES.md with show_token_cost: true enables the preference", () => {
73
73
  const prefs = parsePreferencesMarkdown("---\nshow_token_cost: true\n---\n");
74
74
  assert.ok(prefs);
75
75
  assert.equal(prefs.show_token_cost, true);
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Regression test for #2667: deriveStateFromDb must NOT treat an empty
3
+ * slice array as "all slices done" due to JavaScript's vacuous-truth
4
+ * behavior of Array.prototype.every on an empty array.
5
+ *
6
+ * [].every(predicate) === true in JavaScript. Without a length > 0 guard,
7
+ * this causes a premature phase transition to validating-milestone when
8
+ * the DB returns 0 slices (e.g. after a worktree DB wipe).
9
+ */
10
+ import { test } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+
16
+ import { deriveStateFromDb, invalidateStateCache } from "../state.ts";
17
+ import {
18
+ openDatabase,
19
+ closeDatabase,
20
+ insertMilestone,
21
+ insertSlice,
22
+ } from "../gsd-db.ts";
23
+
24
+ test("deriveStateFromDb does NOT skip to validating when slice array is empty (#2667)", async () => {
25
+ const base = mkdtempSync(join(tmpdir(), "gsd-vacuous-truth-"));
26
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
27
+
28
+ try {
29
+ // Set up a milestone with a roadmap that references slices,
30
+ // but the DB has NO slice rows (simulating a worktree DB wipe)
31
+ writeFileSync(
32
+ join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
33
+ [
34
+ "# M001: Test Milestone",
35
+ "",
36
+ "## Slices",
37
+ "",
38
+ "### S01 — First Slice",
39
+ "Do something.",
40
+ "",
41
+ "### S02 — Second Slice",
42
+ "Do another thing.",
43
+ ].join("\n"),
44
+ );
45
+
46
+ openDatabase(":memory:");
47
+ // Milestone exists but NO slices inserted — simulates DB wipe
48
+ insertMilestone({ id: "M001", title: "Test Milestone", status: "active" });
49
+
50
+ invalidateStateCache();
51
+ const state = await deriveStateFromDb(base);
52
+
53
+ // The phase must NOT be "validating-milestone" or "completing-milestone"
54
+ // because no slices have been executed — the empty array should not
55
+ // trigger the "all slices done" code path.
56
+ assert.notEqual(
57
+ state.phase,
58
+ "validating-milestone",
59
+ "empty slice array must not trigger validating-milestone (vacuous truth)",
60
+ );
61
+ assert.notEqual(
62
+ state.phase,
63
+ "completing-milestone",
64
+ "empty slice array must not trigger completing-milestone (vacuous truth)",
65
+ );
66
+
67
+ closeDatabase();
68
+ } finally {
69
+ closeDatabase();
70
+ rmSync(base, { recursive: true, force: true });
71
+ }
72
+ });
73
+
74
+ test("deriveStateFromDb correctly reaches validating when all slices are done (#2667 guard)", async () => {
75
+ const base = mkdtempSync(join(tmpdir(), "gsd-vacuous-truth-"));
76
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01"), { recursive: true });
77
+
78
+ try {
79
+ writeFileSync(
80
+ join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
81
+ [
82
+ "# M001: Test Milestone",
83
+ "",
84
+ "## Slices",
85
+ "",
86
+ "### S01 — First Slice",
87
+ "Do something.",
88
+ ].join("\n"),
89
+ );
90
+
91
+ // Write a slice summary so the filesystem recognizes it as complete
92
+ writeFileSync(
93
+ join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-SUMMARY.md"),
94
+ "# S01 Summary\n\nDone.",
95
+ );
96
+
97
+ openDatabase(":memory:");
98
+ insertMilestone({ id: "M001", title: "Test Milestone", status: "active" });
99
+ insertSlice({ id: "S01", milestoneId: "M001", title: "First Slice", status: "complete", risk: "low", depends: [] });
100
+
101
+ invalidateStateCache();
102
+ const state = await deriveStateFromDb(base);
103
+
104
+ // With one slice that IS complete, phase should advance
105
+ assert.ok(
106
+ state.phase === "validating-milestone" || state.phase === "completing-milestone",
107
+ `expected validating or completing phase, got "${state.phase}"`,
108
+ );
109
+
110
+ closeDatabase();
111
+ } finally {
112
+ closeDatabase();
113
+ rmSync(base, { recursive: true, force: true });
114
+ }
115
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, it, afterEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdirSync, existsSync, 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 { handleValidateMilestone } from "../tools/validate-milestone.js";
9
+ import { openDatabase, closeDatabase, _getAdapter, insertMilestone } from "../gsd-db.js";
10
+ import { clearPathCache } from "../paths.js";
11
+ import { clearParseCache } from "../files.js";
12
+
13
+ function makeTmpBase(): string {
14
+ const base = join(tmpdir(), `gsd-val-handler-${randomUUID()}`);
15
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
16
+ return base;
17
+ }
18
+
19
+ const VALID_PARAMS = {
20
+ milestoneId: "M001",
21
+ verdict: "pass" as const,
22
+ remediationRound: 0,
23
+ successCriteriaChecklist: "- [x] All pass",
24
+ sliceDeliveryAudit: "| S01 | delivered |",
25
+ crossSliceIntegration: "No issues",
26
+ requirementCoverage: "All covered",
27
+ verdictRationale: "Everything checks out",
28
+ };
29
+
30
+ describe("handleValidateMilestone write ordering (#2725)", () => {
31
+ let base: string;
32
+
33
+ afterEach(() => {
34
+ clearPathCache();
35
+ clearParseCache();
36
+ try { closeDatabase(); } catch { /* */ }
37
+ if (base) {
38
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
39
+ }
40
+ });
41
+
42
+ it("writes DB row and disk file on success", async () => {
43
+ base = makeTmpBase();
44
+ const dbPath = join(base, ".gsd", "gsd.db");
45
+ openDatabase(dbPath);
46
+ insertMilestone({ id: "M001" });
47
+
48
+ const result = await handleValidateMilestone(VALID_PARAMS, base);
49
+ assert.ok(!("error" in result), `unexpected error: ${"error" in result ? result.error : ""}`);
50
+
51
+ // DB row exists
52
+ const adapter = _getAdapter()!;
53
+ const row = adapter.prepare(
54
+ `SELECT status, scope FROM assessments WHERE milestone_id = 'M001' AND scope = 'milestone-validation'`,
55
+ ).get() as { status: string; scope: string } | undefined;
56
+ assert.ok(row, "assessment row should exist in DB");
57
+ assert.equal(row!.status, "pass");
58
+
59
+ // Disk file exists
60
+ const filePath = join(base, ".gsd", "milestones", "M001", "M001-VALIDATION.md");
61
+ assert.ok(existsSync(filePath), "VALIDATION.md should exist on disk");
62
+ });
63
+
64
+ it("rolls back DB row when disk write fails", async () => {
65
+ base = makeTmpBase();
66
+ const dbPath = join(base, ".gsd", "gsd.db");
67
+ openDatabase(dbPath);
68
+ insertMilestone({ id: "M001" });
69
+
70
+ // Force disk write failure by replacing the milestone directory with a
71
+ // regular file. saveFile() will fail because it cannot write inside a
72
+ // non-directory. This works cross-platform (chmod is ignored on Windows).
73
+ const milestoneDir = join(base, ".gsd", "milestones", "M001");
74
+ rmSync(milestoneDir, { recursive: true, force: true });
75
+ writeFileSync(milestoneDir, "not-a-directory");
76
+
77
+ const result = await handleValidateMilestone(VALID_PARAMS, base);
78
+
79
+ // Should return error
80
+ assert.ok("error" in result, "should return error when disk write fails");
81
+ assert.ok(result.error.includes("disk render failed"));
82
+
83
+ // DB row should have been rolled back (deleted)
84
+ const adapter = _getAdapter()!;
85
+ const row = adapter.prepare(
86
+ `SELECT * FROM assessments WHERE milestone_id = 'M001' AND scope = 'milestone-validation'`,
87
+ ).get();
88
+ assert.equal(row, undefined, "assessment row should be deleted after disk-write rollback");
89
+ });
90
+ });
@@ -1,8 +1,11 @@
1
1
  // GSD Extension — Workflow Logger Tests
2
2
  // Tests for the centralized warning/error accumulator.
3
3
 
4
- import { describe, test, beforeEach } from "node:test";
4
+ import { describe, test, beforeEach, afterEach } from "node:test";
5
5
  import assert from "node:assert/strict";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { makeTempDir, cleanup } from "./test-utils.ts";
6
9
  import {
7
10
  logWarning,
8
11
  logError,
@@ -14,6 +17,7 @@ import {
14
17
  hasAnyIssues,
15
18
  summarizeLogs,
16
19
  formatForNotification,
20
+ setLogBasePath,
17
21
  _resetLogs,
18
22
  } from "../workflow-logger.ts";
19
23
 
@@ -222,6 +226,44 @@ describe("workflow-logger", () => {
222
226
  });
223
227
  });
224
228
 
229
+ describe("audit log persistence", () => {
230
+ let dir: string;
231
+
232
+ beforeEach(() => {
233
+ dir = makeTempDir("wl-audit-");
234
+ });
235
+
236
+ afterEach(() => {
237
+ setLogBasePath("");
238
+ cleanup(dir);
239
+ });
240
+
241
+ test("writes entry to .gsd/audit-log.jsonl after setLogBasePath", () => {
242
+ setLogBasePath(dir);
243
+ logWarning("engine", "audit test entry");
244
+
245
+ const auditPath = join(dir, ".gsd", "audit-log.jsonl");
246
+ assert.ok(existsSync(auditPath), "audit-log.jsonl should exist");
247
+ const content = readFileSync(auditPath, "utf-8");
248
+ const entry = JSON.parse(content.trim());
249
+ assert.equal(entry.severity, "warn");
250
+ assert.equal(entry.component, "engine");
251
+ assert.equal(entry.message, "audit test entry");
252
+ });
253
+
254
+ test("_resetLogs does not clear the audit base path", () => {
255
+ setLogBasePath(dir);
256
+ _resetLogs();
257
+ logWarning("engine", "post-reset entry");
258
+
259
+ const auditPath = join(dir, ".gsd", "audit-log.jsonl");
260
+ assert.ok(existsSync(auditPath), "audit-log.jsonl should exist after _resetLogs");
261
+ const content = readFileSync(auditPath, "utf-8");
262
+ const entry = JSON.parse(content.trim());
263
+ assert.equal(entry.message, "post-reset entry");
264
+ });
265
+ });
266
+
225
267
  describe("buffer limit", () => {
226
268
  test("caps at MAX_BUFFER entries, dropping oldest", () => {
227
269
  const OVER = 110;
@@ -237,6 +279,44 @@ describe("workflow-logger", () => {
237
279
  });
238
280
  });
239
281
 
282
+ describe("audit log persistence", () => {
283
+ let dir: string;
284
+
285
+ beforeEach(() => {
286
+ dir = makeTempDir("wl-audit-");
287
+ });
288
+
289
+ afterEach(() => {
290
+ setLogBasePath("");
291
+ cleanup(dir);
292
+ });
293
+
294
+ test("writes entry to .gsd/audit-log.jsonl after setLogBasePath", () => {
295
+ setLogBasePath(dir);
296
+ logWarning("engine", "audit test entry");
297
+
298
+ const auditPath = join(dir, ".gsd", "audit-log.jsonl");
299
+ assert.ok(existsSync(auditPath), "audit-log.jsonl should exist");
300
+ const content = readFileSync(auditPath, "utf-8");
301
+ const entry = JSON.parse(content.trim());
302
+ assert.equal(entry.severity, "warn");
303
+ assert.equal(entry.component, "engine");
304
+ assert.equal(entry.message, "audit test entry");
305
+ });
306
+
307
+ test("_resetLogs does not clear the audit base path", () => {
308
+ setLogBasePath(dir);
309
+ _resetLogs();
310
+ logWarning("engine", "post-reset entry");
311
+
312
+ const auditPath = join(dir, ".gsd", "audit-log.jsonl");
313
+ assert.ok(existsSync(auditPath), "audit-log.jsonl should exist after _resetLogs");
314
+ const content = readFileSync(auditPath, "utf-8");
315
+ const entry = JSON.parse(content.trim());
316
+ assert.equal(entry.message, "post-reset entry");
317
+ });
318
+ });
319
+
240
320
  describe("stderr output", () => {
241
321
  test("writes WARN prefix to stderr for warnings", (t) => {
242
322
  const written: string[] = [];