specrails-desktop 2.55.0 → 2.56.0

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 (368) hide show
  1. package/cli/dist/args.js +80 -0
  2. package/cli/dist/desktop-client.js +125 -0
  3. package/cli/dist/desktop-control.js +339 -0
  4. package/cli/dist/format.js +45 -0
  5. package/cli/dist/help.js +68 -0
  6. package/cli/dist/output.js +47 -0
  7. package/cli/dist/run-command.js +340 -0
  8. package/cli/dist/specrails-desktop.js +41 -1107
  9. package/cli/dist/status.js +111 -0
  10. package/client/dist/assets/{ActivityFeedPage-ILXMZXcb.js → ActivityFeedPage-DdMNmVBk.js} +1 -1
  11. package/client/dist/assets/{AgentBrowserCapture-UcsupCy7.js → AgentBrowserCapture-CeerUf--.js} +1 -1
  12. package/client/dist/assets/{AgentModeAnalyticsPane-CQuxYh8B.js → AgentModeAnalyticsPane-BjGb-8rh.js} +2 -2
  13. package/client/dist/assets/{AgentModeCodePane-zPWbW7ei.js → AgentModeCodePane-8XRkQEo_.js} +2 -2
  14. package/client/dist/assets/{AgentModeJobsPane-CInByIdl.js → AgentModeJobsPane-BN3dgJOA.js} +1 -1
  15. package/client/dist/assets/{AgentsPage-BWvgxaBv.js → AgentsPage-DfHWgP_x.js} +1 -1
  16. package/client/dist/assets/{AnalyticsPage-DP3jakv-.js → AnalyticsPage-CyfhYiBn.js} +1 -1
  17. package/client/dist/assets/{CodePage-BKXiwE5a.js → CodePage-C2Nf-Q5W.js} +1 -1
  18. package/client/dist/assets/{DesktopAnalyticsPage-DryJOaMz.js → DesktopAnalyticsPage-tc0p4jJW.js} +1 -1
  19. package/client/dist/assets/{DocsDialog-BsCG9ksG.js → DocsDialog-Br3bczCp.js} +1 -1
  20. package/client/dist/assets/{DocsPage-Cc-GeKYl.js → DocsPage-BcDr2cBm.js} +1 -1
  21. package/client/dist/assets/{ExportDropdown-ClahJdDn.js → ExportDropdown-BVGKEgkd.js} +1 -1
  22. package/client/dist/assets/{InteractiveJobComposer-DfLF6N0f.js → InteractiveJobComposer-DFPpdg2D.js} +1 -1
  23. package/client/dist/assets/{JobDetailModal-DHeur9YK.js → JobDetailModal-BdOv-l-M.js} +1 -1
  24. package/client/dist/assets/{JobDetailPage-CraDKzeG.js → JobDetailPage-vBbNeRuz.js} +1 -1
  25. package/client/dist/assets/{JobsPage-CcbuSwj7.js → JobsPage-CJCtPhIz.js} +1 -1
  26. package/client/dist/assets/{LoopBuilderPage-BKtNp494.js → LoopBuilderPage-BemvUOxy.js} +1 -1
  27. package/client/dist/assets/{LoopPreviewModal-DbZd9isy.js → LoopPreviewModal-C6hNAeYN.js} +1 -1
  28. package/client/dist/assets/{LoopsPage-DEg6sIkG.js → LoopsPage-BZr0quvh.js} +1 -1
  29. package/client/dist/assets/{MinimizedChatsContext-BGtEjWsc.js → MinimizedChatsContext-D3QzgV0B.js} +1 -1
  30. package/client/dist/assets/{PluginsPage-BVqSsPzL.js → PluginsPage-DOpTV75p.js} +1 -1
  31. package/client/dist/assets/{ProjectSettingsDialog-CAjLgJAZ.js → ProjectSettingsDialog-Dkwrn0z1.js} +1 -1
  32. package/client/dist/assets/{RepositoryDeliveries-DrcrxLe0.js → RepositoryDeliveries-Du2UCf1a.js} +1 -1
  33. package/client/dist/assets/{RepositoryScopeSelector-CAGY3xW6.js → RepositoryScopeSelector-hPGZNRBm.js} +1 -1
  34. package/client/dist/assets/ReviewPacketPage-DcOq-UTA.js +4 -0
  35. package/client/dist/assets/{TemplatePreviewModal-Bqs-9fhn.js → TemplatePreviewModal-Cejcquah.js} +1 -1
  36. package/client/dist/assets/{TicketDetailModalContext-C0jQ5yOk.js → TicketDetailModalContext-DAK-NoK6.js} +1 -1
  37. package/client/dist/assets/{Trans-DtWlCJN9.js → Trans-DEbgkuAY.js} +1 -1
  38. package/client/dist/assets/{dashboard-CTfEtkqX.js → dashboard-B9uWNGO2.js} +1 -1
  39. package/client/dist/assets/{dashboard-DMklepmz.js → dashboard-BIwmO9wc.js} +1 -1
  40. package/client/dist/assets/{dashboard-DBAP2_SQ.js → dashboard-BV4vOZkR.js} +1 -1
  41. package/client/dist/assets/{dashboard-D40HP2ks.js → dashboard-Bf6MPVsM.js} +1 -1
  42. package/client/dist/assets/{dashboard-Csf1yGSN.js → dashboard-C8klWVvZ.js} +1 -1
  43. package/client/dist/assets/{dashboard-C29Qwa-L.js → dashboard-D1ORhPF0.js} +1 -1
  44. package/client/dist/assets/{dashboard-Cr4D8oMn.js → dashboard-DSubvs9x.js} +1 -1
  45. package/client/dist/assets/{dashboard-CSJzf5OT.js → dashboard-px_vlRx_.js} +1 -1
  46. package/client/dist/assets/{formatDistanceToNow-DYmvOA3R.js → formatDistanceToNow-Cc2u5FwQ.js} +1 -1
  47. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BAhW5E6V.js → getTimezoneOffsetInMilliseconds-BHm2U-w1.js} +1 -1
  48. package/client/dist/assets/index-C51x61Bz.css +2 -0
  49. package/client/dist/assets/{index-BfAZg5DG.js → index-Cxb288Rj.js} +29 -29
  50. package/client/dist/assets/{jira-api-B27Za5FP.js → jira-api-DrGxiD9p.js} +1 -1
  51. package/client/dist/assets/packet-8nn9RsU7.js +1 -0
  52. package/client/dist/assets/packet-B0H15elv.js +1 -0
  53. package/client/dist/assets/packet-BQvBgrwh.js +1 -0
  54. package/client/dist/assets/packet-BR7mO0-a.js +1 -0
  55. package/client/dist/assets/packet-BgcQ6q_K.js +1 -0
  56. package/client/dist/assets/packet-D2s92Jnn.js +1 -0
  57. package/client/dist/assets/packet-YbtcD1HS.js +1 -0
  58. package/client/dist/assets/packet-c0Qj_9eX.js +1 -0
  59. package/client/dist/assets/{project-repositories-DixIZHTA.js → project-repositories-9Wzs0coQ.js} +1 -1
  60. package/client/dist/assets/{spending-BGmZBX5B.js → spending-T5GM7wpn.js} +1 -1
  61. package/client/dist/assets/{useDesktop-CPB4cnwI.js → useDesktop-QogiN0zR.js} +1 -1
  62. package/client/dist/assets/{useRuntimeRuns-DEQ9HdAM.js → useRuntimeRuns-Dqmz9fS9.js} +1 -1
  63. package/client/dist/assets/{useSharedWebSocket-DcPRKR8R.js → useSharedWebSocket-Dz-HI6De.js} +2 -2
  64. package/client/dist/index.html +17 -17
  65. package/docs/codex.md +5 -5
  66. package/docs/customizing.md +1 -1
  67. package/docs/gemini-cli-provider-study.md +14 -14
  68. package/docs/gemini-core-support-evaluation.md +1 -1
  69. package/docs/gemini.md +5 -5
  70. package/docs/internals/README.md +6 -2
  71. package/docs/internals/adding-a-provider.md +22 -15
  72. package/docs/internals/agent-runtime-framework-evaluation.md +8 -8
  73. package/docs/internals/api-reference.md +2 -2
  74. package/docs/internals/architecture.md +9 -9
  75. package/docs/internals/browser-capture-performance.md +3 -3
  76. package/docs/internals/code-explorer-story.md +2 -2
  77. package/docs/internals/companion-rails-as-loops-contract.md +2 -2
  78. package/docs/internals/core-runtime-updates.md +5 -0
  79. package/docs/internals/embedded-browser-native-webview-evaluation.md +5 -5
  80. package/docs/internals/gemini-mcp-registration.md +2 -2
  81. package/docs/internals/global-artifacts-alignment-contract.md +1 -1
  82. package/docs/internals/global-artifacts-relocation-evaluation.md +1 -1
  83. package/docs/internals/interactive-jobs.md +8 -8
  84. package/docs/internals/legacy-implementation-notes.md +722 -0
  85. package/docs/internals/loop-step-log-explorer.md +8 -8
  86. package/docs/internals/mission-rail-cards.md +14 -14
  87. package/docs/internals/modular-architecture.md +107 -0
  88. package/docs/internals/profiles.md +1 -1
  89. package/docs/internals/programmatic-agent-runtime.md +66 -0
  90. package/docs/internals/project-builder.md +18 -18
  91. package/docs/internals/review-packet.md +8 -8
  92. package/docs/internals/safe-pr-review-flow.md +11 -11
  93. package/docs/internals/source-architecture.md +131 -0
  94. package/docs/internals/source-map.md +1356 -0
  95. package/docs/internals/spec-addenda.md +65 -9
  96. package/docs/internals/switch-to-agent-mode-design.md +11 -11
  97. package/docs/tracking-cost.md +1 -1
  98. package/package.json +5 -2
  99. package/server/dist/agent-mcp-config.js +1 -1
  100. package/server/dist/attachment-manager.js +1 -1
  101. package/server/dist/build-dirs.js +2 -2
  102. package/server/dist/config.js +0 -3
  103. package/server/dist/core-completion.js +1 -1
  104. package/server/dist/core-execution.js +1 -1
  105. package/server/dist/db/activity.js +39 -0
  106. package/server/dist/db/connection.js +41 -0
  107. package/server/dist/db/conversations.js +48 -0
  108. package/server/dist/db/jobs.js +251 -0
  109. package/server/dist/db/migrations.js +1521 -0
  110. package/server/dist/db/proposals.js +50 -0
  111. package/server/dist/db/settings.js +15 -0
  112. package/server/dist/db/stats.js +61 -0
  113. package/server/dist/db/telemetry.js +56 -0
  114. package/server/dist/db/templates.js +37 -0
  115. package/server/dist/db/types.js +2 -0
  116. package/server/dist/db.js +66 -2224
  117. package/server/dist/desktop-db.js +3 -3
  118. package/server/dist/desktop-router.js +8 -8
  119. package/server/dist/git-diagnostics.js +1 -1
  120. package/server/dist/index.js +11 -11
  121. package/server/dist/integration-branch.js +3 -14
  122. package/server/dist/jira/jira-issue-fields.js +0 -1
  123. package/server/dist/jira/jira-materializer.js +1 -1
  124. package/server/dist/jira/jira-sync-manager.js +1 -1
  125. package/server/dist/mcp/guide.js +16 -2
  126. package/server/dist/mcp/mcp-admin-router.js +1 -1
  127. package/server/dist/mcp/mcp-server.js +1 -1
  128. package/server/dist/mcp/tools/catalog.js +2 -0
  129. package/server/dist/mcp/tools/jobs.js +23 -5
  130. package/server/dist/mcp/tools/rails.js +2 -2
  131. package/server/dist/mcp/tools/recovery.js +35 -0
  132. package/server/dist/mcp/tools/specs.js +2 -2
  133. package/server/dist/mcp/tools/support.js +4 -1
  134. package/server/dist/mcp/tools/types.js +3 -3
  135. package/server/dist/mobile/mobile-gateway.js +1 -1
  136. package/server/dist/mobile/mobile-missions.js +1 -1
  137. package/server/dist/{desktop-analytics.js → modules/accounting/runtime/desktop-analytics.js} +2 -2
  138. package/server/dist/{pricing.js → modules/accounting/runtime/pricing.js} +10 -3
  139. package/server/dist/{result-event.js → modules/accounting/runtime/result-event.js} +3 -3
  140. package/server/dist/{spending.js → modules/accounting/runtime/spending.js} +0 -3
  141. package/server/dist/{telemetry-compactor.js → modules/accounting/runtime/telemetry-compactor.js} +1 -1
  142. package/server/dist/{telemetry-receiver.js → modules/accounting/runtime/telemetry-receiver.js} +2 -2
  143. package/server/dist/{agent-runtime-bridge.js → modules/agent-runtime/runtime/agent-runtime-bridge.js} +2 -2
  144. package/server/dist/{agent-runtime-controls-router.js → modules/agent-runtime/runtime/agent-runtime-controls-router.js} +16 -0
  145. package/server/dist/{agent-runtime-controls.js → modules/agent-runtime/runtime/agent-runtime-controls.js} +100 -8
  146. package/server/dist/{agent-runtime-effective-config.js → modules/agent-runtime/runtime/agent-runtime-effective-config.js} +1 -1
  147. package/server/dist/{agent-runtime-loader.js → modules/agent-runtime/runtime/agent-runtime-loader.js} +3 -3
  148. package/server/dist/{agent-runtime-paths.js → modules/agent-runtime/runtime/agent-runtime-paths.js} +1 -1
  149. package/server/dist/modules/agent-runtime/runtime/agent-runtime-recovery.js +53 -0
  150. package/server/dist/{agent-runtime-settings-router.js → modules/agent-runtime/runtime/agent-runtime-settings-router.js} +2 -2
  151. package/server/dist/{agent-runtime-settings.js → modules/agent-runtime/runtime/agent-runtime-settings.js} +3 -3
  152. package/server/dist/{agent-runtime-settlement.js → modules/agent-runtime/runtime/agent-runtime-settlement.js} +6 -6
  153. package/server/dist/{agent-defaults.js → modules/agents/runtime/agent-defaults.js} +3 -3
  154. package/server/dist/{agent-generator.js → modules/agents/runtime/agent-generator.js} +7 -7
  155. package/server/dist/{agent-refine-manager.js → modules/agents/runtime/agent-refine-manager.js} +5 -5
  156. package/server/dist/{agent-store.js → modules/agents/runtime/agent-store.js} +1 -1
  157. package/server/dist/{profile-manager.js → modules/agents/runtime/profile-manager.js} +2 -2
  158. package/server/dist/{profiles-router.js → modules/agents/runtime/profiles-router.js} +5 -5
  159. package/server/dist/{browser-capture-manager.js → modules/browser/runtime/browser-capture-manager.js} +2 -4
  160. package/server/dist/{browser-playwright.js → modules/browser/runtime/browser-playwright.js} +1 -1
  161. package/server/dist/{blueprint-chat-manager.js → modules/builder/runtime/blueprint-chat-manager.js} +7 -7
  162. package/server/dist/{blueprint-commit.js → modules/builder/runtime/blueprint-commit.js} +8 -8
  163. package/server/dist/{blueprint-draft-parser.js → modules/builder/runtime/blueprint-draft-parser.js} +1 -1
  164. package/server/dist/{blueprint-generation.js → modules/builder/runtime/blueprint-generation.js} +1 -1
  165. package/server/dist/{blueprint-operator-prompt.js → modules/builder/runtime/blueprint-operator-prompt.js} +1 -1
  166. package/server/dist/{blueprint-router.js → modules/builder/runtime/blueprint-router.js} +3 -3
  167. package/server/dist/{blueprint-spec-quality.js → modules/builder/runtime/blueprint-spec-quality.js} +1 -1
  168. package/server/dist/{milestone-chain.js → modules/builder/runtime/milestone-chain.js} +1 -1
  169. package/server/dist/{milestone-progress.js → modules/builder/runtime/milestone-progress.js} +6 -6
  170. package/server/dist/{code-activity.js → modules/code/runtime/code-activity.js} +1 -1
  171. package/server/dist/{code-explorer-router.js → modules/code/runtime/code-explorer-router.js} +5 -5
  172. package/server/dist/{file-provenance.js → modules/code/runtime/file-provenance.js} +1 -1
  173. package/server/dist/{file-story-manager.js → modules/code/runtime/file-story-manager.js} +2 -2
  174. package/server/dist/{file-story.js → modules/code/runtime/file-story.js} +2 -2
  175. package/server/dist/{file-summary-generator.js → modules/code/runtime/file-summary-generator.js} +3 -3
  176. package/server/dist/{file-summary-manager.js → modules/code/runtime/file-summary-manager.js} +3 -3
  177. package/server/dist/{project-code-discovery.js → modules/code/runtime/project-code-discovery.js} +2 -2
  178. package/server/dist/modules/conversations/domain/draft-stream.js +91 -0
  179. package/server/dist/modules/conversations/domain/recovery-context.js +101 -0
  180. package/server/dist/modules/conversations/index.js +10 -0
  181. package/server/dist/{chat-manager.js → modules/conversations/runtime/chat-manager.js} +32 -216
  182. package/server/dist/{context-budget.js → modules/conversations/runtime/context-budget.js} +1 -1
  183. package/server/dist/{explore-cwd-manager.js → modules/conversations/runtime/explore-cwd-manager.js} +1 -1
  184. package/server/dist/{explore-smash.js → modules/conversations/runtime/explore-smash.js} +1 -1
  185. package/server/dist/{explore-stdin-session.js → modules/conversations/runtime/explore-stdin-session.js} +1 -1
  186. package/server/dist/modules/delivery/adapters/decisions/chains.js +138 -0
  187. package/server/dist/modules/delivery/adapters/decisions/contracts.js +2 -0
  188. package/server/dist/modules/delivery/adapters/decisions/create.js +234 -0
  189. package/server/dist/modules/delivery/adapters/decisions/discard.js +251 -0
  190. package/server/dist/modules/delivery/adapters/decisions/dispatch.js +107 -0
  191. package/server/dist/modules/delivery/adapters/decisions/evidence.js +205 -0
  192. package/server/dist/modules/delivery/adapters/decisions/merge-local.js +338 -0
  193. package/server/dist/modules/delivery/adapters/decisions/metadata.js +109 -0
  194. package/server/dist/modules/delivery/adapters/decisions/observations.js +260 -0
  195. package/server/dist/modules/delivery/adapters/decisions/ownership.js +83 -0
  196. package/server/dist/modules/delivery/adapters/decisions/publish.js +242 -0
  197. package/server/dist/modules/delivery/adapters/decisions/recovery.js +425 -0
  198. package/server/dist/modules/delivery/adapters/decisions/retry.js +112 -0
  199. package/server/dist/modules/delivery/adapters/decisions/transitions.js +94 -0
  200. package/server/dist/modules/delivery/domain/decision-policy.js +50 -0
  201. package/server/dist/modules/delivery/domain/state.js +2 -0
  202. package/server/dist/modules/delivery/index.js +7 -0
  203. package/server/dist/{active-pr-continuation.js → modules/delivery/runtime/active-pr-continuation.js} +2 -2
  204. package/server/dist/{delivery-evidence.js → modules/delivery/runtime/delivery-evidence.js} +3 -3
  205. package/server/dist/{multi-repo-bases.js → modules/delivery/runtime/multi-repo-bases.js} +1 -1
  206. package/server/dist/{multi-repo-checkout.js → modules/delivery/runtime/multi-repo-checkout.js} +4 -4
  207. package/server/dist/{multi-repo-delivery.js → modules/delivery/runtime/multi-repo-delivery.js} +4 -4
  208. package/server/dist/{multi-repo-execution.js → modules/delivery/runtime/multi-repo-execution.js} +8 -8
  209. package/server/dist/{pr-body.js → modules/delivery/runtime/pr-body.js} +1 -1
  210. package/server/dist/{pr-follow-up-scope.js → modules/delivery/runtime/pr-follow-up-scope.js} +2 -2
  211. package/server/dist/{pr-follow-up.js → modules/delivery/runtime/pr-follow-up.js} +1 -1
  212. package/server/dist/{pr-naming.js → modules/delivery/runtime/pr-naming.js} +1 -1
  213. package/server/dist/{pr-publisher.js → modules/delivery/runtime/pr-publisher.js} +2 -2
  214. package/server/dist/{rail-isolated-launch.js → modules/delivery/runtime/rail-isolated-launch.js} +29 -27
  215. package/server/dist/{rail-launch-parser.js → modules/delivery/runtime/rail-launch-parser.js} +2 -2
  216. package/server/dist/{rail-merge-orchestrator.js → modules/delivery/runtime/rail-merge-orchestrator.js} +5 -5
  217. package/server/dist/modules/delivery/runtime/rail-pr-decision.js +12 -0
  218. package/server/dist/{rail-pr-delivery.js → modules/delivery/runtime/rail-pr-delivery.js} +1 -1
  219. package/server/dist/{rail-pr-store.js → modules/delivery/runtime/rail-pr-store.js} +2 -2
  220. package/server/dist/{rail-pr-ticket-effects.js → modules/delivery/runtime/rail-pr-ticket-effects.js} +3 -3
  221. package/server/dist/{rail-worktree-release.js → modules/delivery/runtime/rail-worktree-release.js} +4 -4
  222. package/server/dist/{rails-router.js → modules/delivery/runtime/rails-router.js} +66 -67
  223. package/server/dist/{review-packet.js → modules/delivery/runtime/review-packet.js} +3 -3
  224. package/server/dist/modules/execution/adapters/budget-storage.js +21 -0
  225. package/server/dist/modules/execution/adapters/usage-reader.js +38 -0
  226. package/server/dist/modules/execution/application/enforce-budget.js +14 -0
  227. package/server/dist/modules/execution/application/record-job-invocations.js +64 -0
  228. package/server/dist/modules/execution/application/recover-job-usage.js +200 -0
  229. package/server/dist/modules/execution/domain/job-accounting.js +2 -0
  230. package/server/dist/modules/execution/domain/scheduling.js +24 -0
  231. package/server/dist/modules/execution/domain/usage.js +21 -0
  232. package/server/dist/modules/execution/index.js +14 -0
  233. package/server/dist/modules/execution/ports.js +2 -0
  234. package/server/dist/{accept-ladder.js → modules/execution/runtime/accept-ladder.js} +1 -1
  235. package/server/dist/{interactive-job-session.js → modules/execution/runtime/interactive-job-session.js} +11 -11
  236. package/server/dist/{job-phase-breakdown.js → modules/execution/runtime/job-phase-breakdown.js} +1 -1
  237. package/server/dist/{queue-manager.js → modules/execution/runtime/queue-manager.js} +74 -491
  238. package/server/dist/{revision-seed.js → modules/execution/runtime/revision-seed.js} +1 -1
  239. package/server/dist/{spawn-lifecycle.js → modules/execution/runtime/spawn-lifecycle.js} +3 -3
  240. package/server/dist/{stuck-run-detector.js → modules/execution/runtime/stuck-run-detector.js} +1 -1
  241. package/server/dist/{loop-command-catalog.js → modules/loops/runtime/loop-command-catalog.js} +1 -1
  242. package/server/dist/{loop-executors.js → modules/loops/runtime/loop-executors.js} +17 -17
  243. package/server/dist/{loop-factory.js → modules/loops/runtime/loop-factory.js} +2 -21
  244. package/server/dist/{loop-graph.js → modules/loops/runtime/loop-graph.js} +25 -0
  245. package/server/dist/{loop-role-engines.js → modules/loops/runtime/loop-role-engines.js} +4 -4
  246. package/server/dist/{loop-run-manager.js → modules/loops/runtime/loop-run-manager.js} +101 -26
  247. package/server/dist/{loop-runs-store.js → modules/loops/runtime/loop-runs-store.js} +1 -1
  248. package/server/dist/{loop-shell-invocation.js → modules/loops/runtime/loop-shell-invocation.js} +2 -2
  249. package/server/dist/{loop-step-idle.js → modules/loops/runtime/loop-step-idle.js} +1 -1
  250. package/server/dist/{loop-templates.js → modules/loops/runtime/loop-templates.js} +24 -29
  251. package/server/dist/{loops-router.js → modules/loops/runtime/loops-router.js} +2 -2
  252. package/server/dist/{agent-chat-manager.js → modules/missions/runtime/agent-chat-manager.js} +19 -19
  253. package/server/dist/{agent-chat-router.js → modules/missions/runtime/agent-chat-router.js} +6 -6
  254. package/server/dist/{agent-context-resolver.js → modules/missions/runtime/agent-context-resolver.js} +8 -8
  255. package/server/dist/{agent-failure-briefing.js → modules/missions/runtime/agent-failure-briefing.js} +4 -2
  256. package/server/dist/{agent-fence-promotion.js → modules/missions/runtime/agent-fence-promotion.js} +1 -1
  257. package/server/dist/{agent-input-store.js → modules/missions/runtime/agent-input-store.js} +1 -1
  258. package/server/dist/{agent-operator-prompt.js → modules/missions/runtime/agent-operator-prompt.js} +75 -12
  259. package/server/dist/{agent-spec-framing.js → modules/missions/runtime/agent-spec-framing.js} +4 -4
  260. package/server/dist/{agent-steering.js → modules/missions/runtime/agent-steering.js} +1 -1
  261. package/server/dist/{mission-run-notify.js → modules/missions/runtime/mission-run-notify.js} +5 -2
  262. package/server/dist/modules/project-settings/adapters/http.js +24 -0
  263. package/server/dist/modules/project-settings/adapters/sqlite.js +120 -0
  264. package/server/dist/modules/project-settings/application.js +11 -0
  265. package/server/dist/modules/project-settings/domain.js +95 -0
  266. package/server/dist/modules/project-settings/index.js +11 -0
  267. package/server/dist/modules/project-settings/ports.js +2 -0
  268. package/server/dist/{contract-refine-runner.js → modules/specs/runtime/contract-refine-runner.js} +12 -12
  269. package/server/dist/{proposal-manager.js → modules/specs/runtime/proposal-manager.js} +8 -8
  270. package/server/dist/{smash-runner.js → modules/specs/runtime/smash-runner.js} +26 -58
  271. package/server/dist/{spec-addenda-core.js → modules/specs/runtime/spec-addenda-core.js} +2 -2
  272. package/server/dist/{spec-addenda.js → modules/specs/runtime/spec-addenda.js} +3 -3
  273. package/server/dist/{spec-launcher-manager.js → modules/specs/runtime/spec-launcher-manager.js} +8 -8
  274. package/server/dist/{spec-models.js → modules/specs/runtime/spec-models.js} +1 -1
  275. package/server/dist/{ticket-watcher.js → modules/specs/runtime/ticket-watcher.js} +1 -1
  276. package/server/dist/{background-process-control.js → modules/terminals/runtime/background-process-control.js} +1 -1
  277. package/server/dist/{background-process-service.js → modules/terminals/runtime/background-process-service.js} +3 -3
  278. package/server/dist/{background-process-store.js → modules/terminals/runtime/background-process-store.js} +1 -1
  279. package/server/dist/{background-windows-bootstrap.js → modules/terminals/runtime/background-windows-bootstrap.js} +3 -3
  280. package/server/dist/{terminal-manager.js → modules/terminals/runtime/terminal-manager.js} +3 -3
  281. package/server/dist/project-git.js +1 -1
  282. package/server/dist/project-profile-support.js +32 -0
  283. package/server/dist/project-registry.js +34 -40
  284. package/server/dist/project-router-background-processes.js +1 -1
  285. package/server/dist/project-router-chat.js +6 -6
  286. package/server/dist/project-router-git.js +1 -1
  287. package/server/dist/project-router-helpers.js +2 -2
  288. package/server/dist/project-router-jobs.js +14 -12
  289. package/server/dist/project-router-loop-runs.js +12 -12
  290. package/server/dist/project-router-settings.js +15 -75
  291. package/server/dist/project-router-setup.js +2 -2
  292. package/server/dist/project-router-spending.js +4 -4
  293. package/server/dist/project-router-terminals.js +4 -4
  294. package/server/dist/project-router-tickets.js +14 -14
  295. package/server/dist/project-router.js +13 -13
  296. package/server/dist/providers/claude-adapter.js +3 -2
  297. package/server/dist/providers/codex-adapter.js +3 -3
  298. package/server/dist/providers/gemini-adapter.js +1 -1
  299. package/server/dist/providers/telemetry-env.js +55 -0
  300. package/server/dist/runtime-role-prompts-router.js +2 -2
  301. package/server/dist/setup-manager.js +2 -48
  302. package/server/dist/shared/git-branch-name.js +16 -0
  303. package/server/dist/transient-children.js +3 -3
  304. package/server/dist/util/distribute-int.js +33 -0
  305. package/client/dist/assets/ReviewPacketPage-D-UjTef3.js +0 -4
  306. package/client/dist/assets/index-BAtqmAZR.css +0 -2
  307. package/client/dist/assets/packet-CjS_478C.js +0 -1
  308. package/client/dist/assets/packet-D69dKsHN.js +0 -1
  309. package/client/dist/assets/packet-D9rU6Oxo.js +0 -1
  310. package/client/dist/assets/packet-DMDFoamg.js +0 -1
  311. package/client/dist/assets/packet-Dd2RMs1h.js +0 -1
  312. package/client/dist/assets/packet-DjC9OibC.js +0 -1
  313. package/client/dist/assets/packet-DwPtJoDz.js +0 -1
  314. package/client/dist/assets/packet-ceahDg8_.js +0 -1
  315. package/server/dist/rail-pr-decision.js +0 -2377
  316. package/server/dist/ticket-broadcast.js +0 -47
  317. /package/server/dist/{ai-invocations.js → modules/accounting/runtime/ai-invocations.js} +0 -0
  318. /package/server/dist/{codex-otel-bridge.js → modules/accounting/runtime/codex-otel-bridge.js} +0 -0
  319. /package/server/dist/{metrics.js → modules/accounting/runtime/metrics.js} +0 -0
  320. /package/server/dist/{telemetry-export.js → modules/accounting/runtime/telemetry-export.js} +0 -0
  321. /package/server/dist/{agent-runtime-accounting.js → modules/agent-runtime/runtime/agent-runtime-accounting.js} +0 -0
  322. /package/server/dist/{agent-runtime-events.js → modules/agent-runtime/runtime/agent-runtime-events.js} +0 -0
  323. /package/server/dist/{agent-runtime-history.js → modules/agent-runtime/runtime/agent-runtime-history.js} +0 -0
  324. /package/server/dist/{agent-runtime-metrics.js → modules/agent-runtime/runtime/agent-runtime-metrics.js} +0 -0
  325. /package/server/dist/{agent-runtime-package.js → modules/agent-runtime/runtime/agent-runtime-package.js} +0 -0
  326. /package/server/dist/{agent-runtime-repositories.js → modules/agent-runtime/runtime/agent-runtime-repositories.js} +0 -0
  327. /package/server/dist/{agent-runtime-verification-suggestions.js → modules/agent-runtime/runtime/agent-runtime-verification-suggestions.js} +0 -0
  328. /package/server/dist/{agent-refine-db.js → modules/agents/runtime/agent-refine-db.js} +0 -0
  329. /package/server/dist/{browser-capture-types.js → modules/browser/runtime/browser-capture-types.js} +0 -0
  330. /package/server/dist/{browser-context-pool.js → modules/browser/runtime/browser-context-pool.js} +0 -0
  331. /package/server/dist/{browser-network.js → modules/browser/runtime/browser-network.js} +0 -0
  332. /package/server/dist/{browser-viewport.js → modules/browser/runtime/browser-viewport.js} +0 -0
  333. /package/server/dist/{blueprint-render.js → modules/builder/runtime/blueprint-render.js} +0 -0
  334. /package/server/dist/{blueprint-spec-fixtures.js → modules/builder/runtime/blueprint-spec-fixtures.js} +0 -0
  335. /package/server/dist/{blueprint-store.js → modules/builder/runtime/blueprint-store.js} +0 -0
  336. /package/server/dist/{blueprint-types.js → modules/builder/runtime/blueprint-types.js} +0 -0
  337. /package/server/dist/{builder-cwd-manager.js → modules/builder/runtime/builder-cwd-manager.js} +0 -0
  338. /package/server/dist/{milestone-chain-store.js → modules/builder/runtime/milestone-chain-store.js} +0 -0
  339. /package/server/dist/{changes-reader.js → modules/code/runtime/changes-reader.js} +0 -0
  340. /package/server/dist/{context-scope.js → modules/conversations/runtime/context-scope.js} +0 -0
  341. /package/server/dist/{explore-contract-refine.js → modules/conversations/runtime/explore-contract-refine.js} +0 -0
  342. /package/server/dist/{explore-draft-title.js → modules/conversations/runtime/explore-draft-title.js} +0 -0
  343. /package/server/dist/{merge-manager.js → modules/delivery/runtime/merge-manager.js} +0 -0
  344. /package/server/dist/{multi-repo-execution-store.js → modules/delivery/runtime/multi-repo-execution-store.js} +0 -0
  345. /package/server/dist/{pr-lifecycle.js → modules/delivery/runtime/pr-lifecycle.js} +0 -0
  346. /package/server/dist/{rail-isolation.js → modules/delivery/runtime/rail-isolation.js} +0 -0
  347. /package/server/dist/{rail-pr-recovery-git.js → modules/delivery/runtime/rail-pr-recovery-git.js} +0 -0
  348. /package/server/dist/{rail-worktrees-store.js → modules/delivery/runtime/rail-worktrees-store.js} +0 -0
  349. /package/server/dist/{rails-store.js → modules/delivery/runtime/rails-store.js} +0 -0
  350. /package/server/dist/{job-listing.js → modules/execution/runtime/job-listing.js} +0 -0
  351. /package/server/dist/{job-spawn-idempotency.js → modules/execution/runtime/job-spawn-idempotency.js} +0 -0
  352. /package/server/dist/{run-duration-stats.js → modules/execution/runtime/run-duration-stats.js} +0 -0
  353. /package/server/dist/{verification-sentinel.js → modules/execution/runtime/verification-sentinel.js} +0 -0
  354. /package/server/dist/{loop-constants.js → modules/loops/runtime/loop-constants.js} +0 -0
  355. /package/server/dist/{loop-decider.js → modules/loops/runtime/loop-decider.js} +0 -0
  356. /package/server/dist/{loop-effect.js → modules/loops/runtime/loop-effect.js} +0 -0
  357. /package/server/dist/{loop-preview.js → modules/loops/runtime/loop-preview.js} +0 -0
  358. /package/server/dist/{loop-templates-ported.js → modules/loops/runtime/loop-templates-ported.js} +0 -0
  359. /package/server/dist/{loops-store.js → modules/loops/runtime/loops-store.js} +0 -0
  360. /package/server/dist/{agent-chat-registry.js → modules/missions/runtime/agent-chat-registry.js} +0 -0
  361. /package/server/dist/{agent-cwd-manager.js → modules/missions/runtime/agent-cwd-manager.js} +0 -0
  362. /package/server/dist/{agent-tier.js → modules/missions/runtime/agent-tier.js} +0 -0
  363. /package/server/dist/{spec-contract-prompt.js → modules/specs/runtime/spec-contract-prompt.js} +0 -0
  364. /package/server/dist/{spec-draft-parser.js → modules/specs/runtime/spec-draft-parser.js} +0 -0
  365. /package/server/dist/{ticket-store.js → modules/specs/runtime/ticket-store.js} +0 -0
  366. /package/server/dist/{terminal-marks-store.js → modules/terminals/runtime/terminal-marks-store.js} +0 -0
  367. /package/server/dist/{terminal-osc-parser.js → modules/terminals/runtime/terminal-osc-parser.js} +0 -0
  368. /package/server/dist/{terminal-settings.js → modules/terminals/runtime/terminal-settings.js} +0 -0
