@opengsd/gsd-pi 1.2.0-dev.b1abb545 → 1.2.0-dev.e8563f58

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 (604) hide show
  1. package/dist/cli-style.d.ts +17 -0
  2. package/dist/cli-style.js +28 -0
  3. package/dist/cli.js +1 -1
  4. package/dist/headless-events.d.ts +4 -2
  5. package/dist/headless-events.js +14 -34
  6. package/dist/mcp-server.js +2 -1
  7. package/dist/models-resolver.d.ts +3 -13
  8. package/dist/models-resolver.js +3 -22
  9. package/dist/resource-loader.js +2 -14
  10. package/dist/resources/.managed-resources-content-hash +1 -1
  11. package/dist/resources/GSD-WORKFLOW.md +5 -4
  12. package/dist/resources/extensions/async-jobs/async-bash-tool.js +30 -64
  13. package/dist/resources/extensions/async-jobs/await-tool.js +80 -12
  14. package/dist/resources/extensions/async-jobs/index.js +65 -0
  15. package/dist/resources/extensions/async-jobs/job-manager.js +12 -1
  16. package/dist/resources/extensions/bg-shell/bg-shell-command.js +6 -6
  17. package/dist/resources/extensions/bg-shell/bg-shell-tool.js +10 -7
  18. package/dist/resources/extensions/bg-shell/overlay.js +9 -6
  19. package/dist/resources/extensions/bg-shell/process-manager.js +54 -25
  20. package/dist/resources/extensions/bg-shell/readiness-detector.js +11 -0
  21. package/dist/resources/extensions/bg-shell/utilities.js +5 -2
  22. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +209 -88
  23. package/dist/resources/extensions/browser-tools/engine/selection.js +73 -5
  24. package/dist/resources/extensions/browser-tools/index.js +69 -12
  25. package/dist/resources/extensions/claude-code-cli/models.js +9 -0
  26. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +38 -6
  27. package/dist/resources/extensions/gsd/auto/custom-verify-retry-store.js +17 -2
  28. package/dist/resources/extensions/gsd/auto/detect-stuck.js +33 -13
  29. package/dist/resources/extensions/gsd/auto/dispatch-history.js +105 -0
  30. package/dist/resources/extensions/gsd/auto/dispatch-key.js +37 -0
  31. package/dist/resources/extensions/gsd/auto/loop.js +4 -1
  32. package/dist/resources/extensions/gsd/auto/orchestrator.js +122 -58
  33. package/dist/resources/extensions/gsd/auto/phases.js +8 -3
  34. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +8 -32
  35. package/dist/resources/extensions/gsd/auto-dispatch.js +40 -57
  36. package/dist/resources/extensions/gsd/auto-model-selection.js +36 -13
  37. package/dist/resources/extensions/gsd/auto-post-unit.js +31 -14
  38. package/dist/resources/extensions/gsd/auto-prompts.js +81 -19
  39. package/dist/resources/extensions/gsd/auto-start.js +24 -26
  40. package/dist/resources/extensions/gsd/auto-tool-tracking.js +18 -0
  41. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +12 -20
  42. package/dist/resources/extensions/gsd/auto-verification.js +9 -28
  43. package/dist/resources/extensions/gsd/auto-worktree-repair.js +10 -2
  44. package/dist/resources/extensions/gsd/auto-worktree.js +35 -352
  45. package/dist/resources/extensions/gsd/auto.js +15 -20
  46. package/dist/resources/extensions/gsd/blocked-models.js +28 -0
  47. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +29 -8
  48. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +32 -12
  49. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
  50. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +19 -0
  51. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +229 -36
  52. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +319 -71
  53. package/dist/resources/extensions/gsd/branch-patterns.js +2 -0
  54. package/dist/resources/extensions/gsd/browser-daemon-auto-prep.js +83 -0
  55. package/dist/resources/extensions/gsd/browser-evidence.js +8 -2
  56. package/dist/resources/extensions/gsd/captures.js +5 -15
  57. package/dist/resources/extensions/gsd/closeout-recovery.js +3 -2
  58. package/dist/resources/extensions/gsd/closeout-wizard.js +92 -0
  59. package/dist/resources/extensions/gsd/commands/catalog.js +6 -62
  60. package/dist/resources/extensions/gsd/commands-handlers.js +46 -3
  61. package/dist/resources/extensions/gsd/consent-question.js +353 -0
  62. package/dist/resources/extensions/gsd/consent-verdict.js +63 -0
  63. package/dist/resources/extensions/gsd/constants.js +0 -2
  64. package/dist/resources/extensions/gsd/crash-recovery.js +4 -12
  65. package/dist/resources/extensions/gsd/db/engine.js +755 -0
  66. package/dist/resources/extensions/gsd/db/queries.js +398 -0
  67. package/dist/resources/extensions/gsd/db/sql-constants.js +11 -0
  68. package/dist/resources/extensions/gsd/db/writers/cascades.js +194 -0
  69. package/dist/resources/extensions/gsd/db/writers/import-restore.js +182 -0
  70. package/dist/resources/extensions/gsd/db/writers/memory.js +149 -0
  71. package/dist/resources/extensions/gsd/db/writers/reconcile.js +458 -0
  72. package/dist/resources/extensions/gsd/db/writers/status.js +70 -0
  73. package/dist/resources/extensions/gsd/dispatch-guard.js +10 -35
  74. package/dist/resources/extensions/gsd/doctor-environment.js +5 -11
  75. package/dist/resources/extensions/gsd/doctor-format.js +9 -6
  76. package/dist/resources/extensions/gsd/doctor-git-checks.js +6 -21
  77. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +21 -16
  78. package/dist/resources/extensions/gsd/engine-hook-contract.js +70 -0
  79. package/dist/resources/extensions/gsd/error-classifier.js +9 -0
  80. package/dist/resources/extensions/gsd/exec-sandbox.js +30 -10
  81. package/dist/resources/extensions/gsd/files.js +33 -19
  82. package/dist/resources/extensions/gsd/git-service.js +1 -0
  83. package/dist/resources/extensions/gsd/gitignore.js +3 -0
  84. package/dist/resources/extensions/gsd/gsd-command-home.js +22 -12
  85. package/dist/resources/extensions/gsd/gsd-db.js +172 -2048
  86. package/dist/resources/extensions/gsd/guidance.js +158 -0
  87. package/dist/resources/extensions/gsd/guided-flow.js +51 -5
  88. package/dist/resources/extensions/gsd/markdown-renderer.js +10 -0
  89. package/dist/resources/extensions/gsd/mcp-filter.js +2 -19
  90. package/dist/resources/extensions/gsd/mcp-tool-name.js +5 -13
  91. package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +1 -1
  92. package/dist/resources/extensions/gsd/migrate/safety.js +20 -9
  93. package/dist/resources/extensions/gsd/migration-auto-check.js +24 -3
  94. package/dist/resources/extensions/gsd/milestone-closeout.js +85 -24
  95. package/dist/resources/extensions/gsd/model-cost-table.js +1 -0
  96. package/dist/resources/extensions/gsd/model-router.js +3 -0
  97. package/dist/resources/extensions/gsd/notification-store.js +11 -4
  98. package/dist/resources/extensions/gsd/parallel-merge.js +14 -11
  99. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +11 -7
  100. package/dist/resources/extensions/gsd/parsers-legacy.js +16 -4
  101. package/dist/resources/extensions/gsd/paths.js +37 -24
  102. package/dist/resources/extensions/gsd/pre-execution-checks.js +91 -3
  103. package/dist/resources/extensions/gsd/preferences-models.js +14 -48
  104. package/dist/resources/extensions/gsd/preferences.js +14 -0
  105. package/dist/resources/extensions/gsd/prompts/complete-slice.md +2 -2
  106. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  107. package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  108. package/dist/resources/extensions/gsd/prompts/replan-slice.md +1 -1
  109. package/dist/resources/extensions/gsd/prompts/run-uat.md +6 -4
  110. package/dist/resources/extensions/gsd/prompts/system.md +5 -2
  111. package/dist/resources/extensions/gsd/provider-error-guidance.js +1 -5
  112. package/dist/resources/extensions/gsd/provider-switch-observer.js +1 -1
  113. package/dist/resources/extensions/gsd/publication.js +87 -0
  114. package/dist/resources/extensions/gsd/reactive-graph.js +8 -1
  115. package/dist/resources/extensions/gsd/recovery-classification.js +41 -87
  116. package/dist/resources/extensions/gsd/safety/destructive-confirmation.js +108 -0
  117. package/dist/resources/extensions/gsd/safety/evidence-collector.js +37 -4
  118. package/dist/resources/extensions/gsd/safety/evidence-cross-ref.js +7 -2
  119. package/dist/resources/extensions/gsd/safety/file-change-validator.js +10 -0
  120. package/dist/resources/extensions/gsd/state-transition-matrix.js +38 -0
  121. package/dist/resources/extensions/gsd/state.js +6 -20
  122. package/dist/resources/extensions/gsd/status-guards.js +56 -8
  123. package/dist/resources/extensions/gsd/stop-notice.js +57 -0
  124. package/dist/resources/extensions/gsd/tool-presentation-plan.js +4 -4
  125. package/dist/resources/extensions/gsd/tool-surface-readiness.js +56 -0
  126. package/dist/resources/extensions/gsd/tools/complete-slice.js +44 -53
  127. package/dist/resources/extensions/gsd/tools/exec-tool.js +10 -8
  128. package/dist/resources/extensions/gsd/tools/plan-slice.js +12 -6
  129. package/dist/resources/extensions/gsd/tools/reopen-milestone.js +11 -29
  130. package/dist/resources/extensions/gsd/tools/reopen-slice.js +14 -33
  131. package/dist/resources/extensions/gsd/tools/skip-slice.js +18 -36
  132. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +67 -2
  133. package/dist/resources/extensions/gsd/uat-policy.js +42 -16
  134. package/dist/resources/extensions/gsd/undo.js +8 -7
  135. package/dist/resources/extensions/gsd/unit-closeout.js +138 -0
  136. package/dist/resources/extensions/gsd/unit-context-composer.js +74 -1
  137. package/dist/resources/extensions/gsd/unit-context-manifest.js +4 -27
  138. package/dist/resources/extensions/gsd/unit-registry.js +337 -0
  139. package/dist/resources/extensions/gsd/unit-tool-contracts.js +9 -182
  140. package/dist/resources/extensions/gsd/verdict-parser.js +1 -1
  141. package/dist/resources/extensions/gsd/web-app-uat.js +45 -8
  142. package/dist/resources/extensions/gsd/workflow-tool-surface.js +1 -1
  143. package/dist/resources/extensions/gsd/worktree-git-recovery.js +293 -0
  144. package/dist/resources/extensions/gsd/worktree-lifecycle.js +12 -3
  145. package/dist/resources/extensions/gsd/worktree-manager.js +45 -28
  146. package/dist/resources/extensions/gsd/worktree-placement.js +59 -0
  147. package/dist/resources/extensions/gsd/worktree-reentry.js +12 -8
  148. package/dist/resources/extensions/gsd/worktree-root.js +28 -6
  149. package/dist/resources/extensions/gsd/worktree-safety.js +8 -5
  150. package/dist/resources/extensions/gsd/worktree-session-state.js +12 -11
  151. package/dist/resources/extensions/search-the-web/native-search.js +5 -3
  152. package/dist/resources/extensions/shared/browser-contract.js +59 -0
  153. package/dist/resources/extensions/shared/gsd-browser-cli.js +116 -6
  154. package/dist/resources/shared/gsd-browser-path-sync.js +214 -0
  155. package/dist/resources/shared/package-manager-detection.js +1 -1
  156. package/dist/resources/shared/package.json +3 -0
  157. package/dist/resources/skills/create-skill/references/executable-code.md +1 -1
  158. package/dist/resources/skills/create-skill/workflows/add-reference.md +8 -3
  159. package/dist/resources/skills/create-skill/workflows/add-script.md +4 -2
  160. package/dist/resources/skills/create-skill/workflows/add-template.md +3 -1
  161. package/dist/resources/skills/create-skill/workflows/add-workflow.md +8 -3
  162. package/dist/resources/skills/create-skill/workflows/upgrade-to-router.md +10 -5
  163. package/dist/resources/skills/create-skill/workflows/verify-skill.md +9 -4
  164. package/dist/resources/skills/gsd-browser/SKILL.md +1 -1
  165. package/dist/resources/skills/spike-wrap-up/SKILL.md +9 -9
  166. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  167. package/dist/update-check.d.ts +2 -0
  168. package/dist/update-check.js +24 -1
  169. package/dist/update-cmd.js +20 -3
  170. package/dist/web/standalone/.next/BUILD_ID +1 -1
  171. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  172. package/dist/web/standalone/.next/build-manifest.json +3 -3
  173. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  174. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  175. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  176. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  177. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  178. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  179. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  180. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  181. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  182. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  183. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  184. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  185. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  186. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  187. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  188. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  189. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  190. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  191. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  192. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  193. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  194. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  195. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  196. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  197. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  198. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  199. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  200. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  201. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  202. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  203. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  204. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  205. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  206. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  207. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
  208. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  209. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  210. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  211. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  212. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  213. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  214. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  215. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  216. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  217. package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
  218. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  219. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  220. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  221. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  222. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  223. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  224. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  225. package/dist/web/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  226. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  227. package/dist/web/standalone/.next/server/app/index.html +1 -1
  228. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  229. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  230. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  231. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  232. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  233. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  234. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  235. package/dist/web/standalone/.next/server/chunks/5124.js +1 -1
  236. package/dist/web/standalone/.next/server/chunks/{5047.js → 5942.js} +2 -2
  237. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  238. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  239. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  240. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  241. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  242. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  243. package/dist/web/standalone/.next/static/chunks/{796.cf859a427a2cb2ac.js → 796.e0bdc932325d7e03.js} +1 -1
  244. package/dist/web/standalone/.next/static/chunks/{webpack-fbea77b5f9953368.js → webpack-f0285ce91d4ec9ef.js} +1 -1
  245. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  246. package/dist/web/standalone/package.json +1 -1
  247. package/dist/worktree-cli.js +3 -6
  248. package/dist/worktree-status-banner.js +7 -11
  249. package/package.json +1 -1
  250. package/packages/cloud-mcp-gateway/package.json +2 -2
  251. package/packages/contracts/dist/rpc.d.ts +1 -0
  252. package/packages/contracts/dist/rpc.d.ts.map +1 -1
  253. package/packages/contracts/dist/rpc.js.map +1 -1
  254. package/packages/contracts/dist/workflow.d.ts +4 -0
  255. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  256. package/packages/contracts/dist/workflow.js.map +1 -1
  257. package/packages/contracts/package.json +1 -1
  258. package/packages/daemon/package.json +4 -4
  259. package/packages/gsd-agent-core/package.json +5 -5
  260. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts +5 -0
  261. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  262. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +8 -0
  263. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  264. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  265. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +7 -0
  266. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  267. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  268. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.js +8 -1
  269. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  270. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.d.ts.map +1 -1
  271. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js +11 -1
  272. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js.map +1 -1
  273. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.d.ts.map +1 -1
  274. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js +4 -4
  275. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js.map +1 -1
  276. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  277. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-mode.js +3 -1
  278. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-mode.js.map +1 -1
  279. package/packages/gsd-agent-modes/package.json +7 -7
  280. package/packages/mcp-server/dist/cli.js +6 -3
  281. package/packages/mcp-server/dist/cli.js.map +1 -1
  282. package/packages/mcp-server/dist/moonshot-tool-schema.d.ts +29 -0
  283. package/packages/mcp-server/dist/moonshot-tool-schema.d.ts.map +1 -0
  284. package/packages/mcp-server/dist/moonshot-tool-schema.js +50 -0
  285. package/packages/mcp-server/dist/moonshot-tool-schema.js.map +1 -0
  286. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  287. package/packages/mcp-server/dist/server.js +4 -0
  288. package/packages/mcp-server/dist/server.js.map +1 -1
  289. package/packages/mcp-server/dist/workflow-tools.d.ts +26 -18
  290. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  291. package/packages/mcp-server/dist/workflow-tools.js +145 -59
  292. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  293. package/packages/mcp-server/package.json +5 -4
  294. package/packages/native/package.json +1 -1
  295. package/packages/pi-agent-core/dist/harness/env/nodejs.d.ts +1 -0
  296. package/packages/pi-agent-core/dist/harness/env/nodejs.d.ts.map +1 -1
  297. package/packages/pi-agent-core/dist/harness/env/nodejs.js +34 -3
  298. package/packages/pi-agent-core/dist/harness/env/nodejs.js.map +1 -1
  299. package/packages/pi-agent-core/dist/index.d.ts +1 -0
  300. package/packages/pi-agent-core/dist/index.d.ts.map +1 -1
  301. package/packages/pi-agent-core/dist/index.js +3 -0
  302. package/packages/pi-agent-core/dist/index.js.map +1 -1
  303. package/packages/pi-agent-core/package.json +1 -1
  304. package/packages/pi-ai/README.md +1 -0
  305. package/packages/pi-ai/dist/index.d.ts +2 -0
  306. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  307. package/packages/pi-ai/dist/index.js +2 -0
  308. package/packages/pi-ai/dist/index.js.map +1 -1
  309. package/packages/pi-ai/dist/models.generated.d.ts +192 -0
  310. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  311. package/packages/pi-ai/dist/models.generated.js +166 -0
  312. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  313. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  314. package/packages/pi-ai/dist/providers/anthropic.js +12 -7
  315. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  316. package/packages/pi-ai/dist/providers/google-shared.d.ts +5 -0
  317. package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
  318. package/packages/pi-ai/dist/providers/google-shared.js +12 -3
  319. package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
  320. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  321. package/packages/pi-ai/dist/providers/openai-completions.js +7 -3
  322. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  323. package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts +9 -0
  324. package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts.map +1 -0
  325. package/packages/pi-ai/dist/utils/moonshot-tool-schema.js +34 -0
  326. package/packages/pi-ai/dist/utils/moonshot-tool-schema.js.map +1 -0
  327. package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  328. package/packages/pi-ai/dist/utils/oauth/github-copilot.js +6 -2
  329. package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  330. package/packages/pi-ai/package.json +3 -2
  331. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +2 -2
  332. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  333. package/packages/pi-coding-agent/dist/core/auth-storage.js +19 -13
  334. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  335. package/packages/pi-coding-agent/dist/core/capability-patches.d.ts.map +1 -1
  336. package/packages/pi-coding-agent/dist/core/capability-patches.js +3 -1
  337. package/packages/pi-coding-agent/dist/core/capability-patches.js.map +1 -1
  338. package/packages/pi-coding-agent/dist/core/provider-readiness.d.ts.map +1 -1
  339. package/packages/pi-coding-agent/dist/core/provider-readiness.js +13 -6
  340. package/packages/pi-coding-agent/dist/core/provider-readiness.js.map +1 -1
  341. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +11 -0
  342. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  343. package/packages/pi-coding-agent/dist/core/tools/bash.js +53 -11
  344. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  345. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  346. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  347. package/packages/pi-coding-agent/dist/index.js +1 -1
  348. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  349. package/packages/pi-coding-agent/dist/utils/shell.d.ts +28 -2
  350. package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  351. package/packages/pi-coding-agent/dist/utils/shell.js +56 -10
  352. package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
  353. package/packages/pi-coding-agent/package.json +7 -7
  354. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  355. package/packages/pi-tui/dist/tui.js +9 -0
  356. package/packages/pi-tui/dist/tui.js.map +1 -1
  357. package/packages/pi-tui/package.json +2 -2
  358. package/packages/rpc-client/package.json +2 -2
  359. package/pkg/package.json +1 -1
  360. package/src/resources/GSD-WORKFLOW.md +5 -4
  361. package/src/resources/extensions/async-jobs/async-bash-cancel.test.ts +360 -0
  362. package/src/resources/extensions/async-jobs/async-bash-tool.ts +33 -56
  363. package/src/resources/extensions/async-jobs/await-tool.test.ts +139 -0
  364. package/src/resources/extensions/async-jobs/await-tool.ts +82 -12
  365. package/src/resources/extensions/async-jobs/index.ts +79 -0
  366. package/src/resources/extensions/async-jobs/job-manager.ts +21 -1
  367. package/src/resources/extensions/bg-shell/bg-shell-command.ts +6 -6
  368. package/src/resources/extensions/bg-shell/bg-shell-tool.ts +10 -6
  369. package/src/resources/extensions/bg-shell/overlay.ts +9 -5
  370. package/src/resources/extensions/bg-shell/process-manager.ts +50 -25
  371. package/src/resources/extensions/bg-shell/readiness-detector.ts +12 -0
  372. package/src/resources/extensions/bg-shell/tests/lifecycle-and-utilities.test.ts +48 -1
  373. package/src/resources/extensions/bg-shell/utilities.ts +5 -2
  374. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +265 -98
  375. package/src/resources/extensions/browser-tools/engine/selection.ts +90 -4
  376. package/src/resources/extensions/browser-tools/index.ts +71 -13
  377. package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +83 -13
  378. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +40 -1
  379. package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +136 -0
  380. package/src/resources/extensions/claude-code-cli/models.ts +9 -0
  381. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +40 -4
  382. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +28 -0
  383. package/src/resources/extensions/gsd/auto/custom-verify-retry-store.ts +21 -3
  384. package/src/resources/extensions/gsd/auto/detect-stuck.ts +32 -9
  385. package/src/resources/extensions/gsd/auto/dispatch-history.ts +152 -0
  386. package/src/resources/extensions/gsd/auto/dispatch-key.ts +39 -0
  387. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -1
  388. package/src/resources/extensions/gsd/auto/loop.ts +4 -1
  389. package/src/resources/extensions/gsd/auto/orchestrator.ts +137 -61
  390. package/src/resources/extensions/gsd/auto/phases.ts +12 -3
  391. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +8 -32
  392. package/src/resources/extensions/gsd/auto-dispatch.ts +38 -52
  393. package/src/resources/extensions/gsd/auto-model-selection.ts +41 -12
  394. package/src/resources/extensions/gsd/auto-post-unit.ts +37 -13
  395. package/src/resources/extensions/gsd/auto-prompts.ts +118 -35
  396. package/src/resources/extensions/gsd/auto-start.ts +24 -29
  397. package/src/resources/extensions/gsd/auto-tool-tracking.ts +19 -0
  398. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +14 -21
  399. package/src/resources/extensions/gsd/auto-verification.ts +8 -26
  400. package/src/resources/extensions/gsd/auto-worktree-repair.ts +13 -2
  401. package/src/resources/extensions/gsd/auto-worktree.ts +41 -364
  402. package/src/resources/extensions/gsd/auto.ts +28 -24
  403. package/src/resources/extensions/gsd/blocked-models.ts +49 -0
  404. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +37 -10
  405. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +33 -12
  406. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
  407. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +24 -0
  408. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +270 -37
  409. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +368 -78
  410. package/src/resources/extensions/gsd/branch-patterns.ts +3 -0
  411. package/src/resources/extensions/gsd/browser-daemon-auto-prep.ts +108 -0
  412. package/src/resources/extensions/gsd/browser-evidence.ts +18 -2
  413. package/src/resources/extensions/gsd/captures.ts +5 -16
  414. package/src/resources/extensions/gsd/closeout-recovery.ts +2 -1
  415. package/src/resources/extensions/gsd/closeout-wizard.ts +102 -0
  416. package/src/resources/extensions/gsd/commands/catalog.ts +6 -68
  417. package/src/resources/extensions/gsd/commands-handlers.ts +46 -3
  418. package/src/resources/extensions/gsd/consent-question.ts +431 -0
  419. package/src/resources/extensions/gsd/consent-verdict.ts +86 -0
  420. package/src/resources/extensions/gsd/constants.ts +0 -3
  421. package/src/resources/extensions/gsd/crash-recovery.ts +3 -9
  422. package/src/resources/extensions/gsd/db/engine.ts +809 -0
  423. package/src/resources/extensions/gsd/db/queries.ts +490 -0
  424. package/src/resources/extensions/gsd/db/sql-constants.ts +12 -0
  425. package/src/resources/extensions/gsd/db/writers/cascades.ts +237 -0
  426. package/src/resources/extensions/gsd/db/writers/import-restore.ts +310 -0
  427. package/src/resources/extensions/gsd/db/writers/memory.ts +220 -0
  428. package/src/resources/extensions/gsd/db/writers/reconcile.ts +500 -0
  429. package/src/resources/extensions/gsd/db/writers/status.ts +88 -0
  430. package/src/resources/extensions/gsd/dispatch-guard.ts +8 -31
  431. package/src/resources/extensions/gsd/doctor-environment.ts +5 -13
  432. package/src/resources/extensions/gsd/doctor-format.ts +12 -7
  433. package/src/resources/extensions/gsd/doctor-git-checks.ts +5 -22
  434. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +22 -17
  435. package/src/resources/extensions/gsd/engine-hook-contract.ts +79 -0
  436. package/src/resources/extensions/gsd/error-classifier.ts +11 -0
  437. package/src/resources/extensions/gsd/exec-sandbox.ts +49 -9
  438. package/src/resources/extensions/gsd/files.ts +33 -12
  439. package/src/resources/extensions/gsd/git-service.ts +1 -0
  440. package/src/resources/extensions/gsd/gitignore.ts +3 -0
  441. package/src/resources/extensions/gsd/gsd-command-home.ts +13 -3
  442. package/src/resources/extensions/gsd/gsd-db.ts +176 -2375
  443. package/src/resources/extensions/gsd/guidance.ts +217 -0
  444. package/src/resources/extensions/gsd/guided-flow.ts +50 -5
  445. package/src/resources/extensions/gsd/markdown-renderer.ts +11 -0
  446. package/src/resources/extensions/gsd/mcp-filter.ts +2 -23
  447. package/src/resources/extensions/gsd/mcp-tool-name.ts +6 -11
  448. package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +1 -1
  449. package/src/resources/extensions/gsd/migrate/safety.ts +18 -7
  450. package/src/resources/extensions/gsd/migration-auto-check.ts +28 -3
  451. package/src/resources/extensions/gsd/milestone-closeout.ts +109 -24
  452. package/src/resources/extensions/gsd/model-cost-table.ts +1 -0
  453. package/src/resources/extensions/gsd/model-router.ts +3 -0
  454. package/src/resources/extensions/gsd/notification-store.ts +26 -3
  455. package/src/resources/extensions/gsd/parallel-merge.ts +12 -9
  456. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +10 -7
  457. package/src/resources/extensions/gsd/parsers-legacy.ts +16 -4
  458. package/src/resources/extensions/gsd/paths.ts +42 -22
  459. package/src/resources/extensions/gsd/pre-execution-checks.ts +109 -3
  460. package/src/resources/extensions/gsd/preferences-models.ts +12 -47
  461. package/src/resources/extensions/gsd/preferences.ts +18 -0
  462. package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -2
  463. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  464. package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  465. package/src/resources/extensions/gsd/prompts/replan-slice.md +1 -1
  466. package/src/resources/extensions/gsd/prompts/run-uat.md +6 -4
  467. package/src/resources/extensions/gsd/prompts/system.md +5 -2
  468. package/src/resources/extensions/gsd/provider-error-guidance.ts +4 -9
  469. package/src/resources/extensions/gsd/provider-switch-observer.ts +1 -1
  470. package/src/resources/extensions/gsd/publication.ts +122 -0
  471. package/src/resources/extensions/gsd/reactive-graph.ts +11 -1
  472. package/src/resources/extensions/gsd/recovery-classification.ts +47 -88
  473. package/src/resources/extensions/gsd/safety/destructive-confirmation.ts +134 -0
  474. package/src/resources/extensions/gsd/safety/evidence-collector.ts +36 -4
  475. package/src/resources/extensions/gsd/safety/evidence-cross-ref.ts +7 -2
  476. package/src/resources/extensions/gsd/safety/file-change-validator.ts +14 -0
  477. package/src/resources/extensions/gsd/state-transition-matrix.ts +42 -0
  478. package/src/resources/extensions/gsd/state.ts +9 -21
  479. package/src/resources/extensions/gsd/status-guards.ts +59 -8
  480. package/src/resources/extensions/gsd/stop-notice.ts +75 -0
  481. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +123 -0
  482. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +91 -0
  483. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +198 -26
  484. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +3 -1
  485. package/src/resources/extensions/gsd/tests/auto-post-unit-evidence-crossref-4909.test.ts +46 -0
  486. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +2 -2
  487. package/src/resources/extensions/gsd/tests/auto-worktree-repair.test.ts +4 -2
  488. package/src/resources/extensions/gsd/tests/blocked-models.test.ts +19 -0
  489. package/src/resources/extensions/gsd/tests/browser-automation-contract-fixture.ts +39 -0
  490. package/src/resources/extensions/gsd/tests/browser-contract.test.ts +44 -0
  491. package/src/resources/extensions/gsd/tests/browser-daemon-auto-prep.test.ts +144 -0
  492. package/src/resources/extensions/gsd/tests/checkout-branch-stash-guard.test.ts +66 -1
  493. package/src/resources/extensions/gsd/tests/clear-stale-autostart.test.ts +44 -0
  494. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +8 -7
  495. package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +42 -0
  496. package/src/resources/extensions/gsd/tests/consent-question.test.ts +351 -0
  497. package/src/resources/extensions/gsd/tests/custom-verify-retry-store.test.ts +67 -0
  498. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +10 -10
  499. package/src/resources/extensions/gsd/tests/destructive-confirmation.test.ts +303 -0
  500. package/src/resources/extensions/gsd/tests/dispatch-history.test.ts +273 -0
  501. package/src/resources/extensions/gsd/tests/dispatch-run-uat-browser-tools.test.ts +2 -1
  502. package/src/resources/extensions/gsd/tests/doctor-git-checks-terminal.test.ts +73 -0
  503. package/src/resources/extensions/gsd/tests/dynamic-bash-no-cap.test.ts +132 -0
  504. package/src/resources/extensions/gsd/tests/engine-hook-contract.test.ts +148 -0
  505. package/src/resources/extensions/gsd/tests/evidence-xref-gsd-exec.test.ts +157 -0
  506. package/src/resources/extensions/gsd/tests/exec-graceful-kill.test.ts +193 -0
  507. package/src/resources/extensions/gsd/tests/exec-tool.test.ts +29 -1
  508. package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +35 -1
  509. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +33 -1
  510. package/src/resources/extensions/gsd/tests/gsd-command-home.test.ts +120 -0
  511. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +27 -0
  512. package/src/resources/extensions/gsd/tests/guidance.test.ts +148 -0
  513. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +58 -15
  514. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +74 -59
  515. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +3 -2
  516. package/src/resources/extensions/gsd/tests/integration/gsd-integration-fixture.ts +80 -0
  517. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +199 -0
  518. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +3 -1
  519. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +85 -1
  520. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +95 -4
  521. package/src/resources/extensions/gsd/tests/model-unittype-mapping.test.ts +32 -1
  522. package/src/resources/extensions/gsd/tests/notification-store.test.ts +32 -0
  523. package/src/resources/extensions/gsd/tests/oauth-api-model-routing.test.ts +167 -0
  524. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +18 -0
  525. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +138 -0
  526. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +193 -1
  527. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +124 -6
  528. package/src/resources/extensions/gsd/tests/provider-error-guidance.test.ts +3 -3
  529. package/src/resources/extensions/gsd/tests/publication.test.ts +120 -0
  530. package/src/resources/extensions/gsd/tests/recovery-classification-illegal-transition.test.ts +30 -0
  531. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +248 -1
  532. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +1 -0
  533. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +38 -0
  534. package/src/resources/extensions/gsd/tests/session-switch-clears-pending-autostart.test.ts +108 -0
  535. package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +43 -6
  536. package/src/resources/extensions/gsd/tests/state-transition-matrix.test.ts +36 -0
  537. package/src/resources/extensions/gsd/tests/status-guards.test.ts +38 -0
  538. package/src/resources/extensions/gsd/tests/stop-notice.test.ts +70 -0
  539. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +76 -0
  540. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +8 -0
  541. package/src/resources/extensions/gsd/tests/tool-surface-readiness.test.ts +155 -0
  542. package/src/resources/extensions/gsd/tests/uat-policy.test.ts +112 -29
  543. package/src/resources/extensions/gsd/tests/unit-closeout.test.ts +209 -0
  544. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +67 -2
  545. package/src/resources/extensions/gsd/tests/unit-registry.test.ts +163 -0
  546. package/src/resources/extensions/gsd/tests/web-app-uat.test.ts +44 -1
  547. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
  548. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +275 -40
  549. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +41 -4
  550. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +22 -1
  551. package/src/resources/extensions/gsd/tests/worktree-placement.test.ts +113 -0
  552. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +1 -1
  553. package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +3 -1
  554. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +12 -6
  555. package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +2 -2
  556. package/src/resources/extensions/gsd/tests/write-gate-seam.test.ts +358 -0
  557. package/src/resources/extensions/gsd/tests/write-gate.test.ts +109 -1
  558. package/src/resources/extensions/gsd/tool-presentation-plan.ts +4 -4
  559. package/src/resources/extensions/gsd/tool-surface-readiness.ts +76 -0
  560. package/src/resources/extensions/gsd/tools/complete-slice.ts +43 -68
  561. package/src/resources/extensions/gsd/tools/exec-tool.ts +9 -8
  562. package/src/resources/extensions/gsd/tools/plan-slice.ts +12 -6
  563. package/src/resources/extensions/gsd/tools/reopen-milestone.ts +11 -38
  564. package/src/resources/extensions/gsd/tools/reopen-slice.ts +14 -42
  565. package/src/resources/extensions/gsd/tools/skip-slice.ts +18 -44
  566. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +81 -2
  567. package/src/resources/extensions/gsd/uat-policy.ts +62 -16
  568. package/src/resources/extensions/gsd/undo.ts +9 -8
  569. package/src/resources/extensions/gsd/unit-closeout.ts +201 -0
  570. package/src/resources/extensions/gsd/unit-context-composer.ts +111 -1
  571. package/src/resources/extensions/gsd/unit-context-manifest.ts +4 -28
  572. package/src/resources/extensions/gsd/unit-registry.ts +412 -0
  573. package/src/resources/extensions/gsd/unit-tool-contracts.ts +27 -192
  574. package/src/resources/extensions/gsd/verdict-parser.ts +1 -1
  575. package/src/resources/extensions/gsd/web-app-uat.ts +51 -8
  576. package/src/resources/extensions/gsd/workflow-tool-surface.ts +4 -1
  577. package/src/resources/extensions/gsd/worktree-git-recovery.ts +314 -0
  578. package/src/resources/extensions/gsd/worktree-lifecycle.ts +13 -9
  579. package/src/resources/extensions/gsd/worktree-manager.ts +47 -28
  580. package/src/resources/extensions/gsd/worktree-placement.ts +63 -0
  581. package/src/resources/extensions/gsd/worktree-reentry.ts +10 -7
  582. package/src/resources/extensions/gsd/worktree-root.ts +29 -6
  583. package/src/resources/extensions/gsd/worktree-safety.ts +8 -5
  584. package/src/resources/extensions/gsd/worktree-session-state.ts +11 -11
  585. package/src/resources/extensions/search-the-web/native-search.ts +5 -3
  586. package/src/resources/extensions/shared/browser-contract.ts +66 -0
  587. package/src/resources/extensions/shared/gsd-browser-cli.ts +141 -6
  588. package/src/resources/shared/gsd-browser-path-sync.ts +273 -0
  589. package/src/resources/shared/package-manager-detection.ts +1 -1
  590. package/src/resources/shared/package.json +3 -0
  591. package/src/resources/skills/create-skill/references/executable-code.md +1 -1
  592. package/src/resources/skills/create-skill/workflows/add-reference.md +8 -3
  593. package/src/resources/skills/create-skill/workflows/add-script.md +4 -2
  594. package/src/resources/skills/create-skill/workflows/add-template.md +3 -1
  595. package/src/resources/skills/create-skill/workflows/add-workflow.md +8 -3
  596. package/src/resources/skills/create-skill/workflows/upgrade-to-router.md +10 -5
  597. package/src/resources/skills/create-skill/workflows/verify-skill.md +9 -4
  598. package/src/resources/skills/gsd-browser/SKILL.md +1 -1
  599. package/src/resources/skills/spike-wrap-up/SKILL.md +9 -9
  600. package/dist/resources/extensions/gsd/user-input-boundary.js +0 -218
  601. package/src/resources/extensions/gsd/tests/user-input-boundary.test.ts +0 -173
  602. package/src/resources/extensions/gsd/user-input-boundary.ts +0 -216
  603. /package/dist/web/standalone/.next/static/{3PtrU9qGPEXwNLWkIyiqk → LDHRKiRBIVZmiuMjrL1Vy}/_buildManifest.js +0 -0
  604. /package/dist/web/standalone/.next/static/{3PtrU9qGPEXwNLWkIyiqk → LDHRKiRBIVZmiuMjrL1Vy}/_ssgManifest.js +0 -0
