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
@@ -0,0 +1,1521 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyMigrations = applyMigrations;
4
+ const sqlite_migrations_1 = require("../util/sqlite-migrations");
5
+ const multi_repo_execution_store_1 = require("../modules/delivery/runtime/multi-repo-execution-store");
6
+ const project_repository_provenance_1 = require("../project-repository-provenance");
7
+ const file_story_1 = require("../modules/code/runtime/file-story");
8
+ /** Backfill only terminal Safe-PR rows whose ticket ownership is unambiguous.
9
+ * Eligibility is intentionally deferred to the outbox drainer, which freezes
10
+ * only candidates still parked at on_review in the external ticket JSON. */
11
+ function backfillTerminalRailPrTicketEffects(db) {
12
+ const terminalRows = db.prepare(`
13
+ SELECT id, ticket_ids, run_ids, decision, is_continuation,
14
+ supersedes_delivery_id, pr_url
15
+ FROM rail_pr_deliveries
16
+ WHERE decision = 'merged'
17
+ OR (decision = 'discarded' AND is_continuation = 0
18
+ AND supersedes_delivery_id IS NULL AND pr_url IS NULL)
19
+ `).all();
20
+ const insertEffect = db.prepare(`
21
+ INSERT OR IGNORE INTO rail_pr_ticket_effects (
22
+ delivery_id, ticket_ids, causal_owners, target_status, jira_action, pr_url
23
+ ) VALUES (?, ?, ?, ?, ?, ?)
24
+ `);
25
+ const currentOwner = db.prepare(`
26
+ SELECT owner_id FROM ticket_outcome_ownership WHERE ticket_id = ?
27
+ `);
28
+ for (const row of terminalRows) {
29
+ let ticketIds = [];
30
+ let runIds = [];
31
+ try {
32
+ const parsed = JSON.parse(row.ticket_ids);
33
+ if (Array.isArray(parsed)) {
34
+ ticketIds = [...new Set(parsed.filter((id) => Number.isSafeInteger(id) && id > 0))];
35
+ }
36
+ }
37
+ catch { /* malformed historical evidence is not safe to act on */ }
38
+ try {
39
+ const parsed = JSON.parse(row.run_ids);
40
+ if (Array.isArray(parsed)) {
41
+ runIds = [...new Set(parsed.filter((id) => typeof id === 'string' && id.length > 0))];
42
+ }
43
+ }
44
+ catch { /* absent causal evidence means the historical effect is unsafe */ }
45
+ if (ticketIds.length === 0 || runIds.length === 0)
46
+ continue;
47
+ const allowedOwners = new Set(runIds);
48
+ const causalOwners = {};
49
+ ticketIds = ticketIds.filter((ticketId) => {
50
+ const owner = currentOwner.get(ticketId);
51
+ if (!owner || !allowedOwners.has(owner.owner_id))
52
+ return false;
53
+ causalOwners[String(ticketId)] = owner.owner_id;
54
+ return true;
55
+ });
56
+ if (ticketIds.length === 0)
57
+ continue;
58
+ const merged = row.decision === 'merged';
59
+ insertEffect.run(row.id, JSON.stringify(ticketIds), JSON.stringify(causalOwners), merged ? 'done' : 'todo',
60
+ // A v49 fresh discard has lost whether it meant Refine or a real
61
+ // discard. Recover the backlog status without inventing either cause.
62
+ merged ? 'merged' : 'backlog', merged ? row.pr_url : null);
63
+ }
64
+ }
65
+ const MIGRATIONS = [
66
+ // Migration 1: initial schema
67
+ (db) => {
68
+ db.exec(`
69
+ CREATE TABLE IF NOT EXISTS schema_migrations (
70
+ version INTEGER PRIMARY KEY,
71
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
72
+ );
73
+
74
+ CREATE TABLE IF NOT EXISTS jobs (
75
+ id TEXT PRIMARY KEY,
76
+ command TEXT NOT NULL,
77
+ started_at TEXT NOT NULL,
78
+ finished_at TEXT,
79
+ status TEXT NOT NULL DEFAULT 'running',
80
+ exit_code INTEGER,
81
+ tokens_in INTEGER,
82
+ tokens_out INTEGER,
83
+ tokens_cache_read INTEGER,
84
+ tokens_cache_create INTEGER,
85
+ total_cost_usd REAL,
86
+ num_turns INTEGER,
87
+ model TEXT,
88
+ duration_ms INTEGER,
89
+ duration_api_ms INTEGER,
90
+ session_id TEXT
91
+ );
92
+
93
+ CREATE INDEX IF NOT EXISTS idx_jobs_started_at ON jobs(started_at);
94
+ CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
95
+
96
+ CREATE TABLE IF NOT EXISTS events (
97
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
98
+ job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
99
+ seq INTEGER NOT NULL,
100
+ event_type TEXT NOT NULL,
101
+ source TEXT,
102
+ payload TEXT NOT NULL,
103
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
104
+ );
105
+
106
+ CREATE INDEX IF NOT EXISTS idx_events_job_id ON events(job_id);
107
+
108
+ CREATE TABLE IF NOT EXISTS job_phases (
109
+ job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
110
+ phase TEXT NOT NULL,
111
+ state TEXT NOT NULL,
112
+ updated_at TEXT NOT NULL,
113
+ PRIMARY KEY (job_id, phase)
114
+ );
115
+ `);
116
+ },
117
+ // Migration 2: add queue_position column to jobs
118
+ (db) => {
119
+ db.exec(`
120
+ ALTER TABLE jobs ADD COLUMN queue_position INTEGER;
121
+ `);
122
+ },
123
+ // Migration 3: add queue_state table for persisting queue config (e.g., paused)
124
+ (db) => {
125
+ db.exec(`
126
+ CREATE TABLE IF NOT EXISTS queue_state (
127
+ key TEXT PRIMARY KEY,
128
+ value TEXT NOT NULL
129
+ );
130
+
131
+ INSERT OR IGNORE INTO queue_state (key, value) VALUES ('paused', 'false');
132
+ `);
133
+ },
134
+ // Migration 4: chat conversations and messages
135
+ (db) => {
136
+ db.exec(`
137
+ CREATE TABLE IF NOT EXISTS chat_conversations (
138
+ id TEXT PRIMARY KEY,
139
+ title TEXT,
140
+ model TEXT NOT NULL DEFAULT 'sonnet',
141
+ session_id TEXT,
142
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
143
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
144
+ );
145
+
146
+ CREATE TABLE IF NOT EXISTS chat_messages (
147
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
148
+ conversation_id TEXT NOT NULL REFERENCES chat_conversations(id) ON DELETE CASCADE,
149
+ role TEXT NOT NULL CHECK(role IN ('user', 'assistant')),
150
+ content TEXT NOT NULL,
151
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
152
+ );
153
+
154
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_conv ON chat_messages(conversation_id);
155
+ `);
156
+ },
157
+ // Migration 5: proposals table
158
+ (db) => {
159
+ db.exec(`
160
+ CREATE TABLE IF NOT EXISTS proposals (
161
+ id TEXT PRIMARY KEY,
162
+ idea TEXT NOT NULL,
163
+ session_id TEXT,
164
+ status TEXT NOT NULL DEFAULT 'input',
165
+ result_markdown TEXT,
166
+ issue_url TEXT,
167
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
168
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
169
+ );
170
+ CREATE INDEX IF NOT EXISTS idx_proposals_status ON proposals(status);
171
+ CREATE INDEX IF NOT EXISTS idx_proposals_created_at ON proposals(created_at);
172
+ `);
173
+ },
174
+ // Migration 6: job templates
175
+ (db) => {
176
+ db.exec(`
177
+ CREATE TABLE IF NOT EXISTS job_templates (
178
+ id TEXT NOT NULL PRIMARY KEY,
179
+ name TEXT NOT NULL UNIQUE,
180
+ description TEXT,
181
+ commands TEXT NOT NULL,
182
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
183
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
184
+ );
185
+ CREATE INDEX IF NOT EXISTS idx_job_templates_created_at ON job_templates(created_at);
186
+ `);
187
+ },
188
+ // Migration 7: add priority column to jobs
189
+ (db) => {
190
+ db.exec(`
191
+ ALTER TABLE jobs ADD COLUMN priority TEXT NOT NULL DEFAULT 'normal';
192
+ `);
193
+ },
194
+ // Migration 8: job dependencies and pipelines
195
+ (db) => {
196
+ db.exec(`
197
+ ALTER TABLE jobs ADD COLUMN depends_on_job_id TEXT REFERENCES jobs(id);
198
+ ALTER TABLE jobs ADD COLUMN pipeline_id TEXT;
199
+ ALTER TABLE jobs ADD COLUMN skip_reason TEXT;
200
+ CREATE INDEX IF NOT EXISTS idx_jobs_depends_on ON jobs(depends_on_job_id);
201
+ CREATE INDEX IF NOT EXISTS idx_jobs_pipeline_id ON jobs(pipeline_id);
202
+ `);
203
+ },
204
+ // Migration 9: rails table for Rails board job integration
205
+ (db) => {
206
+ db.exec(`
207
+ CREATE TABLE IF NOT EXISTS rails (
208
+ rail_index INTEGER NOT NULL,
209
+ ticket_id INTEGER NOT NULL,
210
+ position INTEGER NOT NULL,
211
+ mode TEXT NOT NULL DEFAULT 'implement',
212
+ PRIMARY KEY (rail_index, ticket_id)
213
+ );
214
+ CREATE INDEX IF NOT EXISTS idx_rails_rail_index ON rails(rail_index);
215
+ `);
216
+ },
217
+ // Migration 10: pipeline telemetry blob and summary tables.
218
+ // The pipelineTelemetryEnabled flag reuses the existing queue_state key-value
219
+ // store (key = 'config.pipeline_telemetry_enabled') so no schema change needed
220
+ // for settings; only the raw-data tables are new.
221
+ (db) => {
222
+ db.exec(`
223
+ CREATE TABLE IF NOT EXISTS telemetry_blobs (
224
+ jobId TEXT PRIMARY KEY,
225
+ path TEXT,
226
+ byteSize INTEGER NOT NULL DEFAULT 0,
227
+ startedAt INTEGER,
228
+ endedAt INTEGER,
229
+ state TEXT NOT NULL DEFAULT 'active'
230
+ CHECK(state IN ('active','compacted','expired'))
231
+ );
232
+
233
+ CREATE TABLE IF NOT EXISTS telemetry_summaries (
234
+ jobId TEXT NOT NULL,
235
+ phase TEXT NOT NULL,
236
+ durationMs INTEGER,
237
+ tokensInput INTEGER,
238
+ tokensOutput INTEGER,
239
+ tokensCache INTEGER,
240
+ toolCalls TEXT,
241
+ apiErrors INTEGER,
242
+ costUsd REAL,
243
+ PRIMARY KEY (jobId, phase)
244
+ );
245
+ `);
246
+ },
247
+ // Migration 11: agent profiles — per-rail profile snapshots, custom agent
248
+ // version history, and sandboxed "test agent" run records. These back the
249
+ // Agents section (profiles + studio) added by add-agents-profiles.
250
+ (db) => {
251
+ db.exec(`
252
+ CREATE TABLE IF NOT EXISTS job_profiles (
253
+ job_id TEXT PRIMARY KEY,
254
+ profile_name TEXT NOT NULL,
255
+ profile_json TEXT NOT NULL,
256
+ created_at INTEGER NOT NULL
257
+ );
258
+
259
+ CREATE INDEX IF NOT EXISTS idx_job_profiles_name ON job_profiles(profile_name);
260
+
261
+ CREATE TABLE IF NOT EXISTS agent_versions (
262
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
263
+ agent_name TEXT NOT NULL,
264
+ version INTEGER NOT NULL,
265
+ body TEXT NOT NULL,
266
+ created_at INTEGER NOT NULL,
267
+ UNIQUE (agent_name, version)
268
+ );
269
+
270
+ CREATE INDEX IF NOT EXISTS idx_agent_versions_name ON agent_versions(agent_name);
271
+
272
+ CREATE TABLE IF NOT EXISTS agent_tests (
273
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
274
+ agent_name TEXT NOT NULL,
275
+ draft_hash TEXT NOT NULL,
276
+ sample_task_id TEXT,
277
+ tokens INTEGER,
278
+ duration_ms INTEGER,
279
+ output TEXT,
280
+ created_at INTEGER NOT NULL
281
+ );
282
+
283
+ CREATE INDEX IF NOT EXISTS idx_agent_tests_name ON agent_tests(agent_name);
284
+ `);
285
+ },
286
+ // Migration 12: remember per-rail agent profile selection across launches.
287
+ (db) => {
288
+ try {
289
+ db.exec(`ALTER TABLE rails ADD COLUMN profile_name TEXT`);
290
+ }
291
+ catch {
292
+ // Column may already exist (partially-migrated DB); no-op.
293
+ }
294
+ },
295
+ // Migration 13: agent_refine_sessions — in-flight AI Edit sessions for
296
+ // custom agents. Distinct from agent_versions (which is committed history);
297
+ // rows here are drafts in progress that may or may not be applied.
298
+ (db) => {
299
+ db.exec(`
300
+ CREATE TABLE IF NOT EXISTS agent_refine_sessions (
301
+ id TEXT PRIMARY KEY,
302
+ agent_id TEXT NOT NULL,
303
+ session_id TEXT,
304
+ base_version INTEGER NOT NULL,
305
+ base_body_hash TEXT NOT NULL,
306
+ draft_body TEXT,
307
+ history_json TEXT NOT NULL DEFAULT '[]',
308
+ phase TEXT NOT NULL DEFAULT 'idle',
309
+ status TEXT NOT NULL DEFAULT 'idle',
310
+ auto_test INTEGER NOT NULL DEFAULT 1,
311
+ last_test_at INTEGER,
312
+ last_test_hash TEXT,
313
+ created_at INTEGER NOT NULL,
314
+ updated_at INTEGER NOT NULL
315
+ );
316
+
317
+ CREATE INDEX IF NOT EXISTS idx_agent_refine_sessions_agent
318
+ ON agent_refine_sessions(agent_id, status);
319
+ CREATE INDEX IF NOT EXISTS idx_agent_refine_sessions_updated
320
+ ON agent_refine_sessions(updated_at);
321
+ `);
322
+ },
323
+ // Migration 14: terminal_settings_override — per-project key/value override
324
+ // for app-wide terminal settings. Absence of a row means "inherit app default".
325
+ (db) => {
326
+ db.exec(`
327
+ CREATE TABLE IF NOT EXISTS terminal_settings_override (
328
+ key TEXT PRIMARY KEY,
329
+ value TEXT NOT NULL
330
+ );
331
+ `);
332
+ },
333
+ // Migration 15: terminal_command_marks — per-session record of completed
334
+ // commands derived from OSC 133 prompt marks. FIFO-capped at 1000 rows per
335
+ // session by the marks store.
336
+ (db) => {
337
+ db.exec(`
338
+ CREATE TABLE IF NOT EXISTS terminal_command_marks (
339
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
340
+ session_id TEXT NOT NULL,
341
+ started_at INTEGER NOT NULL,
342
+ finished_at INTEGER,
343
+ exit_code INTEGER,
344
+ command TEXT,
345
+ cwd TEXT
346
+ );
347
+
348
+ CREATE INDEX IF NOT EXISTS idx_terminal_marks_session_started
349
+ ON terminal_command_marks(session_id, started_at);
350
+ `);
351
+ },
352
+ // Migration 16: ai_invocations — unified per-project AI CLI invocation
353
+ // tracking across surfaces (job, quick-spec, explore-spec, ai-edit).
354
+ (db) => {
355
+ db.exec(`
356
+ CREATE TABLE IF NOT EXISTS ai_invocations (
357
+ id TEXT PRIMARY KEY,
358
+ project_id TEXT NOT NULL,
359
+ surface TEXT NOT NULL,
360
+ surface_ref_id TEXT,
361
+ ticket_id INTEGER,
362
+ conversation_id TEXT,
363
+ model TEXT,
364
+ status TEXT NOT NULL,
365
+ started_at TEXT NOT NULL,
366
+ finished_at TEXT,
367
+ duration_ms INTEGER,
368
+ duration_api_ms INTEGER,
369
+ tokens_in INTEGER,
370
+ tokens_out INTEGER,
371
+ tokens_cache_read INTEGER,
372
+ tokens_cache_create INTEGER,
373
+ total_cost_usd REAL,
374
+ num_turns INTEGER,
375
+ session_id TEXT,
376
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
377
+ );
378
+
379
+ CREATE INDEX IF NOT EXISTS idx_ai_inv_project_started
380
+ ON ai_invocations(project_id, started_at DESC);
381
+ CREATE INDEX IF NOT EXISTS idx_ai_inv_project_surface
382
+ ON ai_invocations(project_id, surface);
383
+ CREATE INDEX IF NOT EXISTS idx_ai_inv_project_ticket
384
+ ON ai_invocations(project_id, ticket_id) WHERE ticket_id IS NOT NULL;
385
+ `);
386
+ },
387
+ // Migration 17: chat_conversations.kind — distinguishes Explore conversations
388
+ // (kind='explore') from sidebar chat (kind='sidebar'). Capture for ai_invocations
389
+ // is gated on kind='explore'.
390
+ (db) => {
391
+ db.exec(`
392
+ ALTER TABLE chat_conversations ADD COLUMN kind TEXT NOT NULL DEFAULT 'sidebar';
393
+ `);
394
+ },
395
+ // Migration 18: chat_conversations.context_scope — per-conversation JSON
396
+ // freezing the Add Spec context scope at creation time. NULL means "legacy
397
+ // behavior" so existing rows behave unchanged.
398
+ //
399
+ // Idempotent: a parallel WIP branch shipped this column under migration #20,
400
+ // so on machines where it already exists we swallow the duplicate-column
401
+ // error rather than crash on a re-run.
402
+ (db) => {
403
+ try {
404
+ db.exec(`ALTER TABLE chat_conversations ADD COLUMN context_scope TEXT;`);
405
+ }
406
+ catch (err) {
407
+ const msg = err.message ?? '';
408
+ if (!/duplicate column name/i.test(msg))
409
+ throw err;
410
+ }
411
+ },
412
+ // Migration 19: ai_invocations.provider — provider id stamped at insert.
413
+ // Existing rows backfill to 'claude' since pre-migration that was the only
414
+ // path. New rows MUST be populated from the resolved adapter's id (see
415
+ // openspec/changes/add-multi-provider-support/specs/project-spending/spec.md).
416
+ //
417
+ // Idempotent: same dual-WIP concern — multi-provider branch originally
418
+ // numbered this #18, so on machines that ran the pre-merge multi-provider
419
+ // build the column already exists.
420
+ (db) => {
421
+ try {
422
+ db.exec(`ALTER TABLE ai_invocations ADD COLUMN provider TEXT;`);
423
+ }
424
+ catch (err) {
425
+ const msg = err.message ?? '';
426
+ if (!/duplicate column name/i.test(msg))
427
+ throw err;
428
+ }
429
+ db.exec(`UPDATE ai_invocations SET provider = 'claude' WHERE provider IS NULL;`);
430
+ db.exec(`
431
+ CREATE INDEX IF NOT EXISTS idx_ai_inv_project_provider
432
+ ON ai_invocations(project_id, provider);
433
+ `);
434
+ },
435
+ // Migration 20: ai_invocations.total_cost_usd_estimated — 1 when the cost
436
+ // came from server/modules/accounting/runtime/pricing.ts (estimated fallback for non-native-cost
437
+ // providers); 0 when authoritative from the provider's terminal event.
438
+ //
439
+ // Idempotent for the same reason as #18/#19.
440
+ (db) => {
441
+ try {
442
+ db.exec(`
443
+ ALTER TABLE ai_invocations
444
+ ADD COLUMN total_cost_usd_estimated INTEGER NOT NULL DEFAULT 0;
445
+ `);
446
+ }
447
+ catch (err) {
448
+ const msg = err.message ?? '';
449
+ if (!/duplicate column name/i.test(msg))
450
+ throw err;
451
+ }
452
+ },
453
+ // Migration 21: self-heal `ai_invocations.provider` and
454
+ // `ai_invocations.total_cost_usd_estimated` for projects whose
455
+ // `schema_migrations` table marked versions 19 / 20 as applied without the
456
+ // corresponding ALTER actually running (parallel WIP branches reshuffled
457
+ // migration indices during development, leaving some on-disk DBs with the
458
+ // applied row but no column). Uses `PRAGMA table_info` so we only ALTER
459
+ // when the column is genuinely missing — safe to re-run.
460
+ (db) => {
461
+ const cols = new Set(db.prepare(`PRAGMA table_info(ai_invocations)`).all()
462
+ .map((r) => r.name));
463
+ if (!cols.has('provider')) {
464
+ db.exec(`ALTER TABLE ai_invocations ADD COLUMN provider TEXT;`);
465
+ db.exec(`UPDATE ai_invocations SET provider = 'claude' WHERE provider IS NULL;`);
466
+ db.exec(`
467
+ CREATE INDEX IF NOT EXISTS idx_ai_inv_project_provider
468
+ ON ai_invocations(project_id, provider);
469
+ `);
470
+ }
471
+ if (!cols.has('total_cost_usd_estimated')) {
472
+ db.exec(`
473
+ ALTER TABLE ai_invocations
474
+ ADD COLUMN total_cost_usd_estimated INTEGER NOT NULL DEFAULT 0;
475
+ `);
476
+ }
477
+ },
478
+ // Migration 22: file_provenance — per-project file ⇄ ticket tracking,
479
+ // populated by the QueueManager post-job hook and consumed by the Code
480
+ // Explorer router + TicketDetailModal.
481
+ (db) => {
482
+ db.exec(`
483
+ CREATE TABLE IF NOT EXISTS file_provenance (
484
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
485
+ file_path TEXT NOT NULL,
486
+ ticket_id INTEGER,
487
+ job_id TEXT,
488
+ kind TEXT NOT NULL CHECK(kind IN ('created','modified','deleted')),
489
+ at INTEGER NOT NULL
490
+ );
491
+ CREATE INDEX IF NOT EXISTS idx_fp_path ON file_provenance(file_path);
492
+ CREATE INDEX IF NOT EXISTS idx_fp_ticket ON file_provenance(ticket_id);
493
+ CREATE INDEX IF NOT EXISTS idx_fp_at ON file_provenance(at DESC);
494
+ `);
495
+ },
496
+ // Migration 23: optional per-job file patch storage for Code Explorer
497
+ // provenance. Older provenance rows remain valid without a patch.
498
+ (db) => {
499
+ db.exec(`
500
+ CREATE TABLE IF NOT EXISTS file_provenance_diffs (
501
+ provenance_id INTEGER PRIMARY KEY REFERENCES file_provenance(id) ON DELETE CASCADE,
502
+ patch TEXT NOT NULL,
503
+ truncated INTEGER NOT NULL DEFAULT 0
504
+ );
505
+ `);
506
+ },
507
+ // Migration 24: chat_conversations.provider — per-conversation AI engine for
508
+ // multi-provider projects. NULL means "fall back to the project's primary
509
+ // provider" (single-provider projects never set it, so behaviour is
510
+ // unchanged). Set at conversation creation from the Add Spec AI Engine
511
+ // selector; resume turns reuse it so the right CLI binary is spawned.
512
+ (db) => {
513
+ db.exec(`ALTER TABLE chat_conversations ADD COLUMN provider TEXT;`);
514
+ },
515
+ // Migration 25: rails.ai_engine — per-rail AI engine override for
516
+ // multi-provider projects. NULL means "use the project's primary provider".
517
+ // Stored on every rail row alongside profile_name; getRail reads the first
518
+ // row's value.
519
+ (db) => {
520
+ db.exec(`ALTER TABLE rails ADD COLUMN ai_engine TEXT;`);
521
+ },
522
+ // Migration 26: self-heal the multi-provider columns. An earlier WIP of the
523
+ // multi-provider feature consumed migration versions 24 and 25 on some
524
+ // databases with DIFFERENT meaning — those DBs already record v24/v25 so
525
+ // Migrations 24/25 above are skipped, leaving `rails.ai_engine` (and possibly
526
+ // `chat_conversations.provider`) missing. This higher-numbered migration is
527
+ // guarded by column checks: a no-op on DBs where the columns already exist,
528
+ // and an additive repair everywhere else. (Mirrors the #18/#19 self-heal
529
+ // precedent in this file.)
530
+ (db) => {
531
+ const convCols = db.prepare("PRAGMA table_info(chat_conversations)").all().map((c) => c.name);
532
+ if (!convCols.includes('provider')) {
533
+ db.exec(`ALTER TABLE chat_conversations ADD COLUMN provider TEXT;`);
534
+ }
535
+ const railCols = db.prepare("PRAGMA table_info(rails)").all().map((c) => c.name);
536
+ if (!railCols.includes('ai_engine')) {
537
+ db.exec(`ALTER TABLE rails ADD COLUMN ai_engine TEXT;`);
538
+ }
539
+ },
540
+ // Migration 27: jobs.total_cost_usd_estimated — 1 when jobs.total_cost_usd
541
+ // came from server/modules/accounting/runtime/pricing.ts (estimated fallback for non-native-cost
542
+ // providers like codex); 0 when authoritative from the provider's terminal
543
+ // event. Mirrors the ai_invocations column (migration 20) so the app
544
+ // dashboard, budget enforcement, and webhook can distinguish a rate-card
545
+ // estimate from a provider-billed figure. Additive + idempotent.
546
+ (db) => {
547
+ const cols = db.prepare(`PRAGMA table_info(jobs)`).all().map((r) => r.name);
548
+ if (!cols.includes('total_cost_usd_estimated')) {
549
+ db.exec(`
550
+ ALTER TABLE jobs
551
+ ADD COLUMN total_cost_usd_estimated INTEGER NOT NULL DEFAULT 0;
552
+ `);
553
+ }
554
+ },
555
+ // Migration 28: rail_meta — per-rail display name, keyed by rail_index.
556
+ // The `rails` table stores name-less ticket rows (and has NO rows for an
557
+ // empty rail), so a rail's user-given name can't live there — a renamed but
558
+ // empty rail would lose its name. rail_meta is a separate, ticket-independent
559
+ // store so every rail (0/1/2) keeps its name regardless of assignments.
560
+ // NULL name = client falls back to the default "Rail N" label. This backs the
561
+ // desktop ⇄ mobile rail-name sync (broadcast via rail.updated).
562
+ (db) => {
563
+ db.exec(`
564
+ CREATE TABLE IF NOT EXISTS rail_meta (
565
+ rail_index INTEGER PRIMARY KEY,
566
+ name TEXT
567
+ );
568
+ `);
569
+ },
570
+ // Migration 29: Jira integration (per-project). Each project syncs with its
571
+ // own Jira board, so every Jira table lives here in the per-project jobs.sqlite
572
+ // and is keyed by nothing but its own rows. See docs/jira-integration-plan.md.
573
+ // - jira_connection: one row, the connection config (token stored encrypted).
574
+ // - jira_links: spec↔issue map keyed on the IMMUTABLE Jira numeric id.
575
+ // - jira_outbox: durable transactional write-back queue (status + comments).
576
+ (db) => {
577
+ db.exec(`
578
+ CREATE TABLE IF NOT EXISTS jira_connection (
579
+ project_id TEXT PRIMARY KEY,
580
+ base_url TEXT NOT NULL,
581
+ deployment TEXT NOT NULL,
582
+ api_version TEXT NOT NULL,
583
+ auth_scheme TEXT NOT NULL,
584
+ account_email TEXT,
585
+ jira_project_key TEXT NOT NULL,
586
+ jira_project_id TEXT NOT NULL,
587
+ encrypted_token TEXT,
588
+ enabled INTEGER NOT NULL DEFAULT 1,
589
+ status_map TEXT,
590
+ high_water_ms INTEGER,
591
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
592
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
593
+ );
594
+
595
+ CREATE TABLE IF NOT EXISTS jira_links (
596
+ local_id INTEGER PRIMARY KEY,
597
+ jira_issue_id TEXT NOT NULL UNIQUE,
598
+ jira_key TEXT,
599
+ jira_project_id TEXT NOT NULL,
600
+ deployment TEXT NOT NULL,
601
+ status_category TEXT,
602
+ state TEXT NOT NULL DEFAULT 'linked',
603
+ tombstoned INTEGER NOT NULL DEFAULT 0,
604
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
605
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
606
+ );
607
+ CREATE INDEX IF NOT EXISTS idx_jira_links_issue ON jira_links(jira_issue_id);
608
+
609
+ CREATE TABLE IF NOT EXISTS jira_outbox (
610
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
611
+ jira_issue_id TEXT NOT NULL,
612
+ op_type TEXT NOT NULL,
613
+ idempotency_key TEXT NOT NULL UNIQUE,
614
+ payload TEXT NOT NULL,
615
+ state TEXT NOT NULL DEFAULT 'pending',
616
+ attempts INTEGER NOT NULL DEFAULT 0,
617
+ next_attempt_at TEXT,
618
+ last_error TEXT,
619
+ dead_reason TEXT,
620
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
621
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
622
+ );
623
+ CREATE INDEX IF NOT EXISTS idx_jira_outbox_state ON jira_outbox(state);
624
+ CREATE INDEX IF NOT EXISTS idx_jira_outbox_issue ON jira_outbox(jira_issue_id);
625
+ `);
626
+ },
627
+ // Migration 30: Jira sprint custom-field id. The field that holds an issue's
628
+ // sprint(s) is a custom field whose id varies per instance; we discover it
629
+ // (schema com.pyxis.greenhopper.jira:gh-sprint) and cache it here. NULL =
630
+ // not yet checked, 'none' = checked and no sprint field exists, '<id>' = found.
631
+ (db) => {
632
+ try {
633
+ db.exec(`ALTER TABLE jira_connection ADD COLUMN sprint_field_id TEXT`);
634
+ }
635
+ catch {
636
+ // Column may already exist on a partially-migrated DB — no-op.
637
+ }
638
+ },
639
+ // Migration 31: Jira discard target status. The user-configured status name to
640
+ // which a discarded spec's issue is transitioned (instead of being deleted) in
641
+ // a Jira-synced project. NULL/empty = not configured (delete behaves normally).
642
+ (db) => {
643
+ try {
644
+ db.exec(`ALTER TABLE jira_connection ADD COLUMN discard_status TEXT`);
645
+ }
646
+ catch {
647
+ // Column may already exist on a partially-migrated DB — no-op.
648
+ }
649
+ },
650
+ // Migration 32: jobs.interactive — 1 when the job is an interactive persistent
651
+ // freestyle session (the user sends multiple prompts across turns; the job
652
+ // stays 'running' until an explicit finalize, at which point every turn's real
653
+ // tokens/cost/num_turns are already summed into the row and status flips to
654
+ // 'completed'); 0 (default) for standard autonomous jobs. Additive + idempotent
655
+ // (guarded by PRAGMA table_info, mirroring migrations 18–26).
656
+ (db) => {
657
+ const cols = db.prepare(`PRAGMA table_info(jobs)`).all().map((r) => r.name);
658
+ if (!cols.includes('interactive')) {
659
+ db.exec(`ALTER TABLE jobs ADD COLUMN interactive INTEGER NOT NULL DEFAULT 0`);
660
+ }
661
+ },
662
+ // Migration 33: loop_runs — per-project record of an executed Loop (the Loops
663
+ // feature). A loop DEFINITION is global (desktop.sqlite `loops`); a RUN happens
664
+ // in a project, bound to a rail + spec. Powers loop analytics (iterations,
665
+ // final outcome, success rate, totals) alongside the per-step ai_invocations
666
+ // rows (surface='loop', linked via loop_run_id). "Running" loop state is
667
+ // derived from rows here with status='running'.
668
+ (db) => {
669
+ db.exec(`
670
+ CREATE TABLE IF NOT EXISTS loop_runs (
671
+ id TEXT PRIMARY KEY,
672
+ project_id TEXT NOT NULL,
673
+ loop_id TEXT NOT NULL,
674
+ loop_name TEXT,
675
+ rail_index INTEGER,
676
+ ticket_id INTEGER,
677
+ provider TEXT,
678
+ model TEXT,
679
+ reasoning_effort TEXT,
680
+ status TEXT NOT NULL DEFAULT 'running',
681
+ final_outcome TEXT,
682
+ iteration_limit INTEGER NOT NULL DEFAULT 0,
683
+ iteration_count INTEGER NOT NULL DEFAULT 0,
684
+ total_cost_usd REAL NOT NULL DEFAULT 0,
685
+ total_tokens INTEGER NOT NULL DEFAULT 0,
686
+ total_duration_ms INTEGER NOT NULL DEFAULT 0,
687
+ started_at TEXT NOT NULL,
688
+ finished_at TEXT,
689
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
690
+ );
691
+ CREATE INDEX IF NOT EXISTS idx_loop_runs_project_started ON loop_runs(project_id, started_at DESC);
692
+ CREATE INDEX IF NOT EXISTS idx_loop_runs_outcome ON loop_runs(project_id, final_outcome);
693
+ CREATE INDEX IF NOT EXISTS idx_loop_runs_loop ON loop_runs(loop_id);
694
+ `);
695
+ },
696
+ // Migration 34: ai_invocations.loop_run_id — links a loop's per-iteration AI
697
+ // invocations (AI Step + Loop Decider, surface='loop') to their loop_runs row
698
+ // for run-level rollups. NULL for every non-loop invocation. Additive +
699
+ // idempotent (guarded by PRAGMA table_info, mirroring migrations 18–32).
700
+ (db) => {
701
+ const cols = db.prepare(`PRAGMA table_info(ai_invocations)`).all().map((r) => r.name);
702
+ if (!cols.includes('loop_run_id')) {
703
+ db.exec(`ALTER TABLE ai_invocations ADD COLUMN loop_run_id TEXT`);
704
+ 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`);
705
+ }
706
+ },
707
+ // Migration 35: rail_worktrees — per-(rail launch, ticket) ledger for parallel
708
+ // worktree isolation + merge-back. Lets a restarted server reconcile orphaned
709
+ // worktrees and lets the UI render integration state. Additive + idempotent.
710
+ (db) => {
711
+ db.exec(`
712
+ CREATE TABLE IF NOT EXISTS rail_worktrees (
713
+ id TEXT PRIMARY KEY,
714
+ rail_index INTEGER NOT NULL,
715
+ ticket_id INTEGER NOT NULL,
716
+ run_id TEXT,
717
+ branch TEXT NOT NULL,
718
+ worktree_path TEXT NOT NULL,
719
+ overlay_path TEXT,
720
+ merge_state TEXT NOT NULL DEFAULT 'building',
721
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
722
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
723
+ );
724
+ CREATE INDEX IF NOT EXISTS idx_rail_worktrees_state ON rail_worktrees(merge_state);
725
+ CREATE INDEX IF NOT EXISTS idx_rail_worktrees_rail ON rail_worktrees(rail_index);
726
+ `);
727
+ },
728
+ // Migration 36: rail_pr_deliveries — one row per isolated rail LAUNCH tracking
729
+ // the ask-first PR decision lifecycle (building → on_review → pr_draft →
730
+ // pr_ready → merged | discarded | implementation_failed | pr_failed). The durable single source of
731
+ // truth both decision surfaces (rail row + agent chat) read and write, so a
732
+ // refresh/restart never loses a pending PR decision. `branches` captures the
733
+ // per-unit DeliverBranch records at build-settle (deferred deliverRailAsPr
734
+ // cannot be reconstructed without them); `worktree_ids` links the
735
+ // rail_worktrees ledger rows for discard cleanup. Additive + idempotent.
736
+ (db) => {
737
+ db.exec(`
738
+ CREATE TABLE IF NOT EXISTS rail_pr_deliveries (
739
+ id TEXT PRIMARY KEY,
740
+ rail_index INTEGER NOT NULL,
741
+ loop_id TEXT,
742
+ rail_key TEXT NOT NULL,
743
+ ticket_ids TEXT NOT NULL,
744
+ base_branch TEXT NOT NULL,
745
+ branch TEXT,
746
+ pr_url TEXT,
747
+ pr_number INTEGER,
748
+ pr_state TEXT NOT NULL DEFAULT 'none',
749
+ decision TEXT NOT NULL DEFAULT 'building',
750
+ branches TEXT NOT NULL DEFAULT '[]',
751
+ loop_name TEXT NOT NULL DEFAULT '',
752
+ worktree_ids TEXT NOT NULL DEFAULT '[]',
753
+ origin_surface TEXT NOT NULL DEFAULT 'dashboard',
754
+ origin_conversation_id TEXT,
755
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
756
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
757
+ );
758
+ CREATE INDEX IF NOT EXISTS idx_rail_pr_deliveries_rail ON rail_pr_deliveries(rail_index);
759
+ CREATE INDEX IF NOT EXISTS idx_rail_pr_deliveries_active ON rail_pr_deliveries(decision);
760
+ `);
761
+ },
762
+ // Migration 37: file_story_contributions — per-intervention "construction
763
+ // story" data for the Code/Files explorer. One row per file_provenance row
764
+ // that had a collectable patch: line stats (added/removed), a ~4KB patch
765
+ // excerpt (the full patch stays in file_provenance_diffs), and a nullable
766
+ // plain-language AI `summary` of what that spec contributed to the file
767
+ // (generated on demand, budget-gated — see file-story-manager.ts). Written
768
+ // inside recordProvenanceForJob's transaction so EVERY provenance producer
769
+ // (queue-manager jobs AND the loop-run seam) gets stats for free. Additive.
770
+ (db) => {
771
+ db.exec(`
772
+ CREATE TABLE IF NOT EXISTS file_story_contributions (
773
+ provenance_id INTEGER PRIMARY KEY,
774
+ job_id TEXT,
775
+ file_path TEXT NOT NULL,
776
+ added_lines INTEGER NOT NULL DEFAULT 0,
777
+ removed_lines INTEGER NOT NULL DEFAULT 0,
778
+ patch_excerpt TEXT,
779
+ summary TEXT,
780
+ summary_model TEXT,
781
+ summary_generated_at TEXT
782
+ );
783
+ CREATE INDEX IF NOT EXISTS idx_file_story_contributions_path ON file_story_contributions(file_path);
784
+ `);
785
+ },
786
+ // Migration 38: rail_pr_deliveries.run_ids — JSON string[] of the loop-run
787
+ // ids an isolated rail launch fanned out (order matches ticket order; a
788
+ // scope='all' launch has exactly one). Persisted right after allocation so
789
+ // the PR decision surfaces (agent-chat card + dashboard strip) can render a
790
+ // per-run "View log" chip with live vitals that survives refresh/restart.
791
+ // Additive + idempotent (guarded by PRAGMA table_info, mirroring 18–36).
792
+ (db) => {
793
+ const cols = db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all().map((r) => r.name);
794
+ if (!cols.includes('run_ids')) {
795
+ db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN run_ids TEXT NOT NULL DEFAULT '[]'`);
796
+ }
797
+ },
798
+ // Migration 39: seed the three BASE rails (indices 0-2) into rail_meta.
799
+ // Dynamic rails make rail_meta the EXISTENCE authority (a rail exists iff it
800
+ // has a rail_meta identity row or leftover ticket rows) — seeding once here,
801
+ // instead of re-seeding on every read, lets a user genuinely delete a base
802
+ // rail (the router still guards: never the last one, never a non-empty or
803
+ // active one). INSERT OR IGNORE keeps any existing renamed-rail rows intact.
804
+ (db) => {
805
+ db.exec(`
806
+ INSERT OR IGNORE INTO rail_meta (rail_index, name) VALUES (0, NULL), (1, NULL), (2, NULL);
807
+ `);
808
+ },
809
+ // Migration 40: durable idempotency ledger for billable job spawns. Clients
810
+ // can safely retry a timed-out POST (or double-submit) without enqueueing a
811
+ // second job. Entries are deliberately short-lived and pruned by the route;
812
+ // the ledger is not job history and therefore has no foreign key to jobs.
813
+ (db) => {
814
+ db.exec(`
815
+ CREATE TABLE IF NOT EXISTS job_spawn_requests (
816
+ idempotency_key TEXT PRIMARY KEY,
817
+ fingerprint TEXT NOT NULL,
818
+ job_id TEXT NOT NULL,
819
+ created_at_ms INTEGER NOT NULL,
820
+ expires_at_ms INTEGER NOT NULL
821
+ );
822
+ CREATE INDEX IF NOT EXISTS idx_job_spawn_requests_expires
823
+ ON job_spawn_requests(expires_at_ms);
824
+ `);
825
+ },
826
+ // Migration 41: durable outbox for jobs left RUNNING by an ungraceful
827
+ // process exit. QueueManager atomically snapshots each orphan here before it
828
+ // flips the job to failed, then checkpoints accounting and the terminal
829
+ // domain callback independently. A second crash can therefore resume either
830
+ // step without duplicating the ai_invocations ledger or losing the callback.
831
+ (db) => {
832
+ db.exec(`
833
+ CREATE TABLE IF NOT EXISTS orphan_job_recovery (
834
+ job_id TEXT PRIMARY KEY,
835
+ payload TEXT NOT NULL,
836
+ accounting_completed INTEGER NOT NULL DEFAULT 0 CHECK (accounting_completed IN (0, 1)),
837
+ callback_completed INTEGER NOT NULL DEFAULT 0 CHECK (callback_completed IN (0, 1)),
838
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
839
+ );
840
+ CREATE INDEX IF NOT EXISTS idx_orphan_job_recovery_pending
841
+ ON orphan_job_recovery(accounting_completed, callback_completed);
842
+ `);
843
+ },
844
+ // Migration 42: persist the provider actually selected for each job and add
845
+ // a third durable recovery checkpoint for queue/pipeline terminal invariants.
846
+ // Both ALTERs are guarded so developer databases that already applied the
847
+ // unreleased orphan-outbox migration can advance safely.
848
+ (db) => {
849
+ const jobCols = db.prepare(`PRAGMA table_info(jobs)`).all().map((r) => r.name);
850
+ if (!jobCols.includes('provider')) {
851
+ db.exec(`ALTER TABLE jobs ADD COLUMN provider TEXT`);
852
+ }
853
+ const recoveryCols = db.prepare(`PRAGMA table_info(orphan_job_recovery)`).all().map((r) => r.name);
854
+ if (!recoveryCols.includes('terminal_completed')) {
855
+ db.exec(`
856
+ ALTER TABLE orphan_job_recovery
857
+ ADD COLUMN terminal_completed INTEGER NOT NULL DEFAULT 0
858
+ CHECK (terminal_completed IN (0, 1))
859
+ `);
860
+ }
861
+ },
862
+ // Migration 43: durable pre-start queue. This is intentionally separate from
863
+ // jobs: a queue admission is not an execution and therefore has no truthful
864
+ // jobs.started_at value. createJob atomically promotes a row out of this table
865
+ // when the provider process actually starts.
866
+ (db) => {
867
+ db.exec(`
868
+ CREATE TABLE IF NOT EXISTS queued_jobs (
869
+ id TEXT PRIMARY KEY,
870
+ command TEXT NOT NULL,
871
+ queue_position INTEGER,
872
+ priority TEXT NOT NULL DEFAULT 'normal',
873
+ depends_on_job_id TEXT,
874
+ pipeline_id TEXT,
875
+ provider TEXT,
876
+ model TEXT,
877
+ profile_name TEXT,
878
+ profile_selection_set INTEGER NOT NULL DEFAULT 0
879
+ CHECK (profile_selection_set IN (0, 1)),
880
+ interactive INTEGER CHECK (interactive IN (0, 1)),
881
+ causal_ownership INTEGER NOT NULL DEFAULT 0
882
+ CHECK (causal_ownership IN (0, 1)),
883
+ enqueued_at TEXT NOT NULL DEFAULT (datetime('now'))
884
+ );
885
+ CREATE INDEX IF NOT EXISTS idx_queued_jobs_position
886
+ ON queued_jobs(queue_position);
887
+ `);
888
+ },
889
+ // Migration 44: persist which manager owns terminal recovery for each jobs
890
+ // row. Before this boundary QueueManager swept every RUNNING row, including
891
+ // LoopRunManager's backing rows, and recorded their already-accounted step
892
+ // spend again as surface='job'. Existing loop rows are classified by their
893
+ // durable loop_runs identity; every other historical job remains queue-owned.
894
+ (db) => {
895
+ const jobCols = db.prepare(`PRAGMA table_info(jobs)`).all().map((r) => r.name);
896
+ if (!jobCols.includes('owner')) {
897
+ db.exec(`
898
+ ALTER TABLE jobs
899
+ ADD COLUMN owner TEXT NOT NULL DEFAULT 'queue'
900
+ CHECK (owner IN ('queue', 'loop'))
901
+ `);
902
+ }
903
+ db.exec(`
904
+ UPDATE jobs
905
+ SET owner = 'loop',
906
+ provider = COALESCE(
907
+ jobs.provider,
908
+ (SELECT loop_runs.provider FROM loop_runs WHERE loop_runs.id = jobs.id)
909
+ )
910
+ WHERE EXISTS (SELECT 1 FROM loop_runs WHERE loop_runs.id = jobs.id);
911
+ CREATE INDEX IF NOT EXISTS idx_jobs_owner_status ON jobs(owner, status);
912
+ `);
913
+ },
914
+ // Migration 45: crash-consistent loop terminal/accounting outboxes and
915
+ // causal ownership for rail/ticket effects. `loop_runs.ticket_ids_json`
916
+ // replaces the old startup heuristic ("one active run means scope=all") with
917
+ // the exact launch set. The recovery tables intentionally have no FK to
918
+ // jobs: terminal history is independently deletable while recovery effects
919
+ // still have to finish.
920
+ (db) => {
921
+ const loopCols = db.prepare(`PRAGMA table_info(loop_runs)`).all().map((r) => r.name);
922
+ if (!loopCols.includes('ticket_ids_json')) {
923
+ db.exec(`ALTER TABLE loop_runs ADD COLUMN ticket_ids_json TEXT NOT NULL DEFAULT '[]'`);
924
+ }
925
+ if (!loopCols.includes('ticket_completion_status')) {
926
+ db.exec(`
927
+ ALTER TABLE loop_runs
928
+ ADD COLUMN ticket_completion_status TEXT NOT NULL DEFAULT 'done'
929
+ CHECK (ticket_completion_status IN ('done', 'on_review'))
930
+ `);
931
+ }
932
+ if (!loopCols.includes('causal_ownership')) {
933
+ db.exec(`
934
+ ALTER TABLE loop_runs
935
+ ADD COLUMN causal_ownership INTEGER NOT NULL DEFAULT 0
936
+ CHECK (causal_ownership IN (0, 1))
937
+ `);
938
+ }
939
+ db.exec(`
940
+ CREATE TABLE IF NOT EXISTS loop_terminal_recovery (
941
+ run_id TEXT PRIMARY KEY,
942
+ payload TEXT NOT NULL,
943
+ callback_completed INTEGER NOT NULL DEFAULT 0
944
+ CHECK (callback_completed IN (0, 1)),
945
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
946
+ );
947
+
948
+ CREATE TABLE IF NOT EXISTS loop_step_recovery (
949
+ run_id TEXT NOT NULL,
950
+ step_key TEXT NOT NULL,
951
+ invocation_id TEXT NOT NULL UNIQUE,
952
+ payload TEXT NOT NULL,
953
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
954
+ PRIMARY KEY (run_id, step_key)
955
+ );
956
+ CREATE INDEX IF NOT EXISTS idx_loop_step_recovery_run
957
+ ON loop_step_recovery(run_id);
958
+
959
+ CREATE TABLE IF NOT EXISTS rail_ticket_ownership (
960
+ rail_index INTEGER NOT NULL,
961
+ ticket_id INTEGER NOT NULL,
962
+ owner_id TEXT NOT NULL,
963
+ generation INTEGER NOT NULL DEFAULT 1,
964
+ claimed_at TEXT NOT NULL DEFAULT (datetime('now')),
965
+ PRIMARY KEY (rail_index, ticket_id)
966
+ );
967
+ CREATE INDEX IF NOT EXISTS idx_rail_ticket_owner
968
+ ON rail_ticket_ownership(owner_id);
969
+
970
+ CREATE TABLE IF NOT EXISTS ticket_outcome_ownership (
971
+ ticket_id INTEGER PRIMARY KEY,
972
+ owner_id TEXT NOT NULL,
973
+ generation INTEGER NOT NULL DEFAULT 1,
974
+ claimed_at TEXT NOT NULL DEFAULT (datetime('now'))
975
+ );
976
+ CREATE INDEX IF NOT EXISTS idx_ticket_outcome_owner
977
+ ON ticket_outcome_ownership(owner_id);
978
+
979
+ -- Builds from the pre-owner WIP may already have captured loop backing
980
+ -- rows in QueueManager's outbox, or even checkpointed the resulting
981
+ -- surface='job' invocation. Those rows are unambiguously wrong: a jobs
982
+ -- id that is also a loop_runs id is exclusively loop-owned.
983
+ DELETE FROM orphan_job_recovery
984
+ WHERE job_id IN (SELECT id FROM loop_runs);
985
+ DELETE FROM ai_invocations
986
+ WHERE surface = 'job'
987
+ AND EXISTS (
988
+ SELECT 1 FROM loop_runs
989
+ WHERE ai_invocations.surface_ref_id = loop_runs.id
990
+ OR ai_invocations.surface_ref_id LIKE loop_runs.id || '#t%'
991
+ );
992
+ `);
993
+ },
994
+ // Migration 46: preserve every pre-spawn semantic override across restart.
995
+ // Nullable provider/model/interactive encode "use the spawn-time default";
996
+ // profile needs a separate presence bit because NULL itself means the
997
+ // deliberate force-legacy selection when the bit is set.
998
+ (db) => {
999
+ const cols = new Set(db.prepare(`PRAGMA table_info(queued_jobs)`).all()
1000
+ .map((row) => row.name));
1001
+ if (!cols.has('provider')) {
1002
+ db.exec(`ALTER TABLE queued_jobs ADD COLUMN provider TEXT`);
1003
+ }
1004
+ if (!cols.has('model')) {
1005
+ db.exec(`ALTER TABLE queued_jobs ADD COLUMN model TEXT`);
1006
+ }
1007
+ if (!cols.has('profile_name')) {
1008
+ db.exec(`ALTER TABLE queued_jobs ADD COLUMN profile_name TEXT`);
1009
+ }
1010
+ if (!cols.has('profile_selection_set')) {
1011
+ db.exec(`
1012
+ ALTER TABLE queued_jobs
1013
+ ADD COLUMN profile_selection_set INTEGER NOT NULL DEFAULT 0
1014
+ CHECK (profile_selection_set IN (0, 1))
1015
+ `);
1016
+ }
1017
+ if (!cols.has('interactive')) {
1018
+ db.exec(`
1019
+ ALTER TABLE queued_jobs
1020
+ ADD COLUMN interactive INTEGER CHECK (interactive IN (0, 1))
1021
+ `);
1022
+ }
1023
+ if (!cols.has('causal_ownership')) {
1024
+ db.exec(`
1025
+ ALTER TABLE queued_jobs
1026
+ ADD COLUMN causal_ownership INTEGER NOT NULL DEFAULT 0
1027
+ CHECK (causal_ownership IN (0, 1))
1028
+ `);
1029
+ }
1030
+ const jobCols = new Set(db.prepare(`PRAGMA table_info(jobs)`).all()
1031
+ .map((row) => row.name));
1032
+ if (!jobCols.has('causal_ownership')) {
1033
+ db.exec(`
1034
+ ALTER TABLE jobs
1035
+ ADD COLUMN causal_ownership INTEGER NOT NULL DEFAULT 0
1036
+ CHECK (causal_ownership IN (0, 1))
1037
+ `);
1038
+ }
1039
+ },
1040
+ // Migration 47: crash recovery reads the bounded newest provider frontier;
1041
+ // this composite index avoids sorting/scanning an unbounded transcript while
1042
+ // the queue is fail-stopped during startup.
1043
+ (db) => {
1044
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_events_job_seq ON events(job_id, seq)`);
1045
+ },
1046
+ // Migration 48: truthful Safe-PR settlement. Execution truth, delivery
1047
+ // readiness, continuation ownership and decision-operation ownership are
1048
+ // independent durable facts; the old decision/pr_state pair cannot explain
1049
+ // a successful run whose commit/ref/push later failed. Before enforcing the
1050
+ // intended one-active-generation-per-rail invariant, terminalize historical
1051
+ // duplicate rows deterministically (newest rowid wins) so an older iteration
1052
+ // can never resurface after the current one closes.
1053
+ (db) => {
1054
+ const cols = new Set(db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all()
1055
+ .map((row) => row.name));
1056
+ const add = (name, ddl) => {
1057
+ if (!cols.has(name))
1058
+ db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN ${ddl}`);
1059
+ };
1060
+ add('implementation_outcome', `implementation_outcome TEXT NOT NULL DEFAULT 'unknown'`);
1061
+ add('delivery_outcome', `delivery_outcome TEXT NOT NULL DEFAULT 'unknown'`);
1062
+ add('status_code', `status_code TEXT`);
1063
+ add('status_detail', `status_detail TEXT`);
1064
+ add('delivery_sha', `delivery_sha TEXT`);
1065
+ add('is_continuation', `is_continuation INTEGER NOT NULL DEFAULT 0 CHECK (is_continuation IN (0, 1))`);
1066
+ add('supersedes_delivery_id', `supersedes_delivery_id TEXT`);
1067
+ add('operation', `operation TEXT`);
1068
+ add('operation_token', `operation_token TEXT`);
1069
+ add('operation_started_at_ms', `operation_started_at_ms INTEGER`);
1070
+ add('cleanup_warnings', `cleanup_warnings TEXT NOT NULL DEFAULT '[]'`);
1071
+ db.exec(`
1072
+ UPDATE rail_pr_deliveries
1073
+ SET implementation_outcome = CASE decision
1074
+ WHEN 'building' THEN 'running'
1075
+ WHEN 'implementation_failed' THEN 'failed'
1076
+ WHEN 'on_review' THEN 'succeeded'
1077
+ WHEN 'pr_draft' THEN 'succeeded'
1078
+ WHEN 'pr_ready' THEN 'succeeded'
1079
+ WHEN 'pr_failed' THEN 'succeeded'
1080
+ WHEN 'merged' THEN 'succeeded'
1081
+ ELSE implementation_outcome
1082
+ END
1083
+ WHERE implementation_outcome = 'unknown';
1084
+
1085
+ UPDATE rail_pr_deliveries
1086
+ SET delivery_outcome = CASE decision
1087
+ WHEN 'building' THEN 'pending'
1088
+ WHEN 'implementation_failed' THEN 'not_started'
1089
+ WHEN 'on_review' THEN 'ready'
1090
+ WHEN 'pr_draft' THEN 'delivered'
1091
+ WHEN 'pr_ready' THEN 'delivered'
1092
+ WHEN 'pr_failed' THEN 'retryable_failure'
1093
+ WHEN 'merged' THEN 'delivered'
1094
+ ELSE delivery_outcome
1095
+ END
1096
+ WHERE delivery_outcome = 'unknown';
1097
+
1098
+ UPDATE rail_pr_deliveries
1099
+ SET status_code = CASE decision
1100
+ WHEN 'building' THEN 'implementation_running'
1101
+ WHEN 'implementation_failed' THEN 'implementation_failed'
1102
+ WHEN 'on_review' THEN 'ready_for_review'
1103
+ WHEN 'pr_draft' THEN 'pr_draft_ready'
1104
+ WHEN 'pr_ready' THEN 'pr_ready'
1105
+ WHEN 'pr_failed' THEN 'delivery_failed'
1106
+ WHEN 'merged' THEN 'merged'
1107
+ ELSE status_code
1108
+ END
1109
+ WHERE status_code IS NULL;
1110
+
1111
+ UPDATE rail_pr_deliveries
1112
+ SET decision = 'superseded',
1113
+ status_code = 'superseded',
1114
+ updated_at = datetime('now')
1115
+ WHERE decision NOT IN ('merged', 'discarded', 'superseded')
1116
+ AND rowid NOT IN (
1117
+ SELECT MAX(rowid)
1118
+ FROM rail_pr_deliveries
1119
+ WHERE decision NOT IN ('merged', 'discarded', 'superseded')
1120
+ GROUP BY rail_index
1121
+ );
1122
+
1123
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_rail_pr_deliveries_one_active_per_rail
1124
+ ON rail_pr_deliveries(rail_index)
1125
+ WHERE decision NOT IN ('merged', 'discarded', 'superseded');
1126
+ `);
1127
+ },
1128
+ // Migration 49: repair the exact legacy false-failure shape that motivated
1129
+ // the orthogonal lifecycle. Older settlement code could write
1130
+ // decision='implementation_failed' after every loop had durably succeeded,
1131
+ // solely because commit/ref delivery failed. Reclassify only rows whose full
1132
+ // run set is present and terminal; ambiguous rows remain conservative. This
1133
+ // migration also makes truthful no-change acknowledgement (`completed`) a
1134
+ // terminal generation for the one-active-per-rail invariant.
1135
+ (db) => {
1136
+ const legacyRows = db.prepare(`
1137
+ SELECT id, run_ids, branches
1138
+ FROM rail_pr_deliveries
1139
+ WHERE decision = 'implementation_failed'
1140
+ `).all();
1141
+ const update = db.prepare(`
1142
+ UPDATE rail_pr_deliveries
1143
+ SET decision = 'pr_failed',
1144
+ implementation_outcome = ?,
1145
+ delivery_outcome = 'blocked',
1146
+ status_code = 'settlement_interrupted',
1147
+ status_detail = ?,
1148
+ branches = ?,
1149
+ updated_at = datetime('now')
1150
+ WHERE id = ? AND decision = 'implementation_failed'
1151
+ `);
1152
+ for (const row of legacyRows) {
1153
+ let runIds = [];
1154
+ try {
1155
+ const parsed = JSON.parse(row.run_ids);
1156
+ if (Array.isArray(parsed)) {
1157
+ runIds = [...new Set(parsed.filter((value) => typeof value === 'string' && value.length > 0))];
1158
+ }
1159
+ }
1160
+ catch { /* malformed legacy evidence stays conservative */ }
1161
+ if (runIds.length === 0)
1162
+ continue;
1163
+ const placeholders = runIds.map(() => '?').join(',');
1164
+ const runs = db.prepare(`
1165
+ SELECT id, status, final_outcome
1166
+ FROM loop_runs
1167
+ WHERE id IN (${placeholders})
1168
+ `).all(...runIds);
1169
+ if (runs.length !== runIds.length || runs.some((run) => run.status !== 'completed'))
1170
+ continue;
1171
+ const outcomes = new Map(runs.map((run) => [run.id, run.final_outcome]));
1172
+ const succeeded = runIds.filter((runId) => outcomes.get(runId) === 'success').length;
1173
+ if (succeeded === 0)
1174
+ continue;
1175
+ let branches = [];
1176
+ try {
1177
+ const parsed = JSON.parse(row.branches);
1178
+ if (Array.isArray(parsed))
1179
+ branches = parsed;
1180
+ }
1181
+ catch { /* retain an empty evidence set rather than malformed JSON */ }
1182
+ const repairedBranches = branches.map((value, index) => {
1183
+ if (!value || typeof value !== 'object')
1184
+ return value;
1185
+ const unit = value;
1186
+ const runId = runIds[index];
1187
+ const implementationSucceeded = runId ? outcomes.get(runId) === 'success' : succeeded === runIds.length;
1188
+ return {
1189
+ ...unit,
1190
+ ...(runId ? { runId } : {}),
1191
+ implementationOutcome: implementationSucceeded ? 'succeeded' : 'failed',
1192
+ deliveryOutcome: implementationSucceeded ? 'blocked' : 'not_started',
1193
+ ...(implementationSucceeded ? { failureCode: 'settlement_interrupted' } : {}),
1194
+ };
1195
+ });
1196
+ 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);
1197
+ }
1198
+ db.exec(`
1199
+ DROP INDEX IF EXISTS idx_rail_pr_deliveries_one_active_per_rail;
1200
+ CREATE UNIQUE INDEX idx_rail_pr_deliveries_one_active_per_rail
1201
+ ON rail_pr_deliveries(rail_index)
1202
+ WHERE decision NOT IN ('completed', 'merged', 'discarded', 'superseded');
1203
+ `);
1204
+ },
1205
+ // Migration 50: terminal Safe-PR decisions and their ticket-file mutation
1206
+ // cross SQLite/JSON boundaries. Persist an idempotent outbox row in the same
1207
+ // transaction as the terminal decision so a crash can never leave tickets
1208
+ // parked on_review with no legal action left to replay.
1209
+ (db) => {
1210
+ db.exec(`
1211
+ CREATE TABLE IF NOT EXISTS rail_pr_ticket_effects (
1212
+ delivery_id TEXT PRIMARY KEY,
1213
+ ticket_ids TEXT NOT NULL,
1214
+ causal_owners TEXT NOT NULL DEFAULT '{}',
1215
+ applied_ticket_ids TEXT,
1216
+ target_status TEXT NOT NULL CHECK (target_status IN ('todo', 'done')),
1217
+ jira_action TEXT NOT NULL CHECK (jira_action IN ('discard', 'merged', 'completed', 'refine', 'backlog')),
1218
+ pr_url TEXT,
1219
+ attempts INTEGER NOT NULL DEFAULT 0,
1220
+ last_error TEXT,
1221
+ tickets_applied_at TEXT,
1222
+ jira_enqueued_at TEXT,
1223
+ completed_at TEXT,
1224
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1225
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1226
+ );
1227
+ CREATE INDEX IF NOT EXISTS idx_rail_pr_ticket_effects_pending
1228
+ ON rail_pr_ticket_effects(completed_at)
1229
+ WHERE completed_at IS NULL;
1230
+ `);
1231
+ // Keep the migration restart-safe for development DBs that may have run an
1232
+ // earlier migration-50 draft before these phase columns were added. Shipped
1233
+ // migrations remain atomic, but additive guards make local upgrade testing
1234
+ // deterministic as well.
1235
+ const effectCols = new Set(db.prepare(`PRAGMA table_info(rail_pr_ticket_effects)`).all()
1236
+ .map((row) => row.name));
1237
+ if (!effectCols.has('applied_ticket_ids')) {
1238
+ db.exec(`ALTER TABLE rail_pr_ticket_effects ADD COLUMN applied_ticket_ids TEXT`);
1239
+ }
1240
+ if (!effectCols.has('tickets_applied_at')) {
1241
+ db.exec(`ALTER TABLE rail_pr_ticket_effects ADD COLUMN tickets_applied_at TEXT`);
1242
+ }
1243
+ if (!effectCols.has('jira_enqueued_at')) {
1244
+ db.exec(`ALTER TABLE rail_pr_ticket_effects ADD COLUMN jira_enqueued_at TEXT`);
1245
+ }
1246
+ if (!effectCols.has('causal_owners')) {
1247
+ db.exec(`ALTER TABLE rail_pr_ticket_effects ADD COLUMN causal_owners TEXT NOT NULL DEFAULT '{}'`);
1248
+ }
1249
+ },
1250
+ // Migration 51: a development build briefly shipped migration 50 without
1251
+ // phase columns and with a CHECK that allowed only discard/merged. A DB that
1252
+ // already records version 50 will never rerun its edited body, so rebuild the
1253
+ // table once, preserve every row, and install the complete action contract.
1254
+ // Migration 52 owns the causal historical backfill after both schemas exist.
1255
+ (db) => {
1256
+ const table = db.prepare(`
1257
+ SELECT sql FROM sqlite_master
1258
+ WHERE type = 'table' AND name = 'rail_pr_ticket_effects'
1259
+ `).get();
1260
+ const createFullTable = (name) => {
1261
+ db.exec(`
1262
+ CREATE TABLE ${name} (
1263
+ delivery_id TEXT PRIMARY KEY,
1264
+ ticket_ids TEXT NOT NULL,
1265
+ causal_owners TEXT NOT NULL DEFAULT '{}',
1266
+ applied_ticket_ids TEXT,
1267
+ target_status TEXT NOT NULL CHECK (target_status IN ('todo', 'done')),
1268
+ jira_action TEXT NOT NULL CHECK (jira_action IN ('discard', 'merged', 'completed', 'refine', 'backlog')),
1269
+ pr_url TEXT,
1270
+ attempts INTEGER NOT NULL DEFAULT 0,
1271
+ last_error TEXT,
1272
+ tickets_applied_at TEXT,
1273
+ jira_enqueued_at TEXT,
1274
+ completed_at TEXT,
1275
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1276
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1277
+ )
1278
+ `);
1279
+ };
1280
+ if (!table) {
1281
+ createFullTable('rail_pr_ticket_effects');
1282
+ }
1283
+ else {
1284
+ const cols = new Set(db.prepare(`PRAGMA table_info(rail_pr_ticket_effects)`).all()
1285
+ .map((row) => row.name));
1286
+ const sql = table.sql ?? '';
1287
+ const complete = ['causal_owners', 'applied_ticket_ids', 'tickets_applied_at', 'jira_enqueued_at']
1288
+ .every((column) => cols.has(column)) &&
1289
+ ['completed', 'refine', 'backlog'].every((action) => sql.includes(`'${action}'`));
1290
+ if (!complete) {
1291
+ db.exec(`DROP TABLE IF EXISTS rail_pr_ticket_effects_v51`);
1292
+ createFullTable('rail_pr_ticket_effects_v51');
1293
+ const value = (column, fallback) => cols.has(column) ? column : fallback;
1294
+ db.exec(`
1295
+ INSERT INTO rail_pr_ticket_effects_v51 (
1296
+ delivery_id, ticket_ids, causal_owners, applied_ticket_ids, target_status,
1297
+ jira_action, pr_url, attempts, last_error, tickets_applied_at,
1298
+ jira_enqueued_at, completed_at, created_at, updated_at
1299
+ )
1300
+ SELECT
1301
+ ${value('delivery_id', "''")},
1302
+ ${value('ticket_ids', "'[]'")},
1303
+ ${value('causal_owners', "'{}'")},
1304
+ ${value('applied_ticket_ids', 'NULL')},
1305
+ ${value('target_status', "'todo'")},
1306
+ ${value('jira_action', "'backlog'")},
1307
+ ${value('pr_url', 'NULL')},
1308
+ ${value('attempts', '0')},
1309
+ ${value('last_error', 'NULL')},
1310
+ ${value('tickets_applied_at', 'NULL')},
1311
+ ${value('jira_enqueued_at', 'NULL')},
1312
+ ${value('completed_at', 'NULL')},
1313
+ ${value('created_at', "datetime('now')")},
1314
+ ${value('updated_at', "datetime('now')")}
1315
+ FROM rail_pr_ticket_effects;
1316
+
1317
+ DROP TABLE rail_pr_ticket_effects;
1318
+ ALTER TABLE rail_pr_ticket_effects_v51 RENAME TO rail_pr_ticket_effects;
1319
+ `);
1320
+ }
1321
+ }
1322
+ db.exec(`
1323
+ CREATE INDEX IF NOT EXISTS idx_rail_pr_ticket_effects_pending
1324
+ ON rail_pr_ticket_effects(completed_at)
1325
+ WHERE completed_at IS NULL
1326
+ `);
1327
+ },
1328
+ // Migration 52: bind every ticket effect to the SQLite ticket-generation
1329
+ // owner that created it. Historical terminal rows are backfilled only when
1330
+ // their run_ids still own the ticket; an old merge/discard can therefore
1331
+ // never mutate a newer iteration merely because it is also on_review.
1332
+ (db) => {
1333
+ // Some development builds recorded migration 45 before its ownership
1334
+ // table was added. Repair that incomplete historical shape before the
1335
+ // causal backfill so one damaged project DB cannot prevent app startup.
1336
+ db.exec(`
1337
+ CREATE TABLE IF NOT EXISTS ticket_outcome_ownership (
1338
+ ticket_id INTEGER PRIMARY KEY,
1339
+ owner_id TEXT NOT NULL,
1340
+ generation INTEGER NOT NULL DEFAULT 1,
1341
+ claimed_at TEXT NOT NULL DEFAULT (datetime('now'))
1342
+ );
1343
+ CREATE INDEX IF NOT EXISTS idx_ticket_outcome_owner
1344
+ ON ticket_outcome_ownership(owner_id);
1345
+ `);
1346
+ const cols = new Set(db.prepare(`PRAGMA table_info(rail_pr_ticket_effects)`).all()
1347
+ .map((row) => row.name));
1348
+ if (!cols.has('causal_owners')) {
1349
+ db.exec(`ALTER TABLE rail_pr_ticket_effects ADD COLUMN causal_owners TEXT NOT NULL DEFAULT '{}'`);
1350
+ }
1351
+ backfillTerminalRailPrTicketEffects(db);
1352
+ },
1353
+ // Migration 53: an allocation rollback is the one legal way a previously
1354
+ // superseded delivery can become active again. Persist the failed replacement
1355
+ // id on the restored predecessor so clients can distinguish that explicit
1356
+ // rollback from an ordinary stale non-terminal replay.
1357
+ (db) => {
1358
+ const cols = new Set(db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all()
1359
+ .map((row) => row.name));
1360
+ if (!cols.has('restored_from_delivery_id')) {
1361
+ db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN restored_from_delivery_id TEXT`);
1362
+ }
1363
+ },
1364
+ // Migration 54: automatic worktree release quarantines authenticated overlay
1365
+ // roots instead of deleting them. Persist the bounded archive locations so
1366
+ // recovery remains discoverable after restart and card refresh.
1367
+ (db) => {
1368
+ const cols = new Set(db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all()
1369
+ .map((row) => row.name));
1370
+ if (!cols.has('safety_archives')) {
1371
+ db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN safety_archives TEXT NOT NULL DEFAULT '[]'`);
1372
+ }
1373
+ },
1374
+ // Migration 55: Agent Studio history belongs to a provider-native role
1375
+ // catalog. The same logical custom role id can validly exist for Claude and
1376
+ // Kimi in one project; keying history only by agent_name mixed bodies and
1377
+ // version counters across those catalogs. Legacy rows predate provider-aware
1378
+ // Studio and therefore belong to Claude.
1379
+ (db) => {
1380
+ const versionCols = new Set(db.prepare(`PRAGMA table_info(agent_versions)`).all()
1381
+ .map((row) => row.name));
1382
+ if (!versionCols.has('provider')) {
1383
+ db.exec(`
1384
+ ALTER TABLE agent_versions RENAME TO agent_versions_v54;
1385
+ CREATE TABLE agent_versions (
1386
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1387
+ provider TEXT NOT NULL DEFAULT 'claude',
1388
+ agent_name TEXT NOT NULL,
1389
+ version INTEGER NOT NULL,
1390
+ body TEXT NOT NULL,
1391
+ created_at INTEGER NOT NULL,
1392
+ UNIQUE (provider, agent_name, version)
1393
+ );
1394
+ INSERT INTO agent_versions (id, provider, agent_name, version, body, created_at)
1395
+ SELECT id, 'claude', agent_name, version, body, created_at
1396
+ FROM agent_versions_v54;
1397
+ DROP TABLE agent_versions_v54;
1398
+ CREATE INDEX idx_agent_versions_provider_name
1399
+ ON agent_versions(provider, agent_name);
1400
+ `);
1401
+ }
1402
+ const testCols = new Set(db.prepare(`PRAGMA table_info(agent_tests)`).all()
1403
+ .map((row) => row.name));
1404
+ if (!testCols.has('provider')) {
1405
+ db.exec(`ALTER TABLE agent_tests ADD COLUMN provider TEXT NOT NULL DEFAULT 'claude'`);
1406
+ }
1407
+ db.exec(`
1408
+ CREATE INDEX IF NOT EXISTS idx_agent_tests_provider_name
1409
+ ON agent_tests(provider, agent_name)
1410
+ `);
1411
+ },
1412
+ // Migration 56: nontech-review-experience Wave 1 — two additive nullable
1413
+ // JSON columns on rail_pr_deliveries. `spec_snapshot` freezes each covered
1414
+ // ticket's title/description/labels at LAUNCH so the review packet renders
1415
+ // what the user actually asked (the live store can be edited mid-run).
1416
+ // `settle_evidence` persists the deterministic settle-time harvest (the
1417
+ // VERIFICATION sentinel, the verify step's output tail, the reviewer's
1418
+ // confidence-score.json) captured BEFORE worktree release — the packet's
1419
+ // proof sections survive worktree teardown. NULL on legacy rows = honest
1420
+ // "not captured". Additive + idempotent (guarded by PRAGMA table_info).
1421
+ (db) => {
1422
+ const cols = db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all()
1423
+ .map((r) => r.name);
1424
+ if (!cols.includes('spec_snapshot')) {
1425
+ db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN spec_snapshot TEXT`);
1426
+ }
1427
+ if (!cols.includes('settle_evidence')) {
1428
+ db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN settle_evidence TEXT`);
1429
+ }
1430
+ },
1431
+ // Migration 57: nontech-review-experience Wave 3 — revision metadata on the
1432
+ // NEW delivery generation a revision creates. `revision_note` is the user's
1433
+ // one-sentence instruction (so packet v2 can show what was asked to change)
1434
+ // and `revision_of` points at the generation being revised. Deliberately NOT
1435
+ // a new `decision` value: an unknown decision makes older clients drop the
1436
+ // whole PR card, and the shipped generation/supersession model already
1437
+ // expresses "a newer attempt replaced the previous one". Additive +
1438
+ // idempotent; NULL on every pre-existing row and on ordinary launches.
1439
+ (db) => {
1440
+ const cols = db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all()
1441
+ .map((r) => r.name);
1442
+ if (!cols.includes('revision_note')) {
1443
+ db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN revision_note TEXT`);
1444
+ }
1445
+ if (!cols.includes('revision_of')) {
1446
+ db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN revision_of TEXT`);
1447
+ }
1448
+ },
1449
+ // Migration 58: premium-milestone-progress — server-durable milestone launch
1450
+ // chains. One row per sequential "Launch Milestone": the chunk plan, the
1451
+ // chunk in flight (rail / runs / delivery), the branch the next chunk stacks
1452
+ // on, and a CAS status. The partial unique index enforces ONE non-terminal
1453
+ // chain per milestone. Additive; nothing else references it.
1454
+ (db) => {
1455
+ db.exec(`
1456
+ CREATE TABLE IF NOT EXISTS milestone_launch_chains (
1457
+ id TEXT PRIMARY KEY,
1458
+ milestone_n INTEGER NOT NULL,
1459
+ milestone_id TEXT NOT NULL,
1460
+ mode TEXT NOT NULL,
1461
+ chunks TEXT NOT NULL DEFAULT '[]',
1462
+ next_chunk INTEGER NOT NULL DEFAULT 0,
1463
+ current_rail_index INTEGER,
1464
+ current_run_ids TEXT NOT NULL DEFAULT '[]',
1465
+ current_delivery_id TEXT,
1466
+ integration_branch TEXT,
1467
+ head_branch TEXT,
1468
+ status TEXT NOT NULL DEFAULT 'running',
1469
+ pause_reason TEXT,
1470
+ launched TEXT NOT NULL DEFAULT '[]',
1471
+ last_run_outcome TEXT,
1472
+ auto_advance INTEGER NOT NULL DEFAULT 1,
1473
+ retry_chunk INTEGER,
1474
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1475
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1476
+ );
1477
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_milestone_chains_active
1478
+ ON milestone_launch_chains(milestone_n) WHERE status IN ('running','waiting','paused','awaiting_approval');
1479
+ CREATE INDEX IF NOT EXISTS idx_milestone_chains_status ON milestone_launch_chains(status);
1480
+ `);
1481
+ },
1482
+ // Migration 59: coordinated repository execution and grouped delivery.
1483
+ multi_repo_execution_store_1.migrateMultiRepoExecution,
1484
+ // Migration 60: repository-scoped provenance and code identity.
1485
+ project_repository_provenance_1.migrateRepositoryProvenance,
1486
+ // Migration 61: version and evidence identity of recorded-change explanations.
1487
+ file_story_1.migrateFileStoryMetadata,
1488
+ // Migration 62: pr-follow-up-fixes — the frozen review follow-up scope a launch
1489
+ // was approved with (`follow_up`, JSON PrFollowUp: selected comments, required
1490
+ // outcomes, exclusions, verification, OpenSpec change name, id/version/hash).
1491
+ // Lives on the DELIVERY, never on the spec: a follow-up must not rewrite the
1492
+ // ticket description (Jira-synced on linked projects). Additive + idempotent;
1493
+ // NULL on every pre-existing row and on ordinary launches.
1494
+ (db) => {
1495
+ const cols = db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all().map((r) => r.name);
1496
+ if (!cols.includes('follow_up'))
1497
+ db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN follow_up TEXT`);
1498
+ },
1499
+ // Migration 63: spec-addenda — the frozen identity (ticketId/id/kind/title/
1500
+ // hash) of the spec addenda a launch carried (`spec_addenda`, JSON
1501
+ // SpecAddendaSnapshotEntry[]). The addenda themselves live on the TICKET
1502
+ // (local-tickets.json, schema 1.4); the delivery only records which ones
1503
+ // this generation was briefed with, so the packet can report them and a
1504
+ // discard can reopen exactly those. Additive + idempotent; NULL when none.
1505
+ (db) => {
1506
+ const cols = db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all().map((r) => r.name);
1507
+ if (!cols.includes('spec_addenda'))
1508
+ db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN spec_addenda TEXT`);
1509
+ },
1510
+ ];
1511
+ function applyMigrations(db) {
1512
+ // Ensure the migrations table exists (migration 1 creates it, but we need
1513
+ // it before we can read from it)
1514
+ db.exec(`
1515
+ CREATE TABLE IF NOT EXISTS schema_migrations (
1516
+ version INTEGER PRIMARY KEY,
1517
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
1518
+ )
1519
+ `);
1520
+ (0, sqlite_migrations_1.applyNumberedMigrations)(db, MIGRATIONS);
1521
+ }