package/server/dist/db.js CHANGED
@@ -1,2226 +1,68 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.WORKTREE_ENV_NAME_RE = exports.DEFAULT_FREESTYLE_PRE_PROMPT = exports.JobRecoveryPendingError = void 0;
7
- exports.initDb = initDb;
8
- exports.createJob = createJob;
9
- exports.upsertQueuedJob = upsertQueuedJob;
10
- exports.deleteQueuedJob = deleteQueuedJob;
11
- exports.markJobInteractive = markJobInteractive;
12
- exports.accumulateInteractiveTurn = accumulateInteractiveTurn;
13
- exports.finalizeInteractiveJob = finalizeInteractiveJob;
14
- exports.finishJob = finishJob;
15
- exports.appendEvent = appendEvent;
16
- exports.upsertPhase = upsertPhase;
17
- exports.listJobs = listJobs;
18
- exports.getJob = getJob;
19
- exports.getJobEvents = getJobEvents;
20
- exports.deleteJob = deleteJob;
21
- exports.purgeJobs = purgeJobs;
22
- exports.getProjectActivity = getProjectActivity;
23
- exports.createConversation = createConversation;
24
- exports.listConversations = listConversations;
25
- exports.getConversation = getConversation;
26
- exports.deleteConversation = deleteConversation;
27
- exports.updateConversation = updateConversation;
28
- exports.addMessage = addMessage;
29
- exports.getMessages = getMessages;
30
- exports.createProposal = createProposal;
31
- exports.getProposal = getProposal;
32
- exports.listProposals = listProposals;
33
- exports.updateProposal = updateProposal;
34
- exports.deleteProposal = deleteProposal;
35
- exports.createTemplate = createTemplate;
36
- exports.listTemplates = listTemplates;
37
- exports.getTemplate = getTemplate;
38
- exports.updateTemplate = updateTemplate;
39
- exports.deleteTemplate = deleteTemplate;
40
- exports.skipJob = skipJob;
41
- exports.getPipelineJobs = getPipelineJobs;
42
- exports.getStats = getStats;
43
- exports.normalizeWorktreeEnvPassthrough = normalizeWorktreeEnvPassthrough;
44
- exports.getProjectSettings = getProjectSettings;
45
- exports.getFreestylePrePrompt = getFreestylePrePrompt;
46
- exports.updateProjectSettings = updateProjectSettings;
47
- exports.getQuickContractRefineLast = getQuickContractRefineLast;
48
- exports.hasQuickContractRefineLast = hasQuickContractRefineLast;
49
- exports.setQuickContractRefineLast = setQuickContractRefineLast;
50
- exports.getTelemetryBlob = getTelemetryBlob;
51
- exports.upsertTelemetryBlob = upsertTelemetryBlob;
52
- exports.listActiveTelemetryBlobs = listActiveTelemetryBlobs;
53
- exports.setTelemetryBlobCompacted = setTelemetryBlobCompacted;
54
- exports.insertTelemetrySummary = insertTelemetrySummary;
55
- exports.getTelemetrySummaries = getTelemetrySummaries;
56
- exports.deleteTelemetryForJob = deleteTelemetryForJob;
57
- exports.getJobsWithTelemetry = getJobsWithTelemetry;
58
- exports.hasJobTelemetry = hasJobTelemetry;
59
- const fs_1 = __importDefault(require("fs"));
60
- const path_1 = __importDefault(require("path"));
61
- const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
62
- const secure_fs_1 = require("./util/secure-fs");
63
- const sqlite_migrations_1 = require("./util/sqlite-migrations");
64
- const multi_repo_execution_store_1 = require("./multi-repo-execution-store");
65
- const project_repository_provenance_1 = require("./project-repository-provenance");
66
- const file_story_1 = require("./file-story");
67
- /** Backfill only terminal Safe-PR rows whose ticket ownership is unambiguous.
68
- * Eligibility is intentionally deferred to the outbox drainer, which freezes
69
- * only candidates still parked at on_review in the external ticket JSON. */
70
- function backfillTerminalRailPrTicketEffects(db) {
71
- const terminalRows = db.prepare(`
72
- SELECT id, ticket_ids, run_ids, decision, is_continuation,
73
- supersedes_delivery_id, pr_url
74
- FROM rail_pr_deliveries
75
- WHERE decision = 'merged'
76
- OR (decision = 'discarded' AND is_continuation = 0
77
- AND supersedes_delivery_id IS NULL AND pr_url IS NULL)
78
- `).all();
79
- const insertEffect = db.prepare(`
80
- INSERT OR IGNORE INTO rail_pr_ticket_effects (
81
- delivery_id, ticket_ids, causal_owners, target_status, jira_action, pr_url
82
- ) VALUES (?, ?, ?, ?, ?, ?)
83
- `);
84
- const currentOwner = db.prepare(`
85
- SELECT owner_id FROM ticket_outcome_ownership WHERE ticket_id = ?
86
- `);
87
- for (const row of terminalRows) {
88
- let ticketIds = [];
89
- let runIds = [];
90
- try {
91
- const parsed = JSON.parse(row.ticket_ids);
92
- if (Array.isArray(parsed)) {
93
- ticketIds = [...new Set(parsed.filter((id) => Number.isSafeInteger(id) && id > 0))];
94
- }
95
- }
96
- catch { /* malformed historical evidence is not safe to act on */ }
97
- try {
98
- const parsed = JSON.parse(row.run_ids);
99
- if (Array.isArray(parsed)) {
100
- runIds = [...new Set(parsed.filter((id) => typeof id === 'string' && id.length > 0))];
101
- }
102
- }
103
- catch { /* absent causal evidence means the historical effect is unsafe */ }
104
- if (ticketIds.length === 0 || runIds.length === 0)
105
- continue;
106
- const allowedOwners = new Set(runIds);
107
- const causalOwners = {};
108
- ticketIds = ticketIds.filter((ticketId) => {
109
- const owner = currentOwner.get(ticketId);
110
- if (!owner || !allowedOwners.has(owner.owner_id))
111
- return false;
112
- causalOwners[String(ticketId)] = owner.owner_id;
113
- return true;
114
- });
115
- if (ticketIds.length === 0)
116
- continue;
117
- const merged = row.decision === 'merged';
118
- insertEffect.run(row.id, JSON.stringify(ticketIds), JSON.stringify(causalOwners), merged ? 'done' : 'todo',
119
- // A v49 fresh discard has lost whether it meant Refine or a real
120
- // discard. Recover the backlog status without inventing either cause.
121
- merged ? 'merged' : 'backlog', merged ? row.pr_url : null);
122
- }
123
- }
124
- const MIGRATIONS = [
125
- // Migration 1: initial schema
126
- (db) => {
127
- db.exec(`
128
- CREATE TABLE IF NOT EXISTS schema_migrations (
129
- version INTEGER PRIMARY KEY,
130
- applied_at TEXT NOT NULL DEFAULT (datetime('now'))
131
- );
132
-
133
- CREATE TABLE IF NOT EXISTS jobs (
134
- id TEXT PRIMARY KEY,
135
- command TEXT NOT NULL,
136
- started_at TEXT NOT NULL,
137
- finished_at TEXT,
138
- status TEXT NOT NULL DEFAULT 'running',
139
- exit_code INTEGER,
140
- tokens_in INTEGER,
141
- tokens_out INTEGER,
142
- tokens_cache_read INTEGER,
143
- tokens_cache_create INTEGER,
144
- total_cost_usd REAL,
145
- num_turns INTEGER,
146
- model TEXT,
147
- duration_ms INTEGER,
148
- duration_api_ms INTEGER,
149
- session_id TEXT
150
- );
151
-
152
- CREATE INDEX IF NOT EXISTS idx_jobs_started_at ON jobs(started_at);
153
- CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
154
-
155
- CREATE TABLE IF NOT EXISTS events (
156
- id INTEGER PRIMARY KEY AUTOINCREMENT,
157
- job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
158
- seq INTEGER NOT NULL,
159
- event_type TEXT NOT NULL,
160
- source TEXT,
161
- payload TEXT NOT NULL,
162
- timestamp TEXT NOT NULL DEFAULT (datetime('now'))
163
- );
164
-
165
- CREATE INDEX IF NOT EXISTS idx_events_job_id ON events(job_id);
166
-
167
- CREATE TABLE IF NOT EXISTS job_phases (
168
- job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
169
- phase TEXT NOT NULL,
170
- state TEXT NOT NULL,
171
- updated_at TEXT NOT NULL,
172
- PRIMARY KEY (job_id, phase)
173
- );
174
- `);
175
- },
176
- // Migration 2: add queue_position column to jobs
177
- (db) => {
178
- db.exec(`
179
- ALTER TABLE jobs ADD COLUMN queue_position INTEGER;
180
- `);
181
- },
182
- // Migration 3: add queue_state table for persisting queue config (e.g., paused)
183
- (db) => {
184
- db.exec(`
185
- CREATE TABLE IF NOT EXISTS queue_state (
186
- key TEXT PRIMARY KEY,
187
- value TEXT NOT NULL
188
- );
189
-
190
- INSERT OR IGNORE INTO queue_state (key, value) VALUES ('paused', 'false');
191
- `);
192
- },
193
- // Migration 4: chat conversations and messages
194
- (db) => {
195
- db.exec(`
196
- CREATE TABLE IF NOT EXISTS chat_conversations (
197
- id TEXT PRIMARY KEY,
198
- title TEXT,
199
- model TEXT NOT NULL DEFAULT 'sonnet',
200
- session_id TEXT,
201
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
202
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
203
- );
204
-
205
- CREATE TABLE IF NOT EXISTS chat_messages (
206
- id INTEGER PRIMARY KEY AUTOINCREMENT,
207
- conversation_id TEXT NOT NULL REFERENCES chat_conversations(id) ON DELETE CASCADE,
208
- role TEXT NOT NULL CHECK(role IN ('user', 'assistant')),
209
- content TEXT NOT NULL,
210
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
211
- );
212
-
213
- CREATE INDEX IF NOT EXISTS idx_chat_messages_conv ON chat_messages(conversation_id);
214
- `);
215
- },
216
- // Migration 5: proposals table
217
- (db) => {
218
- db.exec(`
219
- CREATE TABLE IF NOT EXISTS proposals (
220
- id TEXT PRIMARY KEY,
221
- idea TEXT NOT NULL,
222
- session_id TEXT,
223
- status TEXT NOT NULL DEFAULT 'input',
224
- result_markdown TEXT,
225
- issue_url TEXT,
226
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
227
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
228
- );
229
- CREATE INDEX IF NOT EXISTS idx_proposals_status ON proposals(status);
230
- CREATE INDEX IF NOT EXISTS idx_proposals_created_at ON proposals(created_at);
231
- `);
232
- },
233
- // Migration 6: job templates
234
- (db) => {
235
- db.exec(`
236
- CREATE TABLE IF NOT EXISTS job_templates (
237
- id TEXT NOT NULL PRIMARY KEY,
238
- name TEXT NOT NULL UNIQUE,
239
- description TEXT,
240
- commands TEXT NOT NULL,
241
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
242
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
243
- );
244
- CREATE INDEX IF NOT EXISTS idx_job_templates_created_at ON job_templates(created_at);
245
- `);
246
- },
247
- // Migration 7: add priority column to jobs
248
- (db) => {
249
- db.exec(`
250
- ALTER TABLE jobs ADD COLUMN priority TEXT NOT NULL DEFAULT 'normal';
251
- `);
252
- },
253
- // Migration 8: job dependencies and pipelines
254
- (db) => {
255
- db.exec(`
256
- ALTER TABLE jobs ADD COLUMN depends_on_job_id TEXT REFERENCES jobs(id);
257
- ALTER TABLE jobs ADD COLUMN pipeline_id TEXT;
258
- ALTER TABLE jobs ADD COLUMN skip_reason TEXT;
259
- CREATE INDEX IF NOT EXISTS idx_jobs_depends_on ON jobs(depends_on_job_id);
260
- CREATE INDEX IF NOT EXISTS idx_jobs_pipeline_id ON jobs(pipeline_id);
261
- `);
262
- },
263
- // Migration 9: rails table for Rails board job integration
264
- (db) => {
265
- db.exec(`
266
- CREATE TABLE IF NOT EXISTS rails (
267
- rail_index INTEGER NOT NULL,
268
- ticket_id INTEGER NOT NULL,
269
- position INTEGER NOT NULL,
270
- mode TEXT NOT NULL DEFAULT 'implement',
271
- PRIMARY KEY (rail_index, ticket_id)
272
- );
273
- CREATE INDEX IF NOT EXISTS idx_rails_rail_index ON rails(rail_index);
274
- `);
275
- },
276
- // Migration 10: pipeline telemetry blob and summary tables.
277
- // The pipelineTelemetryEnabled flag reuses the existing queue_state key-value
278
- // store (key = 'config.pipeline_telemetry_enabled') so no schema change needed
279
- // for settings; only the raw-data tables are new.
280
- (db) => {
281
- db.exec(`
282
- CREATE TABLE IF NOT EXISTS telemetry_blobs (
283
- jobId TEXT PRIMARY KEY,
284
- path TEXT,
285
- byteSize INTEGER NOT NULL DEFAULT 0,
286
- startedAt INTEGER,
287
- endedAt INTEGER,
288
- state TEXT NOT NULL DEFAULT 'active'
289
- CHECK(state IN ('active','compacted','expired'))
290
- );
291
-
292
- CREATE TABLE IF NOT EXISTS telemetry_summaries (
293
- jobId TEXT NOT NULL,
294
- phase TEXT NOT NULL,
295
- durationMs INTEGER,
296
- tokensInput INTEGER,
297
- tokensOutput INTEGER,
298
- tokensCache INTEGER,
299
- toolCalls TEXT,
300
- apiErrors INTEGER,
301
- costUsd REAL,
302
- PRIMARY KEY (jobId, phase)
303
- );
304
- `);
305
- },
306
- // Migration 11: agent profiles — per-rail profile snapshots, custom agent
307
- // version history, and sandboxed "test agent" run records. These back the
308
- // Agents section (profiles + studio) added by add-agents-profiles.
309
- (db) => {
310
- db.exec(`
311
- CREATE TABLE IF NOT EXISTS job_profiles (
312
- job_id TEXT PRIMARY KEY,
313
- profile_name TEXT NOT NULL,
314
- profile_json TEXT NOT NULL,
315
- created_at INTEGER NOT NULL
316
- );
317
-
318
- CREATE INDEX IF NOT EXISTS idx_job_profiles_name ON job_profiles(profile_name);
319
-
320
- CREATE TABLE IF NOT EXISTS agent_versions (
321
- id INTEGER PRIMARY KEY AUTOINCREMENT,
322
- agent_name TEXT NOT NULL,
323
- version INTEGER NOT NULL,
324
- body TEXT NOT NULL,
325
- created_at INTEGER NOT NULL,
326
- UNIQUE (agent_name, version)
327
- );
328
-
329
- CREATE INDEX IF NOT EXISTS idx_agent_versions_name ON agent_versions(agent_name);
330
-
331
- CREATE TABLE IF NOT EXISTS agent_tests (
332
- id INTEGER PRIMARY KEY AUTOINCREMENT,
333
- agent_name TEXT NOT NULL,
334
- draft_hash TEXT NOT NULL,
335
- sample_task_id TEXT,
336
- tokens INTEGER,
337
- duration_ms INTEGER,
338
- output TEXT,
339
- created_at INTEGER NOT NULL
340
- );
341
-
342
- CREATE INDEX IF NOT EXISTS idx_agent_tests_name ON agent_tests(agent_name);
343
- `);
344
- },
345
- // Migration 12: remember per-rail agent profile selection across launches.
346
- (db) => {
347
- try {
348
- db.exec(`ALTER TABLE rails ADD COLUMN profile_name TEXT`);
349
- }
350
- catch {
351
- // Column may already exist (partially-migrated DB); no-op.
352
- }
353
- },
354
- // Migration 13: agent_refine_sessions — in-flight AI Edit sessions for
355
- // custom agents. Distinct from agent_versions (which is committed history);
356
- // rows here are drafts in progress that may or may not be applied.
357
- (db) => {
358
- db.exec(`
359
- CREATE TABLE IF NOT EXISTS agent_refine_sessions (
360
- id TEXT PRIMARY KEY,
361
- agent_id TEXT NOT NULL,
362
- session_id TEXT,
363
- base_version INTEGER NOT NULL,
364
- base_body_hash TEXT NOT NULL,
365
- draft_body TEXT,
366
- history_json TEXT NOT NULL DEFAULT '[]',
367
- phase TEXT NOT NULL DEFAULT 'idle',
368
- status TEXT NOT NULL DEFAULT 'idle',
369
- auto_test INTEGER NOT NULL DEFAULT 1,
370
- last_test_at INTEGER,
371
- last_test_hash TEXT,
372
- created_at INTEGER NOT NULL,
373
- updated_at INTEGER NOT NULL
374
- );
375
-
376
- CREATE INDEX IF NOT EXISTS idx_agent_refine_sessions_agent
377
- ON agent_refine_sessions(agent_id, status);
378
- CREATE INDEX IF NOT EXISTS idx_agent_refine_sessions_updated
379
- ON agent_refine_sessions(updated_at);
380
- `);
381
- },
382
- // Migration 14: terminal_settings_override — per-project key/value override
383
- // for app-wide terminal settings. Absence of a row means "inherit app default".
384
- (db) => {
385
- db.exec(`
386
- CREATE TABLE IF NOT EXISTS terminal_settings_override (
387
- key TEXT PRIMARY KEY,
388
- value TEXT NOT NULL
389
- );
390
- `);
391
- },
392
- // Migration 15: terminal_command_marks — per-session record of completed
393
- // commands derived from OSC 133 prompt marks. FIFO-capped at 1000 rows per
394
- // session by the marks store.
395
- (db) => {
396
- db.exec(`
397
- CREATE TABLE IF NOT EXISTS terminal_command_marks (
398
- id INTEGER PRIMARY KEY AUTOINCREMENT,
399
- session_id TEXT NOT NULL,
400
- started_at INTEGER NOT NULL,
401
- finished_at INTEGER,
402
- exit_code INTEGER,
403
- command TEXT,
404
- cwd TEXT
405
- );
406
-
407
- CREATE INDEX IF NOT EXISTS idx_terminal_marks_session_started
408
- ON terminal_command_marks(session_id, started_at);
409
- `);
410
- },
411
- // Migration 16: ai_invocations — unified per-project AI CLI invocation
412
- // tracking across surfaces (job, quick-spec, explore-spec, ai-edit).
413
- (db) => {
414
- db.exec(`
415
- CREATE TABLE IF NOT EXISTS ai_invocations (
416
- id TEXT PRIMARY KEY,
417
- project_id TEXT NOT NULL,
418
- surface TEXT NOT NULL,
419
- surface_ref_id TEXT,
420
- ticket_id INTEGER,
421
- conversation_id TEXT,
422
- model TEXT,
423
- status TEXT NOT NULL,
424
- started_at TEXT NOT NULL,
425
- finished_at TEXT,
426
- duration_ms INTEGER,
427
- duration_api_ms INTEGER,
428
- tokens_in INTEGER,
429
- tokens_out INTEGER,
430
- tokens_cache_read INTEGER,
431
- tokens_cache_create INTEGER,
432
- total_cost_usd REAL,
433
- num_turns INTEGER,
434
- session_id TEXT,
435
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
436
- );
437
-
438
- CREATE INDEX IF NOT EXISTS idx_ai_inv_project_started
439
- ON ai_invocations(project_id, started_at DESC);
440
- CREATE INDEX IF NOT EXISTS idx_ai_inv_project_surface
441
- ON ai_invocations(project_id, surface);
442
- CREATE INDEX IF NOT EXISTS idx_ai_inv_project_ticket
443
- ON ai_invocations(project_id, ticket_id) WHERE ticket_id IS NOT NULL;
444
- `);
445
- },
446
- // Migration 17: chat_conversations.kind — distinguishes Explore conversations
447
- // (kind='explore') from sidebar chat (kind='sidebar'). Capture for ai_invocations
448
- // is gated on kind='explore'.
449
- (db) => {
450
- db.exec(`
451
- ALTER TABLE chat_conversations ADD COLUMN kind TEXT NOT NULL DEFAULT 'sidebar';
452
- `);
453
- },
454
- // Migration 18: chat_conversations.context_scope — per-conversation JSON
455
- // freezing the Add Spec context scope at creation time. NULL means "legacy
456
- // behavior" so existing rows behave unchanged.
457
- //
458
- // Idempotent: a parallel WIP branch shipped this column under migration #20,
459
- // so on machines where it already exists we swallow the duplicate-column
460
- // error rather than crash on a re-run.
461
- (db) => {
462
- try {
463
- db.exec(`ALTER TABLE chat_conversations ADD COLUMN context_scope TEXT;`);
464
- }
465
- catch (err) {
466
- const msg = err.message ?? '';
467
- if (!/duplicate column name/i.test(msg))
468
- throw err;
469
- }
470
- },
471
- // Migration 19: ai_invocations.provider — provider id stamped at insert.
472
- // Existing rows backfill to 'claude' since pre-migration that was the only
473
- // path. New rows MUST be populated from the resolved adapter's id (see
474
- // openspec/changes/add-multi-provider-support/specs/project-spending/spec.md).
475
- //
476
- // Idempotent: same dual-WIP concern — multi-provider branch originally
477
- // numbered this #18, so on machines that ran the pre-merge multi-provider
478
- // build the column already exists.
479
- (db) => {
480
- try {
481
- db.exec(`ALTER TABLE ai_invocations ADD COLUMN provider TEXT;`);
482
- }
483
- catch (err) {
484
- const msg = err.message ?? '';
485
- if (!/duplicate column name/i.test(msg))
486
- throw err;
487
- }
488
- db.exec(`UPDATE ai_invocations SET provider = 'claude' WHERE provider IS NULL;`);
489
- db.exec(`
490
- CREATE INDEX IF NOT EXISTS idx_ai_inv_project_provider
491
- ON ai_invocations(project_id, provider);
492
- `);
493
- },
494
- // Migration 20: ai_invocations.total_cost_usd_estimated — 1 when the cost
495
- // came from server/pricing.ts (estimated fallback for non-native-cost
496
- // providers); 0 when authoritative from the provider's terminal event.
497
- //
498
- // Idempotent for the same reason as #18/#19.
499
- (db) => {
500
- try {
501
- db.exec(`
502
- ALTER TABLE ai_invocations
503
- ADD COLUMN total_cost_usd_estimated INTEGER NOT NULL DEFAULT 0;
504
- `);
505
- }
506
- catch (err) {
507
- const msg = err.message ?? '';
508
- if (!/duplicate column name/i.test(msg))
509
- throw err;
510
- }
511
- },
512
- // Migration 21: self-heal `ai_invocations.provider` and
513
- // `ai_invocations.total_cost_usd_estimated` for projects whose
514
- // `schema_migrations` table marked versions 19 / 20 as applied without the
515
- // corresponding ALTER actually running (parallel WIP branches reshuffled
516
- // migration indices during development, leaving some on-disk DBs with the
517
- // applied row but no column). Uses `PRAGMA table_info` so we only ALTER
518
- // when the column is genuinely missing — safe to re-run.
519
- (db) => {
520
- const cols = new Set(db.prepare(`PRAGMA table_info(ai_invocations)`).all()
521
- .map((r) => r.name));
522
- if (!cols.has('provider')) {
523
- db.exec(`ALTER TABLE ai_invocations ADD COLUMN provider TEXT;`);
524
- db.exec(`UPDATE ai_invocations SET provider = 'claude' WHERE provider IS NULL;`);
525
- db.exec(`
526
- CREATE INDEX IF NOT EXISTS idx_ai_inv_project_provider
527
- ON ai_invocations(project_id, provider);
528
- `);
529
- }
530
- if (!cols.has('total_cost_usd_estimated')) {
531
- db.exec(`
532
- ALTER TABLE ai_invocations
533
- ADD COLUMN total_cost_usd_estimated INTEGER NOT NULL DEFAULT 0;
534
- `);
535
- }
536
- },
537
- // Migration 22: file_provenance — per-project file ⇄ ticket tracking,
538
- // populated by the QueueManager post-job hook and consumed by the Code
539
- // Explorer router + TicketDetailModal.
540
- (db) => {
541
- db.exec(`
542
- CREATE TABLE IF NOT EXISTS file_provenance (
543
- id INTEGER PRIMARY KEY AUTOINCREMENT,
544
- file_path TEXT NOT NULL,
545
- ticket_id INTEGER,
546
- job_id TEXT,
547
- kind TEXT NOT NULL CHECK(kind IN ('created','modified','deleted')),
548
- at INTEGER NOT NULL
549
- );
550
- CREATE INDEX IF NOT EXISTS idx_fp_path ON file_provenance(file_path);
551
- CREATE INDEX IF NOT EXISTS idx_fp_ticket ON file_provenance(ticket_id);
552
- CREATE INDEX IF NOT EXISTS idx_fp_at ON file_provenance(at DESC);
553
- `);
554
- },
555
- // Migration 23: optional per-job file patch storage for Code Explorer
556
- // provenance. Older provenance rows remain valid without a patch.
557
- (db) => {
558
- db.exec(`
559
- CREATE TABLE IF NOT EXISTS file_provenance_diffs (
560
- provenance_id INTEGER PRIMARY KEY REFERENCES file_provenance(id) ON DELETE CASCADE,
561
- patch TEXT NOT NULL,
562
- truncated INTEGER NOT NULL DEFAULT 0
563
- );
564
- `);
565
- },
566
- // Migration 24: chat_conversations.provider — per-conversation AI engine for
567
- // multi-provider projects. NULL means "fall back to the project's primary
568
- // provider" (single-provider projects never set it, so behaviour is
569
- // unchanged). Set at conversation creation from the Add Spec AI Engine
570
- // selector; resume turns reuse it so the right CLI binary is spawned.
571
- (db) => {
572
- db.exec(`ALTER TABLE chat_conversations ADD COLUMN provider TEXT;`);
573
- },
574
- // Migration 25: rails.ai_engine — per-rail AI engine override for
575
- // multi-provider projects. NULL means "use the project's primary provider".
576
- // Stored on every rail row alongside profile_name; getRail reads the first
577
- // row's value.
578
- (db) => {
579
- db.exec(`ALTER TABLE rails ADD COLUMN ai_engine TEXT;`);
580
- },
581
- // Migration 26: self-heal the multi-provider columns. An earlier WIP of the
582
- // multi-provider feature consumed migration versions 24 and 25 on some
583
- // databases with DIFFERENT meaning — those DBs already record v24/v25 so
584
- // Migrations 24/25 above are skipped, leaving `rails.ai_engine` (and possibly
585
- // `chat_conversations.provider`) missing. This higher-numbered migration is
586
- // guarded by column checks: a no-op on DBs where the columns already exist,
587
- // and an additive repair everywhere else. (Mirrors the #18/#19 self-heal
588
- // precedent in this file.)
589
- (db) => {
590
- const convCols = db.prepare("PRAGMA table_info(chat_conversations)").all().map((c) => c.name);
591
- if (!convCols.includes('provider')) {
592
- db.exec(`ALTER TABLE chat_conversations ADD COLUMN provider TEXT;`);
593
- }
594
- const railCols = db.prepare("PRAGMA table_info(rails)").all().map((c) => c.name);
595
- if (!railCols.includes('ai_engine')) {
596
- db.exec(`ALTER TABLE rails ADD COLUMN ai_engine TEXT;`);
597
- }
598
- },
599
- // Migration 27: jobs.total_cost_usd_estimated — 1 when jobs.total_cost_usd
600
- // came from server/pricing.ts (estimated fallback for non-native-cost
601
- // providers like codex); 0 when authoritative from the provider's terminal
602
- // event. Mirrors the ai_invocations column (migration 20) so the app
603
- // dashboard, budget enforcement, and webhook can distinguish a rate-card
604
- // estimate from a provider-billed figure. Additive + idempotent.
605
- (db) => {
606
- const cols = db.prepare(`PRAGMA table_info(jobs)`).all().map((r) => r.name);
607
- if (!cols.includes('total_cost_usd_estimated')) {
608
- db.exec(`
609
- ALTER TABLE jobs
610
- ADD COLUMN total_cost_usd_estimated INTEGER NOT NULL DEFAULT 0;
611
- `);
612
- }
613
- },
614
- // Migration 28: rail_meta — per-rail display name, keyed by rail_index.
615
- // The `rails` table stores name-less ticket rows (and has NO rows for an
616
- // empty rail), so a rail's user-given name can't live there — a renamed but
617
- // empty rail would lose its name. rail_meta is a separate, ticket-independent
618
- // store so every rail (0/1/2) keeps its name regardless of assignments.
619
- // NULL name = client falls back to the default "Rail N" label. This backs the
620
- // desktop ⇄ mobile rail-name sync (broadcast via rail.updated).
621
- (db) => {
622
- db.exec(`
623
- CREATE TABLE IF NOT EXISTS rail_meta (
624
- rail_index INTEGER PRIMARY KEY,
625
- name TEXT
626
- );
627
- `);
628
- },
629
- // Migration 29: Jira integration (per-project). Each project syncs with its
630
- // own Jira board, so every Jira table lives here in the per-project jobs.sqlite
631
- // and is keyed by nothing but its own rows. See docs/jira-integration-plan.md.
632
- // - jira_connection: one row, the connection config (token stored encrypted).
633
- // - jira_links: spec↔issue map keyed on the IMMUTABLE Jira numeric id.
634
- // - jira_outbox: durable transactional write-back queue (status + comments).
635
- (db) => {
636
- db.exec(`
637
- CREATE TABLE IF NOT EXISTS jira_connection (
638
- project_id TEXT PRIMARY KEY,
639
- base_url TEXT NOT NULL,
640
- deployment TEXT NOT NULL,
641
- api_version TEXT NOT NULL,
642
- auth_scheme TEXT NOT NULL,
643
- account_email TEXT,
644
- jira_project_key TEXT NOT NULL,
645
- jira_project_id TEXT NOT NULL,
646
- encrypted_token TEXT,
647
- enabled INTEGER NOT NULL DEFAULT 1,
648
- status_map TEXT,
649
- high_water_ms INTEGER,
650
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
651
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
652
- );
653
-
654
- CREATE TABLE IF NOT EXISTS jira_links (
655
- local_id INTEGER PRIMARY KEY,
656
- jira_issue_id TEXT NOT NULL UNIQUE,
657
- jira_key TEXT,
658
- jira_project_id TEXT NOT NULL,
659
- deployment TEXT NOT NULL,
660
- status_category TEXT,
661
- state TEXT NOT NULL DEFAULT 'linked',
662
- tombstoned INTEGER NOT NULL DEFAULT 0,
663
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
664
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
665
- );
666
- CREATE INDEX IF NOT EXISTS idx_jira_links_issue ON jira_links(jira_issue_id);
667
-
668
- CREATE TABLE IF NOT EXISTS jira_outbox (
669
- id INTEGER PRIMARY KEY AUTOINCREMENT,
670
- jira_issue_id TEXT NOT NULL,
671
- op_type TEXT NOT NULL,
672
- idempotency_key TEXT NOT NULL UNIQUE,
673
- payload TEXT NOT NULL,
674
- state TEXT NOT NULL DEFAULT 'pending',
675
- attempts INTEGER NOT NULL DEFAULT 0,
676
- next_attempt_at TEXT,
677
- last_error TEXT,
678
- dead_reason TEXT,
679
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
680
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
681
- );
682
- CREATE INDEX IF NOT EXISTS idx_jira_outbox_state ON jira_outbox(state);
683
- CREATE INDEX IF NOT EXISTS idx_jira_outbox_issue ON jira_outbox(jira_issue_id);
684
- `);
685
- },
686
- // Migration 30: Jira sprint custom-field id. The field that holds an issue's
687
- // sprint(s) is a custom field whose id varies per instance; we discover it
688
- // (schema com.pyxis.greenhopper.jira:gh-sprint) and cache it here. NULL =
689
- // not yet checked, 'none' = checked and no sprint field exists, '<id>' = found.
690
- (db) => {
691
- try {
692
- db.exec(`ALTER TABLE jira_connection ADD COLUMN sprint_field_id TEXT`);
693
- }
694
- catch {
695
- // Column may already exist on a partially-migrated DB — no-op.
696
- }
697
- },
698
- // Migration 31: Jira discard target status. The user-configured status name to
699
- // which a discarded spec's issue is transitioned (instead of being deleted) in
700
- // a Jira-synced project. NULL/empty = not configured (delete behaves normally).
701
- (db) => {
702
- try {
703
- db.exec(`ALTER TABLE jira_connection ADD COLUMN discard_status TEXT`);
704
- }
705
- catch {
706
- // Column may already exist on a partially-migrated DB — no-op.
707
- }
708
- },
709
- // Migration 32: jobs.interactive — 1 when the job is an interactive persistent
710
- // freestyle session (the user sends multiple prompts across turns; the job
711
- // stays 'running' until an explicit finalize, at which point every turn's real
712
- // tokens/cost/num_turns are already summed into the row and status flips to
713
- // 'completed'); 0 (default) for standard autonomous jobs. Additive + idempotent
714
- // (guarded by PRAGMA table_info, mirroring migrations 18–26).
715
- (db) => {
716
- const cols = db.prepare(`PRAGMA table_info(jobs)`).all().map((r) => r.name);
717
- if (!cols.includes('interactive')) {
718
- db.exec(`ALTER TABLE jobs ADD COLUMN interactive INTEGER NOT NULL DEFAULT 0`);
719
- }
720
- },
721
- // Migration 33: loop_runs — per-project record of an executed Loop (the Loops
722
- // feature). A loop DEFINITION is global (desktop.sqlite `loops`); a RUN happens
723
- // in a project, bound to a rail + spec. Powers loop analytics (iterations,
724
- // final outcome, success rate, totals) alongside the per-step ai_invocations
725
- // rows (surface='loop', linked via loop_run_id). "Running" loop state is
726
- // derived from rows here with status='running'.
727
- (db) => {
728
- db.exec(`
729
- CREATE TABLE IF NOT EXISTS loop_runs (
730
- id TEXT PRIMARY KEY,
731
- project_id TEXT NOT NULL,
732
- loop_id TEXT NOT NULL,
733
- loop_name TEXT,
734
- rail_index INTEGER,
735
- ticket_id INTEGER,
736
- provider TEXT,
737
- model TEXT,
738
- reasoning_effort TEXT,
739
- status TEXT NOT NULL DEFAULT 'running',
740
- final_outcome TEXT,
741
- iteration_limit INTEGER NOT NULL DEFAULT 0,
742
- iteration_count INTEGER NOT NULL DEFAULT 0,
743
- total_cost_usd REAL NOT NULL DEFAULT 0,
744
- total_tokens INTEGER NOT NULL DEFAULT 0,
745
- total_duration_ms INTEGER NOT NULL DEFAULT 0,
746
- started_at TEXT NOT NULL,
747
- finished_at TEXT,
748
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
749
- );
750
- CREATE INDEX IF NOT EXISTS idx_loop_runs_project_started ON loop_runs(project_id, started_at DESC);
751
- CREATE INDEX IF NOT EXISTS idx_loop_runs_outcome ON loop_runs(project_id, final_outcome);
752
- CREATE INDEX IF NOT EXISTS idx_loop_runs_loop ON loop_runs(loop_id);
753
- `);
754
- },
755
- // Migration 34: ai_invocations.loop_run_id — links a loop's per-iteration AI
756
- // invocations (AI Step + Loop Decider, surface='loop') to their loop_runs row
757
- // for run-level rollups. NULL for every non-loop invocation. Additive +
758
- // idempotent (guarded by PRAGMA table_info, mirroring migrations 18–32).
759
- (db) => {
760
- const cols = db.prepare(`PRAGMA table_info(ai_invocations)`).all().map((r) => r.name);
761
- if (!cols.includes('loop_run_id')) {
762
- db.exec(`ALTER TABLE ai_invocations ADD COLUMN loop_run_id TEXT`);
763
- db.exec(`CREATE INDEX IF NOT EXISTS idx_ai_invocations_loop_run ON ai_invocations(loop_run_id) WHERE loop_run_id IS NOT NULL`);
764
- }
765
- },
766
- // Migration 35: rail_worktrees — per-(rail launch, ticket) ledger for parallel
767
- // worktree isolation + merge-back. Lets a restarted server reconcile orphaned
768
- // worktrees and lets the UI render integration state. Additive + idempotent.
769
- (db) => {
770
- db.exec(`
771
- CREATE TABLE IF NOT EXISTS rail_worktrees (
772
- id TEXT PRIMARY KEY,
773
- rail_index INTEGER NOT NULL,
774
- ticket_id INTEGER NOT NULL,
775
- run_id TEXT,
776
- branch TEXT NOT NULL,
777
- worktree_path TEXT NOT NULL,
778
- overlay_path TEXT,
779
- merge_state TEXT NOT NULL DEFAULT 'building',
780
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
781
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
782
- );
783
- CREATE INDEX IF NOT EXISTS idx_rail_worktrees_state ON rail_worktrees(merge_state);
784
- CREATE INDEX IF NOT EXISTS idx_rail_worktrees_rail ON rail_worktrees(rail_index);
785
- `);
786
- },
787
- // Migration 36: rail_pr_deliveries — one row per isolated rail LAUNCH tracking
788
- // the ask-first PR decision lifecycle (building → on_review → pr_draft →
789
- // pr_ready → merged | discarded | implementation_failed | pr_failed). The durable single source of
790
- // truth both decision surfaces (rail row + agent chat) read and write, so a
791
- // refresh/restart never loses a pending PR decision. `branches` captures the
792
- // per-unit DeliverBranch records at build-settle (deferred deliverRailAsPr
793
- // cannot be reconstructed without them); `worktree_ids` links the
794
- // rail_worktrees ledger rows for discard cleanup. Additive + idempotent.
795
- (db) => {
796
- db.exec(`
797
- CREATE TABLE IF NOT EXISTS rail_pr_deliveries (
798
- id TEXT PRIMARY KEY,
799
- rail_index INTEGER NOT NULL,
800
- loop_id TEXT,
801
- rail_key TEXT NOT NULL,
802
- ticket_ids TEXT NOT NULL,
803
- base_branch TEXT NOT NULL,
804
- branch TEXT,
805
- pr_url TEXT,
806
- pr_number INTEGER,
807
- pr_state TEXT NOT NULL DEFAULT 'none',
808
- decision TEXT NOT NULL DEFAULT 'building',
809
- branches TEXT NOT NULL DEFAULT '[]',
810
- loop_name TEXT NOT NULL DEFAULT '',
811
- worktree_ids TEXT NOT NULL DEFAULT '[]',
812
- origin_surface TEXT NOT NULL DEFAULT 'dashboard',
813
- origin_conversation_id TEXT,
814
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
815
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
816
- );
817
- CREATE INDEX IF NOT EXISTS idx_rail_pr_deliveries_rail ON rail_pr_deliveries(rail_index);
818
- CREATE INDEX IF NOT EXISTS idx_rail_pr_deliveries_active ON rail_pr_deliveries(decision);
819
- `);
820
- },
821
- // Migration 37: file_story_contributions — per-intervention "construction
822
- // story" data for the Code/Files explorer. One row per file_provenance row
823
- // that had a collectable patch: line stats (added/removed), a ~4KB patch
824
- // excerpt (the full patch stays in file_provenance_diffs), and a nullable
825
- // plain-language AI `summary` of what that spec contributed to the file
826
- // (generated on demand, budget-gated — see file-story-manager.ts). Written
827
- // inside recordProvenanceForJob's transaction so EVERY provenance producer
828
- // (queue-manager jobs AND the loop-run seam) gets stats for free. Additive.
829
- (db) => {
830
- db.exec(`
831
- CREATE TABLE IF NOT EXISTS file_story_contributions (
832
- provenance_id INTEGER PRIMARY KEY,
833
- job_id TEXT,
834
- file_path TEXT NOT NULL,
835
- added_lines INTEGER NOT NULL DEFAULT 0,
836
- removed_lines INTEGER NOT NULL DEFAULT 0,
837
- patch_excerpt TEXT,
838
- summary TEXT,
839
- summary_model TEXT,
840
- summary_generated_at TEXT
841
- );
842
- CREATE INDEX IF NOT EXISTS idx_file_story_contributions_path ON file_story_contributions(file_path);
843
- `);
844
- },
845
- // Migration 38: rail_pr_deliveries.run_ids — JSON string[] of the loop-run
846
- // ids an isolated rail launch fanned out (order matches ticket order; a
847
- // scope='all' launch has exactly one). Persisted right after allocation so
848
- // the PR decision surfaces (agent-chat card + dashboard strip) can render a
849
- // per-run "View log" chip with live vitals that survives refresh/restart.
850
- // Additive + idempotent (guarded by PRAGMA table_info, mirroring 18–36).
851
- (db) => {
852
- const cols = db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all().map((r) => r.name);
853
- if (!cols.includes('run_ids')) {
854
- db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN run_ids TEXT NOT NULL DEFAULT '[]'`);
855
- }
856
- },
857
- // Migration 39: seed the three BASE rails (indices 0-2) into rail_meta.
858
- // Dynamic rails make rail_meta the EXISTENCE authority (a rail exists iff it
859
- // has a rail_meta identity row or leftover ticket rows) — seeding once here,
860
- // instead of re-seeding on every read, lets a user genuinely delete a base
861
- // rail (the router still guards: never the last one, never a non-empty or
862
- // active one). INSERT OR IGNORE keeps any existing renamed-rail rows intact.
863
- (db) => {
864
- db.exec(`
865
- INSERT OR IGNORE INTO rail_meta (rail_index, name) VALUES (0, NULL), (1, NULL), (2, NULL);
866
- `);
867
- },
868
- // Migration 40: durable idempotency ledger for billable job spawns. Clients
869
- // can safely retry a timed-out POST (or double-submit) without enqueueing a
870
- // second job. Entries are deliberately short-lived and pruned by the route;
871
- // the ledger is not job history and therefore has no foreign key to jobs.
872
- (db) => {
873
- db.exec(`
874
- CREATE TABLE IF NOT EXISTS job_spawn_requests (
875
- idempotency_key TEXT PRIMARY KEY,
876
- fingerprint TEXT NOT NULL,
877
- job_id TEXT NOT NULL,
878
- created_at_ms INTEGER NOT NULL,
879
- expires_at_ms INTEGER NOT NULL
880
- );
881
- CREATE INDEX IF NOT EXISTS idx_job_spawn_requests_expires
882
- ON job_spawn_requests(expires_at_ms);
883
- `);
884
- },
885
- // Migration 41: durable outbox for jobs left RUNNING by an ungraceful
886
- // process exit. QueueManager atomically snapshots each orphan here before it
887
- // flips the job to failed, then checkpoints accounting and the terminal
888
- // domain callback independently. A second crash can therefore resume either
889
- // step without duplicating the ai_invocations ledger or losing the callback.
890
- (db) => {
891
- db.exec(`
892
- CREATE TABLE IF NOT EXISTS orphan_job_recovery (
893
- job_id TEXT PRIMARY KEY,
894
- payload TEXT NOT NULL,
895
- accounting_completed INTEGER NOT NULL DEFAULT 0 CHECK (accounting_completed IN (0, 1)),
896
- callback_completed INTEGER NOT NULL DEFAULT 0 CHECK (callback_completed IN (0, 1)),
897
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
898
- );
899
- CREATE INDEX IF NOT EXISTS idx_orphan_job_recovery_pending
900
- ON orphan_job_recovery(accounting_completed, callback_completed);
901
- `);
902
- },
903
- // Migration 42: persist the provider actually selected for each job and add
904
- // a third durable recovery checkpoint for queue/pipeline terminal invariants.
905
- // Both ALTERs are guarded so developer databases that already applied the
906
- // unreleased orphan-outbox migration can advance safely.
907
- (db) => {
908
- const jobCols = db.prepare(`PRAGMA table_info(jobs)`).all().map((r) => r.name);
909
- if (!jobCols.includes('provider')) {
910
- db.exec(`ALTER TABLE jobs ADD COLUMN provider TEXT`);
911
- }
912
- const recoveryCols = db.prepare(`PRAGMA table_info(orphan_job_recovery)`).all().map((r) => r.name);
913
- if (!recoveryCols.includes('terminal_completed')) {
914
- db.exec(`
915
- ALTER TABLE orphan_job_recovery
916
- ADD COLUMN terminal_completed INTEGER NOT NULL DEFAULT 0
917
- CHECK (terminal_completed IN (0, 1))
918
- `);
919
- }
920
- },
921
- // Migration 43: durable pre-start queue. This is intentionally separate from
922
- // jobs: a queue admission is not an execution and therefore has no truthful
923
- // jobs.started_at value. createJob atomically promotes a row out of this table
924
- // when the provider process actually starts.
925
- (db) => {
926
- db.exec(`
927
- CREATE TABLE IF NOT EXISTS queued_jobs (
928
- id TEXT PRIMARY KEY,
929
- command TEXT NOT NULL,
930
- queue_position INTEGER,
931
- priority TEXT NOT NULL DEFAULT 'normal',
932
- depends_on_job_id TEXT,
933
- pipeline_id TEXT,
934
- provider TEXT,
935
- model TEXT,
936
- profile_name TEXT,
937
- profile_selection_set INTEGER NOT NULL DEFAULT 0
938
- CHECK (profile_selection_set IN (0, 1)),
939
- interactive INTEGER CHECK (interactive IN (0, 1)),
940
- causal_ownership INTEGER NOT NULL DEFAULT 0
941
- CHECK (causal_ownership IN (0, 1)),
942
- enqueued_at TEXT NOT NULL DEFAULT (datetime('now'))
943
- );
944
- CREATE INDEX IF NOT EXISTS idx_queued_jobs_position
945
- ON queued_jobs(queue_position);
946
- `);
947
- },
948
- // Migration 44: persist which manager owns terminal recovery for each jobs
949
- // row. Before this boundary QueueManager swept every RUNNING row, including
950
- // LoopRunManager's backing rows, and recorded their already-accounted step
951
- // spend again as surface='job'. Existing loop rows are classified by their
952
- // durable loop_runs identity; every other historical job remains queue-owned.
953
- (db) => {
954
- const jobCols = db.prepare(`PRAGMA table_info(jobs)`).all().map((r) => r.name);
955
- if (!jobCols.includes('owner')) {
956
- db.exec(`
957
- ALTER TABLE jobs
958
- ADD COLUMN owner TEXT NOT NULL DEFAULT 'queue'
959
- CHECK (owner IN ('queue', 'loop'))
960
- `);
961
- }
962
- db.exec(`
963
- UPDATE jobs
964
- SET owner = 'loop',
965
- provider = COALESCE(
966
- jobs.provider,
967
- (SELECT loop_runs.provider FROM loop_runs WHERE loop_runs.id = jobs.id)
968
- )
969
- WHERE EXISTS (SELECT 1 FROM loop_runs WHERE loop_runs.id = jobs.id);
970
- CREATE INDEX IF NOT EXISTS idx_jobs_owner_status ON jobs(owner, status);
971
- `);
972
- },
973
- // Migration 45: crash-consistent loop terminal/accounting outboxes and
974
- // causal ownership for rail/ticket effects. `loop_runs.ticket_ids_json`
975
- // replaces the old startup heuristic ("one active run means scope=all") with
976
- // the exact launch set. The recovery tables intentionally have no FK to
977
- // jobs: terminal history is independently deletable while recovery effects
978
- // still have to finish.
979
- (db) => {
980
- const loopCols = db.prepare(`PRAGMA table_info(loop_runs)`).all().map((r) => r.name);
981
- if (!loopCols.includes('ticket_ids_json')) {
982
- db.exec(`ALTER TABLE loop_runs ADD COLUMN ticket_ids_json TEXT NOT NULL DEFAULT '[]'`);
983
- }
984
- if (!loopCols.includes('ticket_completion_status')) {
985
- db.exec(`
986
- ALTER TABLE loop_runs
987
- ADD COLUMN ticket_completion_status TEXT NOT NULL DEFAULT 'done'
988
- CHECK (ticket_completion_status IN ('done', 'on_review'))
989
- `);
990
- }
991
- if (!loopCols.includes('causal_ownership')) {
992
- db.exec(`
993
- ALTER TABLE loop_runs
994
- ADD COLUMN causal_ownership INTEGER NOT NULL DEFAULT 0
995
- CHECK (causal_ownership IN (0, 1))
996
- `);
997
- }
998
- db.exec(`
999
- CREATE TABLE IF NOT EXISTS loop_terminal_recovery (
1000
- run_id TEXT PRIMARY KEY,
1001
- payload TEXT NOT NULL,
1002
- callback_completed INTEGER NOT NULL DEFAULT 0
1003
- CHECK (callback_completed IN (0, 1)),
1004
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
1005
- );
1006
-
1007
- CREATE TABLE IF NOT EXISTS loop_step_recovery (
1008
- run_id TEXT NOT NULL,
1009
- step_key TEXT NOT NULL,
1010
- invocation_id TEXT NOT NULL UNIQUE,
1011
- payload TEXT NOT NULL,
1012
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1013
- PRIMARY KEY (run_id, step_key)
1014
- );
1015
- CREATE INDEX IF NOT EXISTS idx_loop_step_recovery_run
1016
- ON loop_step_recovery(run_id);
1017
-
1018
- CREATE TABLE IF NOT EXISTS rail_ticket_ownership (
1019
- rail_index INTEGER NOT NULL,
1020
- ticket_id INTEGER NOT NULL,
1021
- owner_id TEXT NOT NULL,
1022
- generation INTEGER NOT NULL DEFAULT 1,
1023
- claimed_at TEXT NOT NULL DEFAULT (datetime('now')),
1024
- PRIMARY KEY (rail_index, ticket_id)
1025
- );
1026
- CREATE INDEX IF NOT EXISTS idx_rail_ticket_owner
1027
- ON rail_ticket_ownership(owner_id);
1028
-
1029
- CREATE TABLE IF NOT EXISTS ticket_outcome_ownership (
1030
- ticket_id INTEGER PRIMARY KEY,
1031
- owner_id TEXT NOT NULL,
1032
- generation INTEGER NOT NULL DEFAULT 1,
1033
- claimed_at TEXT NOT NULL DEFAULT (datetime('now'))
1034
- );
1035
- CREATE INDEX IF NOT EXISTS idx_ticket_outcome_owner
1036
- ON ticket_outcome_ownership(owner_id);
1037
-
1038
- -- Builds from the pre-owner WIP may already have captured loop backing
1039
- -- rows in QueueManager's outbox, or even checkpointed the resulting
1040
- -- surface='job' invocation. Those rows are unambiguously wrong: a jobs
1041
- -- id that is also a loop_runs id is exclusively loop-owned.
1042
- DELETE FROM orphan_job_recovery
1043
- WHERE job_id IN (SELECT id FROM loop_runs);
1044
- DELETE FROM ai_invocations
1045
- WHERE surface = 'job'
1046
- AND EXISTS (
1047
- SELECT 1 FROM loop_runs
1048
- WHERE ai_invocations.surface_ref_id = loop_runs.id
1049
- OR ai_invocations.surface_ref_id LIKE loop_runs.id || '#t%'
1050
- );
1051
- `);
1052
- },
1053
- // Migration 46: preserve every pre-spawn semantic override across restart.
1054
- // Nullable provider/model/interactive encode "use the spawn-time default";
1055
- // profile needs a separate presence bit because NULL itself means the
1056
- // deliberate force-legacy selection when the bit is set.
1057
- (db) => {
1058
- const cols = new Set(db.prepare(`PRAGMA table_info(queued_jobs)`).all()
1059
- .map((row) => row.name));
1060
- if (!cols.has('provider')) {
1061
- db.exec(`ALTER TABLE queued_jobs ADD COLUMN provider TEXT`);
1062
- }
1063
- if (!cols.has('model')) {
1064
- db.exec(`ALTER TABLE queued_jobs ADD COLUMN model TEXT`);
1065
- }
1066
- if (!cols.has('profile_name')) {
1067
- db.exec(`ALTER TABLE queued_jobs ADD COLUMN profile_name TEXT`);
1068
- }
1069
- if (!cols.has('profile_selection_set')) {
1070
- db.exec(`
1071
- ALTER TABLE queued_jobs
1072
- ADD COLUMN profile_selection_set INTEGER NOT NULL DEFAULT 0
1073
- CHECK (profile_selection_set IN (0, 1))
1074
- `);
1075
- }
1076
- if (!cols.has('interactive')) {
1077
- db.exec(`
1078
- ALTER TABLE queued_jobs
1079
- ADD COLUMN interactive INTEGER CHECK (interactive IN (0, 1))
1080
- `);
1081
- }
1082
- if (!cols.has('causal_ownership')) {
1083
- db.exec(`
1084
- ALTER TABLE queued_jobs
1085
- ADD COLUMN causal_ownership INTEGER NOT NULL DEFAULT 0
1086
- CHECK (causal_ownership IN (0, 1))
1087
- `);
1088
- }
1089
- const jobCols = new Set(db.prepare(`PRAGMA table_info(jobs)`).all()
1090
- .map((row) => row.name));
1091
- if (!jobCols.has('causal_ownership')) {
1092
- db.exec(`
1093
- ALTER TABLE jobs
1094
- ADD COLUMN causal_ownership INTEGER NOT NULL DEFAULT 0
1095
- CHECK (causal_ownership IN (0, 1))
1096
- `);
1097
- }
1098
- },
1099
- // Migration 47: crash recovery reads the bounded newest provider frontier;
1100
- // this composite index avoids sorting/scanning an unbounded transcript while
1101
- // the queue is fail-stopped during startup.
1102
- (db) => {
1103
- db.exec(`CREATE INDEX IF NOT EXISTS idx_events_job_seq ON events(job_id, seq)`);
1104
- },
1105
- // Migration 48: truthful Safe-PR settlement. Execution truth, delivery
1106
- // readiness, continuation ownership and decision-operation ownership are
1107
- // independent durable facts; the old decision/pr_state pair cannot explain
1108
- // a successful run whose commit/ref/push later failed. Before enforcing the
1109
- // intended one-active-generation-per-rail invariant, terminalize historical
1110
- // duplicate rows deterministically (newest rowid wins) so an older iteration
1111
- // can never resurface after the current one closes.
1112
- (db) => {
1113
- const cols = new Set(db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all()
1114
- .map((row) => row.name));
1115
- const add = (name, ddl) => {
1116
- if (!cols.has(name))
1117
- db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN ${ddl}`);
1118
- };
1119
- add('implementation_outcome', `implementation_outcome TEXT NOT NULL DEFAULT 'unknown'`);
1120
- add('delivery_outcome', `delivery_outcome TEXT NOT NULL DEFAULT 'unknown'`);
1121
- add('status_code', `status_code TEXT`);
1122
- add('status_detail', `status_detail TEXT`);
1123
- add('delivery_sha', `delivery_sha TEXT`);
1124
- add('is_continuation', `is_continuation INTEGER NOT NULL DEFAULT 0 CHECK (is_continuation IN (0, 1))`);
1125
- add('supersedes_delivery_id', `supersedes_delivery_id TEXT`);
1126
- add('operation', `operation TEXT`);
1127
- add('operation_token', `operation_token TEXT`);
1128
- add('operation_started_at_ms', `operation_started_at_ms INTEGER`);
1129
- add('cleanup_warnings', `cleanup_warnings TEXT NOT NULL DEFAULT '[]'`);
1130
- db.exec(`
1131
- UPDATE rail_pr_deliveries
1132
- SET implementation_outcome = CASE decision
1133
- WHEN 'building' THEN 'running'
1134
- WHEN 'implementation_failed' THEN 'failed'
1135
- WHEN 'on_review' THEN 'succeeded'
1136
- WHEN 'pr_draft' THEN 'succeeded'
1137
- WHEN 'pr_ready' THEN 'succeeded'
1138
- WHEN 'pr_failed' THEN 'succeeded'
1139
- WHEN 'merged' THEN 'succeeded'
1140
- ELSE implementation_outcome
1141
- END
1142
- WHERE implementation_outcome = 'unknown';
1143
-
1144
- UPDATE rail_pr_deliveries
1145
- SET delivery_outcome = CASE decision
1146
- WHEN 'building' THEN 'pending'
1147
- WHEN 'implementation_failed' THEN 'not_started'
1148
- WHEN 'on_review' THEN 'ready'
1149
- WHEN 'pr_draft' THEN 'delivered'
1150
- WHEN 'pr_ready' THEN 'delivered'
1151
- WHEN 'pr_failed' THEN 'retryable_failure'
1152
- WHEN 'merged' THEN 'delivered'
1153
- ELSE delivery_outcome
1154
- END
1155
- WHERE delivery_outcome = 'unknown';
1156
-
1157
- UPDATE rail_pr_deliveries
1158
- SET status_code = CASE decision
1159
- WHEN 'building' THEN 'implementation_running'
1160
- WHEN 'implementation_failed' THEN 'implementation_failed'
1161
- WHEN 'on_review' THEN 'ready_for_review'
1162
- WHEN 'pr_draft' THEN 'pr_draft_ready'
1163
- WHEN 'pr_ready' THEN 'pr_ready'
1164
- WHEN 'pr_failed' THEN 'delivery_failed'
1165
- WHEN 'merged' THEN 'merged'
1166
- ELSE status_code
1167
- END
1168
- WHERE status_code IS NULL;
1169
-
1170
- UPDATE rail_pr_deliveries
1171
- SET decision = 'superseded',
1172
- status_code = 'superseded',
1173
- updated_at = datetime('now')
1174
- WHERE decision NOT IN ('merged', 'discarded', 'superseded')
1175
- AND rowid NOT IN (
1176
- SELECT MAX(rowid)
1177
- FROM rail_pr_deliveries
1178
- WHERE decision NOT IN ('merged', 'discarded', 'superseded')
1179
- GROUP BY rail_index
1180
- );
1181
-
1182
- CREATE UNIQUE INDEX IF NOT EXISTS idx_rail_pr_deliveries_one_active_per_rail
1183
- ON rail_pr_deliveries(rail_index)
1184
- WHERE decision NOT IN ('merged', 'discarded', 'superseded');
1185
- `);
1186
- },
1187
- // Migration 49: repair the exact legacy false-failure shape that motivated
1188
- // the orthogonal lifecycle. Older settlement code could write
1189
- // decision='implementation_failed' after every loop had durably succeeded,
1190
- // solely because commit/ref delivery failed. Reclassify only rows whose full
1191
- // run set is present and terminal; ambiguous rows remain conservative. This
1192
- // migration also makes truthful no-change acknowledgement (`completed`) a
1193
- // terminal generation for the one-active-per-rail invariant.
1194
- (db) => {
1195
- const legacyRows = db.prepare(`
1196
- SELECT id, run_ids, branches
1197
- FROM rail_pr_deliveries
1198
- WHERE decision = 'implementation_failed'
1199
- `).all();
1200
- const update = db.prepare(`
1201
- UPDATE rail_pr_deliveries
1202
- SET decision = 'pr_failed',
1203
- implementation_outcome = ?,
1204
- delivery_outcome = 'blocked',
1205
- status_code = 'settlement_interrupted',
1206
- status_detail = ?,
1207
- branches = ?,
1208
- updated_at = datetime('now')
1209
- WHERE id = ? AND decision = 'implementation_failed'
1210
- `);
1211
- for (const row of legacyRows) {
1212
- let runIds = [];
1213
- try {
1214
- const parsed = JSON.parse(row.run_ids);
1215
- if (Array.isArray(parsed)) {
1216
- runIds = [...new Set(parsed.filter((value) => typeof value === 'string' && value.length > 0))];
1217
- }
1218
- }
1219
- catch { /* malformed legacy evidence stays conservative */ }
1220
- if (runIds.length === 0)
1221
- continue;
1222
- const placeholders = runIds.map(() => '?').join(',');
1223
- const runs = db.prepare(`
1224
- SELECT id, status, final_outcome
1225
- FROM loop_runs
1226
- WHERE id IN (${placeholders})
1227
- `).all(...runIds);
1228
- if (runs.length !== runIds.length || runs.some((run) => run.status !== 'completed'))
1229
- continue;
1230
- const outcomes = new Map(runs.map((run) => [run.id, run.final_outcome]));
1231
- const succeeded = runIds.filter((runId) => outcomes.get(runId) === 'success').length;
1232
- if (succeeded === 0)
1233
- continue;
1234
- let branches = [];
1235
- try {
1236
- const parsed = JSON.parse(row.branches);
1237
- if (Array.isArray(parsed))
1238
- branches = parsed;
1239
- }
1240
- catch { /* retain an empty evidence set rather than malformed JSON */ }
1241
- const repairedBranches = branches.map((value, index) => {
1242
- if (!value || typeof value !== 'object')
1243
- return value;
1244
- const unit = value;
1245
- const runId = runIds[index];
1246
- const implementationSucceeded = runId ? outcomes.get(runId) === 'success' : succeeded === runIds.length;
1247
- return {
1248
- ...unit,
1249
- ...(runId ? { runId } : {}),
1250
- implementationOutcome: implementationSucceeded ? 'succeeded' : 'failed',
1251
- deliveryOutcome: implementationSucceeded ? 'blocked' : 'not_started',
1252
- ...(implementationSucceeded ? { failureCode: 'settlement_interrupted' } : {}),
1253
- };
1254
- });
1255
- update.run(succeeded === runIds.length ? 'succeeded' : 'partially_succeeded', 'Recovered the successful implementation result from durable run logs; legacy delivery settlement was interrupted.', JSON.stringify(repairedBranches), row.id);
1256
- }
1257
- db.exec(`
1258
- DROP INDEX IF EXISTS idx_rail_pr_deliveries_one_active_per_rail;
1259
- CREATE UNIQUE INDEX idx_rail_pr_deliveries_one_active_per_rail
1260
- ON rail_pr_deliveries(rail_index)
1261
- WHERE decision NOT IN ('completed', 'merged', 'discarded', 'superseded');
1262
- `);
1263
- },
1264
- // Migration 50: terminal Safe-PR decisions and their ticket-file mutation
1265
- // cross SQLite/JSON boundaries. Persist an idempotent outbox row in the same
1266
- // transaction as the terminal decision so a crash can never leave tickets
1267
- // parked on_review with no legal action left to replay.
1268
- (db) => {
1269
- db.exec(`
1270
- CREATE TABLE IF NOT EXISTS rail_pr_ticket_effects (
1271
- delivery_id TEXT PRIMARY KEY,
1272
- ticket_ids TEXT NOT NULL,
1273
- causal_owners TEXT NOT NULL DEFAULT '{}',
1274
- applied_ticket_ids TEXT,
1275
- target_status TEXT NOT NULL CHECK (target_status IN ('todo', 'done')),
1276
- jira_action TEXT NOT NULL CHECK (jira_action IN ('discard', 'merged', 'completed', 'refine', 'backlog')),
1277
- pr_url TEXT,
1278
- attempts INTEGER NOT NULL DEFAULT 0,
1279
- last_error TEXT,
1280
- tickets_applied_at TEXT,
1281
- jira_enqueued_at TEXT,
1282
- completed_at TEXT,
1283
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1284
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1285
- );
1286
- CREATE INDEX IF NOT EXISTS idx_rail_pr_ticket_effects_pending
1287
- ON rail_pr_ticket_effects(completed_at)
1288
- WHERE completed_at IS NULL;
1289
- `);
1290
- // Keep the migration restart-safe for development DBs that may have run an
1291
- // earlier migration-50 draft before these phase columns were added. Shipped
1292
- // migrations remain atomic, but additive guards make local upgrade testing
1293
- // deterministic as well.
1294
- const effectCols = new Set(db.prepare(`PRAGMA table_info(rail_pr_ticket_effects)`).all()
1295
- .map((row) => row.name));
1296
- if (!effectCols.has('applied_ticket_ids')) {
1297
- db.exec(`ALTER TABLE rail_pr_ticket_effects ADD COLUMN applied_ticket_ids TEXT`);
1298
- }
1299
- if (!effectCols.has('tickets_applied_at')) {
1300
- db.exec(`ALTER TABLE rail_pr_ticket_effects ADD COLUMN tickets_applied_at TEXT`);
1301
- }
1302
- if (!effectCols.has('jira_enqueued_at')) {
1303
- db.exec(`ALTER TABLE rail_pr_ticket_effects ADD COLUMN jira_enqueued_at TEXT`);
1304
- }
1305
- if (!effectCols.has('causal_owners')) {
1306
- db.exec(`ALTER TABLE rail_pr_ticket_effects ADD COLUMN causal_owners TEXT NOT NULL DEFAULT '{}'`);
1307
- }
1308
- },
1309
- // Migration 51: a development build briefly shipped migration 50 without
1310
- // phase columns and with a CHECK that allowed only discard/merged. A DB that
1311
- // already records version 50 will never rerun its edited body, so rebuild the
1312
- // table once, preserve every row, and install the complete action contract.
1313
- // Migration 52 owns the causal historical backfill after both schemas exist.
1314
- (db) => {
1315
- const table = db.prepare(`
1316
- SELECT sql FROM sqlite_master
1317
- WHERE type = 'table' AND name = 'rail_pr_ticket_effects'
1318
- `).get();
1319
- const createFullTable = (name) => {
1320
- db.exec(`
1321
- CREATE TABLE ${name} (
1322
- delivery_id TEXT PRIMARY KEY,
1323
- ticket_ids TEXT NOT NULL,
1324
- causal_owners TEXT NOT NULL DEFAULT '{}',
1325
- applied_ticket_ids TEXT,
1326
- target_status TEXT NOT NULL CHECK (target_status IN ('todo', 'done')),
1327
- jira_action TEXT NOT NULL CHECK (jira_action IN ('discard', 'merged', 'completed', 'refine', 'backlog')),
1328
- pr_url TEXT,
1329
- attempts INTEGER NOT NULL DEFAULT 0,
1330
- last_error TEXT,
1331
- tickets_applied_at TEXT,
1332
- jira_enqueued_at TEXT,
1333
- completed_at TEXT,
1334
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1335
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1336
- )
1337
- `);
1338
- };
1339
- if (!table) {
1340
- createFullTable('rail_pr_ticket_effects');
1341
- }
1342
- else {
1343
- const cols = new Set(db.prepare(`PRAGMA table_info(rail_pr_ticket_effects)`).all()
1344
- .map((row) => row.name));
1345
- const sql = table.sql ?? '';
1346
- const complete = ['causal_owners', 'applied_ticket_ids', 'tickets_applied_at', 'jira_enqueued_at']
1347
- .every((column) => cols.has(column)) &&
1348
- ['completed', 'refine', 'backlog'].every((action) => sql.includes(`'${action}'`));
1349
- if (!complete) {
1350
- db.exec(`DROP TABLE IF EXISTS rail_pr_ticket_effects_v51`);
1351
- createFullTable('rail_pr_ticket_effects_v51');
1352
- const value = (column, fallback) => cols.has(column) ? column : fallback;
1353
- db.exec(`
1354
- INSERT INTO rail_pr_ticket_effects_v51 (
1355
- delivery_id, ticket_ids, causal_owners, applied_ticket_ids, target_status,
1356
- jira_action, pr_url, attempts, last_error, tickets_applied_at,
1357
- jira_enqueued_at, completed_at, created_at, updated_at
1358
- )
1359
- SELECT
1360
- ${value('delivery_id', "''")},
1361
- ${value('ticket_ids', "'[]'")},
1362
- ${value('causal_owners', "'{}'")},
1363
- ${value('applied_ticket_ids', 'NULL')},
1364
- ${value('target_status', "'todo'")},
1365
- ${value('jira_action', "'backlog'")},
1366
- ${value('pr_url', 'NULL')},
1367
- ${value('attempts', '0')},
1368
- ${value('last_error', 'NULL')},
1369
- ${value('tickets_applied_at', 'NULL')},
1370
- ${value('jira_enqueued_at', 'NULL')},
1371
- ${value('completed_at', 'NULL')},
1372
- ${value('created_at', "datetime('now')")},
1373
- ${value('updated_at', "datetime('now')")}
1374
- FROM rail_pr_ticket_effects;
1375
-
1376
- DROP TABLE rail_pr_ticket_effects;
1377
- ALTER TABLE rail_pr_ticket_effects_v51 RENAME TO rail_pr_ticket_effects;
1378
- `);
1379
- }
1380
- }
1381
- db.exec(`
1382
- CREATE INDEX IF NOT EXISTS idx_rail_pr_ticket_effects_pending
1383
- ON rail_pr_ticket_effects(completed_at)
1384
- WHERE completed_at IS NULL
1385
- `);
1386
- },
1387
- // Migration 52: bind every ticket effect to the SQLite ticket-generation
1388
- // owner that created it. Historical terminal rows are backfilled only when
1389
- // their run_ids still own the ticket; an old merge/discard can therefore
1390
- // never mutate a newer iteration merely because it is also on_review.
1391
- (db) => {
1392
- // Some development builds recorded migration 45 before its ownership
1393
- // table was added. Repair that incomplete historical shape before the
1394
- // causal backfill so one damaged project DB cannot prevent app startup.
1395
- db.exec(`
1396
- CREATE TABLE IF NOT EXISTS ticket_outcome_ownership (
1397
- ticket_id INTEGER PRIMARY KEY,
1398
- owner_id TEXT NOT NULL,
1399
- generation INTEGER NOT NULL DEFAULT 1,
1400
- claimed_at TEXT NOT NULL DEFAULT (datetime('now'))
1401
- );
1402
- CREATE INDEX IF NOT EXISTS idx_ticket_outcome_owner
1403
- ON ticket_outcome_ownership(owner_id);
1404
- `);
1405
- const cols = new Set(db.prepare(`PRAGMA table_info(rail_pr_ticket_effects)`).all()
1406
- .map((row) => row.name));
1407
- if (!cols.has('causal_owners')) {
1408
- db.exec(`ALTER TABLE rail_pr_ticket_effects ADD COLUMN causal_owners TEXT NOT NULL DEFAULT '{}'`);
1409
- }
1410
- backfillTerminalRailPrTicketEffects(db);
1411
- },
1412
- // Migration 53: an allocation rollback is the one legal way a previously
1413
- // superseded delivery can become active again. Persist the failed replacement
1414
- // id on the restored predecessor so clients can distinguish that explicit
1415
- // rollback from an ordinary stale non-terminal replay.
1416
- (db) => {
1417
- const cols = new Set(db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all()
1418
- .map((row) => row.name));
1419
- if (!cols.has('restored_from_delivery_id')) {
1420
- db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN restored_from_delivery_id TEXT`);
1421
- }
1422
- },
1423
- // Migration 54: automatic worktree release quarantines authenticated overlay
1424
- // roots instead of deleting them. Persist the bounded archive locations so
1425
- // recovery remains discoverable after restart and card refresh.
1426
- (db) => {
1427
- const cols = new Set(db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all()
1428
- .map((row) => row.name));
1429
- if (!cols.has('safety_archives')) {
1430
- db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN safety_archives TEXT NOT NULL DEFAULT '[]'`);
1431
- }
1432
- },
1433
- // Migration 55: Agent Studio history belongs to a provider-native role
1434
- // catalog. The same logical custom role id can validly exist for Claude and
1435
- // Kimi in one project; keying history only by agent_name mixed bodies and
1436
- // version counters across those catalogs. Legacy rows predate provider-aware
1437
- // Studio and therefore belong to Claude.
1438
- (db) => {
1439
- const versionCols = new Set(db.prepare(`PRAGMA table_info(agent_versions)`).all()
1440
- .map((row) => row.name));
1441
- if (!versionCols.has('provider')) {
1442
- db.exec(`
1443
- ALTER TABLE agent_versions RENAME TO agent_versions_v54;
1444
- CREATE TABLE agent_versions (
1445
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1446
- provider TEXT NOT NULL DEFAULT 'claude',
1447
- agent_name TEXT NOT NULL,
1448
- version INTEGER NOT NULL,
1449
- body TEXT NOT NULL,
1450
- created_at INTEGER NOT NULL,
1451
- UNIQUE (provider, agent_name, version)
1452
- );
1453
- INSERT INTO agent_versions (id, provider, agent_name, version, body, created_at)
1454
- SELECT id, 'claude', agent_name, version, body, created_at
1455
- FROM agent_versions_v54;
1456
- DROP TABLE agent_versions_v54;
1457
- CREATE INDEX idx_agent_versions_provider_name
1458
- ON agent_versions(provider, agent_name);
1459
- `);
1460
- }
1461
- const testCols = new Set(db.prepare(`PRAGMA table_info(agent_tests)`).all()
1462
- .map((row) => row.name));
1463
- if (!testCols.has('provider')) {
1464
- db.exec(`ALTER TABLE agent_tests ADD COLUMN provider TEXT NOT NULL DEFAULT 'claude'`);
1465
- }
1466
- db.exec(`
1467
- CREATE INDEX IF NOT EXISTS idx_agent_tests_provider_name
1468
- ON agent_tests(provider, agent_name)
1469
- `);
1470
- },
1471
- // Migration 56: nontech-review-experience Wave 1 — two additive nullable
1472
- // JSON columns on rail_pr_deliveries. `spec_snapshot` freezes each covered
1473
- // ticket's title/description/labels at LAUNCH so the review packet renders
1474
- // what the user actually asked (the live store can be edited mid-run).
1475
- // `settle_evidence` persists the deterministic settle-time harvest (the
1476
- // VERIFICATION sentinel, the verify step's output tail, the reviewer's
1477
- // confidence-score.json) captured BEFORE worktree release — the packet's
1478
- // proof sections survive worktree teardown. NULL on legacy rows = honest
1479
- // "not captured". Additive + idempotent (guarded by PRAGMA table_info).
1480
- (db) => {
1481
- const cols = db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all()
1482
- .map((r) => r.name);
1483
- if (!cols.includes('spec_snapshot')) {
1484
- db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN spec_snapshot TEXT`);
1485
- }
1486
- if (!cols.includes('settle_evidence')) {
1487
- db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN settle_evidence TEXT`);
1488
- }
1489
- },
1490
- // Migration 57: nontech-review-experience Wave 3 — revision metadata on the
1491
- // NEW delivery generation a revision creates. `revision_note` is the user's
1492
- // one-sentence instruction (so packet v2 can show what was asked to change)
1493
- // and `revision_of` points at the generation being revised. Deliberately NOT
1494
- // a new `decision` value: an unknown decision makes older clients drop the
1495
- // whole PR card, and the shipped generation/supersession model already
1496
- // expresses "a newer attempt replaced the previous one". Additive +
1497
- // idempotent; NULL on every pre-existing row and on ordinary launches.
1498
- (db) => {
1499
- const cols = db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all()
1500
- .map((r) => r.name);
1501
- if (!cols.includes('revision_note')) {
1502
- db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN revision_note TEXT`);
1503
- }
1504
- if (!cols.includes('revision_of')) {
1505
- db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN revision_of TEXT`);
1506
- }
1507
- },
1508
- // Migration 58: premium-milestone-progress — server-durable milestone launch
1509
- // chains. One row per sequential "Launch Milestone": the chunk plan, the
1510
- // chunk in flight (rail / runs / delivery), the branch the next chunk stacks
1511
- // on, and a CAS status. The partial unique index enforces ONE non-terminal
1512
- // chain per milestone. Additive; nothing else references it.
1513
- (db) => {
1514
- db.exec(`
1515
- CREATE TABLE IF NOT EXISTS milestone_launch_chains (
1516
- id TEXT PRIMARY KEY,
1517
- milestone_n INTEGER NOT NULL,
1518
- milestone_id TEXT NOT NULL,
1519
- mode TEXT NOT NULL,
1520
- chunks TEXT NOT NULL DEFAULT '[]',
1521
- next_chunk INTEGER NOT NULL DEFAULT 0,
1522
- current_rail_index INTEGER,
1523
- current_run_ids TEXT NOT NULL DEFAULT '[]',
1524
- current_delivery_id TEXT,
1525
- integration_branch TEXT,
1526
- head_branch TEXT,
1527
- status TEXT NOT NULL DEFAULT 'running',
1528
- pause_reason TEXT,
1529
- launched TEXT NOT NULL DEFAULT '[]',
1530
- last_run_outcome TEXT,
1531
- auto_advance INTEGER NOT NULL DEFAULT 1,
1532
- retry_chunk INTEGER,
1533
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1534
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1535
- );
1536
- CREATE UNIQUE INDEX IF NOT EXISTS idx_milestone_chains_active
1537
- ON milestone_launch_chains(milestone_n) WHERE status IN ('running','waiting','paused','awaiting_approval');
1538
- CREATE INDEX IF NOT EXISTS idx_milestone_chains_status ON milestone_launch_chains(status);
1539
- `);
1540
- },
1541
- // Migration 59: coordinated repository execution and grouped delivery.
1542
- multi_repo_execution_store_1.migrateMultiRepoExecution,
1543
- // Migration 60: repository-scoped provenance and code identity.
1544
- project_repository_provenance_1.migrateRepositoryProvenance,
1545
- // Migration 61: version and evidence identity of recorded-change explanations.
1546
- file_story_1.migrateFileStoryMetadata,
1547
- // Migration 62: pr-follow-up-fixes — the frozen review follow-up scope a launch
1548
- // was approved with (`follow_up`, JSON PrFollowUp: selected comments, required
1549
- // outcomes, exclusions, verification, OpenSpec change name, id/version/hash).
1550
- // Lives on the DELIVERY, never on the spec: a follow-up must not rewrite the
1551
- // ticket description (Jira-synced on linked projects). Additive + idempotent;
1552
- // NULL on every pre-existing row and on ordinary launches.
1553
- (db) => {
1554
- const cols = db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all().map((r) => r.name);
1555
- if (!cols.includes('follow_up'))
1556
- db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN follow_up TEXT`);
1557
- },
1558
- // Migration 63: spec-addenda — the frozen identity (ticketId/id/kind/title/
1559
- // hash) of the spec addenda a launch carried (`spec_addenda`, JSON
1560
- // SpecAddendaSnapshotEntry[]). The addenda themselves live on the TICKET
1561
- // (local-tickets.json, schema 1.4); the delivery only records which ones
1562
- // this generation was briefed with, so the packet can report them and a
1563
- // discard can reopen exactly those. Additive + idempotent; NULL when none.
1564
- (db) => {
1565
- const cols = db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all().map((r) => r.name);
1566
- if (!cols.includes('spec_addenda'))
1567
- db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN spec_addenda TEXT`);
1568
- },
1569
- ];
1570
- function applyMigrations(db) {
1571
- // Ensure the migrations table exists (migration 1 creates it, but we need
1572
- // it before we can read from it)
1573
- db.exec(`
1574
- CREATE TABLE IF NOT EXISTS schema_migrations (
1575
- version INTEGER PRIMARY KEY,
1576
- applied_at TEXT NOT NULL DEFAULT (datetime('now'))
1577
- )
1578
- `);
1579
- (0, sqlite_migrations_1.applyNumberedMigrations)(db, MIGRATIONS);
1580
- }
1581
- // ─── Public API ──────────────────────────────────────────────────────────────
1582
- function initDb(dbPath) {
1583
- if (dbPath !== ':memory:') {
1584
- const dir = path_1.default.dirname(dbPath);
1585
- fs_1.default.mkdirSync(dir, { recursive: true });
1586
- (0, secure_fs_1.secureDir)(dir); // H-13: owner-only data dir
1587
- }
1588
- const db = new better_sqlite3_1.default(dbPath);
1589
- try {
1590
- db.pragma('journal_mode = WAL');
1591
- db.pragma('foreign_keys = ON');
1592
- // Under load, QueueManager / ChatManager / FileSummaryManager write the same
1593
- // per-project DB concurrently with /analytics reads. Wait up to 5s on a lock
1594
- // instead of throwing SQLITE_BUSY, and cap the WAL so a long checkpoint can't
1595
- // grow it without bound.
1596
- db.pragma('busy_timeout = 5000');
1597
- db.pragma('journal_size_limit = 10000000'); // ~10 MB
1598
- applyMigrations(db);
1599
- // H-13: restrict the db + its WAL sidecars to 0600 (jobs.sqlite holds chat
1600
- // transcripts and verbatim terminal command history). After migrations the
1601
- // WAL/SHM files exist, so this covers them too.
1602
- (0, secure_fs_1.secureDbFile)(dbPath);
1603
- // Orphan sweep: cancel any in-flight proposals from a previous server session
1604
- db.prepare("UPDATE proposals SET status = 'cancelled', updated_at = ? WHERE status IN ('exploring', 'refining')").run(new Date().toISOString());
1605
- }
1606
- catch (err) {
1607
- db.close();
1608
- throw err;
1609
- }
1610
- return db;
1611
- }
1612
- function createJob(db, job) {
1613
- // Promotion is atomic: after a crash the job is either still replayable in
1614
- // queued_jobs or is a running jobs row, never absent from both. INSERT OR
1615
- // IGNORE preserves idempotency for legacy/restored jobs that already exist.
1616
- const promote = db.transaction(() => {
1617
- db.prepare('INSERT OR IGNORE INTO jobs (id, command, started_at, status, provider, owner, priority, depends_on_job_id, pipeline_id, interactive, causal_ownership) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(job.id, job.command, job.started_at, 'running', job.provider ?? null, job.owner ?? 'queue', job.priority ?? 'normal', job.depends_on_job_id ?? null, job.pipeline_id ?? null, job.interactive ? 1 : 0, job.causal_ownership ? 1 : 0);
1618
- db.prepare('UPDATE jobs SET status = ?, started_at = ?, provider = ?, owner = ?, interactive = ?, causal_ownership = ? WHERE id = ?').run('running', job.started_at, job.provider ?? null, job.owner ?? 'queue', job.interactive ? 1 : 0, job.causal_ownership ? 1 : 0, job.id);
1619
- db.prepare('DELETE FROM queued_jobs WHERE id = ?').run(job.id);
1620
- });
1621
- promote();
1622
- }
1623
- /** Idempotently persist a job that has been admitted but has not started. */
1624
- function upsertQueuedJob(db, job) {
1625
- db.prepare(`
1626
- INSERT INTO queued_jobs (
1627
- id, command, queue_position, priority, depends_on_job_id, pipeline_id,
1628
- provider, model, profile_name, profile_selection_set, interactive,
1629
- causal_ownership
1630
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1631
- ON CONFLICT(id) DO UPDATE SET
1632
- command = excluded.command,
1633
- queue_position = excluded.queue_position,
1634
- priority = excluded.priority,
1635
- depends_on_job_id = excluded.depends_on_job_id,
1636
- pipeline_id = excluded.pipeline_id,
1637
- provider = excluded.provider,
1638
- model = excluded.model,
1639
- profile_name = excluded.profile_name,
1640
- profile_selection_set = excluded.profile_selection_set,
1641
- interactive = excluded.interactive,
1642
- causal_ownership = excluded.causal_ownership
1643
- `).run(job.id, job.command, job.queue_position, job.priority, job.depends_on_job_id ?? null, job.pipeline_id ?? null, job.provider ?? null, job.model ?? null, job.profile_name ?? null, job.profile_selection_set ? 1 : 0, job.interactive == null ? null : (job.interactive ? 1 : 0), job.causal_ownership ? 1 : 0);
1644
- }
1645
- /** Remove a queued admission after cancellation/skip. Idempotent.
1646
- *
1647
- * Migration 43 deliberately keeps a read fallback for pre-migration builds
1648
- * that represented queued work in `jobs`. Remove that legacy representation
1649
- * too, but only while it is still queued: promotion changes the status to
1650
- * running before calling this helper, and terminal history must never be
1651
- * deleted here. */
1652
- function deleteQueuedJob(db, jobId) {
1653
- db.prepare('DELETE FROM queued_jobs WHERE id = ?').run(jobId);
1654
- db.prepare("DELETE FROM jobs WHERE id = ? AND status = 'queued'").run(jobId);
1655
- }
1656
- /**
1657
- * Flip a job row's `interactive` flag AFTER creation. Used by the loop engine
1658
- * when an ai-step upgrades the run's backing job to a resident interactive
1659
- * session — the row was created at run start (before the step's provider
1660
- * capability/kill-switch gate is consulted), so the flag lands lazily at the
1661
- * first interactive step spawn. Idempotent.
1662
- */
1663
- function markJobInteractive(db, jobId) {
1664
- db.prepare('UPDATE jobs SET interactive = 1 WHERE id = ?').run(jobId);
1665
- }
1666
- /**
1667
- * Add one completed interactive turn's REAL usage into the job row. Token/cost/
1668
- * turn columns accumulate (COALESCE so the first turn starts from a clean base);
1669
- * model + session_id are stamped from the first turn that reports them. The job
1670
- * stays 'running' — only finalizeInteractiveJob flips the terminal status. This
1671
- * keeps the live Job Detail totals honest (sum of completed turns, never an
1672
- * estimate) between turns.
1673
- */
1674
- function accumulateInteractiveTurn(db, jobId, turn) {
1675
- db.prepare(`
1676
- UPDATE jobs SET
1677
- tokens_in = COALESCE(tokens_in, 0) + ?,
1678
- tokens_out = COALESCE(tokens_out, 0) + ?,
1679
- tokens_cache_read = COALESCE(tokens_cache_read, 0) + ?,
1680
- tokens_cache_create = COALESCE(tokens_cache_create, 0) + ?,
1681
- total_cost_usd = COALESCE(total_cost_usd, 0) + ?,
1682
- num_turns = COALESCE(num_turns, 0) + ?,
1683
- total_cost_usd_estimated = CASE WHEN ? = 1 THEN 1 ELSE total_cost_usd_estimated END,
1684
- model = COALESCE(model, ?),
1685
- session_id = COALESCE(?, session_id)
1686
- WHERE id = ?
1687
- `).run(turn.tokens_in, turn.tokens_out, turn.tokens_cache_read, turn.tokens_cache_create, turn.total_cost_usd, turn.num_turns, turn.estimated ? 1 : 0, turn.model ?? null, turn.session_id ?? null, jobId);
1688
- }
1689
- /**
1690
- * Flip an interactive job to its terminal status (completed on finalize, failed
1691
- * on crash) and stamp finished_at. Token/cost/turn columns are left untouched —
1692
- * they were already accumulated turn-by-turn via accumulateInteractiveTurn.
1693
- */
1694
- function finalizeInteractiveJob(db, jobId, status) {
1695
- db.prepare('UPDATE jobs SET status = ?, finished_at = ? WHERE id = ?').run(status, new Date().toISOString(), jobId);
1696
- }
1697
- function finishJob(db, jobId, result) {
1698
- db.prepare(`
1699
- UPDATE jobs SET
1700
- status = ?,
1701
- exit_code = ?,
1702
- finished_at = ?,
1703
- tokens_in = ?,
1704
- tokens_out = ?,
1705
- tokens_cache_read = ?,
1706
- tokens_cache_create = ?,
1707
- total_cost_usd = ?,
1708
- total_cost_usd_estimated = ?,
1709
- num_turns = ?,
1710
- model = ?,
1711
- duration_ms = ?,
1712
- duration_api_ms = ?,
1713
- session_id = ?
1714
- WHERE id = ?
1715
- `).run(result.status, result.exit_code, new Date().toISOString(), result.tokens_in ?? null, result.tokens_out ?? null, result.tokens_cache_read ?? null, result.tokens_cache_create ?? null, result.total_cost_usd ?? null, result.total_cost_usd_estimated ? 1 : 0, result.num_turns ?? null, result.model ?? null, result.duration_ms ?? null, result.duration_api_ms ?? null, result.session_id ?? null, jobId);
1716
- }
1717
- function appendEvent(db, jobId, seq, event) {
1718
- db.prepare('INSERT INTO events (job_id, seq, event_type, source, payload) VALUES (?, ?, ?, ?, ?)').run(jobId, seq, event.event_type, event.source ?? null, event.payload);
1719
- }
1720
- function upsertPhase(db, jobId, phase, state) {
1721
- db.prepare('INSERT OR REPLACE INTO job_phases (job_id, phase, state, updated_at) VALUES (?, ?, ?, ?)').run(jobId, phase, state, new Date().toISOString());
1722
- }
1723
- function listJobs(db, opts) {
1724
- const limit = Math.min(opts.limit ?? 50, 200);
1725
- const offset = opts.offset ?? 0;
1726
- const conditions = [];
1727
- const params = [];
1728
- if (opts.status) {
1729
- conditions.push('status = ?');
1730
- params.push(opts.status);
1731
- }
1732
- if (opts.from) {
1733
- conditions.push('started_at >= ?');
1734
- params.push(opts.from);
1735
- }
1736
- if (opts.to) {
1737
- conditions.push('started_at <= ?');
1738
- params.push(opts.to);
1739
- }
1740
- const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
1741
- const countRow = db
1742
- .prepare(`SELECT COUNT(*) as count FROM jobs ${where}`)
1743
- .get(...params);
1744
- const jobs = db
1745
- .prepare(`SELECT jobs.*, jp.profile_name AS profile_name
1746
- FROM jobs LEFT JOIN job_profiles jp ON jp.job_id = jobs.id
1747
- ${where}
1748
- ORDER BY started_at DESC LIMIT ? OFFSET ?`)
1749
- .all(...params, limit, offset);
1750
- return { jobs, total: countRow.count };
1751
- }
1752
- function getJob(db, jobId) {
1753
- return db
1754
- .prepare(`
1755
- SELECT jobs.*, jp.profile_name AS profile_name
1756
- FROM jobs
1757
- LEFT JOIN job_profiles jp ON jp.job_id = jobs.id
1758
- WHERE jobs.id = ?
1759
- `)
1760
- .get(jobId);
1761
- }
1762
- function getJobEvents(db, jobId) {
1763
- return db
1764
- .prepare('SELECT * FROM events WHERE job_id = ? ORDER BY seq ASC')
1765
- .all(jobId);
1766
- }
1767
- class JobRecoveryPendingError extends Error {
1768
- jobId;
1769
- constructor(jobId) {
1770
- super('Job recovery is still pending; retry deletion after recovery completes');
1771
- this.jobId = jobId;
1772
- this.name = 'JobRecoveryPendingError';
1773
- }
1774
- }
1775
- exports.JobRecoveryPendingError = JobRecoveryPendingError;
1776
- function deleteJob(db, jobId) {
1777
- // M7: jobs.depends_on_job_id REFERENCES jobs(id) with no ON DELETE action and
1778
- // foreign_keys=ON, so deleting a pipeline parent throws 'FOREIGN KEY
1779
- // constraint failed' and the job becomes undeletable from the UI. Clear inbound
1780
- // references first, in the same transaction as the delete, so it always
1781
- // succeeds (children keep running; they just lose the now-irrelevant pointer).
1782
- const tx = db.transaction((id) => {
1783
- const pendingLoopStep = db.prepare(`SELECT 1 FROM loop_step_recovery WHERE run_id = ? LIMIT 1`).get(id);
1784
- const pendingQueueRecovery = db.prepare(`SELECT 1 FROM orphan_job_recovery WHERE job_id = ? LIMIT 1`).get(id);
1785
- if (pendingLoopStep || pendingQueueRecovery) {
1786
- throw new JobRecoveryPendingError(id);
1787
- }
1788
- db.prepare('UPDATE jobs SET depends_on_job_id = NULL WHERE depends_on_job_id = ?').run(id);
1789
- // B41: events/job_phases cascade on the jobs FK, but telemetry_blobs/
1790
- // telemetry_summaries (keyed `jobId`), job_profiles and file_provenance
1791
- // (keyed `job_id`) have no FK — without these they accumulate forever. (The
1792
- // on-disk .ndjson.gz blob is reclaimed by the 7-day startup compactor.)
1793
- db.prepare('DELETE FROM telemetry_blobs WHERE jobId = ?').run(id);
1794
- db.prepare('DELETE FROM telemetry_summaries WHERE jobId = ?').run(id);
1795
- db.prepare('DELETE FROM job_profiles WHERE job_id = ?').run(id);
1796
- db.prepare('DELETE FROM file_provenance WHERE job_id = ?').run(id);
1797
- db.prepare('DELETE FROM jobs WHERE id = ?').run(id);
1798
- });
1799
- tx(jobId);
1800
- }
1801
- function purgeJobs(db, opts) {
1802
- const conditions = [
1803
- "status IN ('completed', 'failed', 'canceled', 'zombie_terminated', 'skipped')",
1804
- 'NOT EXISTS (SELECT 1 FROM loop_step_recovery WHERE loop_step_recovery.run_id = jobs.id)',
1805
- 'NOT EXISTS (SELECT 1 FROM orphan_job_recovery WHERE orphan_job_recovery.job_id = jobs.id)',
1806
- ];
1807
- const params = [];
1808
- if (opts?.from) {
1809
- conditions.push('started_at >= ?');
1810
- params.push(opts.from);
1811
- }
1812
- if (opts?.to) {
1813
- conditions.push('started_at <= ?');
1814
- params.push(opts.to);
1815
- }
1816
- const where = conditions.join(' AND ');
1817
- // M6: run the whole purge atomically. Previously these statements ran without a
1818
- // transaction, so when the final `DELETE FROM jobs` aborted on the
1819
- // depends_on_job_id FK (a purged job still referenced by a non-purged one), the
1820
- // events/phases deletes had already committed — destroying log history while
1821
- // deleting zero job rows, and a misleading 500. The transaction rolls back on
1822
- // any failure, and NULL-ing inbound references first makes the delete succeed.
1823
- const tx = db.transaction(() => {
1824
- const sel = `SELECT id FROM jobs WHERE ${where}`;
1825
- db.prepare(`DELETE FROM events WHERE job_id IN (${sel})`).run(...params);
1826
- db.prepare(`DELETE FROM job_phases WHERE job_id IN (${sel})`).run(...params);
1827
- // B41: also purge the no-FK orphan tables for the same jobs.
1828
- db.prepare(`DELETE FROM telemetry_blobs WHERE jobId IN (${sel})`).run(...params);
1829
- db.prepare(`DELETE FROM telemetry_summaries WHERE jobId IN (${sel})`).run(...params);
1830
- db.prepare(`DELETE FROM job_profiles WHERE job_id IN (${sel})`).run(...params);
1831
- db.prepare(`DELETE FROM file_provenance WHERE job_id IN (${sel})`).run(...params);
1832
- // Clear inbound FK references from NON-purged jobs to purged jobs.
1833
- db.prepare(`UPDATE jobs SET depends_on_job_id = NULL WHERE depends_on_job_id IN (${sel})`).run(...params);
1834
- return db.prepare(`DELETE FROM jobs WHERE ${where}`).run(...params).changes;
1835
- });
1836
- return tx();
1837
- }
1838
- function getProjectActivity(db, opts) {
1839
- const limit = Math.min(opts.limit, 100);
1840
- const conditions = [];
1841
- const params = [];
1842
- if (opts.before) {
1843
- conditions.push('started_at < ?');
1844
- params.push(opts.before);
1845
- }
1846
- const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
1847
- const jobs = db
1848
- .prepare(`SELECT * FROM jobs ${where} ORDER BY started_at DESC LIMIT ?`)
1849
- .all(...params, limit);
1850
- return jobs.map((j) => {
1851
- const isTerminal = j.status === 'completed' || j.status === 'failed' || j.status === 'canceled' || j.status === 'zombie_terminated';
1852
- const type = j.status === 'completed' ? 'job_completed'
1853
- : j.status === 'failed' ? 'job_failed'
1854
- : (j.status === 'canceled' || j.status === 'zombie_terminated') ? 'job_canceled'
1855
- : 'job_started';
1856
- const timestamp = isTerminal && j.finished_at ? j.finished_at : j.started_at;
1857
- const shortCmd = j.command.length > 60 ? j.command.slice(0, 57) + '...' : j.command;
1858
- const summary = type === 'job_started' ? `Job started: ${shortCmd}`
1859
- : type === 'job_completed' ? `Job completed: ${shortCmd}`
1860
- : type === 'job_failed' ? `Job failed: ${shortCmd}`
1861
- : j.status === 'zombie_terminated' ? `Job auto-terminated (zombie): ${shortCmd}`
1862
- : `Job canceled: ${shortCmd}`;
1863
- return {
1864
- id: j.id,
1865
- type,
1866
- jobId: j.id,
1867
- jobCommand: j.command,
1868
- timestamp,
1869
- summary,
1870
- costUsd: isTerminal ? (j.total_cost_usd ?? null) : null,
1871
- };
1872
- });
1873
- }
1874
- // ─── Chat DB functions ────────────────────────────────────────────────────────
1875
- function createConversation(db, opts) {
1876
- const scopeJson = opts.contextScope != null ? JSON.stringify(opts.contextScope) : null;
1877
- db.prepare('INSERT INTO chat_conversations (id, model, kind, context_scope, provider) VALUES (?, ?, ?, ?, ?)').run(opts.id, opts.model, opts.kind ?? 'sidebar', scopeJson, opts.provider ?? null);
1878
- }
1879
- function listConversations(db) {
1880
- return db.prepare('SELECT * FROM chat_conversations ORDER BY updated_at DESC').all();
1881
- }
1882
- function getConversation(db, id) {
1883
- return db.prepare('SELECT * FROM chat_conversations WHERE id = ?').get(id);
1884
- }
1885
- function deleteConversation(db, id) {
1886
- db.prepare('DELETE FROM chat_conversations WHERE id = ?').run(id);
1887
- }
1888
- function updateConversation(db, id, patch) {
1889
- const sets = ['updated_at = ?'];
1890
- const params = [new Date().toISOString()];
1891
- if (patch.title !== undefined) {
1892
- sets.push('title = ?');
1893
- params.push(patch.title);
1894
- }
1895
- if (patch.session_id !== undefined) {
1896
- sets.push('session_id = ?');
1897
- params.push(patch.session_id);
1898
- }
1899
- if (patch.model !== undefined) {
1900
- sets.push('model = ?');
1901
- params.push(patch.model);
1902
- }
1903
- params.push(id);
1904
- db.prepare(`UPDATE chat_conversations SET ${sets.join(', ')} WHERE id = ?`).run(...params);
1905
- }
1906
- function addMessage(db, msg) {
1907
- const result = db.prepare('INSERT INTO chat_messages (conversation_id, role, content) VALUES (?, ?, ?)').run(msg.conversation_id, msg.role, msg.content);
1908
- return db.prepare('SELECT * FROM chat_messages WHERE id = ?').get(Number(result.lastInsertRowid));
1909
- }
1910
- function getMessages(db, conversationId) {
1911
- return db.prepare('SELECT * FROM chat_messages WHERE conversation_id = ? ORDER BY id ASC').all(conversationId);
1912
- }
1913
- // ─── Proposal DB functions ────────────────────────────────────────────────────
1914
- function createProposal(db, opts) {
1915
- db.prepare('INSERT INTO proposals (id, idea, status) VALUES (?, ?, ?)').run(opts.id, opts.idea, 'input');
1916
- }
1917
- function getProposal(db, id) {
1918
- return db.prepare('SELECT * FROM proposals WHERE id = ?').get(id);
1919
- }
1920
- function listProposals(db, opts) {
1921
- const limit = Math.min(opts?.limit ?? 20, 100);
1922
- const offset = opts?.offset ?? 0;
1923
- const countRow = db
1924
- .prepare('SELECT COUNT(*) as count FROM proposals')
1925
- .get();
1926
- const proposals = db
1927
- .prepare('SELECT * FROM proposals ORDER BY created_at DESC LIMIT ? OFFSET ?')
1928
- .all(limit, offset);
1929
- return { proposals, total: countRow.count };
1930
- }
1931
- function updateProposal(db, id, patch) {
1932
- const sets = ['updated_at = ?'];
1933
- const params = [new Date().toISOString()];
1934
- if (patch.status !== undefined) {
1935
- sets.push('status = ?');
1936
- params.push(patch.status);
1937
- }
1938
- if (patch.session_id !== undefined) {
1939
- sets.push('session_id = ?');
1940
- params.push(patch.session_id);
1941
- }
1942
- if (patch.result_markdown !== undefined) {
1943
- sets.push('result_markdown = ?');
1944
- params.push(patch.result_markdown);
1945
- }
1946
- if (patch.issue_url !== undefined) {
1947
- sets.push('issue_url = ?');
1948
- params.push(patch.issue_url);
1949
- }
1950
- params.push(id);
1951
- db.prepare(`UPDATE proposals SET ${sets.join(', ')} WHERE id = ?`).run(...params);
1952
- }
1953
- function deleteProposal(db, id) {
1954
- db.prepare('DELETE FROM proposals WHERE id = ?').run(id);
1955
- }
1956
- function createTemplate(db, t) {
1957
- db.prepare('INSERT INTO job_templates (id, name, description, commands) VALUES (?, ?, ?, ?)').run(t.id, t.name, t.description ?? null, JSON.stringify(t.commands));
1958
- }
1959
- function listTemplates(db) {
1960
- return db.prepare('SELECT * FROM job_templates ORDER BY created_at DESC').all();
1961
- }
1962
- function getTemplate(db, id) {
1963
- return db.prepare('SELECT * FROM job_templates WHERE id = ?').get(id);
1964
- }
1965
- function updateTemplate(db, id, patch) {
1966
- const sets = ['updated_at = ?'];
1967
- const params = [new Date().toISOString()];
1968
- if (patch.name !== undefined) {
1969
- sets.push('name = ?');
1970
- params.push(patch.name);
1971
- }
1972
- if (patch.description !== undefined) {
1973
- sets.push('description = ?');
1974
- params.push(patch.description);
1975
- }
1976
- if (patch.commands !== undefined) {
1977
- sets.push('commands = ?');
1978
- params.push(JSON.stringify(patch.commands));
1979
- }
1980
- params.push(id);
1981
- db.prepare(`UPDATE job_templates SET ${sets.join(', ')} WHERE id = ?`).run(...params);
1982
- }
1983
- function deleteTemplate(db, id) {
1984
- db.prepare('DELETE FROM job_templates WHERE id = ?').run(id);
1985
- }
1986
- function skipJob(db, jobId, reason) {
1987
- db.prepare(`UPDATE jobs SET status = 'skipped', skip_reason = ?, finished_at = ? WHERE id = ?`).run(reason, new Date().toISOString(), jobId);
1988
- }
1989
- function getPipelineJobs(db, pipelineId) {
1990
- return db.prepare('SELECT * FROM jobs WHERE pipeline_id = ? ORDER BY queue_position ASC, started_at ASC').all(pipelineId);
1991
- }
1992
- function getStats(db) {
1993
- const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
1994
- // Job-COUNT metrics (totalJobs / failedJobs / jobsToday / avgDurationMs) are
1995
- // genuinely about pipeline jobs, so they stay on the jobs table.
1996
- const totalRow = db.prepare(`
1997
- SELECT
1998
- COUNT(*) as totalJobs,
1999
- SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failedJobs,
2000
- AVG(duration_ms) as avgDurationMs
2001
- FROM jobs
2002
- `).get();
2003
- const jobsTodayRow = db.prepare(`
2004
- SELECT COUNT(*) as jobsToday
2005
- FROM jobs
2006
- WHERE strftime('%Y-%m-%d', started_at) = ?
2007
- `).get(today);
2008
- // MED-8: cost SUMs come from ai_invocations — ALL billable surfaces (job,
2009
- // explore-spec, chat-sidebar, quick-spec, ai-edit, agent-studio, spec-launcher,
2010
- // proposal, setup, smash, file-summary, loop), not just the jobs table — across
2011
- // every status, mirroring server/desktop-analytics.ts + server/spending.ts so
2012
- // the per-project StatusBar reconciles with /analytics and /budget for the same
2013
- // project. A killed/failed run's rate-card estimate now counts (it billed real
2014
- // tokens) instead of vanishing; estimatedCost* is the portion sourced from the
2015
- // rate card so the StatusBar can badge the total with `~`.
2016
- const costSum = `COALESCE(SUM(total_cost_usd), 0)`;
2017
- const estSum = `COALESCE(SUM(CASE WHEN total_cost_usd_estimated = 1 THEN total_cost_usd ELSE 0 END), 0)`;
2018
- const costRow = db.prepare(`
2019
- SELECT
2020
- ${costSum} as totalCostUsd,
2021
- ${estSum} as estimatedCostUsd,
2022
- COUNT(total_cost_usd) as pricedRuns,
2023
- SUM(CASE WHEN total_cost_usd IS NULL THEN 1 ELSE 0 END) as unpricedRuns
2024
- FROM ai_invocations
2025
- `).get();
2026
- const costTodayRow = db.prepare(`
2027
- SELECT
2028
- ${costSum} as costToday,
2029
- ${estSum} as estimatedCostToday,
2030
- COUNT(total_cost_usd) as pricedTodayRuns,
2031
- SUM(CASE WHEN total_cost_usd IS NULL THEN 1 ELSE 0 END) as unpricedTodayRuns
2032
- FROM ai_invocations
2033
- WHERE strftime('%Y-%m-%d', started_at) = ?
2034
- `).get(today);
2035
- return {
2036
- totalJobs: totalRow.totalJobs,
2037
- failedJobs: totalRow.failedJobs ?? 0,
2038
- jobsToday: jobsTodayRow.jobsToday,
2039
- totalCostUsd: costRow.totalCostUsd,
2040
- costToday: costTodayRow.costToday,
2041
- estimatedCostUsd: costRow.estimatedCostUsd,
2042
- estimatedCostToday: costTodayRow.estimatedCostToday,
2043
- pricedRuns: costRow.pricedRuns,
2044
- unpricedRuns: costRow.unpricedRuns ?? 0,
2045
- pricedTodayRuns: costTodayRow.pricedTodayRuns,
2046
- unpricedTodayRuns: costTodayRow.unpricedTodayRuns ?? 0,
2047
- avgDurationMs: totalRow.avgDurationMs,
2048
- };
2049
- }
2050
- // ─── Project settings ─────────────────────────────────────────────────────────
2051
- /**
2052
- * Default pre-prompt used by provider-capable Freestyle rails when the project
2053
- * has no per-project override. Freestyle skips the OpenSpec pipeline entirely:
2054
- * it hands the selected autonomous provider the spec text plus this instruction and lets it work
2055
- * autonomously end-to-end.
2056
- */
2057
- exports.DEFAULT_FREESTYLE_PRE_PROMPT = [
2058
- 'You are operating in FREESTYLE: fully autonomous, end-to-end implementation.',
2059
- 'Implement the following spec COMPLETELY in this repository. You have full access to the codebase and tools.',
2060
- 'Work independently until the feature is done: write the code, the tests, update docs as needed, and make sure everything builds and the test suite passes.',
2061
- 'Do NOT follow any structured architect/developer/reviewer pipeline — use your own judgement and the repo conventions.',
2062
- 'Never ask for confirmation; there is no human to answer. Choose the recommended option and proceed.',
2063
- 'Run every command in the FOREGROUND and wait for it to finish — never background a command (no run_in_background, no trailing `&`): your reply ends the step and anything still running is killed with its output lost.',
2064
- ].join('\n');
2065
- exports.WORKTREE_ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
2066
- const WORKTREE_ENV_MAX_NAMES = 64;
2067
- const WORKTREE_ENV_MAX_NAME_LENGTH = 128;
2068
- function normalizeWorktreeEnvPassthrough(value) {
2069
- if (!Array.isArray(value))
2070
- throw new Error('worktreeEnvPassthrough must be an array of environment variable names');
2071
- const out = [];
2072
- const seen = new Set();
2073
- for (const raw of value) {
2074
- if (typeof raw !== 'string')
2075
- throw new Error('worktreeEnvPassthrough entries must be strings');
2076
- const name = raw.trim();
2077
- if (!name)
2078
- continue;
2079
- if (name.length > WORKTREE_ENV_MAX_NAME_LENGTH)
2080
- throw new Error(`environment variable name is too long: ${name.slice(0, 24)}...`);
2081
- if (!exports.WORKTREE_ENV_NAME_RE.test(name))
2082
- throw new Error(`invalid environment variable name: ${name}`);
2083
- if (seen.has(name))
2084
- continue;
2085
- seen.add(name);
2086
- out.push(name);
2087
- if (out.length > WORKTREE_ENV_MAX_NAMES)
2088
- throw new Error(`worktreeEnvPassthrough can contain at most ${WORKTREE_ENV_MAX_NAMES} names`);
2089
- }
2090
- return out;
2091
- }
2092
- function parseWorktreeEnvPassthrough(raw) {
2093
- if (!raw)
2094
- return [];
2095
- try {
2096
- return normalizeWorktreeEnvPassthrough(JSON.parse(raw));
2097
- }
2098
- catch {
2099
- return [];
2100
- }
2101
- }
2102
- function getProjectSettings(db) {
2103
- const telemetryRow = db.prepare(`SELECT value FROM queue_state WHERE key = 'config.pipeline_telemetry_enabled'`).get();
2104
- const modelRow = db.prepare(`SELECT value FROM queue_state WHERE key = 'config.orchestrator_model'`).get();
2105
- const prePromptRow = db.prepare(`SELECT value FROM queue_state WHERE key = 'config.pre_prompt'`).get();
2106
- const freestylePrePromptRow = db.prepare(`SELECT value FROM queue_state WHERE key = 'config.freestyle_pre_prompt'`).get();
2107
- const integrationBranchRow = db.prepare(`SELECT value FROM queue_state WHERE key = 'config.integration_branch'`).get();
2108
- const worktreeEnvPassthroughRow = db.prepare(`SELECT value FROM queue_state WHERE key = 'config.worktree_env_passthrough'`).get();
2109
- return {
2110
- pipelineTelemetryEnabled: telemetryRow?.value !== 'false',
2111
- orchestratorModel: modelRow?.value ?? 'sonnet',
2112
- orchestratorModelExplicit: modelRow?.value !== undefined,
2113
- prePrompt: prePromptRow?.value ?? '',
2114
- freestylePrePrompt: freestylePrePromptRow?.value ?? '',
2115
- integrationBranch: integrationBranchRow?.value ?? '',
2116
- worktreeEnvPassthrough: parseWorktreeEnvPassthrough(worktreeEnvPassthroughRow?.value),
2117
- };
2118
- }
2119
- /** Resolve the effective Freestyle pre-prompt: the per-project override when
2120
- * set, otherwise the built-in default. */
2121
- function getFreestylePrePrompt(db) {
2122
- const override = getProjectSettings(db).freestylePrePrompt.trim();
2123
- return override || exports.DEFAULT_FREESTYLE_PRE_PROMPT;
2124
- }
2125
- function updateProjectSettings(db, patch) {
2126
- if (patch.pipelineTelemetryEnabled !== undefined) {
2127
- db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.pipeline_telemetry_enabled', ?)`).run(patch.pipelineTelemetryEnabled ? 'true' : 'false');
2128
- }
2129
- if (patch.orchestratorModel !== undefined) {
2130
- db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.orchestrator_model', ?)`).run(patch.orchestratorModel);
2131
- }
2132
- if (patch.prePrompt !== undefined) {
2133
- if (patch.prePrompt.trim() === '') {
2134
- db.prepare(`DELETE FROM queue_state WHERE key = 'config.pre_prompt'`).run();
2135
- }
2136
- else {
2137
- db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.pre_prompt', ?)`).run(patch.prePrompt);
2138
- }
2139
- }
2140
- if (patch.freestylePrePrompt !== undefined) {
2141
- if (patch.freestylePrePrompt.trim() === '') {
2142
- db.prepare(`DELETE FROM queue_state WHERE key = 'config.freestyle_pre_prompt'`).run();
2143
- }
2144
- else {
2145
- db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.freestyle_pre_prompt', ?)`).run(patch.freestylePrePrompt);
2146
- }
2147
- }
2148
- if (patch.integrationBranch !== undefined) {
2149
- if (patch.integrationBranch.trim() === '') {
2150
- db.prepare(`DELETE FROM queue_state WHERE key = 'config.integration_branch'`).run();
2151
- }
2152
- else {
2153
- db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.integration_branch', ?)`).run(patch.integrationBranch.trim());
2154
- }
2155
- }
2156
- if (patch.worktreeEnvPassthrough !== undefined) {
2157
- const names = normalizeWorktreeEnvPassthrough(patch.worktreeEnvPassthrough);
2158
- if (names.length === 0) {
2159
- db.prepare(`DELETE FROM queue_state WHERE key = 'config.worktree_env_passthrough'`).run();
2160
- }
2161
- else {
2162
- db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.worktree_env_passthrough', ?)`).run(JSON.stringify(names));
2163
- }
2164
- }
2165
- }
2166
- // ─── Explore Spec acceleration ────────────────────────────────────────────────
2167
- /**
2168
- * Per-project last-used value for the Quick mode Contract Refine toggle in
2169
- * the Add Spec modal. Default `false` when never set.
2170
- */
2171
- function getQuickContractRefineLast(db) {
2172
- const row = db.prepare(`SELECT value FROM queue_state WHERE key = 'config.add_spec_quick_contract_refine_last'`).get();
2173
- return row?.value === 'true';
2174
- }
2175
- function hasQuickContractRefineLast(db) {
2176
- const row = db.prepare(`SELECT 1 FROM queue_state WHERE key = 'config.add_spec_quick_contract_refine_last'`).get();
2177
- return !!row;
2178
- }
2179
- function setQuickContractRefineLast(db, enabled) {
2180
- db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.add_spec_quick_contract_refine_last', ?)`).run(enabled ? 'true' : 'false');
2181
- }
2182
- function getTelemetryBlob(db, jobId) {
2183
- return db.prepare('SELECT * FROM telemetry_blobs WHERE jobId = ?').get(jobId);
2184
- }
2185
- function upsertTelemetryBlob(db, row) {
2186
- db.prepare(`
2187
- INSERT INTO telemetry_blobs (jobId, path, byteSize, startedAt, endedAt, state)
2188
- VALUES (?, ?, ?, ?, ?, ?)
2189
- ON CONFLICT(jobId) DO UPDATE SET
2190
- path = excluded.path,
2191
- byteSize = excluded.byteSize,
2192
- startedAt = COALESCE(telemetry_blobs.startedAt, excluded.startedAt),
2193
- endedAt = excluded.endedAt,
2194
- state = excluded.state
2195
- `).run(row.jobId, row.path ?? null, row.byteSize, row.startedAt ?? null, row.endedAt ?? null, row.state);
2196
- }
2197
- function listActiveTelemetryBlobs(db) {
2198
- return db.prepare(`SELECT * FROM telemetry_blobs WHERE state = 'active'`).all();
2199
- }
2200
- function setTelemetryBlobCompacted(db, jobId) {
2201
- db.prepare(`UPDATE telemetry_blobs SET state = 'compacted', path = NULL WHERE jobId = ?`).run(jobId);
2202
- }
2203
- function insertTelemetrySummary(db, row) {
2204
- db.prepare(`
2205
- INSERT OR REPLACE INTO telemetry_summaries
2206
- (jobId, phase, durationMs, tokensInput, tokensOutput, tokensCache, toolCalls, apiErrors, costUsd)
2207
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
2208
- `).run(row.jobId, row.phase, row.durationMs ?? null, row.tokensInput ?? null, row.tokensOutput ?? null, row.tokensCache ?? null, row.toolCalls ?? null, row.apiErrors ?? null, row.costUsd ?? null);
2209
- }
2210
- function getTelemetrySummaries(db, jobId) {
2211
- return db.prepare('SELECT * FROM telemetry_summaries WHERE jobId = ?').all(jobId);
2212
- }
2213
- function deleteTelemetryForJob(db, jobId) {
2214
- db.prepare('DELETE FROM telemetry_blobs WHERE jobId = ?').run(jobId);
2215
- db.prepare('DELETE FROM telemetry_summaries WHERE jobId = ?').run(jobId);
2216
- }
2217
- /** Returns a Set of jobIds that have active or compacted telemetry blobs. */
2218
- function getJobsWithTelemetry(db) {
2219
- const rows = db.prepare(`SELECT jobId FROM telemetry_blobs WHERE state IN ('active','compacted')`).all();
2220
- return new Set(rows.map((r) => r.jobId));
2221
- }
2222
- /** True iff the job has an active or compacted telemetry blob row. */
2223
- function hasJobTelemetry(db, jobId) {
2224
- const row = db.prepare(`SELECT 1 FROM telemetry_blobs WHERE jobId = ? AND state IN ('active','compacted') LIMIT 1`).get(jobId);
2225
- return row !== undefined;
2226
- }
3
+ exports.setTelemetryBlobCompacted = exports.listActiveTelemetryBlobs = exports.upsertTelemetryBlob = exports.getTelemetryBlob = exports.setQuickContractRefineLast = exports.hasQuickContractRefineLast = exports.getQuickContractRefineLast = exports.updateProjectSettings = exports.getFreestylePrePrompt = exports.getProjectSettings = exports.normalizeWorktreeEnvPassthrough = exports.WORKTREE_ENV_NAME_RE = exports.DEFAULT_FREESTYLE_PRE_PROMPT = exports.getStats = exports.deleteTemplate = exports.updateTemplate = exports.getTemplate = exports.listTemplates = exports.createTemplate = exports.deleteProposal = exports.updateProposal = exports.listProposals = exports.getProposal = exports.createProposal = exports.getMessages = exports.addMessage = exports.updateConversation = exports.deleteConversation = exports.getConversation = exports.listConversations = exports.createConversation = exports.getProjectActivity = exports.getPipelineJobs = exports.skipJob = exports.purgeJobs = exports.deleteJob = exports.JobRecoveryPendingError = exports.getJobEvents = exports.getJob = exports.listJobs = exports.upsertPhase = exports.appendEvent = exports.finishJob = exports.finalizeInteractiveJob = exports.accumulateInteractiveTurn = exports.markJobInteractive = exports.deleteQueuedJob = exports.upsertQueuedJob = exports.createJob = exports.initDb = void 0;
4
+ exports.hasJobTelemetry = exports.getJobsWithTelemetry = exports.deleteTelemetryForJob = exports.getTelemetrySummaries = exports.insertTelemetrySummary = void 0;
5
+ var connection_1 = require("./db/connection");
6
+ Object.defineProperty(exports, "initDb", { enumerable: true, get: function () { return connection_1.initDb; } });
7
+ var jobs_1 = require("./db/jobs");
8
+ Object.defineProperty(exports, "createJob", { enumerable: true, get: function () { return jobs_1.createJob; } });
9
+ Object.defineProperty(exports, "upsertQueuedJob", { enumerable: true, get: function () { return jobs_1.upsertQueuedJob; } });
10
+ Object.defineProperty(exports, "deleteQueuedJob", { enumerable: true, get: function () { return jobs_1.deleteQueuedJob; } });
11
+ Object.defineProperty(exports, "markJobInteractive", { enumerable: true, get: function () { return jobs_1.markJobInteractive; } });
12
+ Object.defineProperty(exports, "accumulateInteractiveTurn", { enumerable: true, get: function () { return jobs_1.accumulateInteractiveTurn; } });
13
+ Object.defineProperty(exports, "finalizeInteractiveJob", { enumerable: true, get: function () { return jobs_1.finalizeInteractiveJob; } });
14
+ Object.defineProperty(exports, "finishJob", { enumerable: true, get: function () { return jobs_1.finishJob; } });
15
+ Object.defineProperty(exports, "appendEvent", { enumerable: true, get: function () { return jobs_1.appendEvent; } });
16
+ Object.defineProperty(exports, "upsertPhase", { enumerable: true, get: function () { return jobs_1.upsertPhase; } });
17
+ Object.defineProperty(exports, "listJobs", { enumerable: true, get: function () { return jobs_1.listJobs; } });
18
+ Object.defineProperty(exports, "getJob", { enumerable: true, get: function () { return jobs_1.getJob; } });
19
+ Object.defineProperty(exports, "getJobEvents", { enumerable: true, get: function () { return jobs_1.getJobEvents; } });
20
+ Object.defineProperty(exports, "JobRecoveryPendingError", { enumerable: true, get: function () { return jobs_1.JobRecoveryPendingError; } });
21
+ Object.defineProperty(exports, "deleteJob", { enumerable: true, get: function () { return jobs_1.deleteJob; } });
22
+ Object.defineProperty(exports, "purgeJobs", { enumerable: true, get: function () { return jobs_1.purgeJobs; } });
23
+ Object.defineProperty(exports, "skipJob", { enumerable: true, get: function () { return jobs_1.skipJob; } });
24
+ Object.defineProperty(exports, "getPipelineJobs", { enumerable: true, get: function () { return jobs_1.getPipelineJobs; } });
25
+ var activity_1 = require("./db/activity");
26
+ Object.defineProperty(exports, "getProjectActivity", { enumerable: true, get: function () { return activity_1.getProjectActivity; } });
27
+ var conversations_1 = require("./db/conversations");
28
+ Object.defineProperty(exports, "createConversation", { enumerable: true, get: function () { return conversations_1.createConversation; } });
29
+ Object.defineProperty(exports, "listConversations", { enumerable: true, get: function () { return conversations_1.listConversations; } });
30
+ Object.defineProperty(exports, "getConversation", { enumerable: true, get: function () { return conversations_1.getConversation; } });
31
+ Object.defineProperty(exports, "deleteConversation", { enumerable: true, get: function () { return conversations_1.deleteConversation; } });
32
+ Object.defineProperty(exports, "updateConversation", { enumerable: true, get: function () { return conversations_1.updateConversation; } });
33
+ Object.defineProperty(exports, "addMessage", { enumerable: true, get: function () { return conversations_1.addMessage; } });
34
+ Object.defineProperty(exports, "getMessages", { enumerable: true, get: function () { return conversations_1.getMessages; } });
35
+ var proposals_1 = require("./db/proposals");
36
+ Object.defineProperty(exports, "createProposal", { enumerable: true, get: function () { return proposals_1.createProposal; } });
37
+ Object.defineProperty(exports, "getProposal", { enumerable: true, get: function () { return proposals_1.getProposal; } });
38
+ Object.defineProperty(exports, "listProposals", { enumerable: true, get: function () { return proposals_1.listProposals; } });
39
+ Object.defineProperty(exports, "updateProposal", { enumerable: true, get: function () { return proposals_1.updateProposal; } });
40
+ Object.defineProperty(exports, "deleteProposal", { enumerable: true, get: function () { return proposals_1.deleteProposal; } });
41
+ var templates_1 = require("./db/templates");
42
+ Object.defineProperty(exports, "createTemplate", { enumerable: true, get: function () { return templates_1.createTemplate; } });
43
+ Object.defineProperty(exports, "listTemplates", { enumerable: true, get: function () { return templates_1.listTemplates; } });
44
+ Object.defineProperty(exports, "getTemplate", { enumerable: true, get: function () { return templates_1.getTemplate; } });
45
+ Object.defineProperty(exports, "updateTemplate", { enumerable: true, get: function () { return templates_1.updateTemplate; } });
46
+ Object.defineProperty(exports, "deleteTemplate", { enumerable: true, get: function () { return templates_1.deleteTemplate; } });
47
+ var stats_1 = require("./db/stats");
48
+ Object.defineProperty(exports, "getStats", { enumerable: true, get: function () { return stats_1.getStats; } });
49
+ var settings_1 = require("./db/settings");
50
+ Object.defineProperty(exports, "DEFAULT_FREESTYLE_PRE_PROMPT", { enumerable: true, get: function () { return settings_1.DEFAULT_FREESTYLE_PRE_PROMPT; } });
51
+ Object.defineProperty(exports, "WORKTREE_ENV_NAME_RE", { enumerable: true, get: function () { return settings_1.WORKTREE_ENV_NAME_RE; } });
52
+ Object.defineProperty(exports, "normalizeWorktreeEnvPassthrough", { enumerable: true, get: function () { return settings_1.normalizeWorktreeEnvPassthrough; } });
53
+ Object.defineProperty(exports, "getProjectSettings", { enumerable: true, get: function () { return settings_1.getProjectSettings; } });
54
+ Object.defineProperty(exports, "getFreestylePrePrompt", { enumerable: true, get: function () { return settings_1.getFreestylePrePrompt; } });
55
+ Object.defineProperty(exports, "updateProjectSettings", { enumerable: true, get: function () { return settings_1.updateProjectSettings; } });
56
+ Object.defineProperty(exports, "getQuickContractRefineLast", { enumerable: true, get: function () { return settings_1.getQuickContractRefineLast; } });
57
+ Object.defineProperty(exports, "hasQuickContractRefineLast", { enumerable: true, get: function () { return settings_1.hasQuickContractRefineLast; } });
58
+ Object.defineProperty(exports, "setQuickContractRefineLast", { enumerable: true, get: function () { return settings_1.setQuickContractRefineLast; } });
59
+ var telemetry_1 = require("./db/telemetry");
60
+ Object.defineProperty(exports, "getTelemetryBlob", { enumerable: true, get: function () { return telemetry_1.getTelemetryBlob; } });
61
+ Object.defineProperty(exports, "upsertTelemetryBlob", { enumerable: true, get: function () { return telemetry_1.upsertTelemetryBlob; } });
62
+ Object.defineProperty(exports, "listActiveTelemetryBlobs", { enumerable: true, get: function () { return telemetry_1.listActiveTelemetryBlobs; } });
63
+ Object.defineProperty(exports, "setTelemetryBlobCompacted", { enumerable: true, get: function () { return telemetry_1.setTelemetryBlobCompacted; } });
64
+ Object.defineProperty(exports, "insertTelemetrySummary", { enumerable: true, get: function () { return telemetry_1.insertTelemetrySummary; } });
65
+ Object.defineProperty(exports, "getTelemetrySummaries", { enumerable: true, get: function () { return telemetry_1.getTelemetrySummaries; } });
66
+ Object.defineProperty(exports, "deleteTelemetryForJob", { enumerable: true, get: function () { return telemetry_1.deleteTelemetryForJob; } });
67
+ Object.defineProperty(exports, "getJobsWithTelemetry", { enumerable: true, get: function () { return telemetry_1.getJobsWithTelemetry; } });
68
+ Object.defineProperty(exports, "hasJobTelemetry", { enumerable: true, get: function () { return telemetry_1.hasJobTelemetry; } });