@@ -1,40 +1,25 @@
1
1
  // Project/App: gsd-pi
2
- // File Purpose: GSD database facade, schema, migrations, and single-writer write API.
3
- // GSD Database Abstraction Layer
4
- // Provides a SQLite database with provider fallback chain:
5
- // node:sqlite (built-in) → better-sqlite3 (npm) → null (unavailable)
6
- //
7
- // Exposes a unified sync API for decisions and requirements storage.
8
- // Schema is initialized on first open with WAL mode for file-backed DBs.
2
+ // File Purpose: GSD single-writer barrel + write/read wrappers.
9
3
  //
10
4
  // ─── Single-writer invariant ─────────────────────────────────────────────
11
- // This file is the ONLY place in the codebase that issues write SQL
12
- // (INSERT / UPDATE / DELETE / REPLACE / BEGIN-COMMIT transactions) against
13
- // the engine database at `.gsd/gsd.db`. All other modules must call the
14
- // typed wrappers exported here. The structural test
15
- // `tests/single-writer-invariant.test.ts` fails CI if a new bypass appears.
5
+ // Every write-SQL statement against `.gsd/gsd.db` lives behind a typed
6
+ // wrapper in the single-writer layer (this file plus db/writers/*). Connection
7
+ // ownership, lifecycle, schema/migrations and transaction primitives live in
8
+ // db/engine.ts and are re-exported here for backward compatibility, so callers
9
+ // keep importing from "./gsd-db.js".
16
10
  //
17
- // `_getAdapter()` is retained for read-only SELECTs in query modules
18
- // (context-store, memory-store queries, doctor checks, projections).
19
- // Do NOT use it for writes — add a wrapper here instead.
11
+ // `_getAdapter()` (re-exported from the engine) is retained for read-only
12
+ // SELECTs in query modules. Do NOT use it for writes — add a wrapper here.
20
13
  //
21
- // The separate `.gsd/unit-claims.db` managed by `unit-ownership.ts` is an
22
- // intentionally independent store for cross-worktree claim races and is
23
- // excluded from this invariant.
24
-
25
- import { createRequire } from "node:module";
14
+ // The separate `.gsd/unit-claims.db` (unit-ownership.ts) is an intentionally
15
+ // independent store and is excluded from this invariant.
26
16
  import { createHash } from "node:crypto";
27
- import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
28
- import { dirname, join } from "node:path";
17
+ import { join } from "node:path";
29
18
  import type { Decision, Requirement, GateRow, GateId, GateScope, GateStatus, GateVerdict } from "./types.js";
30
19
  import { GSDError, GSD_STALE_STATE } from "./errors.js";
31
- import type { GsdWorkspace, MilestoneScope } from "./workspace.js";
32
20
  import { getGateIdsForTurn, type OwnerTurn } from "./gate-registry.js";
33
21
  import { logError, logWarning } from "./workflow-logger.js";
34
- import { createDbAdapter, type DbAdapter } from "./db-adapter.js";
35
- import { createBaseSchemaObjects } from "./db-base-schema.js";
36
- import { createCoordinationTablesV24 } from "./db-coordination-schema.js";
37
- import { createDbConnectionCache, type DbConnectionCacheEntry } from "./db-connection-cache.js";
22
+ import { type DbAdapter } from "./db-adapter.js";
38
23
  import {
39
24
  emptyTaskStatusCounts,
40
25
  rowToActiveTaskSummary,
@@ -54,783 +39,36 @@ import {
54
39
  } from "./db-decision-requirement-rows.js";
55
40
  import { rowToGate } from "./db-gate-rows.js";
56
41
  import { rowToArtifact, rowToMilestone, type ArtifactRow, type MilestoneRow } from "./db-milestone-artifact-rows.js";
57
- import { backupDatabaseBeforeMigration } from "./db-migration-backup.js";
58
42
  import { isClosedStatus } from "./status-guards.js";
59
- import {
60
- applyMigrationV2Artifacts,
61
- applyMigrationV3Memories,
62
- applyMigrationV4DecisionMadeBy,
63
- applyMigrationV5HierarchyTables,
64
- applyMigrationV6SliceSummaries,
65
- applyMigrationV7Dependencies,
66
- applyMigrationV8PlanningFields,
67
- applyMigrationV9Ordering,
68
- applyMigrationV10ReplanTrigger,
69
- applyMigrationV11TaskPlanning,
70
- applyMigrationV12QualityGates,
71
- applyMigrationV13HotPathIndexes,
72
- applyMigrationV14SliceDependencies,
73
- applyMigrationV15AuditTables,
74
- applyMigrationV16EscalationSource,
75
- applyMigrationV17TaskEscalation,
76
- applyMigrationV18MemorySources,
77
- applyMigrationV19MemoryFts,
78
- applyMigrationV20MemoryRelations,
79
- applyMigrationV21StructuredMemories,
80
- applyMigrationV22QualityGateRepair,
81
- applyMigrationV23MilestoneQueue,
82
- applyMigrationV26MilestoneCommitAttributions,
83
- applyMigrationV27ArtifactHash,
84
- applyMigrationV28MemoryLastHitAt,
85
- applyMigrationV29RepositoryTargets,
86
- } from "./db-migration-steps.js";
87
- import { isMemoriesFtsAvailableSchema, tryCreateMemoriesFtsSchema } from "./db-memory-fts-schema.js";
88
- import { createDbOpenState, type DbOpenPhase } from "./db-open-state.js";
89
- import { createRuntimeKvTableV25 } from "./db-runtime-kv-schema.js";
90
- import { ensureColumn, getCurrentSchemaVersion, recordSchemaVersion } from "./db-schema-metadata.js";
91
43
  import { rowToSlice, rowToTask, type SliceRow, type TaskRow } from "./db-task-slice-rows.js";
92
- import { createDbTransactionRunner } from "./db-transaction.js";
93
- import { ensureVerificationEvidenceDedupIndex } from "./db-verification-evidence-schema.js";
94
- import {
95
- BETTER_SQLITE3_PACKAGE,
96
- createSqliteProviderLoader,
97
- suppressSqliteWarning,
98
- type DbProviderName,
99
- type SqliteFallbackOpen,
100
- } from "./db-provider.js";
101
- // Type-only import to avoid a circular runtime dep. The runtime side of
102
- // workflow-manifest.ts depends on this file, but the StateManifest type is
103
- // pure structure with no runtime coupling.
104
- import type { StateManifest } from "./workflow-manifest.js";
105
-
106
- let _gsdRequire: ReturnType<typeof createRequire> | null | undefined;
107
-
108
- function getGsdRequire(): ReturnType<typeof createRequire> | null {
109
- if (_gsdRequire !== undefined) return _gsdRequire;
110
- try {
111
- _gsdRequire = createRequire(import.meta.url);
112
- } catch {
113
- _gsdRequire = null;
114
- }
115
- return _gsdRequire;
116
- }
117
44
 
118
- type ProviderName = DbProviderName;
45
+ // Connection ownership, lifecycle, schema/migrations and transaction
46
+ // primitives now live in the engine; re-export the full public surface so
47
+ // existing `from "./gsd-db.js"` imports keep working.
48
+ export * from "./db/engine.js";
49
+ import { transaction, getDb, getDbOrNull } from "./db/engine.js";
50
+
51
+ // ─── Single Writer Layer re-exports ──────────────────────────────────────
52
+ // Write subsystems live in db/writers/*; re-exported here so callers keep
53
+ // importing from "./gsd-db.js".
54
+ export * from "./db/writers/memory.js";
55
+ export * from "./db/writers/reconcile.js";
56
+ export * from "./db/writers/import-restore.js";
57
+ // Query Module (read-only seam) — extracted from the single-writer file.
58
+ export * from "./db/queries.js";
59
+ // Domain Write Operations (Hierarchy Status Cascades).
60
+ export * from "./db/writers/cascades.js";
119
61
 
120
62
  export type { ArtifactRow, MilestoneRow } from "./db-milestone-artifact-rows.js";
121
63
  export type { ActiveTaskSummary, IdStatusSummary, TaskStatusCounts } from "./db-lightweight-query-rows.js";
122
64
  export type { SliceRow, TaskRow } from "./db-task-slice-rows.js";
123
65
 
124
- const providerLoader = createSqliteProviderLoader({
125
- tryRequireNodeSqlite: () => {
126
- const req = getGsdRequire();
127
- if (!req) throw new Error("unavailable");
128
- return req("node:sqlite");
129
- },
130
- tryRequireBetterSqlite3: () => {
131
- const req = getGsdRequire();
132
- if (!req) throw new Error("unavailable");
133
- return req(BETTER_SQLITE3_PACKAGE);
134
- },
135
- suppressSqliteWarning,
136
- nodeVersion: process.versions.node,
137
- writeStderr: (message: string) => process.stderr.write(message),
138
- });
139
-
140
- export const SCHEMA_VERSION = 29;
141
- const TERMINAL_STATUS_SQL = "'complete', 'done', 'skipped', 'closed'";
142
-
143
- function initSchema(db: DbAdapter, fileBacked: boolean, dbPath: string | null): void {
144
- const conservativeFilePragmas = fileBacked && _isLikelyWslDrvFsPathForTest(dbPath);
145
- if (fileBacked) db.exec(conservativeFilePragmas ? "PRAGMA journal_mode=DELETE" : "PRAGMA journal_mode=WAL");
146
- if (fileBacked) db.exec("PRAGMA busy_timeout = 5000");
147
- if (fileBacked) db.exec(conservativeFilePragmas ? "PRAGMA synchronous = FULL" : "PRAGMA synchronous = NORMAL");
148
- if (fileBacked) db.exec("PRAGMA auto_vacuum = INCREMENTAL");
149
- if (fileBacked) db.exec("PRAGMA cache_size = -8000"); // 8 MB page cache
150
- if (fileBacked && !conservativeFilePragmas && process.platform !== "darwin") db.exec("PRAGMA mmap_size = 67108864"); // 64 MB mmap
151
- db.exec("PRAGMA temp_store = MEMORY");
152
- db.exec("PRAGMA foreign_keys = ON");
153
-
154
- db.exec("BEGIN");
155
- try {
156
- createBaseSchemaObjects(db, {
157
- tryCreateMemoriesFts,
158
- ensureVerificationEvidenceDedupIndex,
159
- });
160
-
161
- const existing = db.prepare("SELECT count(*) as cnt FROM schema_version").get();
162
- if (existing && (existing["cnt"] as number) === 0) {
163
- createCoordinationTablesV24(db);
164
- createRuntimeKvTableV25(db);
165
-
166
- // Fresh install — all tables are created above with the full current schema,
167
- // so it is safe to create all migration-specific indexes here. For existing
168
- // databases these indexes are created inside the individual migration guards
169
- // in migrateSchema() after the corresponding columns have been added.
170
- db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_escalation_pending ON tasks(milestone_id, slice_id, escalation_pending)");
171
- db.exec("CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope)");
172
- db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_kind ON memory_sources(kind)");
173
- db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_scope ON memory_sources(scope)");
174
- db.exec("CREATE INDEX IF NOT EXISTS idx_memory_relations_from ON memory_relations(from_id)");
175
- db.exec("CREATE INDEX IF NOT EXISTS idx_memory_relations_to ON memory_relations(to_id)");
176
-
177
- recordSchemaVersion(db, SCHEMA_VERSION);
178
- }
179
-
180
- db.exec("COMMIT");
181
- } catch (err) {
182
- db.exec("ROLLBACK");
183
- throw err;
184
- }
185
-
186
- migrateSchema(db);
187
- }
188
-
189
- export function _isLikelyWslDrvFsPathForTest(dbPath: string | null): boolean {
190
- if (!dbPath || process.platform !== "linux") return false;
191
- const drvFsPathPattern = /^\/mnt\/[a-z](?:\/|$)/i;
192
- if (drvFsPathPattern.test(dbPath)) return true;
193
- try {
194
- return drvFsPathPattern.test(realpathSync(dbPath));
195
- } catch {
196
- return false;
197
- }
198
- }
199
-
200
- /**
201
- * Create the FTS5 virtual table for memories plus the triggers that keep it
202
- * in sync with the base table. FTS5 may be unavailable on stripped-down
203
- * SQLite builds — callers should treat failure as non-fatal and fall back
204
- * to LIKE-based scans in `memory-store.queryMemoriesRanked`.
205
- */
206
- export function tryCreateMemoriesFts(db: DbAdapter): boolean {
207
- return tryCreateMemoriesFtsSchema(db, {
208
- onUnavailable: (message) => logWarning("db", message),
209
- });
210
- }
211
-
212
- export function isMemoriesFtsAvailable(db: DbAdapter): boolean {
213
- return isMemoriesFtsAvailableSchema(db);
214
- }
215
-
216
- function backfillMemoriesFts(db: DbAdapter): void {
217
- db.exec(`INSERT INTO memories_fts(rowid, content) SELECT seq, content FROM memories`);
218
- }
219
-
220
- function copyQualityGateRowsToRepairedTable(db: DbAdapter): void {
221
- db.exec(`
222
- INSERT OR IGNORE INTO quality_gates_new
223
- (milestone_id, slice_id, gate_id, scope, task_id, status, verdict, rationale, findings, evaluated_at)
224
- SELECT milestone_id, slice_id, gate_id, scope, COALESCE(task_id, ''), status, verdict, rationale, findings, evaluated_at
225
- FROM quality_gates
226
- `);
227
- }
228
-
229
- function migrateSchema(db: DbAdapter): void {
230
- const currentVersion = getCurrentSchemaVersion(db);
231
- if (currentVersion >= SCHEMA_VERSION) return;
232
-
233
- backupDatabaseBeforeMigration(db, currentPath, currentVersion, {
234
- existsSync,
235
- copyFileSync,
236
- logWarning,
237
- });
238
-
239
- db.exec("BEGIN");
240
- try {
241
- if (currentVersion < 2) {
242
- applyMigrationV2Artifacts(db);
243
- recordSchemaVersion(db, 2);
244
- }
245
-
246
- if (currentVersion < 3) {
247
- applyMigrationV3Memories(db);
248
- recordSchemaVersion(db, 3);
249
- }
250
-
251
- if (currentVersion < 4) {
252
- applyMigrationV4DecisionMadeBy(db);
253
- recordSchemaVersion(db, 4);
254
- }
255
-
256
- if (currentVersion < 5) {
257
- applyMigrationV5HierarchyTables(db);
258
- recordSchemaVersion(db, 5);
259
- }
260
-
261
- if (currentVersion < 6) {
262
- applyMigrationV6SliceSummaries(db);
263
- recordSchemaVersion(db, 6);
264
- }
265
-
266
- if (currentVersion < 7) {
267
- applyMigrationV7Dependencies(db);
268
- recordSchemaVersion(db, 7);
269
- }
270
-
271
- if (currentVersion < 8) {
272
- applyMigrationV8PlanningFields(db);
273
- recordSchemaVersion(db, 8);
274
- }
275
-
276
- if (currentVersion < 9) {
277
- applyMigrationV9Ordering(db);
278
- recordSchemaVersion(db, 9);
279
- }
280
-
281
- if (currentVersion < 10) {
282
- applyMigrationV10ReplanTrigger(db);
283
- recordSchemaVersion(db, 10);
284
- }
285
-
286
- if (currentVersion < 11) {
287
- applyMigrationV11TaskPlanning(db);
288
- recordSchemaVersion(db, 11);
289
- }
290
-
291
- if (currentVersion < 12) {
292
- // NOTE: The original DDL used COALESCE(task_id, '') in the PRIMARY KEY
293
- // expression, which is invalid SQLite syntax and causes startup errors on
294
- // DBs that migrate through v12. The corrected DDL uses
295
- // task_id TEXT NOT NULL DEFAULT '' with a plain column list PK. DBs that
296
- // were created with the broken DDL are repaired by the v22 migration below.
297
- applyMigrationV12QualityGates(db);
298
- recordSchemaVersion(db, 12);
299
- }
300
-
301
- if (currentVersion < 13) {
302
- applyMigrationV13HotPathIndexes(db, ensureVerificationEvidenceDedupIndex);
303
- recordSchemaVersion(db, 13);
304
- }
305
-
306
- if (currentVersion < 14) {
307
- applyMigrationV14SliceDependencies(db);
308
- recordSchemaVersion(db, 14);
309
- }
310
-
311
- if (currentVersion < 15) {
312
- applyMigrationV15AuditTables(db);
313
- recordSchemaVersion(db, 15);
314
- }
315
-
316
- if (currentVersion < 16) {
317
- applyMigrationV16EscalationSource(db);
318
- recordSchemaVersion(db, 16);
319
- }
320
-
321
- if (currentVersion < 17) {
322
- applyMigrationV17TaskEscalation(db);
323
- recordSchemaVersion(db, 17);
324
- }
325
-
326
- if (currentVersion < 18) {
327
- applyMigrationV18MemorySources(db);
328
- recordSchemaVersion(db, 18);
329
- }
330
-
331
- if (currentVersion < 19) {
332
- applyMigrationV19MemoryFts(db, {
333
- tryCreateMemoriesFts,
334
- isMemoriesFtsAvailable,
335
- backfillMemoriesFts,
336
- logWarning,
337
- });
338
- recordSchemaVersion(db, 19);
339
- }
340
-
341
- if (currentVersion < 20) {
342
- applyMigrationV20MemoryRelations(db);
343
- recordSchemaVersion(db, 20);
344
- }
345
-
346
- if (currentVersion < 21) {
347
- applyMigrationV21StructuredMemories(db);
348
- recordSchemaVersion(db, 21);
349
- }
350
-
351
- if (currentVersion < 22) {
352
- applyMigrationV22QualityGateRepair(db, { copyQualityGateRowsToRepairedTable });
353
- recordSchemaVersion(db, 22);
354
- }
355
-
356
- if (currentVersion < 23) {
357
- applyMigrationV23MilestoneQueue(db);
358
- recordSchemaVersion(db, 23);
359
- }
360
-
361
- if (currentVersion < 24) {
362
- // v24: auto-mode coordination tables. See createCoordinationTablesV24
363
- // for full schema + invariants. No-op for fresh installs (the same
364
- // helper runs in the fresh-install path); for upgraded DBs this is
365
- // the only place these tables get created.
366
- createCoordinationTablesV24(db);
367
- recordSchemaVersion(db, 24);
368
- }
369
-
370
- if (currentVersion < 25) {
371
- // v25: runtime_kv non-correctness-critical key-value storage. See
372
- // createRuntimeKvTableV25 for the full schema + invariants.
373
- createRuntimeKvTableV25(db);
374
- recordSchemaVersion(db, 25);
375
- }
376
-
377
- if (currentVersion < 26) {
378
- applyMigrationV26MilestoneCommitAttributions(db);
379
- recordSchemaVersion(db, 26);
380
- }
381
-
382
- if (currentVersion < 27) {
383
- applyMigrationV27ArtifactHash(db);
384
- recordSchemaVersion(db, 27);
385
- }
386
-
387
- if (currentVersion < 28) {
388
- applyMigrationV28MemoryLastHitAt(db);
389
- recordSchemaVersion(db, 28);
390
- }
391
-
392
- if (currentVersion < 29) {
393
- applyMigrationV29RepositoryTargets(db);
394
- recordSchemaVersion(db, 29);
395
- }
396
-
397
- db.exec("COMMIT");
398
- } catch (err) {
399
- db.exec("ROLLBACK");
400
- throw err;
401
- }
402
- }
403
-
404
- let currentDb: DbAdapter | null = null;
405
- let currentPath: string | null = null;
406
- let currentPid: number = 0;
407
- let _exitHandlerRegistered = false;
408
- const _dbOpenState = createDbOpenState();
409
- /**
410
- * Identity key of the workspace whose connection is currently active
411
- * (currentDb). Set by openDatabaseByWorkspace(); null when the active
412
- * connection was opened via the legacy openDatabase(path) path.
413
- */
414
- let _currentIdentityKey: string | null = null;
415
-
416
- /**
417
- * Workspace-scoped connection cache.
418
- * Key: GsdWorkspace.identityKey (realpath-normalized project root).
419
- * Value: the DB path and open adapter for that workspace.
420
- *
421
- * Sibling worktrees of the same project share the same identityKey (set by
422
- * createWorkspace) and therefore reuse the same cached connection, preserving
423
- * shared-WAL semantics. Different projects get distinct cache entries.
424
- *
425
- * NOTE: Only one connection is "active" at a time (currentDb/currentPath).
426
- * The cache allows fast re-activation of a previously opened connection when
427
- * callers switch between known workspaces via openDatabaseByWorkspace().
428
- */
429
- const _dbCache = createDbConnectionCache();
430
-
431
- /** Test helper: expose the internal cache for inspection. Not for production use. */
432
- export function _getDbCache(): ReadonlyMap<string, DbConnectionCacheEntry> {
433
- return _dbCache.asReadonlyMap();
434
- }
435
-
436
- function closeCachedConnection(entry: DbConnectionCacheEntry, source: "all" | "workspace"): void {
437
- try {
438
- entry.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
439
- } catch (e) {
440
- if (source === "workspace") logWarning("db", `WAL checkpoint (byWorkspace) failed: ${(e as Error).message}`);
441
- }
442
- try {
443
- entry.db.exec("PRAGMA incremental_vacuum(64)");
444
- } catch (e) {
445
- if (source === "workspace") logWarning("db", `incremental vacuum (byWorkspace) failed: ${(e as Error).message}`);
446
- }
447
- try {
448
- entry.db.close();
449
- } catch (e) {
450
- if (source === "workspace") logWarning("db", `database close (byWorkspace) failed: ${(e as Error).message}`);
451
- }
452
- }
453
-
454
- /**
455
- * Close and evict every entry in the workspace connection cache, then call
456
- * closeDatabase() to close the active connection.
457
- *
458
- * Use this for test teardown or process-shutdown paths where every open
459
- * connection must be flushed. Normal callers should use closeDatabase() or
460
- * closeDatabaseByWorkspace() instead.
461
- */
462
- export function closeAllDatabases(): void {
463
- // Close all non-active cached connections first.
464
- _dbCache.closeNonActive(currentDb, (entry) => closeCachedConnection(entry, "all"));
465
- closeDatabase();
466
- }
467
-
468
- /**
469
- * Open (or reuse) the database connection scoped to the given workspace.
470
- *
471
- * Uses workspace.identityKey as the cache key, so sibling worktrees of the
472
- * same project resolve to the same connection. On a cache hit the existing
473
- * adapter is reactivated as the current connection without re-opening the
474
- * file. On a cache miss, delegates to openDatabase() for the full
475
- * open + schema-init + migration flow, then caches the result.
476
- *
477
- * When switching to a different workspace, the previously active connection
478
- * is preserved in the cache (not closed), so callers can switch back to it
479
- * cheaply via a subsequent openDatabaseByWorkspace() call.
480
- *
481
- * @param workspace A GsdWorkspace created by createWorkspace().
482
- * @returns true if the connection is open and ready, false otherwise.
483
- */
484
- export function openDatabaseByWorkspace(workspace: GsdWorkspace): boolean {
485
- const key = workspace.identityKey;
486
- const dbPath = workspace.contract.projectDb;
487
-
488
- const cached = _dbCache.get(key);
489
- if (cached) {
490
- // Reactivate the cached connection as the current singleton.
491
- currentDb = cached.db;
492
- currentPath = cached.dbPath;
493
- currentPid = process.pid;
494
- _dbOpenState.markAttempted();
495
- _currentIdentityKey = key;
496
- return true;
497
- }
498
-
499
- // Cache miss — need to open a new connection.
500
- //
501
- // If there is a currently active workspace connection, stash it in the
502
- // cache under its identity key before calling openDatabase(), because
503
- // openDatabase() will call closeDatabase() when the path changes (which
504
- // would destroy the existing adapter). By nulling out currentDb first,
505
- // we prevent openDatabase() from closing the live adapter.
506
- let oldDb: typeof currentDb = null;
507
- let oldPath: typeof currentPath = null;
508
- let oldPid: typeof currentPid = 0;
509
- let oldKey: typeof _currentIdentityKey = null;
510
-
511
- if (currentDb !== null && _currentIdentityKey !== null) {
512
- // Snapshot the old globals so we can restore them on failure.
513
- oldDb = currentDb;
514
- oldPath = currentPath;
515
- oldPid = currentPid;
516
- oldKey = _currentIdentityKey;
517
- // Save the current connection so it stays alive in the cache.
518
- _dbCache.set(_currentIdentityKey, {
519
- dbPath: currentPath!,
520
- db: currentDb,
521
- });
522
- // Detach from globals so openDatabase() opens fresh without closing it.
523
- currentDb = null;
524
- currentPath = null;
525
- currentPid = 0;
526
- _currentIdentityKey = null;
527
- }
528
-
529
- // Run the full open/schema/migration flow for the new workspace.
530
- // openDatabase() can throw on corrupt DB or permission error — catch so we
531
- // can restore the previous connection rather than leaving globals null.
532
- let opened: boolean;
533
- try {
534
- opened = openDatabase(dbPath);
535
- } catch (err) {
536
- // Failed to open the new DB. Restore the previous workspace connection so
537
- // the caller's workspace remains active (it is still safe in _dbCache).
538
- if (oldDb !== null) {
539
- currentDb = oldDb;
540
- currentPath = oldPath;
541
- currentPid = oldPid;
542
- _currentIdentityKey = oldKey;
543
- }
544
- throw err;
545
- }
546
- if (opened && currentDb) {
547
- _dbCache.set(key, { dbPath, db: currentDb });
548
- _currentIdentityKey = key;
549
- } else if (!opened && oldDb !== null) {
550
- // Restore the previous connection so the caller's workspace remains active.
551
- // The failed attempt left no live adapter, so the globals stayed null.
552
- currentDb = oldDb;
553
- currentPath = oldPath;
554
- currentPid = oldPid;
555
- _currentIdentityKey = oldKey;
556
- }
557
- return opened;
558
- }
559
-
560
- /**
561
- * Open (or reuse) the database connection scoped to the workspace in a
562
- * MilestoneScope. Thin delegation to openDatabaseByWorkspace().
563
- */
564
- export function openDatabaseByScope(scope: MilestoneScope): boolean {
565
- return openDatabaseByWorkspace(scope.workspace);
566
- }
567
-
568
- /**
569
- * Close the database connection for the given workspace and remove it from
570
- * the cache. If the workspace's connection is currently active (currentDb),
571
- * performs a full closeDatabase() including WAL checkpoint. Otherwise only
572
- * removes the cache entry (the adapter was already replaced by a later open).
573
- */
574
- export function closeDatabaseByWorkspace(workspace: GsdWorkspace): void {
575
- const key = workspace.identityKey;
576
- const cached = _dbCache.get(key);
577
- if (!cached) return;
578
-
579
- _dbCache.delete(key);
580
-
581
- if (currentDb === cached.db) {
582
- // This workspace's connection is the active one — full close.
583
- closeDatabase();
584
- } else {
585
- // Connection was displaced by a later open; close the adapter directly.
586
- closeCachedConnection(cached, "workspace");
587
- }
588
- }
589
-
590
- export function getDbProvider(): ProviderName | null {
591
- providerLoader.load();
592
- return providerLoader.getProviderName();
593
- }
594
-
595
- export function isDbAvailable(): boolean {
596
- return currentDb !== null;
597
- }
598
-
599
- /**
600
- * Returns true if openDatabase() has been called at least once this session.
601
- * Used to distinguish "DB not yet initialized" from "DB genuinely unavailable"
602
- * so that early callers (e.g. before_agent_start context injection) don't
603
- * trigger a false degraded-mode warning.
604
- */
605
- export function wasDbOpenAttempted(): boolean {
606
- return _dbOpenState.snapshot().attempted;
607
- }
608
-
609
- export function getDbStatus(): {
610
- available: boolean;
611
- provider: ProviderName | null;
612
- attempted: boolean;
613
- lastError: Error | null;
614
- lastPhase: DbOpenPhase | null;
615
- } {
616
- providerLoader.load();
617
- const openState = _dbOpenState.snapshot();
618
- return {
619
- available: currentDb !== null,
620
- provider: providerLoader.getProviderName(),
621
- attempted: openState.attempted,
622
- lastError: openState.lastError,
623
- lastPhase: openState.lastPhase,
624
- };
625
- }
626
-
627
- export function openDatabase(path: string): boolean {
628
- _dbOpenState.markAttempted();
629
- if (currentDb && currentPath !== path) closeDatabase();
630
- if (currentDb && currentPath === path) return true;
631
-
632
- // Reset error state only when a new open attempt is actually going to run.
633
- _dbOpenState.clearError();
634
-
635
- let rawDb: unknown;
636
- let fallbackOpen: SqliteFallbackOpen | null = null;
637
- try {
638
- rawDb = providerLoader.openRaw(path);
639
- } catch (primaryErr) {
640
- _dbOpenState.recordError("open", primaryErr);
641
- // node:sqlite loaded but failed to open this file — try better-sqlite3 as fallback.
642
- fallbackOpen = providerLoader.tryOpenBetterSqliteFallback(path);
643
- if (fallbackOpen) {
644
- rawDb = fallbackOpen.rawDb;
645
- _dbOpenState.clearError();
646
- }
647
- if (!rawDb) throw primaryErr;
648
- }
649
- if (!rawDb) return false;
650
-
651
- const adapter = createDbAdapter(rawDb);
652
- const fileBacked = path !== ":memory:";
653
- try {
654
- initSchema(adapter, fileBacked, path);
655
- } catch (err) {
656
- // Corrupt freelist: DDL fails with "malformed" but VACUUM can rebuild.
657
- // Attempt VACUUM recovery before giving up (see #2519).
658
- if (fileBacked && err instanceof Error && err.message?.includes("malformed")) {
659
- try {
660
- adapter.exec("VACUUM");
661
- initSchema(adapter, fileBacked, path);
662
- process.stderr.write("gsd-db: recovered corrupt database via VACUUM\n");
663
- } catch (retryErr) {
664
- _dbOpenState.recordError("vacuum-recovery", retryErr);
665
- try { adapter.close(); } catch (e) { logWarning("db", `close after VACUUM failed: ${(e as Error).message}`); }
666
- throw retryErr;
667
- }
668
- } else {
669
- _dbOpenState.recordError("initSchema", err);
670
- try { adapter.close(); } catch (e) { logWarning("db", `close after initSchema failed: ${(e as Error).message}`); }
671
- throw err;
672
- }
673
- }
674
-
675
- // Commit fallback provider switch only after open + schema both succeeded.
676
- if (fallbackOpen) providerLoader.commitFallback(fallbackOpen);
677
-
678
- currentDb = adapter;
679
- currentPath = path;
680
- currentPid = process.pid;
681
-
682
- if (!_exitHandlerRegistered) {
683
- _exitHandlerRegistered = true;
684
- process.on("exit", () => { try { closeDatabase(); } catch (e) { logWarning("db", `exit handler close failed: ${(e as Error).message}`); } });
685
- }
686
-
687
- return true;
688
- }
689
-
690
- export function closeDatabase(): void {
691
- if (currentDb) {
692
- try {
693
- currentDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
694
- } catch (e) { logWarning("db", `WAL checkpoint failed: ${(e as Error).message}`); }
695
- try {
696
- // Incremental vacuum to reclaim space without blocking
697
- currentDb.exec('PRAGMA incremental_vacuum(64)');
698
- } catch (e) { logWarning("db", `incremental vacuum failed: ${(e as Error).message}`); }
699
- try {
700
- currentDb.close();
701
- } catch (e) { logWarning("db", `database close failed: ${(e as Error).message}`); }
702
- // If this connection was workspace-tracked, evict it from the cache so
703
- // subsequent openDatabaseByWorkspace() calls re-open rather than reactivate
704
- // a closed adapter.
705
- if (_currentIdentityKey !== null) {
706
- _dbCache.delete(_currentIdentityKey);
707
- _currentIdentityKey = null;
708
- }
709
- currentDb = null;
710
- currentPath = null;
711
- currentPid = 0;
712
- }
713
- // Reset session-scoped state unconditionally so stale error info from a
714
- // failed open doesn't persist into the next open attempt or status check.
715
- _dbOpenState.reset();
716
- }
717
-
718
- /**
719
- * Re-open the active database connection from disk.
720
- *
721
- * Auto-mode can observe artifacts written by a workflow server running in a
722
- * different process before its long-lived singleton has re-synchronized. The
723
- * recovery path uses this to force the next state derivation to read from the
724
- * current on-disk database instead of continuing with a possibly stale handle.
725
- */
726
- export function refreshOpenDatabaseFromDisk(): boolean {
727
- if (!currentDb || !currentPath) return false;
728
- if (currentPath === ":memory:") return false;
729
-
730
- const dbPath = currentPath;
731
- const identityKey = _currentIdentityKey;
732
-
733
- try {
734
- closeDatabase();
735
- const opened = openDatabase(dbPath);
736
- if (opened && identityKey && currentDb) {
737
- _dbCache.set(identityKey, { dbPath, db: currentDb });
738
- _currentIdentityKey = identityKey;
739
- }
740
- return opened;
741
- } catch (e) {
742
- logWarning("db", `database refresh failed: ${(e as Error).message}`);
743
- return false;
744
- }
745
- }
746
-
747
- /** Run a full VACUUM — call sparingly (e.g. after milestone completion). */
748
- export function vacuumDatabase(): void {
749
- if (!currentDb) return;
750
- try {
751
- currentDb.exec('VACUUM');
752
- } catch (e) { logWarning("db", `VACUUM failed: ${(e as Error).message}`); }
753
- }
754
-
755
- /** Flush WAL into gsd.db so `git add .gsd/gsd.db` stages current state — safe while DB is open. */
756
- export function checkpointDatabase(): void {
757
- if (!currentDb) return;
758
- try {
759
- currentDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
760
- } catch (e) { logWarning("db", `WAL checkpoint failed: ${(e as Error).message}`); }
761
- }
762
-
763
- /**
764
- * Copy the live database file to `.gsd/backups/<label>-<timestamp>.db` so a
765
- * destructive operation (e.g. recover, which clears the hierarchy tables) is
766
- * reversible. Checkpoints the WAL first so the snapshot is complete. Returns
767
- * the backup path, or null if no DB is open or the copy failed.
768
- */
769
- export function backupDatabaseSnapshot(label: string): string | null {
770
- if (!currentPath) return null;
771
- try {
772
- checkpointDatabase();
773
- const backupsDir = join(dirname(currentPath), "backups");
774
- mkdirSync(backupsDir, { recursive: true });
775
- const stamp = new Date().toISOString().replace(/[:.]/g, "-");
776
- const dest = join(backupsDir, `${label}-${stamp}.db`);
777
- copyFileSync(currentPath, dest);
778
- return dest;
779
- } catch (e) {
780
- logWarning("db", `database snapshot failed: ${(e as Error).message}`);
781
- return null;
782
- }
783
- }
784
-
785
- const _transactionRunner = createDbTransactionRunner();
786
-
787
- function createTransactionControls(db: DbAdapter) {
788
- return {
789
- begin: () => db.exec("BEGIN"),
790
- beginRead: () => db.exec("BEGIN DEFERRED"),
791
- commit: () => db.exec("COMMIT"),
792
- rollback: () => db.exec("ROLLBACK"),
793
- };
794
- }
795
-
796
- /**
797
- * Whether the current call is running inside an active SQLite transaction.
798
- * Statement-time recovery paths (e.g. VACUUM retry on a malformed memory
799
- * store) MUST gate on this — SQLite refuses VACUUM inside a transaction
800
- * and would mask the original error with a secondary "cannot VACUUM" throw.
801
- */
802
- export function isInTransaction(): boolean {
803
- return _transactionRunner.isInTransaction();
804
- }
805
-
806
- export function transaction<T>(fn: () => T): T {
807
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
808
- return _transactionRunner.transaction(createTransactionControls(currentDb), fn);
809
- }
810
-
811
- /**
812
- * Wrap a block of reads in a DEFERRED transaction so that all SELECTs observe
813
- * a consistent snapshot of the DB even if a concurrent writer commits between
814
- * them. Use this for multi-query read flows (e.g. tool executors that query
815
- * milestone + slices + counts and want one snapshot). Re-entrant — if already
816
- * inside a transaction, runs fn() without starting a nested one.
817
- */
818
- export function readTransaction<T>(fn: () => T): T {
819
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
820
-
821
- return _transactionRunner.readTransaction(createTransactionControls(currentDb), fn, (rollbackErr) => {
822
- // A failed ROLLBACK after a failed read is a split-brain signal —
823
- // the transaction is in an indeterminate state. Surface it via the
824
- // logger instead of swallowing it.
825
- logError("db", "snapshotState ROLLBACK failed", {
826
- error: rollbackErr.message,
827
- });
828
- });
829
- }
66
+ import { TERMINAL_STATUS_SQL } from "./db/sql-constants.js";
67
+ import { applyStatusTransition } from "./db/writers/status.js";
830
68
 
831
69
  export function insertDecision(d: Omit<Decision, "seq">): void {
832
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
833
- currentDb.prepare(
70
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
71
+ getDbOrNull()!.prepare(
834
72
  `INSERT INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, made_by, source, superseded_by)
835
73
  VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :made_by, :source, :superseded_by)`,
836
74
  ).run({
@@ -847,22 +85,11 @@ export function insertDecision(d: Omit<Decision, "seq">): void {
847
85
  });
848
86
  }
849
87
 
850
- export function getDecisionById(id: string): Decision | null {
851
- if (!currentDb) return null;
852
- const row = currentDb.prepare("SELECT * FROM decisions WHERE id = ?").get(id);
853
- if (!row) return null;
854
- return rowToDecision(row);
855
- }
856
88
 
857
- export function getActiveDecisions(): Decision[] {
858
- if (!currentDb) return [];
859
- const rows = currentDb.prepare("SELECT * FROM active_decisions").all();
860
- return rows.map(rowToActiveDecision);
861
- }
862
89
 
863
90
  export function insertRequirement(r: Requirement): void {
864
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
865
- currentDb.prepare(
91
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
92
+ getDbOrNull()!.prepare(
866
93
  `INSERT INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
867
94
  VALUES (:id, :class, :status, :description, :why, :source, :primary_owner, :supporting_slices, :validation, :notes, :full_content, :superseded_by)`,
868
95
  ).run({
@@ -881,58 +108,15 @@ export function insertRequirement(r: Requirement): void {
881
108
  });
882
109
  }
883
110
 
884
- export function getRequirementById(id: string): Requirement | null {
885
- if (!currentDb) return null;
886
- const row = currentDb.prepare("SELECT * FROM requirements WHERE id = ?").get(id);
887
- if (!row) return null;
888
- return rowToRequirement(row);
889
- }
890
111
 
891
- export function getActiveRequirements(): Requirement[] {
892
- if (!currentDb) return [];
893
- const rows = currentDb.prepare("SELECT * FROM active_requirements").all();
894
- return rows.map(rowToActiveRequirement);
895
- }
896
112
 
897
- export function getRequirementCounts(): {
898
- active: number;
899
- validated: number;
900
- deferred: number;
901
- outOfScope: number;
902
- blocked: number;
903
- total: number;
904
- } {
905
- if (!currentDb) {
906
- return { active: 0, validated: 0, deferred: 0, outOfScope: 0, blocked: 0, total: 0 };
907
- }
908
- const rows = currentDb
909
- .prepare("SELECT lower(status) as status, COUNT(*) as count FROM requirements GROUP BY lower(status)")
910
- .all();
911
- return rowsToRequirementCounts(rows);
912
- }
913
-
914
- export function getDbOwnerPid(): number {
915
- return currentPid;
916
- }
917
-
918
- export function getDbPath(): string | null {
919
- return currentPath;
920
- }
921
-
922
- export function _getAdapter(): DbAdapter | null {
923
- return currentDb;
924
- }
925
-
926
- export function _resetProvider(): void {
927
- providerLoader.reset();
928
- }
929
113
 
930
114
  export function upsertDecision(d: Omit<Decision, "seq">): void {
931
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
115
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
932
116
  // Use ON CONFLICT DO UPDATE instead of INSERT OR REPLACE to preserve the
933
117
  // seq column. INSERT OR REPLACE deletes then reinserts, resetting seq and
934
118
  // corrupting decision ordering in DECISIONS.md after reconcile replay.
935
- currentDb.prepare(
119
+ getDbOrNull()!.prepare(
936
120
  `INSERT INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, made_by, source, superseded_by)
937
121
  VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :made_by, :source, :superseded_by)
938
122
  ON CONFLICT(id) DO UPDATE SET
@@ -960,8 +144,8 @@ export function upsertDecision(d: Omit<Decision, "seq">): void {
960
144
  }
961
145
 
962
146
  export function upsertRequirement(r: Requirement): void {
963
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
964
- currentDb.prepare(
147
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
148
+ getDbOrNull()!.prepare(
965
149
  `INSERT OR REPLACE INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
966
150
  VALUES (:id, :class, :status, :description, :why, :source, :primary_owner, :supporting_slices, :validation, :notes, :full_content, :superseded_by)`,
967
151
  ).run({
@@ -981,18 +165,18 @@ export function upsertRequirement(r: Requirement): void {
981
165
  }
982
166
 
983
167
  export function clearArtifacts(): void {
984
- if (!currentDb) return;
985
- try { currentDb.exec("DELETE FROM artifacts"); } catch (e) { logWarning("db", `clearArtifacts failed: ${(e as Error).message}`); }
168
+ if (!getDbOrNull()!) return;
169
+ try { getDbOrNull()!.exec("DELETE FROM artifacts"); } catch (e) { logWarning("db", `clearArtifacts failed: ${(e as Error).message}`); }
986
170
  }
987
171
 
988
172
  export function clearDecisions(): void {
989
- if (!currentDb) return;
990
- try { currentDb.exec("DELETE FROM decisions"); } catch (e) { logWarning("db", `clearDecisions failed: ${(e as Error).message}`); }
173
+ if (!getDbOrNull()!) return;
174
+ try { getDbOrNull()!.exec("DELETE FROM decisions"); } catch (e) { logWarning("db", `clearDecisions failed: ${(e as Error).message}`); }
991
175
  }
992
176
 
993
177
  export function clearRequirements(): void {
994
- if (!currentDb) return;
995
- try { currentDb.exec("DELETE FROM requirements"); } catch (e) { logWarning("db", `clearRequirements failed: ${(e as Error).message}`); }
178
+ if (!getDbOrNull()!) return;
179
+ try { getDbOrNull()!.exec("DELETE FROM requirements"); } catch (e) { logWarning("db", `clearRequirements failed: ${(e as Error).message}`); }
996
180
  }
997
181
 
998
182
  export function insertArtifact(a: {
@@ -1003,9 +187,9 @@ export function insertArtifact(a: {
1003
187
  task_id: string | null;
1004
188
  full_content: string;
1005
189
  }): void {
1006
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
190
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1007
191
  const contentHash = createHash("sha256").update(a.full_content).digest("hex");
1008
- currentDb.prepare(
192
+ getDbOrNull()!.prepare(
1009
193
  `INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash)
1010
194
  VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at, :content_hash)`,
1011
195
  ).run({
@@ -1062,9 +246,9 @@ export function insertMilestone(m: {
1062
246
  status?: string;
1063
247
  depends_on?: string[];
1064
248
  planning?: Partial<MilestonePlanningRecord>;
1065
- }): void {
1066
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1067
- currentDb.prepare(
249
+ }): boolean {
250
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
251
+ const result = getDbOrNull()!.prepare(
1068
252
  `INSERT OR IGNORE INTO milestones (
1069
253
  id, title, status, depends_on, created_at,
1070
254
  vision, success_criteria, key_risks, proof_strategy,
@@ -1095,12 +279,13 @@ export function insertMilestone(m: {
1095
279
  ":definition_of_done": JSON.stringify(m.planning?.definitionOfDone ?? []),
1096
280
  ":requirement_coverage": m.planning?.requirementCoverage ?? "",
1097
281
  ":boundary_map_markdown": m.planning?.boundaryMapMarkdown ?? "",
1098
- });
282
+ }) as { changes?: number };
283
+ return (result.changes ?? 0) > 0;
1099
284
  }
1100
285
 
1101
286
  export function upsertMilestonePlanning(milestoneId: string, planning: Partial<MilestonePlanningRecord> & { title?: string; status?: string; depends_on?: string[] }): void {
1102
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1103
- currentDb.prepare(
287
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
288
+ getDbOrNull()!.prepare(
1104
289
  `UPDATE milestones SET
1105
290
  title = COALESCE(NULLIF(:title, ''), title),
1106
291
  status = COALESCE(NULLIF(:status, ''), status),
@@ -1149,13 +334,13 @@ export function insertSlice(s: {
1149
334
  sketchScope?: string;
1150
335
  planning?: Partial<SlicePlanningRecord>;
1151
336
  }): void {
1152
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
337
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1153
338
  const SLICE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
1154
339
  const invalidDep = (s.depends ?? []).find(d => !SLICE_ID_RE.test(d));
1155
340
  if (invalidDep !== undefined) {
1156
341
  throw new GSDError(GSD_STALE_STATE, `insertSlice: depends element "${invalidDep}" is not a valid slice ID`);
1157
342
  }
1158
- currentDb.prepare(
343
+ getDbOrNull()!.prepare(
1159
344
  `INSERT INTO slices (
1160
345
  milestone_id, id, title, status, risk, depends, demo, created_at,
1161
346
  goal, success_criteria, proof_level, integration_closure, observability_impact, target_repositories, sequence,
@@ -1219,29 +404,16 @@ export function insertSlice(s: {
1219
404
 
1220
405
  // ADR-011: sketch-then-refine helpers
1221
406
  export function setSliceSketchFlag(milestoneId: string, sliceId: string, isSketch: boolean): void {
1222
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1223
- currentDb.prepare(
407
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
408
+ getDbOrNull()!.prepare(
1224
409
  `UPDATE slices SET is_sketch = :is_sketch WHERE milestone_id = :mid AND id = :sid`,
1225
410
  ).run({ ":is_sketch": isSketch ? 1 : 0, ":mid": milestoneId, ":sid": sliceId });
1226
411
  }
1227
412
 
1228
- /**
1229
- * ADR-017 raw primitive: returns slice IDs in a milestone whose is_sketch flag
1230
- * is still 1. The stale-sketch-flag drift handler at
1231
- * `state-reconciliation/drift/sketch-flag.ts` composes this with PLAN.md
1232
- * existence checks to detect drift, then writes via `setSliceSketchFlag`.
1233
- */
1234
- export function getSketchedSliceIds(milestoneId: string): string[] {
1235
- if (!currentDb) return [];
1236
- const rows = currentDb.prepare(
1237
- `SELECT id FROM slices WHERE milestone_id = :mid AND is_sketch = 1`,
1238
- ).all({ ":mid": milestoneId }) as Array<{ id: string }>;
1239
- return rows.map((r) => r.id);
1240
- }
1241
413
 
1242
414
  export function upsertSlicePlanning(milestoneId: string, sliceId: string, planning: Partial<SlicePlanningRecord>): void {
1243
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1244
- currentDb.prepare(
415
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
416
+ getDbOrNull()!.prepare(
1245
417
  `UPDATE slices SET
1246
418
  goal = COALESCE(:goal, goal),
1247
419
  success_criteria = COALESCE(:success_criteria, success_criteria),
@@ -1281,8 +453,8 @@ export function insertTask(t: {
1281
453
  sequence?: number;
1282
454
  planning?: Partial<TaskPlanningRecord>;
1283
455
  }): void {
1284
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1285
- currentDb.prepare(
456
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
457
+ getDbOrNull()!.prepare(
1286
458
  `INSERT INTO tasks (
1287
459
  milestone_id, slice_id, id, title, status, one_liner, narrative,
1288
460
  verification_result, duration, completed_at, blocker_discovered,
@@ -1358,29 +530,19 @@ export function insertTask(t: {
1358
530
  }
1359
531
 
1360
532
  export function updateTaskStatus(milestoneId: string, sliceId: string, taskId: string, status: string, completedAt?: string): void {
1361
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1362
- currentDb.prepare(
1363
- `UPDATE tasks SET status = :status, completed_at = :completed_at
1364
- WHERE milestone_id = :milestone_id AND slice_id = :slice_id AND id = :id`,
1365
- ).run({
1366
- ":status": status,
1367
- ":completed_at": completedAt ?? null,
1368
- ":milestone_id": milestoneId,
1369
- ":slice_id": sliceId,
1370
- ":id": taskId,
1371
- });
533
+ applyStatusTransition({ entity: "task", milestoneId, sliceId, taskId, status, completedAt });
1372
534
  }
1373
535
 
1374
536
  export function setTaskBlockerDiscovered(milestoneId: string, sliceId: string, taskId: string, discovered: boolean): void {
1375
- if (!currentDb) return;
1376
- currentDb.prepare(
537
+ if (!getDbOrNull()!) return;
538
+ getDbOrNull()!.prepare(
1377
539
  `UPDATE tasks SET blocker_discovered = :discovered WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`,
1378
540
  ).run({ ":discovered": discovered ? 1 : 0, ":mid": milestoneId, ":sid": sliceId, ":tid": taskId });
1379
541
  }
1380
542
 
1381
543
  export function upsertTaskPlanning(milestoneId: string, sliceId: string, taskId: string, planning: Partial<TaskPlanningRecord>): void {
1382
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1383
- currentDb.prepare(
544
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
545
+ getDbOrNull()!.prepare(
1384
546
  `UPDATE tasks SET
1385
547
  title = COALESCE(:title, title),
1386
548
  description = COALESCE(:description, description),
@@ -1410,95 +572,28 @@ export function upsertTaskPlanning(milestoneId: string, sliceId: string, taskId:
1410
572
  });
1411
573
  }
1412
574
 
1413
- export function getSlice(milestoneId: string, sliceId: string): SliceRow | null {
1414
- if (!currentDb) return null;
1415
- const row = currentDb.prepare("SELECT * FROM slices WHERE milestone_id = :mid AND id = :sid").get({ ":mid": milestoneId, ":sid": sliceId });
1416
- if (!row) return null;
1417
- return rowToSlice(row);
1418
- }
1419
575
 
1420
576
  export function updateSliceStatus(milestoneId: string, sliceId: string, status: string, completedAt?: string): void {
1421
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1422
- currentDb.prepare(
1423
- `UPDATE slices SET status = :status, completed_at = :completed_at
1424
- WHERE milestone_id = :milestone_id AND id = :id`,
1425
- ).run({
1426
- ":status": status,
1427
- ":completed_at": completedAt ?? null,
1428
- ":milestone_id": milestoneId,
1429
- ":id": sliceId,
1430
- });
577
+ applyStatusTransition({ entity: "slice", milestoneId, sliceId, status, completedAt });
1431
578
  }
1432
579
 
1433
580
  export function setTaskSummaryMd(milestoneId: string, sliceId: string, taskId: string, md: string): void {
1434
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1435
- currentDb.prepare(
581
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
582
+ getDbOrNull()!.prepare(
1436
583
  `UPDATE tasks SET full_summary_md = :md WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`,
1437
584
  ).run({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId, ":md": md });
1438
585
  }
1439
586
 
1440
587
  export function setSliceSummaryMd(milestoneId: string, sliceId: string, summaryMd: string, uatMd: string): void {
1441
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1442
- currentDb.prepare(
588
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
589
+ getDbOrNull()!.prepare(
1443
590
  `UPDATE slices SET full_summary_md = :summary_md, full_uat_md = :uat_md WHERE milestone_id = :mid AND id = :sid`,
1444
591
  ).run({ ":mid": milestoneId, ":sid": sliceId, ":summary_md": summaryMd, ":uat_md": uatMd });
1445
592
  }
1446
593
 
1447
- export function getTask(milestoneId: string, sliceId: string, taskId: string): TaskRow | null {
1448
- if (!currentDb) return null;
1449
- const row = currentDb.prepare(
1450
- "SELECT * FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid",
1451
- ).get({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId });
1452
- if (!row) return null;
1453
- return rowToTask(row);
1454
- }
1455
-
1456
- export function getSliceTasks(milestoneId: string, sliceId: string): TaskRow[] {
1457
- if (!currentDb) return [];
1458
- const rows = currentDb.prepare(
1459
- "SELECT * FROM tasks WHERE milestone_id = :mid AND slice_id = :sid ORDER BY sequence, id",
1460
- ).all({ ":mid": milestoneId, ":sid": sliceId });
1461
- return rows.map(rowToTask);
1462
- }
1463
594
 
1464
- export function getCompletedMilestoneTaskFileHints(milestoneId: string): string[] {
1465
- if (!currentDb) return [];
1466
- const rows = currentDb.prepare(
1467
- `SELECT files, key_files
1468
- FROM tasks
1469
- WHERE milestone_id = :mid AND status IN ('complete', 'done')`,
1470
- ).all({ ":mid": milestoneId }) as Array<Record<string, unknown>>;
1471
-
1472
- const hints = new Set<string>();
1473
- for (const row of rows) {
1474
- for (const raw of [row["files"], row["key_files"]]) {
1475
- for (const file of parseStringArrayColumn(raw)) {
1476
- const normalized = normalizeRepoPath(file);
1477
- if (normalized) hints.add(normalized);
1478
- }
1479
- }
1480
- }
1481
- return [...hints];
1482
- }
1483
595
 
1484
- function parseStringArrayColumn(raw: unknown): string[] {
1485
- if (Array.isArray(raw)) return raw.filter((entry): entry is string => typeof entry === "string");
1486
- if (typeof raw !== "string") return [];
1487
- const trimmed = raw.trim();
1488
- if (!trimmed) return [];
1489
- try {
1490
- const parsed = JSON.parse(trimmed);
1491
- if (Array.isArray(parsed)) return parsed.filter((entry): entry is string => typeof entry === "string");
1492
- if (typeof parsed === "string") return [parsed];
1493
- } catch {
1494
- return trimmed.split(",");
1495
- }
1496
- return [];
1497
- }
1498
596
 
1499
- function normalizeRepoPath(file: string): string {
1500
- return file.trim().replace(/\\/g, "/").replace(/^\.\/+/, "");
1501
- }
1502
597
 
1503
598
  // ─── ADR-011 Phase 2 escalation helpers ──────────────────────────────────
1504
599
 
@@ -1507,8 +602,8 @@ export function setTaskEscalationPending(
1507
602
  milestoneId: string, sliceId: string, taskId: string,
1508
603
  artifactPath: string,
1509
604
  ): void {
1510
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1511
- currentDb.prepare(
605
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
606
+ getDbOrNull()!.prepare(
1512
607
  `UPDATE tasks
1513
608
  SET escalation_pending = 1,
1514
609
  escalation_awaiting_review = 0,
@@ -1522,8 +617,8 @@ export function setTaskEscalationAwaitingReview(
1522
617
  milestoneId: string, sliceId: string, taskId: string,
1523
618
  artifactPath: string,
1524
619
  ): void {
1525
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1526
- currentDb.prepare(
620
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
621
+ getDbOrNull()!.prepare(
1527
622
  `UPDATE tasks
1528
623
  SET escalation_awaiting_review = 1,
1529
624
  escalation_pending = 0,
@@ -1536,8 +631,8 @@ export function setTaskEscalationAwaitingReview(
1536
631
  export function clearTaskEscalationFlags(
1537
632
  milestoneId: string, sliceId: string, taskId: string,
1538
633
  ): void {
1539
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1540
- currentDb.prepare(
634
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
635
+ getDbOrNull()!.prepare(
1541
636
  `UPDATE tasks
1542
637
  SET escalation_pending = 0,
1543
638
  escalation_awaiting_review = 0
@@ -1553,9 +648,9 @@ export function clearTaskEscalationFlags(
1553
648
  export function claimEscalationOverride(
1554
649
  milestoneId: string, sliceId: string, sourceTaskId: string,
1555
650
  ): boolean {
1556
- if (!currentDb) return false;
651
+ if (!getDbOrNull()!) return false;
1557
652
  const now = new Date().toISOString();
1558
- const result = currentDb.prepare(
653
+ const result = getDbOrNull()!.prepare(
1559
654
  `UPDATE tasks
1560
655
  SET escalation_override_applied_at = :now
1561
656
  WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid
@@ -1567,39 +662,13 @@ export function claimEscalationOverride(
1567
662
  return changes > 0;
1568
663
  }
1569
664
 
1570
- /** Find the most recent resolved-but-unapplied escalation override in a slice. */
1571
- export function findUnappliedEscalationOverride(
1572
- milestoneId: string, sliceId: string,
1573
- ): { taskId: string; artifactPath: string } | null {
1574
- if (!currentDb) return null;
1575
- // Filter BOTH flags: escalation_pending=0 AND escalation_awaiting_review=0
1576
- // ensures we only claim overrides the user has explicitly resolved.
1577
- // Without the awaiting_review filter, continueWithDefault=true artifacts
1578
- // (not yet responded to) would be prematurely claimed, causing the override
1579
- // to be lost when the user later resolves (#ADR-011 Phase 2 peer-review Bug 2).
1580
- const row = currentDb.prepare(
1581
- `SELECT id, escalation_artifact_path AS path
1582
- FROM tasks
1583
- WHERE milestone_id = :mid AND slice_id = :sid
1584
- AND escalation_artifact_path IS NOT NULL
1585
- AND escalation_override_applied_at IS NULL
1586
- AND escalation_pending = 0
1587
- AND escalation_awaiting_review = 0
1588
- ORDER BY sequence DESC, id DESC
1589
- LIMIT 1`,
1590
- ).get({ ":mid": milestoneId, ":sid": sliceId }) as
1591
- | { id: string; path: string | null }
1592
- | undefined;
1593
- if (!row || !row.path) return null;
1594
- return { taskId: row.id, artifactPath: row.path };
1595
- }
1596
665
 
1597
666
  /** Set the blocker_source provenance field (used when rejecting an escalation). */
1598
667
  export function setTaskBlockerSource(
1599
668
  milestoneId: string, sliceId: string, taskId: string, source: string,
1600
669
  ): void {
1601
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1602
- currentDb.prepare(
670
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
671
+ getDbOrNull()!.prepare(
1603
672
  `UPDATE tasks
1604
673
  SET blocker_discovered = 1,
1605
674
  blocker_source = :src
@@ -1607,17 +676,6 @@ export function setTaskBlockerSource(
1607
676
  ).run({ ":src": source, ":mid": milestoneId, ":sid": sliceId, ":tid": taskId });
1608
677
  }
1609
678
 
1610
- /** List tasks with active escalation artifacts across a milestone (for /gsd escalate list). */
1611
- export function listEscalationArtifacts(milestoneId: string, includeResolved: boolean = false): TaskRow[] {
1612
- if (!currentDb) return [];
1613
- const filter = includeResolved
1614
- ? "escalation_artifact_path IS NOT NULL"
1615
- : "(escalation_pending = 1 OR escalation_awaiting_review = 1) AND escalation_artifact_path IS NOT NULL";
1616
- const rows = currentDb.prepare(
1617
- `SELECT * FROM tasks WHERE milestone_id = :mid AND ${filter} ORDER BY slice_id, sequence, id`,
1618
- ).all({ ":mid": milestoneId });
1619
- return rows.map(rowToTask);
1620
- }
1621
679
 
1622
680
  export function insertVerificationEvidence(e: {
1623
681
  taskId: string;
@@ -1628,8 +686,8 @@ export function insertVerificationEvidence(e: {
1628
686
  verdict: string;
1629
687
  durationMs: number;
1630
688
  }): void {
1631
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1632
- currentDb.prepare(
689
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
690
+ getDbOrNull()!.prepare(
1633
691
  `INSERT OR IGNORE INTO verification_evidence (task_id, slice_id, milestone_id, command, exit_code, verdict, duration_ms, created_at)
1634
692
  VALUES (:task_id, :slice_id, :milestone_id, :command, :exit_code, :verdict, :duration_ms, :created_at)`,
1635
693
  ).run({
@@ -1644,66 +702,35 @@ export function insertVerificationEvidence(e: {
1644
702
  });
1645
703
  }
1646
704
 
1647
- export interface VerificationEvidenceRow {
1648
- id: number;
1649
- task_id: string;
1650
- slice_id: string;
1651
- milestone_id: string;
1652
- command: string;
1653
- exit_code: number;
1654
- verdict: string;
1655
- duration_ms: number;
1656
- created_at: string;
1657
- }
1658
705
 
1659
- export function getVerificationEvidence(milestoneId: string, sliceId: string, taskId: string): VerificationEvidenceRow[] {
1660
- if (!currentDb) return [];
1661
- const rows = currentDb.prepare(
1662
- "SELECT * FROM verification_evidence WHERE milestone_id = :mid AND slice_id = :sid AND task_id = :tid ORDER BY id",
1663
- ).all({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId });
1664
- return rows as unknown as VerificationEvidenceRow[];
1665
- }
1666
706
 
1667
- export function getAllMilestones(): MilestoneRow[] {
1668
- if (!currentDb) return [];
1669
- const rows = currentDb.prepare(
1670
- "SELECT * FROM milestones ORDER BY CASE WHEN sequence > 0 THEN 0 ELSE 1 END, sequence, id",
1671
- ).all();
1672
- return rows.map(rowToMilestone);
1673
- }
1674
707
 
1675
- export function getMilestone(id: string): MilestoneRow | null {
1676
- if (!currentDb) return null;
1677
- const row = currentDb.prepare("SELECT * FROM milestones WHERE id = :id").get({ ":id": id });
1678
- if (!row) return null;
1679
- return rowToMilestone(row);
1680
- }
1681
708
 
1682
709
  export function setMilestoneQueueOrder(order: string[]): void {
1683
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1684
- currentDb.exec("BEGIN IMMEDIATE");
710
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
711
+ getDbOrNull()!.exec("BEGIN IMMEDIATE");
1685
712
  try {
1686
- currentDb.prepare("UPDATE milestones SET sequence = 0").run();
1687
- const stmt = currentDb.prepare("UPDATE milestones SET sequence = :sequence WHERE id = :id");
713
+ getDbOrNull()!.prepare("UPDATE milestones SET sequence = 0").run();
714
+ const stmt = getDbOrNull()!.prepare("UPDATE milestones SET sequence = :sequence WHERE id = :id");
1688
715
  order.forEach((id, index) => {
1689
716
  stmt.run({ ":id": id, ":sequence": index + 1 });
1690
717
  });
1691
- currentDb.exec("COMMIT");
718
+ getDbOrNull()!.exec("COMMIT");
1692
719
  } catch (err) {
1693
- currentDb.exec("ROLLBACK");
720
+ getDbOrNull()!.exec("ROLLBACK");
1694
721
  throw err;
1695
722
  }
1696
723
  }
1697
724
 
1698
725
  function getMilestoneStatusForUpdate(milestoneId: string): string | null {
1699
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1700
- const row = currentDb.prepare("SELECT status FROM milestones WHERE id = :id").get({ ":id": milestoneId });
726
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
727
+ const row = getDbOrNull()!.prepare("SELECT status FROM milestones WHERE id = :id").get({ ":id": milestoneId });
1701
728
  return typeof row?.["status"] === "string" ? row["status"] : null;
1702
729
  }
1703
730
 
1704
731
  function writeMilestoneStatus(milestoneId: string, status: string, completedAt?: string | null): void {
1705
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1706
- currentDb.prepare(
732
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
733
+ getDbOrNull()!.prepare(
1707
734
  `UPDATE milestones SET status = :status, completed_at = :completed_at WHERE id = :id`,
1708
735
  ).run({ ":status": status, ":completed_at": completedAt ?? null, ":id": milestoneId });
1709
736
  }
@@ -1716,21 +743,14 @@ function writeMilestoneStatus(milestoneId: string, status: string, completedAt?:
1716
743
  * must use reopenMilestoneStatus(), which is reserved for gsd_milestone_reopen.
1717
744
  */
1718
745
  export function updateMilestoneStatus(milestoneId: string, status: string, completedAt?: string | null): void {
1719
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1720
- const currentStatus = getMilestoneStatusForUpdate(milestoneId);
1721
- if (currentStatus && isClosedStatus(currentStatus) && !isClosedStatus(status)) {
1722
- throw new Error(
1723
- `Cannot update closed milestone ${milestoneId} from ${currentStatus} to ${status}; use gsd_milestone_reopen for an explicit reopen.`,
1724
- );
1725
- }
1726
- writeMilestoneStatus(milestoneId, status, completedAt);
746
+ applyStatusTransition({ entity: "milestone", milestoneId, status, completedAt });
1727
747
  }
1728
748
 
1729
749
  /**
1730
750
  * Explicit closed -> active transition for gsd_milestone_reopen only.
1731
751
  */
1732
752
  export function reopenMilestoneStatus(milestoneId: string): void {
1733
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
753
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1734
754
  const currentStatus = getMilestoneStatusForUpdate(milestoneId);
1735
755
  if (!currentStatus) {
1736
756
  throw new Error(`Cannot reopen missing milestone ${milestoneId}`);
@@ -1741,615 +761,35 @@ export function reopenMilestoneStatus(milestoneId: string): void {
1741
761
  writeMilestoneStatus(milestoneId, "active", null);
1742
762
  }
1743
763
 
1744
- export function getActiveMilestoneFromDb(): MilestoneRow | null {
1745
- if (!currentDb) return null;
1746
- const row = currentDb.prepare(
1747
- "SELECT * FROM milestones WHERE status NOT IN ('complete', 'done', 'skipped', 'closed', 'parked') ORDER BY id LIMIT 1",
1748
- ).get();
1749
- if (!row) return null;
1750
- return rowToMilestone(row);
1751
- }
1752
764
 
1753
- export function getActiveSliceFromDb(milestoneId: string): SliceRow | null {
1754
- if (!currentDb) return null;
1755
-
1756
- // Single query: find the first non-complete slice whose dependencies are all satisfied.
1757
- // Uses json_each() to expand the JSON depends array and checks each dep is complete.
1758
- const row = currentDb.prepare(
1759
- `SELECT s.* FROM slices s
1760
- WHERE s.milestone_id = :mid
1761
- AND s.status NOT IN ('complete', 'done', 'skipped')
1762
- AND NOT EXISTS (
1763
- SELECT 1 FROM json_each(s.depends) AS dep
1764
- WHERE dep.value NOT IN (
1765
- SELECT id FROM slices WHERE milestone_id = :mid AND status IN ('complete', 'done', 'skipped')
1766
- )
1767
- )
1768
- ORDER BY s.sequence, s.id
1769
- LIMIT 1`,
1770
- ).get({ ":mid": milestoneId });
1771
- if (!row) return null;
1772
- return rowToSlice(row);
1773
- }
1774
765
 
1775
- export function getActiveTaskFromDb(milestoneId: string, sliceId: string): TaskRow | null {
1776
- if (!currentDb) return null;
1777
- const row = currentDb.prepare(
1778
- "SELECT * FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND status NOT IN ('complete', 'done') ORDER BY sequence, id LIMIT 1",
1779
- ).get({ ":mid": milestoneId, ":sid": sliceId });
1780
- if (!row) return null;
1781
- return rowToTask(row);
1782
- }
1783
766
 
1784
- export function getMilestoneSlices(milestoneId: string): SliceRow[] {
1785
- if (!currentDb) return [];
1786
- const rows = currentDb.prepare("SELECT * FROM slices WHERE milestone_id = :mid ORDER BY sequence, id").all({ ":mid": milestoneId });
1787
- return rows.map(rowToSlice);
1788
- }
1789
767
 
1790
- export function getArtifact(path: string): ArtifactRow | null {
1791
- if (!currentDb) return null;
1792
- const row = currentDb.prepare("SELECT * FROM artifacts WHERE path = :path").get({ ":path": path });
1793
- if (!row) return null;
1794
- return rowToArtifact(row);
1795
- }
1796
768
 
1797
769
  // ─── Lightweight Query Variants (hot-path optimized) ─────────────────────
1798
770
 
1799
- /** Fast milestone status check — avoids deserializing JSON planning fields. */
1800
- export function getActiveMilestoneIdFromDb(): IdStatusSummary | null {
1801
- if (!currentDb) return null;
1802
- const row = currentDb.prepare(
1803
- "SELECT id, status FROM milestones WHERE status NOT IN ('complete', 'done', 'skipped', 'closed', 'parked') ORDER BY id LIMIT 1",
1804
- ).get();
1805
- if (!row) return null;
1806
- return rowToIdStatusSummary(row);
1807
- }
1808
771
 
1809
- /** Fast slice status check — avoids deserializing JSON depends/planning fields. */
1810
- export function getSliceStatusSummary(milestoneId: string): IdStatusSummary[] {
1811
- if (!currentDb) return [];
1812
- return currentDb.prepare(
1813
- "SELECT id, status FROM slices WHERE milestone_id = :mid ORDER BY sequence, id",
1814
- ).all({ ":mid": milestoneId }).map(rowToIdStatusSummary);
1815
- }
1816
772
 
1817
- /** Fast task status check — avoids deserializing JSON arrays and large text fields. */
1818
- export function getActiveTaskIdFromDb(milestoneId: string, sliceId: string): ActiveTaskSummary | null {
1819
- if (!currentDb) return null;
1820
- const row = currentDb.prepare(
1821
- "SELECT id, status, title FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND status NOT IN ('complete', 'done') ORDER BY sequence, id LIMIT 1",
1822
- ).get({ ":mid": milestoneId, ":sid": sliceId });
1823
- if (!row) return null;
1824
- return rowToActiveTaskSummary(row);
1825
- }
1826
773
 
1827
- /** Count tasks by status for a slice — useful for progress reporting without full row load. */
1828
- export function getSliceTaskCounts(milestoneId: string, sliceId: string): TaskStatusCounts {
1829
- if (!currentDb) return emptyTaskStatusCounts();
1830
- const row = currentDb.prepare(
1831
- `SELECT
1832
- COUNT(*) as total,
1833
- SUM(CASE WHEN status IN ('complete', 'done') THEN 1 ELSE 0 END) as done,
1834
- SUM(CASE WHEN status NOT IN ('complete', 'done') THEN 1 ELSE 0 END) as pending
1835
- FROM tasks WHERE milestone_id = :mid AND slice_id = :sid`,
1836
- ).get({ ":mid": milestoneId, ":sid": sliceId });
1837
- return rowToTaskStatusCounts(row);
1838
- }
1839
774
 
1840
775
  // ─── Slice Dependencies (junction table) ─────────────────────────────────
1841
776
 
1842
777
  /** Sync the slice_dependencies junction table from a slice's JSON depends array. */
1843
778
  export function syncSliceDependencies(milestoneId: string, sliceId: string, depends: string[]): void {
1844
- if (!currentDb) return;
1845
- currentDb.prepare(
779
+ if (!getDbOrNull()!) return;
780
+ getDbOrNull()!.prepare(
1846
781
  "DELETE FROM slice_dependencies WHERE milestone_id = :mid AND slice_id = :sid",
1847
782
  ).run({ ":mid": milestoneId, ":sid": sliceId });
1848
783
  for (const dep of depends) {
1849
- currentDb.prepare(
784
+ getDbOrNull()!.prepare(
1850
785
  "INSERT OR IGNORE INTO slice_dependencies (milestone_id, slice_id, depends_on_slice_id) VALUES (:mid, :sid, :dep)",
1851
786
  ).run({ ":mid": milestoneId, ":sid": sliceId, ":dep": dep });
1852
787
  }
1853
788
  }
1854
789
 
1855
- /** Get all slices that depend on a given slice. */
1856
- export function getDependentSlices(milestoneId: string, sliceId: string): string[] {
1857
- if (!currentDb) return [];
1858
- const rows = currentDb.prepare(
1859
- "SELECT slice_id FROM slice_dependencies WHERE milestone_id = :mid AND depends_on_slice_id = :sid",
1860
- ).all({ ":mid": milestoneId, ":sid": sliceId });
1861
- return rowsToStringColumn(rows, "slice_id");
1862
- }
1863
790
 
1864
791
  // ─── Worktree DB Helpers ──────────────────────────────────────────────────
1865
792
 
1866
- export function copyWorktreeDb(srcDbPath: string, destDbPath: string): boolean {
1867
- try {
1868
- if (!existsSync(srcDbPath)) return false;
1869
- const destDir = dirname(destDbPath);
1870
- mkdirSync(destDir, { recursive: true });
1871
- copyFileSync(srcDbPath, destDbPath);
1872
- return true;
1873
- } catch (err) {
1874
- logError("db", "failed to copy DB to worktree", { error: (err as Error).message });
1875
- return false;
1876
- }
1877
- }
1878
-
1879
- export interface ReconcileResult {
1880
- decisions: number;
1881
- requirements: number;
1882
- artifacts: number;
1883
- milestones: number;
1884
- slices: number;
1885
- tasks: number;
1886
- memories: number;
1887
- replan_history: number;
1888
- assessments: number;
1889
- quality_gates: number;
1890
- slice_dependencies: number;
1891
- verification_evidence: number;
1892
- gate_runs: number;
1893
- milestone_commit_attributions: number;
1894
- conflicts: string[];
1895
- }
1896
-
1897
- export function reconcileWorktreeDb(
1898
- mainDbPath: string,
1899
- worktreeDbPath: string,
1900
- ): ReconcileResult {
1901
- const zero: ReconcileResult = {
1902
- decisions: 0,
1903
- requirements: 0,
1904
- artifacts: 0,
1905
- milestones: 0,
1906
- slices: 0,
1907
- tasks: 0,
1908
- memories: 0,
1909
- replan_history: 0,
1910
- assessments: 0,
1911
- quality_gates: 0,
1912
- slice_dependencies: 0,
1913
- verification_evidence: 0,
1914
- gate_runs: 0,
1915
- milestone_commit_attributions: 0,
1916
- conflicts: [],
1917
- };
1918
- if (!existsSync(worktreeDbPath)) return zero;
1919
- // Guard: bail when both paths resolve to the same physical file.
1920
- // ATTACHing a WAL-mode DB to itself corrupts the WAL (#2823).
1921
- try {
1922
- if (realpathSync(mainDbPath) === realpathSync(worktreeDbPath)) return zero;
1923
- } catch (e) { logWarning("db", `realpathSync failed: ${(e as Error).message}`); }
1924
- // Sanitize path: reject any characters that could break ATTACH syntax.
1925
- // ATTACH DATABASE doesn't support parameterized paths in all providers,
1926
- // so we use strict allowlist validation instead.
1927
- if (/['";\x00]/.test(worktreeDbPath)) {
1928
- logError("db", "worktree DB reconciliation failed: path contains unsafe characters");
1929
- return zero;
1930
- }
1931
- if (!currentDb) {
1932
- const opened = openDatabase(mainDbPath);
1933
- if (!opened) {
1934
- logError("db", "worktree DB reconciliation failed: cannot open main DB");
1935
- return zero;
1936
- }
1937
- }
1938
- const adapter = currentDb!;
1939
- const conflicts: string[] = [];
1940
- try {
1941
- adapter.exec(`ATTACH DATABASE '${worktreeDbPath}' AS wt`);
1942
- try {
1943
- function countChanges(result: unknown): number {
1944
- return typeof result === "object" && result !== null ? ((result as { changes?: number }).changes ?? 0) : 0;
1945
- }
1946
-
1947
- function wtTableInfo(tableName: string): Array<Record<string, unknown>> {
1948
- return adapter.prepare(`PRAGMA wt.table_info('${tableName}')`).all() as Array<Record<string, unknown>>;
1949
- }
1950
-
1951
- const wtInfo = wtTableInfo("decisions");
1952
- const hasWtDecisions = wtInfo.length > 0;
1953
- const hasMadeBy = wtInfo.some((col) => col["name"] === "made_by");
1954
- // ADR-011: worktree may predate schema v16/v17. For missing columns we
1955
- // fall through to the main DB's existing value (not a literal default)
1956
- // so reconcile never silently clears state the main tree has recorded.
1957
- const hasDecisionSource = wtInfo.some((col) => col["name"] === "source");
1958
- const wtRequirementInfo = wtTableInfo("requirements");
1959
- const hasWtRequirements = wtRequirementInfo.length > 0;
1960
- const wtMilestoneInfo = wtTableInfo("milestones");
1961
- const hasWtMilestones = wtMilestoneInfo.length > 0;
1962
- const hasMilestoneSequence = wtMilestoneInfo.some((col) => col["name"] === "sequence");
1963
- const wtSliceInfo = wtTableInfo("slices");
1964
- const hasWtSlices = wtSliceInfo.length > 0;
1965
- const hasIsSketch = wtSliceInfo.some((col) => col["name"] === "is_sketch");
1966
- const hasSketchScope = wtSliceInfo.some((col) => col["name"] === "sketch_scope");
1967
- const hasSliceTargetRepositories = wtSliceInfo.some((col) => col["name"] === "target_repositories");
1968
- const wtTaskInfo = wtTableInfo("tasks");
1969
- const hasWtTasks = wtTaskInfo.length > 0;
1970
- const hasTaskTargetRepositories = wtTaskInfo.some((col) => col["name"] === "target_repositories");
1971
- const hasBlockerSource = wtTaskInfo.some((col) => col["name"] === "blocker_source");
1972
- const hasEscalationPending = wtTaskInfo.some((col) => col["name"] === "escalation_pending");
1973
- const hasEscalationAwaiting = wtTaskInfo.some((col) => col["name"] === "escalation_awaiting_review");
1974
- const hasEscalationArtifact = wtTaskInfo.some((col) => col["name"] === "escalation_artifact_path");
1975
- const hasEscalationOverride = wtTaskInfo.some((col) => col["name"] === "escalation_override_applied_at");
1976
- const wtArtifactInfo = wtTableInfo("artifacts");
1977
- const hasWtArtifacts = wtArtifactInfo.length > 0;
1978
- const wtMemoryInfo = wtTableInfo("memories");
1979
- const hasWtMemories = wtMemoryInfo.length > 0;
1980
- const hasMemoryScope = wtMemoryInfo.some((col) => col["name"] === "scope");
1981
- const hasMemoryTags = wtMemoryInfo.some((col) => col["name"] === "tags");
1982
- const hasMemoryStructuredFields = wtMemoryInfo.some((col) => col["name"] === "structured_fields");
1983
- const hasMemoryLastHitAt = wtMemoryInfo.some((col) => col["name"] === "last_hit_at");
1984
- const hasWtReplanHistory = wtTableInfo("replan_history").length > 0;
1985
- const hasWtAssessments = wtTableInfo("assessments").length > 0;
1986
- const hasWtQualityGates = wtTableInfo("quality_gates").length > 0;
1987
- const hasWtSliceDependencies = wtTableInfo("slice_dependencies").length > 0;
1988
- const hasWtVerificationEvidence = wtTableInfo("verification_evidence").length > 0;
1989
- const hasWtGateRuns = wtTableInfo("gate_runs").length > 0;
1990
- const hasWtMilestoneCommitAttributions = wtTableInfo("milestone_commit_attributions").length > 0;
1991
-
1992
- if (hasWtDecisions) {
1993
- const decConf = adapter.prepare(
1994
- `SELECT m.id FROM decisions m INNER JOIN wt.decisions w ON m.id = w.id WHERE m.decision != w.decision OR m.choice != w.choice OR m.rationale != w.rationale OR ${
1995
- hasMadeBy ? "m.made_by != w.made_by" : "'agent' != 'agent'"
1996
- } OR m.superseded_by IS NOT w.superseded_by`,
1997
- ).all();
1998
- for (const row of decConf) conflicts.push(`decision ${(row as Record<string, unknown>)["id"]}: modified in both`);
1999
- }
2000
-
2001
- if (hasWtRequirements) {
2002
- const reqConf = adapter.prepare(
2003
- `SELECT m.id FROM requirements m INNER JOIN wt.requirements w ON m.id = w.id WHERE m.description != w.description OR m.status != w.status OR m.notes != w.notes OR m.superseded_by IS NOT w.superseded_by`,
2004
- ).all();
2005
- for (const row of reqConf) conflicts.push(`requirement ${(row as Record<string, unknown>)["id"]}: modified in both`);
2006
- }
2007
-
2008
- const merged: Omit<ReconcileResult, "conflicts"> = {
2009
- decisions: 0,
2010
- requirements: 0,
2011
- artifacts: 0,
2012
- milestones: 0,
2013
- slices: 0,
2014
- tasks: 0,
2015
- memories: 0,
2016
- replan_history: 0,
2017
- assessments: 0,
2018
- quality_gates: 0,
2019
- slice_dependencies: 0,
2020
- verification_evidence: 0,
2021
- gate_runs: 0,
2022
- milestone_commit_attributions: 0,
2023
- };
2024
- const sliceTargetRepositoriesSql = hasSliceTargetRepositories
2025
- ? `CASE
2026
- WHEN w.target_repositories = '[]' AND COALESCE(m.target_repositories, '[]') <> '[]'
2027
- THEN m.target_repositories
2028
- ELSE COALESCE(w.target_repositories, m.target_repositories, '[]')
2029
- END`
2030
- : "COALESCE(m.target_repositories, '[]')";
2031
- const taskTargetRepositoriesSql = hasTaskTargetRepositories
2032
- ? `CASE
2033
- WHEN w.target_repositories = '[]' AND COALESCE(m.target_repositories, '[]') <> '[]'
2034
- THEN m.target_repositories
2035
- ELSE COALESCE(w.target_repositories, m.target_repositories, '[]')
2036
- END`
2037
- : "COALESCE(m.target_repositories, '[]')";
2038
-
2039
- adapter.exec("BEGIN");
2040
- try {
2041
- // Join the target decisions so we can prefer an existing main.source
2042
- // when the worktree predates v16 — otherwise a write-through reconcile
2043
- // would clobber 'escalation'-sourced decisions with the literal default.
2044
- if (hasWtDecisions) {
2045
- merged.decisions = countChanges(adapter.prepare(`
2046
- INSERT INTO decisions (
2047
- id, when_context, scope, decision, choice, rationale, revisable, made_by, source, superseded_by
2048
- )
2049
- SELECT w.id, w.when_context, w.scope, w.decision, w.choice, w.rationale, w.revisable, ${
2050
- hasMadeBy ? "w.made_by" : "COALESCE(m.made_by, 'agent')"
2051
- }, ${
2052
- hasDecisionSource ? "w.source" : "COALESCE(m.source, 'discussion')"
2053
- }, w.superseded_by
2054
- FROM wt.decisions w
2055
- LEFT JOIN decisions m ON m.id = w.id
2056
- WHERE true
2057
- ON CONFLICT(id) DO UPDATE SET
2058
- when_context = excluded.when_context,
2059
- scope = excluded.scope,
2060
- decision = excluded.decision,
2061
- choice = excluded.choice,
2062
- rationale = excluded.rationale,
2063
- revisable = excluded.revisable,
2064
- made_by = excluded.made_by,
2065
- source = excluded.source,
2066
- superseded_by = excluded.superseded_by
2067
- `).run());
2068
- }
2069
-
2070
- if (hasWtRequirements) {
2071
- merged.requirements = countChanges(adapter.prepare(`
2072
- INSERT OR REPLACE INTO requirements (
2073
- id, class, status, description, why, source, primary_owner,
2074
- supporting_slices, validation, notes, full_content, superseded_by
2075
- )
2076
- SELECT id, class, status, description, why, source, primary_owner,
2077
- supporting_slices, validation, notes, full_content, superseded_by
2078
- FROM wt.requirements
2079
- `).run());
2080
- }
2081
-
2082
- // Always recompute artifact hashes from the content being merged. Older
2083
- // worktree DBs may not have content_hash at all, and migrated old DBs can
2084
- // carry stale default/null hashes after their content changed.
2085
- if (hasWtArtifacts) {
2086
- const artifactRows = adapter.prepare(`
2087
- SELECT path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
2088
- FROM wt.artifacts
2089
- `).all() as Array<Record<string, unknown>>;
2090
- const artifactStmt = adapter.prepare(`
2091
- INSERT OR REPLACE INTO artifacts (
2092
- path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash
2093
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
2094
- `);
2095
- for (const row of artifactRows) {
2096
- const fullContent = String(row["full_content"] ?? "");
2097
- merged.artifacts += countChanges(artifactStmt.run(
2098
- row["path"],
2099
- row["artifact_type"],
2100
- row["milestone_id"] ?? null,
2101
- row["slice_id"] ?? null,
2102
- row["task_id"] ?? null,
2103
- fullContent,
2104
- row["imported_at"],
2105
- createHash("sha256").update(fullContent).digest("hex"),
2106
- ));
2107
- }
2108
- }
2109
-
2110
- // Merge milestones — worktree may have updated status/planning fields.
2111
- // Never downgrade status: complete > active > pre-planning (#4372).
2112
- // A stale worktree may carry an older 'active' status for a milestone
2113
- // that the main DB has already marked 'complete'; preserve the higher status.
2114
- if (hasWtMilestones) {
2115
- merged.milestones = countChanges(adapter.prepare(`
2116
- INSERT OR REPLACE INTO milestones (
2117
- id, title, status, depends_on, created_at, completed_at,
2118
- vision, success_criteria, key_risks, proof_strategy,
2119
- verification_contract, verification_integration, verification_operational, verification_uat,
2120
- definition_of_done, requirement_coverage, boundary_map_markdown, sequence
2121
- )
2122
- SELECT w.id, w.title,
2123
- CASE
2124
- WHEN m.status IN (${TERMINAL_STATUS_SQL}) AND w.status NOT IN (${TERMINAL_STATUS_SQL})
2125
- THEN m.status ELSE w.status
2126
- END,
2127
- w.depends_on,
2128
- CASE
2129
- WHEN m.status IN (${TERMINAL_STATUS_SQL}) AND w.status NOT IN (${TERMINAL_STATUS_SQL})
2130
- THEN m.created_at ELSE w.created_at
2131
- END,
2132
- CASE
2133
- WHEN m.status IN (${TERMINAL_STATUS_SQL}) AND w.status NOT IN (${TERMINAL_STATUS_SQL})
2134
- THEN m.completed_at ELSE w.completed_at
2135
- END,
2136
- w.vision, w.success_criteria, w.key_risks, w.proof_strategy,
2137
- w.verification_contract, w.verification_integration, w.verification_operational, w.verification_uat,
2138
- w.definition_of_done, w.requirement_coverage, w.boundary_map_markdown,
2139
- ${hasMilestoneSequence ? "COALESCE(w.sequence, 0)" : "COALESCE(m.sequence, 0)"}
2140
- FROM wt.milestones w
2141
- LEFT JOIN milestones m ON m.id = w.id
2142
- `).run());
2143
- }
2144
-
2145
- // Merge slices — preserve worktree progress but never downgrade completed status (#2558).
2146
- // ADR-011 Phase 1: carry is_sketch + sketch_scope so reconcile doesn't
2147
- // silently clear sketch metadata. When the worktree predates v16,
2148
- // fall back to the main DB's existing value rather than a literal 0/''.
2149
- if (hasWtSlices) {
2150
- merged.slices = countChanges(adapter.prepare(`
2151
- INSERT OR REPLACE INTO slices (
2152
- milestone_id, id, title, status, risk, depends, demo, created_at, completed_at,
2153
- full_summary_md, full_uat_md, goal, success_criteria, proof_level,
2154
- integration_closure, observability_impact, target_repositories, sequence, replan_triggered_at,
2155
- is_sketch, sketch_scope
2156
- )
2157
- SELECT w.milestone_id, w.id, w.title,
2158
- CASE
2159
- WHEN m.status IN (${TERMINAL_STATUS_SQL}) AND w.status NOT IN (${TERMINAL_STATUS_SQL})
2160
- THEN m.status ELSE w.status
2161
- END,
2162
- w.risk, w.depends, w.demo, w.created_at,
2163
- CASE
2164
- WHEN m.status IN (${TERMINAL_STATUS_SQL}) AND w.status NOT IN (${TERMINAL_STATUS_SQL})
2165
- THEN m.completed_at ELSE w.completed_at
2166
- END,
2167
- w.full_summary_md, w.full_uat_md, w.goal, w.success_criteria, w.proof_level,
2168
- w.integration_closure, w.observability_impact,
2169
- ${sliceTargetRepositoriesSql},
2170
- w.sequence, w.replan_triggered_at,
2171
- ${hasIsSketch ? "w.is_sketch" : "COALESCE(m.is_sketch, 0)"},
2172
- ${hasSketchScope ? "w.sketch_scope" : "COALESCE(m.sketch_scope, '')"}
2173
- FROM wt.slices w
2174
- LEFT JOIN slices m ON m.milestone_id = w.milestone_id AND m.id = w.id
2175
- `).run());
2176
- }
2177
-
2178
- // Merge tasks — preserve execution results, never downgrade completed status (#2558).
2179
- // ADR-011 P2: carry blocker_source + escalation_* columns so worktree reconcile
2180
- // doesn't silently clear escalation state back to defaults.
2181
- if (hasWtTasks) {
2182
- merged.tasks = countChanges(adapter.prepare(`
2183
- INSERT OR REPLACE INTO tasks (
2184
- milestone_id, slice_id, id, title, status, one_liner, narrative,
2185
- verification_result, duration, completed_at, blocker_discovered,
2186
- deviations, known_issues, key_files, key_decisions, full_summary_md,
2187
- description, estimate, files, verify, inputs, expected_output,
2188
- observability_impact, full_plan_md, target_repositories, sequence,
2189
- blocker_source, escalation_pending, escalation_awaiting_review,
2190
- escalation_artifact_path, escalation_override_applied_at
2191
- )
2192
- SELECT w.milestone_id, w.slice_id, w.id, w.title,
2193
- CASE
2194
- WHEN m.status IN (${TERMINAL_STATUS_SQL}) AND w.status NOT IN (${TERMINAL_STATUS_SQL})
2195
- THEN m.status ELSE w.status
2196
- END,
2197
- w.one_liner, w.narrative,
2198
- w.verification_result, w.duration,
2199
- CASE
2200
- WHEN m.status IN (${TERMINAL_STATUS_SQL}) AND w.status NOT IN (${TERMINAL_STATUS_SQL})
2201
- THEN m.completed_at ELSE w.completed_at
2202
- END,
2203
- w.blocker_discovered,
2204
- w.deviations, w.known_issues, w.key_files, w.key_decisions, w.full_summary_md,
2205
- w.description, w.estimate, w.files, w.verify, w.inputs, w.expected_output,
2206
- w.observability_impact, w.full_plan_md,
2207
- ${taskTargetRepositoriesSql},
2208
- w.sequence,
2209
- ${hasBlockerSource ? "w.blocker_source" : "COALESCE(m.blocker_source, '')"},
2210
- ${hasEscalationPending ? "w.escalation_pending" : "COALESCE(m.escalation_pending, 0)"},
2211
- ${hasEscalationAwaiting ? "w.escalation_awaiting_review" : "COALESCE(m.escalation_awaiting_review, 0)"},
2212
- ${hasEscalationArtifact ? "w.escalation_artifact_path" : "m.escalation_artifact_path"},
2213
- ${hasEscalationOverride ? "w.escalation_override_applied_at" : "m.escalation_override_applied_at"}
2214
- FROM wt.tasks w
2215
- LEFT JOIN tasks m ON m.milestone_id = w.milestone_id AND m.slice_id = w.slice_id AND m.id = w.id
2216
- `).run());
2217
- }
2218
-
2219
- // Merge memories — keep worktree-learned insights.
2220
- // V18 (scope, tags), V21 (structured_fields), V28 (last_hit_at): for each
2221
- // column the wt may not yet have (older worktree DB), fall back to the
2222
- // main DB's existing value via LEFT JOIN so reconcile never silently
2223
- // resets these fields to defaults on rows that already had them.
2224
- if (hasWtMemories) {
2225
- merged.memories = countChanges(adapter.prepare(`
2226
- INSERT OR REPLACE INTO memories (
2227
- seq, id, category, content, confidence, source_unit_type, source_unit_id,
2228
- created_at, updated_at, superseded_by, hit_count,
2229
- scope, tags, structured_fields, last_hit_at
2230
- )
2231
- SELECT w.seq, w.id, w.category, w.content, w.confidence, w.source_unit_type, w.source_unit_id,
2232
- w.created_at, w.updated_at, w.superseded_by, w.hit_count,
2233
- ${hasMemoryScope ? "w.scope" : "COALESCE(m.scope, 'project')"},
2234
- ${hasMemoryTags ? "w.tags" : "COALESCE(m.tags, '[]')"},
2235
- ${hasMemoryStructuredFields ? "w.structured_fields" : "m.structured_fields"},
2236
- ${hasMemoryLastHitAt ? "w.last_hit_at" : "m.last_hit_at"}
2237
- FROM wt.memories w
2238
- LEFT JOIN memories m ON m.id = w.id
2239
- `).run());
2240
- }
2241
-
2242
- if (hasWtReplanHistory) {
2243
- merged.replan_history = countChanges(adapter.prepare(`
2244
- INSERT INTO replan_history (
2245
- milestone_id, slice_id, task_id, summary, previous_artifact_path, replacement_artifact_path, created_at
2246
- )
2247
- SELECT w.milestone_id, w.slice_id, w.task_id, w.summary, w.previous_artifact_path, w.replacement_artifact_path, w.created_at
2248
- FROM wt.replan_history w
2249
- WHERE EXISTS (SELECT 1 FROM milestones m WHERE m.id = w.milestone_id)
2250
- AND NOT EXISTS (
2251
- SELECT 1 FROM replan_history m
2252
- WHERE m.milestone_id = w.milestone_id
2253
- AND m.slice_id IS w.slice_id
2254
- AND m.task_id IS w.task_id
2255
- AND m.summary = w.summary
2256
- AND m.previous_artifact_path IS w.previous_artifact_path
2257
- AND m.replacement_artifact_path IS w.replacement_artifact_path
2258
- )
2259
- `).run());
2260
- }
2261
-
2262
- if (hasWtAssessments) {
2263
- merged.assessments = countChanges(adapter.prepare(`
2264
- INSERT OR REPLACE INTO assessments (
2265
- path, milestone_id, slice_id, task_id, status, scope, full_content, created_at
2266
- )
2267
- SELECT w.path, w.milestone_id, w.slice_id, w.task_id, w.status, w.scope, w.full_content, w.created_at
2268
- FROM wt.assessments w
2269
- WHERE EXISTS (SELECT 1 FROM milestones m WHERE m.id = w.milestone_id)
2270
- `).run());
2271
- }
2272
-
2273
- if (hasWtQualityGates) {
2274
- merged.quality_gates = countChanges(adapter.prepare(`
2275
- INSERT OR REPLACE INTO quality_gates (
2276
- milestone_id, slice_id, gate_id, scope, task_id, status, verdict, rationale, findings, evaluated_at
2277
- )
2278
- SELECT w.milestone_id, w.slice_id, w.gate_id, w.scope, COALESCE(w.task_id, ''), w.status, w.verdict, w.rationale, w.findings, w.evaluated_at
2279
- FROM wt.quality_gates w
2280
- WHERE EXISTS (SELECT 1 FROM slices s WHERE s.milestone_id = w.milestone_id AND s.id = w.slice_id)
2281
- `).run());
2282
- }
2283
-
2284
- if (hasWtSliceDependencies) {
2285
- merged.slice_dependencies = countChanges(adapter.prepare(`
2286
- INSERT OR IGNORE INTO slice_dependencies (milestone_id, slice_id, depends_on_slice_id)
2287
- SELECT w.milestone_id, w.slice_id, w.depends_on_slice_id
2288
- FROM wt.slice_dependencies w
2289
- WHERE EXISTS (SELECT 1 FROM slices s WHERE s.milestone_id = w.milestone_id AND s.id = w.slice_id)
2290
- AND EXISTS (SELECT 1 FROM slices d WHERE d.milestone_id = w.milestone_id AND d.id = w.depends_on_slice_id)
2291
- `).run());
2292
- }
2293
-
2294
- // Merge verification evidence — append-only, use INSERT OR IGNORE to avoid duplicates
2295
- if (hasWtVerificationEvidence) {
2296
- merged.verification_evidence = countChanges(adapter.prepare(`
2297
- INSERT OR IGNORE INTO verification_evidence (
2298
- task_id, slice_id, milestone_id, command, exit_code, verdict, duration_ms, created_at
2299
- )
2300
- SELECT task_id, slice_id, milestone_id, command, exit_code, verdict, duration_ms, created_at
2301
- FROM wt.verification_evidence
2302
- `).run());
2303
- }
2304
-
2305
- if (hasWtGateRuns) {
2306
- merged.gate_runs = countChanges(adapter.prepare(`
2307
- INSERT INTO gate_runs (
2308
- trace_id, turn_id, gate_id, gate_type, unit_type, unit_id, milestone_id, slice_id, task_id,
2309
- outcome, failure_class, rationale, findings, attempt, max_attempts, retryable, evaluated_at
2310
- )
2311
- SELECT w.trace_id, w.turn_id, w.gate_id, w.gate_type, w.unit_type, w.unit_id, w.milestone_id, w.slice_id, w.task_id,
2312
- w.outcome, w.failure_class, w.rationale, w.findings, w.attempt, w.max_attempts, w.retryable, w.evaluated_at
2313
- FROM wt.gate_runs w
2314
- WHERE NOT EXISTS (
2315
- SELECT 1 FROM gate_runs m
2316
- WHERE m.trace_id = w.trace_id
2317
- AND m.turn_id = w.turn_id
2318
- AND m.gate_id = w.gate_id
2319
- AND m.attempt = w.attempt
2320
- AND m.evaluated_at = w.evaluated_at
2321
- )
2322
- `).run());
2323
- }
2324
-
2325
- if (hasWtMilestoneCommitAttributions) {
2326
- merged.milestone_commit_attributions = countChanges(adapter.prepare(`
2327
- INSERT OR REPLACE INTO milestone_commit_attributions (
2328
- commit_sha, milestone_id, slice_id, task_id, source, confidence, files_json, created_at
2329
- )
2330
- SELECT w.commit_sha, w.milestone_id, w.slice_id, w.task_id, w.source, w.confidence, w.files_json, w.created_at
2331
- FROM wt.milestone_commit_attributions w
2332
- WHERE EXISTS (SELECT 1 FROM milestones m WHERE m.id = w.milestone_id)
2333
- `).run());
2334
- }
2335
-
2336
- adapter.exec("COMMIT");
2337
- } catch (txErr) {
2338
- try { adapter.exec("ROLLBACK"); } catch (e) { logWarning("db", `rollback failed: ${(e as Error).message}`); }
2339
- throw txErr;
2340
- }
2341
- return { ...merged, conflicts };
2342
- } finally {
2343
- try { adapter.exec("DETACH DATABASE wt"); } catch (e) { logWarning("db", `detach worktree DB failed: ${(e as Error).message}`); }
2344
- }
2345
- } catch (err) {
2346
- logError("db", "worktree DB reconciliation failed", { error: (err as Error).message });
2347
- return { ...zero, conflicts };
2348
- }
2349
- }
2350
-
2351
- // ─── Replan & Assessment Helpers ──────────────────────────────────────────
2352
-
2353
793
  export function insertReplanHistory(entry: {
2354
794
  milestoneId: string;
2355
795
  sliceId?: string | null;
@@ -2358,10 +798,10 @@ export function insertReplanHistory(entry: {
2358
798
  previousArtifactPath?: string | null;
2359
799
  replacementArtifactPath?: string | null;
2360
800
  }): void {
2361
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
801
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2362
802
  // INSERT OR REPLACE: idempotent on (milestone_id, slice_id, task_id) via schema v11 unique index.
2363
803
  // Retrying the same replan silently updates summary instead of accumulating duplicate rows.
2364
- currentDb.prepare(
804
+ getDbOrNull()!.prepare(
2365
805
  `INSERT OR REPLACE INTO replan_history (milestone_id, slice_id, task_id, summary, previous_artifact_path, replacement_artifact_path, created_at)
2366
806
  VALUES (:milestone_id, :slice_id, :task_id, :summary, :previous_artifact_path, :replacement_artifact_path, :created_at)`,
2367
807
  ).run({
@@ -2384,11 +824,11 @@ export function insertAssessment(entry: {
2384
824
  scope: string;
2385
825
  fullContent: string;
2386
826
  }): void {
2387
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
827
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2388
828
  // Idempotent: PRIMARY KEY is `path`, which is deterministic given (milestone_id, scope) per
2389
829
  // the artifact-path resolver. Retrying the same reassess-roadmap silently overwrites the row
2390
830
  // instead of accumulating duplicates.
2391
- currentDb.prepare(
831
+ getDbOrNull()!.prepare(
2392
832
  `INSERT OR REPLACE INTO assessments (path, milestone_id, slice_id, task_id, status, scope, full_content, created_at)
2393
833
  VALUES (:path, :milestone_id, :slice_id, :task_id, :status, :scope, :full_content, :created_at)`,
2394
834
  ).run({
@@ -2404,94 +844,94 @@ export function insertAssessment(entry: {
2404
844
  }
2405
845
 
2406
846
  export function deleteAssessmentByScope(milestoneId: string, scope: string): void {
2407
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2408
- currentDb.prepare(
847
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
848
+ getDbOrNull()!.prepare(
2409
849
  `DELETE FROM assessments WHERE milestone_id = :mid AND scope = :scope`,
2410
850
  ).run({ ":mid": milestoneId, ":scope": scope });
2411
851
  }
2412
852
 
2413
853
  export function deleteVerificationEvidence(milestoneId: string, sliceId: string, taskId: string): void {
2414
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2415
- currentDb.prepare(
854
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
855
+ getDbOrNull()!.prepare(
2416
856
  `DELETE FROM verification_evidence WHERE milestone_id = :mid AND slice_id = :sid AND task_id = :tid`,
2417
857
  ).run({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId });
2418
858
  }
2419
859
 
2420
860
  export function deleteTask(milestoneId: string, sliceId: string, taskId: string): void {
2421
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
861
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2422
862
  transaction(() => {
2423
863
  // Must delete verification_evidence first (FK constraint)
2424
- currentDb!.prepare(
864
+ getDbOrNull()!!.prepare(
2425
865
  `DELETE FROM verification_evidence WHERE milestone_id = :mid AND slice_id = :sid AND task_id = :tid`,
2426
866
  ).run({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId });
2427
- currentDb!.prepare(
867
+ getDbOrNull()!!.prepare(
2428
868
  `DELETE FROM quality_gates WHERE milestone_id = :mid AND slice_id = :sid AND task_id = :tid`,
2429
869
  ).run({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId });
2430
- currentDb!.prepare(
870
+ getDbOrNull()!!.prepare(
2431
871
  `DELETE FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`,
2432
872
  ).run({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId });
2433
873
  });
2434
874
  }
2435
875
 
2436
876
  export function deleteSlice(milestoneId: string, sliceId: string): void {
2437
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
877
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2438
878
  transaction(() => {
2439
879
  // Cascade-style manual deletion: evidence → tasks → dependencies → slice
2440
- currentDb!.prepare(
880
+ getDbOrNull()!!.prepare(
2441
881
  `DELETE FROM verification_evidence WHERE milestone_id = :mid AND slice_id = :sid`,
2442
882
  ).run({ ":mid": milestoneId, ":sid": sliceId });
2443
- currentDb!.prepare(
883
+ getDbOrNull()!!.prepare(
2444
884
  `DELETE FROM tasks WHERE milestone_id = :mid AND slice_id = :sid`,
2445
885
  ).run({ ":mid": milestoneId, ":sid": sliceId });
2446
- currentDb!.prepare(
886
+ getDbOrNull()!!.prepare(
2447
887
  `DELETE FROM slice_dependencies WHERE milestone_id = :mid AND slice_id = :sid`,
2448
888
  ).run({ ":mid": milestoneId, ":sid": sliceId });
2449
- currentDb!.prepare(
889
+ getDbOrNull()!!.prepare(
2450
890
  `DELETE FROM slice_dependencies WHERE milestone_id = :mid AND depends_on_slice_id = :sid`,
2451
891
  ).run({ ":mid": milestoneId, ":sid": sliceId });
2452
- currentDb!.prepare(
892
+ getDbOrNull()!!.prepare(
2453
893
  `DELETE FROM slices WHERE milestone_id = :mid AND id = :sid`,
2454
894
  ).run({ ":mid": milestoneId, ":sid": sliceId });
2455
895
  });
2456
896
  }
2457
897
 
2458
898
  export function deleteMilestone(milestoneId: string): void {
2459
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
899
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2460
900
  transaction(() => {
2461
- currentDb!.prepare(
901
+ getDbOrNull()!!.prepare(
2462
902
  `DELETE FROM verification_evidence WHERE milestone_id = :mid`,
2463
903
  ).run({ ":mid": milestoneId });
2464
- currentDb!.prepare(
904
+ getDbOrNull()!!.prepare(
2465
905
  `DELETE FROM quality_gates WHERE milestone_id = :mid`,
2466
906
  ).run({ ":mid": milestoneId });
2467
- currentDb!.prepare(
907
+ getDbOrNull()!!.prepare(
2468
908
  `DELETE FROM gate_runs WHERE milestone_id = :mid`,
2469
909
  ).run({ ":mid": milestoneId });
2470
- currentDb!.prepare(
910
+ getDbOrNull()!!.prepare(
2471
911
  `DELETE FROM tasks WHERE milestone_id = :mid`,
2472
912
  ).run({ ":mid": milestoneId });
2473
- currentDb!.prepare(
913
+ getDbOrNull()!!.prepare(
2474
914
  `DELETE FROM slice_dependencies WHERE milestone_id = :mid`,
2475
915
  ).run({ ":mid": milestoneId });
2476
- currentDb!.prepare(
916
+ getDbOrNull()!!.prepare(
2477
917
  `DELETE FROM slices WHERE milestone_id = :mid`,
2478
918
  ).run({ ":mid": milestoneId });
2479
- currentDb!.prepare(
919
+ getDbOrNull()!!.prepare(
2480
920
  `DELETE FROM replan_history WHERE milestone_id = :mid`,
2481
921
  ).run({ ":mid": milestoneId });
2482
- currentDb!.prepare(
922
+ getDbOrNull()!!.prepare(
2483
923
  `DELETE FROM assessments WHERE milestone_id = :mid`,
2484
924
  ).run({ ":mid": milestoneId });
2485
- currentDb!.prepare(
925
+ getDbOrNull()!!.prepare(
2486
926
  `DELETE FROM artifacts WHERE milestone_id = :mid`,
2487
927
  ).run({ ":mid": milestoneId });
2488
- currentDb!.prepare(
928
+ getDbOrNull()!!.prepare(
2489
929
  `DELETE FROM milestone_commit_attributions WHERE milestone_id = :mid`,
2490
930
  ).run({ ":mid": milestoneId });
2491
- currentDb!.prepare(
931
+ getDbOrNull()!!.prepare(
2492
932
  `DELETE FROM milestone_leases WHERE milestone_id = :mid`,
2493
933
  ).run({ ":mid": milestoneId });
2494
- currentDb!.prepare(
934
+ getDbOrNull()!!.prepare(
2495
935
  `DELETE FROM milestones WHERE id = :mid`,
2496
936
  ).run({ ":mid": milestoneId });
2497
937
  });
@@ -2503,7 +943,7 @@ export function updateSliceFields(milestoneId: string, sliceId: string, fields:
2503
943
  depends?: string[];
2504
944
  demo?: string;
2505
945
  }): void {
2506
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
946
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2507
947
  const SLICE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
2508
948
  if (fields.depends !== undefined) {
2509
949
  const invalidDep = fields.depends.find(d => !SLICE_ID_RE.test(d));
@@ -2511,7 +951,7 @@ export function updateSliceFields(milestoneId: string, sliceId: string, fields:
2511
951
  throw new GSDError(GSD_STALE_STATE, `updateSliceFields: depends element "${invalidDep}" is not a valid slice ID`);
2512
952
  }
2513
953
  }
2514
- currentDb.prepare(
954
+ getDbOrNull()!.prepare(
2515
955
  `UPDATE slices SET
2516
956
  title = COALESCE(:title, title),
2517
957
  risk = COALESCE(:risk, risk),
@@ -2528,39 +968,8 @@ export function updateSliceFields(milestoneId: string, sliceId: string, fields:
2528
968
  });
2529
969
  }
2530
970
 
2531
- export function getReplanHistory(milestoneId: string, sliceId?: string): Array<Record<string, unknown>> {
2532
- if (!currentDb) return [];
2533
- if (sliceId) {
2534
- return currentDb.prepare(
2535
- `SELECT * FROM replan_history WHERE milestone_id = :mid AND slice_id = :sid ORDER BY created_at DESC`,
2536
- ).all({ ":mid": milestoneId, ":sid": sliceId });
2537
- }
2538
- return currentDb.prepare(
2539
- `SELECT * FROM replan_history WHERE milestone_id = :mid ORDER BY created_at DESC`,
2540
- ).all({ ":mid": milestoneId });
2541
- }
2542
971
 
2543
- export function getAssessment(path: string): Record<string, unknown> | null {
2544
- if (!currentDb) return null;
2545
- const row = currentDb.prepare(
2546
- `SELECT * FROM assessments WHERE path = :path`,
2547
- ).get({ ":path": path });
2548
- return row ?? null;
2549
- }
2550
972
 
2551
- export function getLatestAssessmentByScope(
2552
- milestoneId: string,
2553
- scope: string,
2554
- ): Record<string, unknown> | null {
2555
- if (!currentDb) return null;
2556
- const row = currentDb.prepare(
2557
- `SELECT * FROM assessments
2558
- WHERE milestone_id = :mid AND scope = :scope
2559
- ORDER BY created_at DESC
2560
- LIMIT 1`,
2561
- ).get({ ":mid": milestoneId, ":scope": scope });
2562
- return row ?? null;
2563
- }
2564
973
 
2565
974
  // ─── Quality Gates ───────────────────────────────────────────────────────
2566
975
 
@@ -2572,8 +981,8 @@ export function insertGateRow(g: {
2572
981
  taskId?: string | null;
2573
982
  status?: GateStatus;
2574
983
  }): void {
2575
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2576
- currentDb.prepare(
984
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
985
+ getDbOrNull()!.prepare(
2577
986
  `INSERT OR IGNORE INTO quality_gates (milestone_id, slice_id, gate_id, scope, task_id, status)
2578
987
  VALUES (:mid, :sid, :gid, :scope, :tid, :status)`,
2579
988
  ).run({
@@ -2595,9 +1004,9 @@ export function saveGateResult(g: {
2595
1004
  rationale: string;
2596
1005
  findings: string;
2597
1006
  }): void {
2598
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1007
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2599
1008
  const evaluatedAt = new Date().toISOString();
2600
- const result = currentDb.prepare(
1009
+ const result = getDbOrNull()!.prepare(
2601
1010
  `UPDATE quality_gates
2602
1011
  SET status = 'complete', verdict = :verdict, rationale = :rationale,
2603
1012
  findings = :findings, evaluated_at = :evaluated_at
@@ -2646,29 +1055,11 @@ export function saveGateResult(g: {
2646
1055
  });
2647
1056
  }
2648
1057
 
2649
- export function getPendingGates(milestoneId: string, sliceId: string, scope?: GateScope): GateRow[] {
2650
- if (!currentDb) return [];
2651
- const sql = scope
2652
- ? `SELECT * FROM quality_gates WHERE milestone_id = :mid AND slice_id = :sid AND scope = :scope AND status = 'pending'`
2653
- : `SELECT * FROM quality_gates WHERE milestone_id = :mid AND slice_id = :sid AND status = 'pending'`;
2654
- const params: Record<string, unknown> = { ":mid": milestoneId, ":sid": sliceId };
2655
- if (scope) params[":scope"] = scope;
2656
- return currentDb.prepare(sql).all(params).map(rowToGate);
2657
- }
2658
1058
 
2659
- export function getGateResults(milestoneId: string, sliceId: string, scope?: GateScope): GateRow[] {
2660
- if (!currentDb) return [];
2661
- const sql = scope
2662
- ? `SELECT * FROM quality_gates WHERE milestone_id = :mid AND slice_id = :sid AND scope = :scope`
2663
- : `SELECT * FROM quality_gates WHERE milestone_id = :mid AND slice_id = :sid`;
2664
- const params: Record<string, unknown> = { ":mid": milestoneId, ":sid": sliceId };
2665
- if (scope) params[":scope"] = scope;
2666
- return currentDb.prepare(sql).all(params).map(rowToGate);
2667
- }
2668
1059
 
2669
1060
  export function markAllGatesOmitted(milestoneId: string, sliceId: string): void {
2670
- if (!currentDb) return;
2671
- currentDb.prepare(
1061
+ if (!getDbOrNull()!) return;
1062
+ getDbOrNull()!.prepare(
2672
1063
  `UPDATE quality_gates SET status = 'complete', verdict = 'omitted', evaluated_at = :now
2673
1064
  WHERE milestone_id = :mid AND slice_id = :sid AND status = 'pending'`,
2674
1065
  ).run({
@@ -2683,7 +1074,7 @@ export function markPendingGatesOmittedForTurn(
2683
1074
  sliceId: string,
2684
1075
  turn: OwnerTurn,
2685
1076
  ): void {
2686
- if (!currentDb) return;
1077
+ if (!getDbOrNull()!) return;
2687
1078
  const gateIds = [...getGateIdsForTurn(turn)];
2688
1079
  if (gateIds.length === 0) return;
2689
1080
  const placeholders = gateIds.map((_, i) => `:gid${i}`).join(",");
@@ -2695,71 +1086,15 @@ export function markPendingGatesOmittedForTurn(
2695
1086
  gateIds.forEach((id, index) => {
2696
1087
  params[`:gid${index}`] = id;
2697
1088
  });
2698
- currentDb.prepare(
1089
+ getDbOrNull()!.prepare(
2699
1090
  `UPDATE quality_gates SET status = 'complete', verdict = 'omitted', evaluated_at = :now
2700
1091
  WHERE milestone_id = :mid AND slice_id = :sid AND status = 'pending'
2701
1092
  AND gate_id IN (${placeholders})`,
2702
1093
  ).run(params);
2703
1094
  }
2704
1095
 
2705
- export function getPendingSliceGateCount(milestoneId: string, sliceId: string): number {
2706
- if (!currentDb) return 0;
2707
- const row = currentDb.prepare(
2708
- `SELECT COUNT(*) as cnt FROM quality_gates
2709
- WHERE milestone_id = :mid AND slice_id = :sid AND scope = 'slice' AND status = 'pending'`,
2710
- ).get({ ":mid": milestoneId, ":sid": sliceId });
2711
- return row ? (row["cnt"] as number) : 0;
2712
- }
2713
1096
 
2714
- /**
2715
- * Return pending gate rows owned by a specific workflow turn.
2716
- *
2717
- * Unlike `getPendingGates(..., scope)`, this filters by the registry's
2718
- * `ownerTurn` metadata so callers can distinguish Q3/Q4 (owned by
2719
- * gate-evaluate) from Q8 (owned by complete-slice) even though both are
2720
- * scope:"slice". Pass `taskId` to narrow task-scoped results to one task.
2721
- */
2722
- export function getPendingGatesForTurn(
2723
- milestoneId: string,
2724
- sliceId: string,
2725
- turn: OwnerTurn,
2726
- taskId?: string,
2727
- ): GateRow[] {
2728
- if (!currentDb) return [];
2729
- const ids = getGateIdsForTurn(turn);
2730
- if (ids.size === 0) return [];
2731
- const idList = [...ids];
2732
- const placeholders = idList.map((_, i) => `:gid${i}`).join(",");
2733
- const params: Record<string, unknown> = {
2734
- ":mid": milestoneId,
2735
- ":sid": sliceId,
2736
- };
2737
- idList.forEach((id, i) => {
2738
- params[`:gid${i}`] = id;
2739
- });
2740
- let sql =
2741
- `SELECT * FROM quality_gates
2742
- WHERE milestone_id = :mid AND slice_id = :sid
2743
- AND status = 'pending'
2744
- AND gate_id IN (${placeholders})`;
2745
- if (taskId !== undefined) {
2746
- sql += ` AND task_id = :tid`;
2747
- params[":tid"] = taskId;
2748
- }
2749
- return currentDb.prepare(sql).all(params).map(rowToGate);
2750
- }
2751
1097
 
2752
- /**
2753
- * Count pending gates for a turn. Convenience wrapper used by state
2754
- * derivation to decide whether a phase transition should pause.
2755
- */
2756
- export function getPendingGateCountForTurn(
2757
- milestoneId: string,
2758
- sliceId: string,
2759
- turn: OwnerTurn,
2760
- ): number {
2761
- return getPendingGatesForTurn(milestoneId, sliceId, turn).length;
2762
- }
2763
1098
 
2764
1099
  export function insertGateRun(entry: {
2765
1100
  traceId: string;
@@ -2780,8 +1115,8 @@ export function insertGateRun(entry: {
2780
1115
  retryable: boolean;
2781
1116
  evaluatedAt: string;
2782
1117
  }): void {
2783
- if (!currentDb) return;
2784
- currentDb.prepare(
1118
+ if (!getDbOrNull()!) return;
1119
+ getDbOrNull()!.prepare(
2785
1120
  `INSERT INTO gate_runs (
2786
1121
  trace_id, turn_id, gate_id, gate_type, unit_type, unit_id, milestone_id, slice_id, task_id,
2787
1122
  outcome, failure_class, rationale, findings, attempt, max_attempts, retryable, evaluated_at
@@ -2823,8 +1158,8 @@ export function upsertTurnGitTransaction(entry: {
2823
1158
  metadata?: Record<string, unknown>;
2824
1159
  updatedAt: string;
2825
1160
  }): void {
2826
- if (!currentDb) return;
2827
- currentDb.prepare(
1161
+ if (!getDbOrNull()!) return;
1162
+ getDbOrNull()!.prepare(
2828
1163
  `INSERT OR REPLACE INTO turn_git_transactions (
2829
1164
  trace_id, turn_id, unit_type, unit_id, stage, action, push, status, error, metadata_json, updated_at
2830
1165
  ) VALUES (
@@ -2845,18 +1180,6 @@ export function upsertTurnGitTransaction(entry: {
2845
1180
  });
2846
1181
  }
2847
1182
 
2848
- export function getMilestoneCommitAttributionShas(milestoneId: string): string[] {
2849
- if (!currentDb) return [];
2850
- const rows = currentDb.prepare(
2851
- `SELECT commit_sha
2852
- FROM milestone_commit_attributions
2853
- WHERE milestone_id = :mid
2854
- ORDER BY created_at, commit_sha`,
2855
- ).all({ ":mid": milestoneId }) as Array<Record<string, unknown>>;
2856
- return rows
2857
- .map((row) => typeof row["commit_sha"] === "string" ? row["commit_sha"] : "")
2858
- .filter(Boolean);
2859
- }
2860
1183
 
2861
1184
  export function recordMilestoneCommitAttribution(entry: {
2862
1185
  commitSha: string;
@@ -2868,9 +1191,9 @@ export function recordMilestoneCommitAttribution(entry: {
2868
1191
  files: string[];
2869
1192
  createdAt: string;
2870
1193
  }): void {
2871
- if (!currentDb) return;
1194
+ if (!getDbOrNull()!) return;
2872
1195
  transaction(() => {
2873
- currentDb!.prepare(
1196
+ getDbOrNull()!!.prepare(
2874
1197
  `INSERT OR REPLACE INTO milestone_commit_attributions (
2875
1198
  commit_sha, milestone_id, slice_id, task_id, source, confidence, files_json, created_at
2876
1199
  ) VALUES (
@@ -2887,7 +1210,7 @@ export function recordMilestoneCommitAttribution(entry: {
2887
1210
  ":created_at": entry.createdAt,
2888
1211
  });
2889
1212
 
2890
- currentDb!.prepare(
1213
+ getDbOrNull()!!.prepare(
2891
1214
  `INSERT OR IGNORE INTO audit_events (
2892
1215
  event_id, trace_id, turn_id, caused_by, category, type, ts, payload_json
2893
1216
  ) VALUES (
@@ -2924,9 +1247,9 @@ export function insertAuditEvent(entry: {
2924
1247
  ts: string;
2925
1248
  payload: Record<string, unknown>;
2926
1249
  }): void {
2927
- if (!currentDb) return;
1250
+ if (!getDbOrNull()!) return;
2928
1251
  transaction(() => {
2929
- currentDb!.prepare(
1252
+ getDbOrNull()!!.prepare(
2930
1253
  `INSERT OR IGNORE INTO audit_events (
2931
1254
  event_id, trace_id, turn_id, caused_by, category, type, ts, payload_json
2932
1255
  ) VALUES (
@@ -2944,7 +1267,7 @@ export function insertAuditEvent(entry: {
2944
1267
  });
2945
1268
 
2946
1269
  if (entry.turnId) {
2947
- const row = currentDb!.prepare(
1270
+ const row = getDbOrNull()!!.prepare(
2948
1271
  `SELECT event_count, first_ts, last_ts
2949
1272
  FROM audit_turn_index
2950
1273
  WHERE trace_id = :trace_id AND turn_id = :turn_id`,
@@ -2953,7 +1276,7 @@ export function insertAuditEvent(entry: {
2953
1276
  ":turn_id": entry.turnId,
2954
1277
  });
2955
1278
  if (row) {
2956
- currentDb!.prepare(
1279
+ getDbOrNull()!!.prepare(
2957
1280
  `UPDATE audit_turn_index
2958
1281
  SET first_ts = CASE WHEN :ts < first_ts THEN :ts ELSE first_ts END,
2959
1282
  last_ts = CASE WHEN :ts > last_ts THEN :ts ELSE last_ts END,
@@ -2965,7 +1288,7 @@ export function insertAuditEvent(entry: {
2965
1288
  ":ts": entry.ts,
2966
1289
  });
2967
1290
  } else {
2968
- currentDb!.prepare(
1291
+ getDbOrNull()!!.prepare(
2969
1292
  `INSERT INTO audit_turn_index (trace_id, turn_id, first_ts, last_ts, event_count)
2970
1293
  VALUES (:trace_id, :turn_id, :first_ts, :last_ts, :event_count)`,
2971
1294
  ).run({
@@ -2989,20 +1312,20 @@ export function insertAuditEvent(entry: {
2989
1312
 
2990
1313
  /** Delete a decision row by id. Used by db-writer.ts rollback on disk-write failure. */
2991
1314
  export function deleteDecisionById(id: string): void {
2992
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2993
- currentDb.prepare("DELETE FROM decisions WHERE id = :id").run({ ":id": id });
1315
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1316
+ getDbOrNull()!.prepare("DELETE FROM decisions WHERE id = :id").run({ ":id": id });
2994
1317
  }
2995
1318
 
2996
1319
  /** Delete a requirement row by id. Used by db-writer.ts rollback on disk-write failure. */
2997
1320
  export function deleteRequirementById(id: string): void {
2998
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2999
- currentDb.prepare("DELETE FROM requirements WHERE id = :id").run({ ":id": id });
1321
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1322
+ getDbOrNull()!.prepare("DELETE FROM requirements WHERE id = :id").run({ ":id": id });
3000
1323
  }
3001
1324
 
3002
1325
  /** Delete an artifact row by path. Used by db-writer.ts rollback on disk-write failure. */
3003
1326
  export function deleteArtifactByPath(path: string): void {
3004
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3005
- currentDb.prepare("DELETE FROM artifacts WHERE path = :path").run({ ":path": path });
1327
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1328
+ getDbOrNull()!.prepare("DELETE FROM artifacts WHERE path = :path").run({ ":path": path });
3006
1329
  }
3007
1330
 
3008
1331
  /**
@@ -3010,18 +1333,18 @@ export function deleteArtifactByPath(path: string): void {
3010
1333
  * `gsd recover --confirm` to rebuild engine state from markdown.
3011
1334
  */
3012
1335
  export function clearEngineHierarchy(): void {
3013
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1336
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3014
1337
  transaction(() => {
3015
- currentDb!.exec("DELETE FROM verification_evidence");
3016
- currentDb!.exec("DELETE FROM quality_gates");
3017
- currentDb!.exec("DELETE FROM slice_dependencies");
3018
- currentDb!.exec("DELETE FROM assessments");
3019
- currentDb!.exec("DELETE FROM replan_history");
3020
- currentDb!.exec("DELETE FROM milestone_commit_attributions");
3021
- currentDb!.exec("DELETE FROM tasks");
3022
- currentDb!.exec("DELETE FROM slices");
3023
- currentDb!.exec("DELETE FROM milestone_leases");
3024
- currentDb!.exec("DELETE FROM milestones");
1338
+ getDbOrNull()!!.exec("DELETE FROM verification_evidence");
1339
+ getDbOrNull()!!.exec("DELETE FROM quality_gates");
1340
+ getDbOrNull()!!.exec("DELETE FROM slice_dependencies");
1341
+ getDbOrNull()!!.exec("DELETE FROM assessments");
1342
+ getDbOrNull()!!.exec("DELETE FROM replan_history");
1343
+ getDbOrNull()!!.exec("DELETE FROM milestone_commit_attributions");
1344
+ getDbOrNull()!!.exec("DELETE FROM tasks");
1345
+ getDbOrNull()!!.exec("DELETE FROM slices");
1346
+ getDbOrNull()!!.exec("DELETE FROM milestone_leases");
1347
+ getDbOrNull()!!.exec("DELETE FROM milestones");
3025
1348
  });
3026
1349
  }
3027
1350
 
@@ -3037,8 +1360,8 @@ export function insertOrIgnoreSlice(args: {
3037
1360
  title: string;
3038
1361
  createdAt: string;
3039
1362
  }): void {
3040
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3041
- currentDb.prepare(
1363
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1364
+ getDbOrNull()!.prepare(
3042
1365
  `INSERT OR IGNORE INTO slices (milestone_id, id, title, status, created_at)
3043
1366
  VALUES (:mid, :sid, :title, 'pending', :ts)`,
3044
1367
  ).run({
@@ -3060,8 +1383,8 @@ export function insertOrIgnoreTask(args: {
3060
1383
  title: string;
3061
1384
  createdAt: string;
3062
1385
  }): void {
3063
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3064
- currentDb.prepare(
1386
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1387
+ getDbOrNull()!.prepare(
3065
1388
  `INSERT OR IGNORE INTO tasks (milestone_id, slice_id, id, title, status, created_at)
3066
1389
  VALUES (:mid, :sid, :tid, :title, 'pending', :ts)`,
3067
1390
  ).run({
@@ -3079,8 +1402,8 @@ export function insertOrIgnoreTask(args: {
3079
1402
  * trigger via DB in addition to the on-disk REPLAN-TRIGGER.md marker.
3080
1403
  */
3081
1404
  export function setSliceReplanTriggeredAt(milestoneId: string, sliceId: string, ts: string): void {
3082
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3083
- currentDb.prepare(
1405
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1406
+ getDbOrNull()!.prepare(
3084
1407
  "UPDATE slices SET replan_triggered_at = :ts WHERE milestone_id = :mid AND id = :sid",
3085
1408
  ).run({ ":ts": ts, ":mid": milestoneId, ":sid": sliceId });
3086
1409
  }
@@ -3101,8 +1424,8 @@ export function upsertQualityGate(g: {
3101
1424
  findings: string;
3102
1425
  evaluatedAt: string;
3103
1426
  }): void {
3104
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3105
- currentDb.prepare(
1427
+ if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1428
+ getDbOrNull()!.prepare(
3106
1429
  `INSERT OR REPLACE INTO quality_gates
3107
1430
  (milestone_id, slice_id, gate_id, scope, task_id, status, verdict, rationale, findings, evaluated_at)
3108
1431
  VALUES (:mid, :sid, :gid, :scope, :tid, :status, :verdict, :rationale, :findings, :evaluated_at)`,
@@ -3120,525 +1443,3 @@ export function upsertQualityGate(g: {
3120
1443
  });
3121
1444
  }
3122
1445
 
3123
- /**
3124
- * Atomically replace all workflow state from a manifest. Lifted verbatim from
3125
- * workflow-manifest.ts so the single-writer invariant holds. Restores
3126
- * correctness-bearing workflow tables; runtime soft state and append-only audit
3127
- * streams stay outside this recovery path.
3128
- */
3129
- export function restoreManifest(manifest: StateManifest): void {
3130
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3131
- const db = currentDb;
3132
-
3133
- transaction(() => {
3134
- const restoredMilestoneIds = new Set(manifest.milestones.map((m) => m.id));
3135
- const restoredSliceKeys = new Set(manifest.slices.map((s) => JSON.stringify([s.milestone_id, s.id])));
3136
- const preservedReplanHistory = manifest.replan_history === undefined
3137
- ? db.prepare("SELECT * FROM replan_history ORDER BY id").all() as unknown as NonNullable<StateManifest["replan_history"]>
3138
- : [];
3139
- const preservedAssessments = manifest.assessments === undefined
3140
- ? db.prepare("SELECT * FROM assessments ORDER BY path").all() as unknown as NonNullable<StateManifest["assessments"]>
3141
- : [];
3142
- const preservedQualityGates = manifest.quality_gates === undefined
3143
- ? db.prepare("SELECT * FROM quality_gates ORDER BY milestone_id, slice_id, gate_id, task_id").all() as unknown as NonNullable<StateManifest["quality_gates"]>
3144
- : [];
3145
- const preservedCommitAttributions = manifest.milestone_commit_attributions === undefined
3146
- ? db.prepare("SELECT * FROM milestone_commit_attributions ORDER BY milestone_id, commit_sha").all() as unknown as NonNullable<StateManifest["milestone_commit_attributions"]>
3147
- : [];
3148
-
3149
- // Clear workflow tables in dependency order.
3150
- db.exec("DELETE FROM verification_evidence");
3151
- db.exec("DELETE FROM quality_gates");
3152
- db.exec("DELETE FROM slice_dependencies");
3153
- db.exec("DELETE FROM assessments");
3154
- db.exec("DELETE FROM replan_history");
3155
- db.exec("DELETE FROM milestone_commit_attributions");
3156
- db.exec("DELETE FROM tasks");
3157
- db.exec("DELETE FROM slices");
3158
- db.exec("DELETE FROM milestone_leases");
3159
- db.exec("DELETE FROM milestones");
3160
- db.exec("DELETE FROM decisions WHERE 1=1");
3161
- db.exec(`DELETE FROM memories WHERE category = 'architecture' AND structured_fields LIKE '%"sourceDecisionId":"%'`);
3162
- if (manifest.artifacts !== undefined) db.exec("DELETE FROM artifacts");
3163
- if (manifest.requirements !== undefined) db.exec("DELETE FROM requirements");
3164
-
3165
- if (manifest.requirements !== undefined) {
3166
- const reqStmt = db.prepare(
3167
- `INSERT INTO requirements (
3168
- id, class, status, description, why, source, primary_owner,
3169
- supporting_slices, validation, notes, full_content, superseded_by
3170
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3171
- );
3172
- for (const r of manifest.requirements) {
3173
- reqStmt.run(
3174
- r.id, r.class, r.status, r.description, r.why, r.source, r.primary_owner,
3175
- r.supporting_slices, r.validation, r.notes, r.full_content, r.superseded_by,
3176
- );
3177
- }
3178
- }
3179
-
3180
- if (manifest.artifacts !== undefined) {
3181
- const artStmt = db.prepare(
3182
- `INSERT INTO artifacts (
3183
- path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash
3184
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
3185
- );
3186
- for (const a of manifest.artifacts) {
3187
- const fullContent = a.full_content ?? "";
3188
- artStmt.run(
3189
- a.path, a.artifact_type, a.milestone_id, a.slice_id, a.task_id,
3190
- fullContent, a.imported_at, a.content_hash ?? createHash("sha256").update(fullContent).digest("hex"),
3191
- );
3192
- }
3193
- }
3194
-
3195
- // Restore milestones
3196
- const msStmt = db.prepare(
3197
- `INSERT INTO milestones (id, title, status, depends_on, created_at, completed_at,
3198
- vision, success_criteria, key_risks, proof_strategy,
3199
- verification_contract, verification_integration, verification_operational, verification_uat,
3200
- definition_of_done, requirement_coverage, boundary_map_markdown, sequence)
3201
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3202
- );
3203
- for (const m of manifest.milestones) {
3204
- msStmt.run(
3205
- m.id, m.title, m.status,
3206
- JSON.stringify(m.depends_on), m.created_at, m.completed_at,
3207
- m.vision, JSON.stringify(m.success_criteria), JSON.stringify(m.key_risks),
3208
- JSON.stringify(m.proof_strategy),
3209
- m.verification_contract, m.verification_integration, m.verification_operational, m.verification_uat,
3210
- JSON.stringify(m.definition_of_done), m.requirement_coverage, m.boundary_map_markdown, m.sequence ?? 0,
3211
- );
3212
- }
3213
-
3214
- // Restore slices (ADR-011 Phase 1: includes is_sketch + sketch_scope)
3215
- const slStmt = db.prepare(
3216
- `INSERT INTO slices (milestone_id, id, title, status, risk, depends, demo,
3217
- created_at, completed_at, full_summary_md, full_uat_md,
3218
- goal, success_criteria, proof_level, integration_closure, observability_impact,
3219
- target_repositories, sequence, replan_triggered_at, is_sketch, sketch_scope)
3220
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3221
- );
3222
- for (const s of manifest.slices) {
3223
- slStmt.run(
3224
- s.milestone_id, s.id, s.title, s.status, s.risk,
3225
- JSON.stringify(s.depends), s.demo,
3226
- s.created_at, s.completed_at, s.full_summary_md, s.full_uat_md,
3227
- s.goal, s.success_criteria, s.proof_level, s.integration_closure, s.observability_impact,
3228
- JSON.stringify(s.target_repositories ?? []),
3229
- s.sequence, s.replan_triggered_at,
3230
- s.is_sketch ?? 0,
3231
- s.sketch_scope ?? "",
3232
- );
3233
- }
3234
-
3235
- const depStmt = db.prepare(
3236
- "INSERT OR IGNORE INTO slice_dependencies (milestone_id, slice_id, depends_on_slice_id) VALUES (?, ?, ?)",
3237
- );
3238
- for (const s of manifest.slices) {
3239
- for (const dep of s.depends ?? []) {
3240
- depStmt.run(s.milestone_id, s.id, dep);
3241
- }
3242
- }
3243
-
3244
- // Restore tasks (ADR-011 P2: includes blocker_source + escalation_* columns)
3245
- const tkStmt = db.prepare(
3246
- `INSERT INTO tasks (milestone_id, slice_id, id, title, status,
3247
- one_liner, narrative, verification_result, duration, completed_at,
3248
- blocker_discovered, deviations, known_issues, key_files, key_decisions,
3249
- full_summary_md, description, estimate, files, verify,
3250
- inputs, expected_output, observability_impact, full_plan_md, target_repositories, sequence,
3251
- blocker_source, escalation_pending, escalation_awaiting_review,
3252
- escalation_artifact_path, escalation_override_applied_at)
3253
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3254
- );
3255
- for (const t of manifest.tasks) {
3256
- tkStmt.run(
3257
- t.milestone_id, t.slice_id, t.id, t.title, t.status,
3258
- t.one_liner, t.narrative, t.verification_result, t.duration, t.completed_at,
3259
- t.blocker_discovered ? 1 : 0, t.deviations, t.known_issues,
3260
- JSON.stringify(t.key_files), JSON.stringify(t.key_decisions),
3261
- t.full_summary_md, t.description, t.estimate, JSON.stringify(t.files), t.verify,
3262
- JSON.stringify(t.inputs), JSON.stringify(t.expected_output),
3263
- t.observability_impact, t.full_plan_md ?? "", JSON.stringify(t.target_repositories ?? []), t.sequence,
3264
- t.blocker_source ?? "",
3265
- t.escalation_pending ?? 0,
3266
- t.escalation_awaiting_review ?? 0,
3267
- t.escalation_artifact_path ?? null,
3268
- t.escalation_override_applied_at ?? null,
3269
- );
3270
- }
3271
-
3272
- // Restore decisions (ADR-011 P2: include source so escalation decisions survive)
3273
- const dcStmt = db.prepare(
3274
- `INSERT INTO decisions (seq, id, when_context, scope, decision, choice, rationale, revisable, made_by, source, superseded_by)
3275
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3276
- );
3277
- for (const d of manifest.decisions) {
3278
- dcStmt.run(d.seq, d.id, d.when_context, d.scope, d.decision, d.choice, d.rationale, d.revisable, d.made_by, d.source ?? "discussion", d.superseded_by);
3279
- }
3280
-
3281
- const replanHistoryRows = manifest.replan_history ?? preservedReplanHistory.filter((r) => restoredMilestoneIds.has(r.milestone_id));
3282
- if (replanHistoryRows.length > 0) {
3283
- const replStmt = db.prepare(
3284
- `INSERT INTO replan_history (
3285
- id, milestone_id, slice_id, task_id, summary, previous_artifact_path, replacement_artifact_path, created_at
3286
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
3287
- );
3288
- for (const r of replanHistoryRows) {
3289
- replStmt.run(
3290
- r.id, r.milestone_id, r.slice_id, r.task_id, r.summary,
3291
- r.previous_artifact_path, r.replacement_artifact_path, r.created_at,
3292
- );
3293
- }
3294
- }
3295
-
3296
- const assessmentRows = manifest.assessments ?? preservedAssessments.filter((a) => restoredMilestoneIds.has(a.milestone_id));
3297
- if (assessmentRows.length > 0) {
3298
- const assessStmt = db.prepare(
3299
- `INSERT INTO assessments (
3300
- path, milestone_id, slice_id, task_id, status, scope, full_content, created_at
3301
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
3302
- );
3303
- for (const a of assessmentRows) {
3304
- assessStmt.run(
3305
- a.path, a.milestone_id, a.slice_id, a.task_id,
3306
- a.status, a.scope, a.full_content, a.created_at,
3307
- );
3308
- }
3309
- }
3310
-
3311
- const qualityGateRows = manifest.quality_gates ?? preservedQualityGates.filter((g) => (
3312
- restoredSliceKeys.has(JSON.stringify([g.milestone_id, g.slice_id]))
3313
- ));
3314
- if (qualityGateRows.length > 0) {
3315
- const gateStmt = db.prepare(
3316
- `INSERT INTO quality_gates (
3317
- milestone_id, slice_id, gate_id, scope, task_id, status, verdict, rationale, findings, evaluated_at
3318
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3319
- );
3320
- for (const g of qualityGateRows) {
3321
- gateStmt.run(
3322
- g.milestone_id, g.slice_id, g.gate_id, g.scope, g.task_id,
3323
- g.status, g.verdict ?? "", g.rationale, g.findings, g.evaluated_at,
3324
- );
3325
- }
3326
- }
3327
-
3328
- const commitAttributionRows = manifest.milestone_commit_attributions ??
3329
- preservedCommitAttributions.filter((a) => restoredMilestoneIds.has(a.milestone_id));
3330
- if (commitAttributionRows.length > 0) {
3331
- const attrStmt = db.prepare(
3332
- `INSERT OR REPLACE INTO milestone_commit_attributions (
3333
- commit_sha, milestone_id, slice_id, task_id, source, confidence, files_json, created_at
3334
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
3335
- );
3336
- for (const a of commitAttributionRows) {
3337
- attrStmt.run(
3338
- a.commit_sha, a.milestone_id, a.slice_id, a.task_id,
3339
- a.source, a.confidence, a.files_json, a.created_at,
3340
- );
3341
- }
3342
- }
3343
-
3344
- // Restore verification evidence
3345
- const evStmt = db.prepare(
3346
- `INSERT INTO verification_evidence (task_id, slice_id, milestone_id, command, exit_code, verdict, duration_ms, created_at)
3347
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
3348
- );
3349
- for (const e of manifest.verification_evidence) {
3350
- evStmt.run(e.task_id, e.slice_id, e.milestone_id, e.command, e.exit_code, e.verdict, e.duration_ms, e.created_at);
3351
- }
3352
- });
3353
- }
3354
-
3355
- // ─── Legacy markdown → DB bulk migration ─────────────────────────────────
3356
-
3357
- export interface LegacyMilestoneInsert {
3358
- id: string;
3359
- title: string;
3360
- status: string;
3361
- }
3362
-
3363
- export interface LegacySliceInsert {
3364
- id: string;
3365
- milestoneId: string;
3366
- title: string;
3367
- status: string;
3368
- risk: string;
3369
- sequence: number;
3370
- }
3371
-
3372
- export interface LegacyTaskInsert {
3373
- id: string;
3374
- sliceId: string;
3375
- milestoneId: string;
3376
- title: string;
3377
- status: string;
3378
- sequence: number;
3379
- }
3380
-
3381
- /**
3382
- * Bulk delete + insert a legacy milestone hierarchy for markdown → DB migration.
3383
- * Used by workflow-migration.ts to populate engine tables from parsed ROADMAP/PLAN
3384
- * files. All operations run inside a single transaction.
3385
- */
3386
- export function bulkInsertLegacyHierarchy(payload: {
3387
- milestones: LegacyMilestoneInsert[];
3388
- slices: LegacySliceInsert[];
3389
- tasks: LegacyTaskInsert[];
3390
- clearMilestoneIds: string[];
3391
- createdAt: string;
3392
- }): void {
3393
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3394
- const db = currentDb;
3395
- const { milestones, slices, tasks, clearMilestoneIds, createdAt } = payload;
3396
-
3397
- if (clearMilestoneIds.length === 0) return;
3398
- const placeholders = clearMilestoneIds.map(() => "?").join(",");
3399
-
3400
- transaction(() => {
3401
- db.prepare(`DELETE FROM tasks WHERE milestone_id IN (${placeholders})`).run(...clearMilestoneIds);
3402
- db.prepare(`DELETE FROM slices WHERE milestone_id IN (${placeholders})`).run(...clearMilestoneIds);
3403
- db.prepare(`DELETE FROM milestone_leases WHERE milestone_id IN (${placeholders})`).run(...clearMilestoneIds);
3404
- db.prepare(`DELETE FROM milestones WHERE id IN (${placeholders})`).run(...clearMilestoneIds);
3405
-
3406
- const insertMilestone = db.prepare(
3407
- "INSERT INTO milestones (id, title, status, created_at) VALUES (?, ?, ?, ?)",
3408
- );
3409
- for (const m of milestones) {
3410
- insertMilestone.run(m.id, m.title, m.status, createdAt);
3411
- }
3412
-
3413
- const insertSliceStmt = db.prepare(
3414
- "INSERT INTO slices (id, milestone_id, title, status, risk, depends, sequence, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
3415
- );
3416
- for (const s of slices) {
3417
- insertSliceStmt.run(s.id, s.milestoneId, s.title, s.status, s.risk, "[]", s.sequence, createdAt);
3418
- }
3419
-
3420
- const insertTaskStmt = db.prepare(
3421
- "INSERT INTO tasks (id, slice_id, milestone_id, title, description, status, estimate, files, sequence) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
3422
- );
3423
- for (const t of tasks) {
3424
- insertTaskStmt.run(t.id, t.sliceId, t.milestoneId, t.title, "", t.status, "", "[]", t.sequence);
3425
- }
3426
- });
3427
- }
3428
-
3429
- // ─── Memory store writers ────────────────────────────────────────────────
3430
- // All memory writes go through gsd-db.ts so the single-writer invariant
3431
- // holds. These are direct pass-throughs to the SQL previously in
3432
- // memory-store.ts — same bindings, same behavior.
3433
-
3434
- export function insertMemoryRow(args: {
3435
- id: string;
3436
- category: string;
3437
- content: string;
3438
- confidence: number;
3439
- sourceUnitType: string | null;
3440
- sourceUnitId: string | null;
3441
- createdAt: string;
3442
- updatedAt: string;
3443
- scope?: string;
3444
- tags?: string[];
3445
- /**
3446
- * ADR-013 Step 2: optional structured payload preserved alongside the flat
3447
- * `content` field. Used to retain gsd_save_decision-style fields (scope,
3448
- * decision, choice, rationale, made_by, revisable) on architecture-category
3449
- * memories so the cutover in Step 6 is lossless. Schema is intentionally
3450
- * open inside the JSON; documented per category in ADR-013.
3451
- */
3452
- structuredFields?: Record<string, unknown> | null;
3453
- }): void {
3454
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3455
- currentDb.prepare(
3456
- `INSERT INTO memories (id, category, content, confidence, source_unit_type, source_unit_id, created_at, updated_at, scope, tags, structured_fields)
3457
- VALUES (:id, :category, :content, :confidence, :source_unit_type, :source_unit_id, :created_at, :updated_at, :scope, :tags, :structured_fields)`,
3458
- ).run({
3459
- ":id": args.id,
3460
- ":category": args.category,
3461
- ":content": args.content,
3462
- ":confidence": args.confidence,
3463
- ":source_unit_type": args.sourceUnitType,
3464
- ":source_unit_id": args.sourceUnitId,
3465
- ":created_at": args.createdAt,
3466
- ":updated_at": args.updatedAt,
3467
- ":scope": args.scope ?? "project",
3468
- ":tags": JSON.stringify(args.tags ?? []),
3469
- ":structured_fields": args.structuredFields == null ? null : JSON.stringify(args.structuredFields),
3470
- });
3471
- }
3472
-
3473
- export function insertMemorySourceRow(args: {
3474
- id: string;
3475
- kind: string;
3476
- uri: string | null;
3477
- title: string | null;
3478
- content: string;
3479
- contentHash: string;
3480
- importedAt: string;
3481
- scope?: string;
3482
- tags?: string[];
3483
- }): void {
3484
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3485
- currentDb.prepare(
3486
- `INSERT OR IGNORE INTO memory_sources (id, kind, uri, title, content, content_hash, imported_at, scope, tags)
3487
- VALUES (:id, :kind, :uri, :title, :content, :content_hash, :imported_at, :scope, :tags)`,
3488
- ).run({
3489
- ":id": args.id,
3490
- ":kind": args.kind,
3491
- ":uri": args.uri,
3492
- ":title": args.title,
3493
- ":content": args.content,
3494
- ":content_hash": args.contentHash,
3495
- ":imported_at": args.importedAt,
3496
- ":scope": args.scope ?? "project",
3497
- ":tags": JSON.stringify(args.tags ?? []),
3498
- });
3499
- }
3500
-
3501
- export function deleteMemorySourceRow(id: string): boolean {
3502
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3503
- const res = currentDb
3504
- .prepare("DELETE FROM memory_sources WHERE id = :id")
3505
- .run({ ":id": id }) as { changes?: number };
3506
- return (res?.changes ?? 0) > 0;
3507
- }
3508
-
3509
- export function upsertMemoryEmbedding(args: {
3510
- memoryId: string;
3511
- model: string;
3512
- dim: number;
3513
- vector: Uint8Array;
3514
- updatedAt: string;
3515
- }): void {
3516
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3517
- currentDb.prepare(
3518
- `INSERT INTO memory_embeddings (memory_id, model, dim, vector, updated_at)
3519
- VALUES (:memory_id, :model, :dim, :vector, :updated_at)
3520
- ON CONFLICT(memory_id) DO UPDATE SET
3521
- model = excluded.model,
3522
- dim = excluded.dim,
3523
- vector = excluded.vector,
3524
- updated_at = excluded.updated_at`,
3525
- ).run({
3526
- ":memory_id": args.memoryId,
3527
- ":model": args.model,
3528
- ":dim": args.dim,
3529
- ":vector": args.vector,
3530
- ":updated_at": args.updatedAt,
3531
- });
3532
- }
3533
-
3534
- export function deleteMemoryEmbedding(memoryId: string): boolean {
3535
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3536
- const res = currentDb
3537
- .prepare("DELETE FROM memory_embeddings WHERE memory_id = :id")
3538
- .run({ ":id": memoryId }) as { changes?: number };
3539
- return (res?.changes ?? 0) > 0;
3540
- }
3541
-
3542
- export function insertMemoryRelationRow(args: {
3543
- fromId: string;
3544
- toId: string;
3545
- rel: string;
3546
- confidence: number;
3547
- createdAt: string;
3548
- }): void {
3549
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3550
- currentDb.prepare(
3551
- `INSERT OR REPLACE INTO memory_relations (from_id, to_id, rel, confidence, created_at)
3552
- VALUES (:from_id, :to_id, :rel, :confidence, :created_at)`,
3553
- ).run({
3554
- ":from_id": args.fromId,
3555
- ":to_id": args.toId,
3556
- ":rel": args.rel,
3557
- ":confidence": args.confidence,
3558
- ":created_at": args.createdAt,
3559
- });
3560
- }
3561
-
3562
- export function deleteMemoryRelationsFor(memoryId: string): void {
3563
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3564
- currentDb
3565
- .prepare("DELETE FROM memory_relations WHERE from_id = :id OR to_id = :id")
3566
- .run({ ":id": memoryId });
3567
- }
3568
-
3569
- export function rewriteMemoryId(placeholderId: string, realId: string): void {
3570
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3571
- currentDb.prepare("UPDATE memories SET id = :real_id WHERE id = :placeholder").run({
3572
- ":real_id": realId,
3573
- ":placeholder": placeholderId,
3574
- });
3575
- }
3576
-
3577
- export function updateMemoryContentRow(
3578
- id: string,
3579
- content: string,
3580
- confidence: number | undefined,
3581
- updatedAt: string,
3582
- ): void {
3583
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3584
- if (confidence != null) {
3585
- currentDb.prepare(
3586
- "UPDATE memories SET content = :content, confidence = :confidence, updated_at = :updated_at WHERE id = :id",
3587
- ).run({ ":content": content, ":confidence": confidence, ":updated_at": updatedAt, ":id": id });
3588
- } else {
3589
- currentDb.prepare(
3590
- "UPDATE memories SET content = :content, updated_at = :updated_at WHERE id = :id",
3591
- ).run({ ":content": content, ":updated_at": updatedAt, ":id": id });
3592
- }
3593
- }
3594
-
3595
- export function incrementMemoryHitCount(id: string, updatedAt: string): void {
3596
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3597
- currentDb.prepare(
3598
- "UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at, last_hit_at = :last_hit_at WHERE id = :id",
3599
- ).run({ ":updated_at": updatedAt, ":last_hit_at": updatedAt, ":id": id });
3600
- }
3601
-
3602
- export function supersedeMemoryRow(oldId: string, newId: string, updatedAt: string): void {
3603
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3604
- currentDb.prepare(
3605
- "UPDATE memories SET superseded_by = :new_id, updated_at = :updated_at WHERE id = :old_id",
3606
- ).run({ ":new_id": newId, ":updated_at": updatedAt, ":old_id": oldId });
3607
- }
3608
-
3609
- export function markMemoryUnitProcessed(
3610
- unitKey: string,
3611
- activityFile: string,
3612
- processedAt: string,
3613
- ): void {
3614
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3615
- currentDb.prepare(
3616
- `INSERT OR IGNORE INTO memory_processed_units (unit_key, activity_file, processed_at)
3617
- VALUES (:key, :file, :at)`,
3618
- ).run({ ":key": unitKey, ":file": activityFile, ":at": processedAt });
3619
- }
3620
-
3621
- export function decayMemoriesBefore(cutoffTs: string, now: string): void {
3622
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3623
- currentDb.prepare(
3624
- `UPDATE memories
3625
- SET confidence = MAX(0.1, confidence - 0.1), updated_at = :now
3626
- WHERE superseded_by IS NULL
3627
- AND updated_at < :cutoff
3628
- AND confidence > 0.1
3629
- AND (structured_fields IS NULL OR structured_fields NOT LIKE '%"sourceDecisionId"%')`,
3630
- ).run({ ":now": now, ":cutoff": cutoffTs });
3631
- }
3632
-
3633
- export function supersedeLowestRankedMemories(limit: number, now: string): void {
3634
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3635
- currentDb.prepare(
3636
- `UPDATE memories SET superseded_by = 'CAP_EXCEEDED', updated_at = :now
3637
- WHERE id IN (
3638
- SELECT id FROM memories
3639
- WHERE superseded_by IS NULL
3640
- ORDER BY (confidence * (1.0 + hit_count * 0.1)) ASC
3641
- LIMIT :limit
3642
- )`,
3643
- ).run({ ":now": now, ":limit": limit });
3644
- }