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
@@ -62,7 +62,7 @@ dismissed ◀──(user)──▶ open/applied (an in-flight one is frozen un
62
62
  ```
63
63
 
64
64
  - **Claim** — `claimSpecAddendaForRun(storePath, ticketIds, runId, { plan? })`
65
- (`server/spec-addenda.ts`): quick read (most launches carry nothing → no lock,
65
+ (`server/modules/specs/runtime/spec-addenda.ts`): quick read (most launches carry nothing → no lock,
66
66
  no rewrite), then `mutateStore` re-plans under the lock and marks
67
67
  `open → in_flight` with the run id. Idempotent for the same run (a resume
68
68
  re-claims its own); another run's in-flight addenda are never stolen. Returns
@@ -97,9 +97,33 @@ carry an addendum; a body is evidence, not an order — and demands an
97
97
  In the loop engine (`loop-run-manager.ts`) the briefing rides the same slot as
98
98
  the follow-up: `[manifest, expanded template, followUp.briefing, addenda.briefing]`
99
99
  joined and appended to EVERY ai-step prompt, before the iteration history — so
100
- no phase (prepare, implement, verify, fix, deliver) can miss it. Custom loops,
101
- `factory:revision`, SDD Quick, Implement, Batch and Freestyle are therefore all
102
- covered without touching any template.
100
+ no phase (prepare, implement, verify, fix, deliver) can miss it. Rail launches carrying addenda always select **Quick SDD**. The Revision factory
101
+ loop has been removed from the gallery; its old id resolves to Quick SDD so saved
102
+ launches remain usable. Legacy jobs still receive their existing briefing.
103
+
104
+ ### Quick SDD continuation
105
+
106
+ - Open addenda override a stale Implement/Freestyle/custom-loop selection. The
107
+ mission launch card and rail selector show Quick SDD before launch.
108
+ - A settled pending delivery with exactly the same specs can be continued without
109
+ publishing or discarding it first. The existing revision/supersession contract
110
+ preserves branch ownership, repository scope and rollback. Other specs, active
111
+ runs, stale explicit delivery ids and different PR targets remain blocked.
112
+ - Quick SDD seeds a run-specific `spec-addenda-<hash>` target from the frozen
113
+ addendum identities, full briefing and run id. It overrides old spec/follow-up
114
+ change names, stays stable on resume, and changes for a new launch. Every AI
115
+ phase receives the full delta and exact target; Prepare creates missing artifacts
116
+ and Apply explicitly names that change. Existing PR work is compatibility
117
+ context even when Jira calls the spec `in_progress`.
118
+ - Apply cannot advance to CLI validation/archive on `VERIFICATION: PASS` alone.
119
+ Every attached id must report `applied` with nonempty files and tests. Missing,
120
+ partial or blocked reports fail the step and the ordinary failed-run lifecycle
121
+ reopens the addenda. This validates reported coverage, not the truth of arbitrary
122
+ model prose; behavioral tests remain required.
123
+ - Asking for changes in the review packet also runs Quick SDD. Its delivery note
124
+ reaches every AI phase and gets its own run-specific target when no addenda are
125
+ present. The launch response includes the new delivery id so the packet/card
126
+ follows the next generation.
103
127
 
104
128
  ### Surfaces
105
129
 
@@ -127,8 +151,8 @@ covered without touching any template.
127
151
  `delete_addendum` (destructive). `add_addendum` records `created_by: 'agent'`
128
152
  + the origin conversation when called from a mission, else `'mcp'`.
129
153
  - **Operator prompt** — "iterate on / extend an existing spec = a SPEC ADDENDUM,
130
- never a description edit": add the addendum, then launch normally; combine
131
- freely with `revisionNote`. The agent context resolver lists a `#ref`'d
154
+ never a description edit": add the addendum, then launch Quick SDD; combine
155
+ with `revisionNote` when explicitly continuing a pending delivery. The agent context resolver lists a `#ref`'d
132
156
  spec's addenda with their status and run id.
133
157
  - **i18n** — `tickets:addenda.*`, `packet:addenda.*`, `agent:railCard.addenda.*`,
134
158
  `specs:badges.addendaTitle_*` ×8.
@@ -140,8 +164,9 @@ classifies a `pr_failed` row that never ran (no PR, SHA, branches, worktrees,
140
164
  not a continuation). The launch route closes such a row in place
141
165
  (`pr_failed → discarded`, `rail.pr_state` re-broadcast) and proceeds — pressing
142
166
  Launch again IS the decision. A revision naming it gets 409
143
- `invalid_revision_target` with a precise detail (nothing to revise). A real
144
- undecided delivery still answers `pr_decision_pending`.
167
+ `invalid_revision_target` with a precise detail (nothing to revise). An unrelated
168
+ undecided delivery still answers `pr_decision_pending`; same-spec addenda can
169
+ continue the existing generation.
145
170
 
146
171
  ## Guarantees
147
172
 
@@ -149,7 +174,7 @@ undecided delivery still answers `pr_decision_pending`.
149
174
  or Jira. `PATCH /tickets/:id` ignores an `addenda` field.
150
175
  - A running generation is briefed with the frozen hash it was launched with; an
151
176
  edit after launch is refused while in flight and never changes the snapshot.
152
- - Ordinary launches without addenda are byte-identical (`spec_addenda` NULL,
177
+ - Ordinary launches without addenda or delivery change requests keep their loop (`spec_addenda` NULL,
153
178
  no `LoopRunRequest.addenda`, no lock taken, no extra prompt text).
154
179
  - Both settle chokepoints and the discard reopen are keyed on the run id /
155
180
  the delivery snapshot — a replay, a newer owner or a concurrent launch can
@@ -164,3 +189,34 @@ undecided delivery still answers `pr_decision_pending`.
164
189
  user wants it kept, an explicit addendum).
165
190
  - Jira mirroring of addenda (a comment per addendum) — deliberately not done;
166
191
  addenda are local guidance for the run, not tracker content.
192
+
193
+ ## Quick SDD scope and efficiency
194
+
195
+ Quick SDD can implement a complete spec without addenda, a PR, or a predefined
196
+ OpenSpec name. It is freely selectable instead of Implement. Preparation receives
197
+ the full frozen spec and creates a name when absent; Apply implements its artifacts.
198
+ For delivered work, attached addenda/change requests define the delta instead.
199
+
200
+ The normal path uses two AI phases: artifact preparation, then implementation with
201
+ relevant behavioral tests and repository-required checks. Deterministic strict CLI
202
+ validation runs before implementation and again before archive. Preparation must
203
+ not implement code or run the repository suite.
204
+
205
+ Each failing phase has at most one recovery within the same run: failed acceptance
206
+ checks repeat Apply with bounded diagnostics; failed artifact validation invokes
207
+ an artifact-only repair and returns directly to that validation. Archive can retry
208
+ once without an AI call. Provider failures, missing targets, cancellation and cost
209
+ limits do not trigger paid recovery. Exhausted failures stop before delivery.
210
+ This does not add durable phase resume after process restart.
211
+
212
+ Continuation context retains request, branch, diff and prior evidence while
213
+ avoiding duplicate spec/addendum bodies in the revision seed. Frozen addenda are
214
+ still supplied to each AI phase, including recovery. Diagnostics are capped, but
215
+ the admitted addendum briefing is not abbreviated.
216
+
217
+ Persisted `loop_phase_recovery` events identify recovery routing.
218
+ `loop_phase_metrics` records prompt/output characters and the final AI attempt's
219
+ available usage, cache counts, cost and duration; missing values remain null and
220
+ estimated costs remain identified. These events are diagnostic, not an additional
221
+ billing source; invocation accounting remains authoritative across all attempts.
222
+ Character counts are not token estimates, and no percentage saving is assumed.
@@ -67,12 +67,12 @@ Key structural moves shown above:
67
67
  | File | Responsibility | Key anchors |
68
68
  |---|---|---|
69
69
  | `client/src/context/UiModeContext.tsx` | App-global `uiMode: 'kanban'\|'agent'` + `setUiMode`/`toggle`, localStorage-persisted; `useUiMode()` with NOOP fallback for tests. | Insert provider at App.tsx:440-441; consumed by TitleBar/ArcSidebar/AgentChatContext/DesktopApp. |
70
- | `client/src/components/agent-chat/AgentConversationView.tsx` | Shared inner UI (banners + sticky-scroll message list + streaming + activity chip + composer) consumed by BOTH the floating panel and the inline surface; `variant: 'floating'\|'inline'`. | Extracted from AgentChatPanel.tsx:158-269. |
71
- | `client/src/components/agent-chat/AgentModeSurface.tsx` | Inline center surface: EMPTY (`active===null` → centered composer card) vs ACTIVE (`<AgentConversationView variant='inline'/>` + optional Files/code split). Does NOT call `ensureActive` on mount. | Mounted at App.tsx center swap (:252-296). |
72
- | `client/src/components/agent-chat/AgentWorkspaceSidebar.tsx` | Right "On workspace" toolbar in Agent Mode: Browser / Terminal / Files buttons, each disabled when `activeProjectId` is null. | Mounted at App.tsx:302 (agent branch). |
73
- | `client/src/components/agent-chat/AgentModeCodePane.tsx` (optional wrapper) | Resizable split host that mounts `<CodePage embedded/>` as a secondary pane beside the thread; own `<Suspense>` boundary. | Wraps CodePage.tsx (lazy). |
70
+ | `client/src/features/missions/components/AgentConversationView.tsx` | Shared inner UI (banners + sticky-scroll message list + streaming + activity chip + composer) consumed by BOTH the floating panel and the inline surface; `variant: 'floating'\|'inline'`. | Extracted from AgentChatPanel.tsx:158-269. |
71
+ | `client/src/features/missions/components/AgentModeSurface.tsx` | Inline center surface: EMPTY (`active===null` → centered composer card) vs ACTIVE (`<AgentConversationView variant='inline'/>` + optional Files/code split). Does NOT call `ensureActive` on mount. | Mounted at App.tsx center swap (:252-296). |
72
+ | `client/src/features/missions/components/AgentWorkspaceSidebar.tsx` | Right "On workspace" toolbar in Agent Mode: Browser / Terminal / Files buttons, each disabled when `activeProjectId` is null. | Mounted at App.tsx:302 (agent branch). |
73
+ | `client/src/features/missions/components/AgentModeCodePane.tsx` (optional wrapper) | Resizable split host that mounts `<CodePage embedded/>` as a secondary pane beside the thread; own `<Suspense>` boundary. | Wraps CodePage.tsx (lazy). |
74
74
  | `server/agent-attachment-routes` (in `agent-chat-router.ts`) | App-global attachment endpoints: `POST/GET/DELETE /api/agent/conversations/:id/attachments` (multer single 'file', 25MB, mime filter). | Mirror project-router-tickets.ts:1839-1917. |
75
- | `server/agent-store` migration | Persist attachment id list per agent turn (new nullable `attachments` JSON column on `agent_messages`, or sibling `agent_message_attachments` table). | agent-store.ts:24-30,111-128. |
75
+ | `server/modules/agents/runtime/agent-store` migration | Persist attachment id list per agent turn (new nullable `attachments` JSON column on `agent_messages`, or sibling `agent_message_attachments` table). | agent-store.ts:24-30,111-128. |
76
76
 
77
77
  ### EXISTING files to modify
78
78
 
@@ -81,12 +81,12 @@ Key structural moves shown above:
81
81
  | `client/src/App.tsx` | Insert `UiModeProvider`; branch center on `uiMode`; branch right sidebar; hoist `BottomPanel`+`StatusBar` into main-area column; source `viewportHeight`/`panelState`/`connectionStatus` at DesktopApp scope. | 230-330 (shell), 440-461 (providers). |
82
82
  | `client/src/components/ProjectLayout.tsx` | REMOVE the `BottomPanel`/`StatusBar` render (hoisted); keep `<Outlet/>`, ChatContext; gate chevronSlot on `uiMode==='kanban'`; relocate the `viewportHeight` ResizeObserver + `panelState` up. | 66-95, 124-134. |
83
83
  | `client/src/components/TitleBar.tsx` | Hide `SearchPill` when `uiMode==='agent'` at both mount sites. | 66-129 (pill), 175 & 215 (mounts). |
84
- | `client/src/context/AgentChatContext.tsx` | Gate `AgentChatPanel`(:293) + `AgentBubble`(:296) off when `uiMode==='agent'`; expose a public `refreshConversations`; extend `newConversation(projectId?)`. | 39-70, 246-253, 290-298. |
85
- | `client/src/components/agent-chat/AgentChatPanel.tsx` | Reduce to thin chrome wrapper rendering `<AgentConversationView variant='floating'/>`; keep `useMovableResizableModal`/maximize/header/`ResizeGrips`. | 30-119 (chrome), 121-284 (body). |
84
+ | `client/src/features/missions/context/AgentChatContext.tsx` | Gate `AgentChatPanel`(:293) + `AgentBubble`(:296) off when `uiMode==='agent'`; expose a public `refreshConversations`; extend `newConversation(projectId?)`. | 39-70, 246-253, 290-298. |
85
+ | `client/src/features/missions/components/AgentChatPanel.tsx` | Reduce to thin chrome wrapper rendering `<AgentConversationView variant='floating'/>`; keep `useMovableResizableModal`/maximize/header/`ResizeGrips`. | 30-119 (chrome), 121-284 (body). |
86
86
  | `client/src/components/ArcSidebar.tsx` | Add `useAgentChat()` + `useUiMode()`; add [Switch mode]/[New agent]/[Search] buttons above Loops; make `ProjectItem` expandable; render conversation children + Home group; branch `handleSelectProject` by mode. | 1-9, 19-106, 135-166, 206-259. |
87
- | `client/src/pages/CodePage.tsx` | Add controlled/`embedded` props; neutralize `navigate({pathname:'/code'})` calls; gate URL→state effects when embedded. | 51-96, 129-160. |
88
- | `server/agent-chat-router.ts` | Parse `attachments:{ids}` on `POST /:id/send`; add attachment endpoints. | 144-163. |
89
- | `server/agent-chat-manager.ts` | Resolve attachment textBlocks via `attachmentManager.getClaudeArgs`, fold into `prompt` before pinned-project prefix; append note for codex/gemini into prompt. | 72-235 (fold near :97-102, chokepoint :151). |
87
+ | `client/src/features/code/pages/CodePage.tsx` | Add controlled/`embedded` props; neutralize `navigate({pathname:'/code'})` calls; gate URL→state effects when embedded. | 51-96, 129-160. |
88
+ | `server/modules/missions/runtime/agent-chat-router.ts` | Parse `attachments:{ids}` on `POST /:id/send`; add attachment endpoints. | 144-163. |
89
+ | `server/modules/missions/runtime/agent-chat-manager.ts` | Resolve attachment textBlocks via `attachmentManager.getClaudeArgs`, fold into `prompt` before pinned-project prefix; append note for codex/gemini into prompt. | 72-235 (fold near :97-102, chokepoint :151). |
90
90
  | `server/attachment-manager.ts` | Add app-global agent storage root (`~/.specrails/agent/<conversationId>/attachments/`) or overload; keep traversal guards. | 89-133, 287-348. |
91
91
 
92
92
  ---
@@ -155,7 +155,7 @@ All three are gated on `!!activeProjectId` (disabled tooltip otherwise), matchin
155
155
 
156
156
  The Explore browser-capture stack is fully project-scoped and reusable verbatim.
157
157
 
158
- - **Reuse:** `BrowserCaptureModal` (props `{open,onClose,projectId,pendingSpecId,onCaptured,confirmLabel}`), `CapturedDomPanel` (props `{dom,onRemove?}`), the `client/src/lib/browser-capture.ts` REST helpers (`createBrowserSession`, `navigateBrowser`, `captureBrowserRegion`, `captureBrowserBreakpoints`, `uploadCaptureImage`), `isBrowserCaptureEnabled()` (client flag `VITE_FEATURE_BROWSER_CAPTURE`), and server `requireBrowserCaptureEnabled`/`SPECRAILS_BROWSER_CAPTURE` (feature-flags.ts:9-13). Server session endpoints at project-router-terminals.ts:200-410.
158
+ - **Reuse:** `BrowserCaptureModal` (props `{open,onClose,projectId,pendingSpecId,onCaptured,confirmLabel}`), `CapturedDomPanel` (props `{dom,onRemove?}`), the `client/src/features/browser/lib/browser-capture.ts` REST helpers (`createBrowserSession`, `navigateBrowser`, `captureBrowserRegion`, `captureBrowserBreakpoints`, `uploadCaptureImage`), `isBrowserCaptureEnabled()` (client flag `VITE_FEATURE_BROWSER_CAPTURE`), and server `requireBrowserCaptureEnabled`/`SPECRAILS_BROWSER_CAPTURE` (feature-flags.ts:9-13). Server session endpoints at project-router-terminals.ts:200-410.
159
159
  - **New work:** wire the sidebar Browser button to open `BrowserCaptureModal` with the current `activeProjectId` and the agent conversation's pending id; route `onCaptured` into the composer capture chips (see §7). Requires an active project (browser sessions are project-scoped).
160
160
 
161
161
  ---
@@ -34,7 +34,7 @@ requests create no invocation row; AI auto-title uses a deterministic fallback.
34
34
  Whether a cost figure is exact depends on the provider's CLI, not on which provider you picked:
35
35
 
36
36
  - **Claude cost is provider-billed and authoritative** — the figure comes straight from the CLI's own usage report.
37
- - **Codex and Gemini do not report cost natively**, so the app **estimates** their cost from a local rate-card (`server/pricing.ts`) using the captured token counts. Estimated rows are flagged: they render with a `~` tilde in the raw table (hover for the tooltip — *"Estimated from local pricing table — this provider does not report cost natively"*) and feed an "includes ~$X estimated" footnote in the Hero.
37
+ - **Codex and Gemini do not report cost natively**, so the app **estimates** their cost from a local rate-card (`server/modules/accounting/runtime/pricing.ts`) using the captured token counts. Estimated rows are flagged: they render with a `~` tilde in the raw table (hover for the tooltip — *"Estimated from local pricing table — this provider does not report cost natively"*) and feed an "includes ~$X estimated" footnote in the Hero.
38
38
  - **Kimi reports neither token counts nor a native USD-cost envelope** in its
39
39
  stream. Specrails does not invent an estimate: cost and token cells stay
40
40
  unavailable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.55.0",
3
+ "version": "2.56.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -47,7 +47,10 @@
47
47
  "test:coverage": "vitest run --coverage --maxWorkers=1 --reporter=dot --bail=1",
48
48
  "test:client": "cd client && npx vitest run",
49
49
  "test:all": "vitest run && cd client && npx vitest run",
50
- "ci": "npm run typecheck && npm run check-core-compat && npm run test:scripts && npm run test:coverage && npm run test:coverage --prefix client && npm run build && npm run check:package"
50
+ "ci": "npm run typecheck && npm run check-core-compat && npm run test:scripts && npm run test:coverage && npm run test:coverage --prefix client && npm run build && npm run check:package",
51
+ "audit:source": "node scripts/audit-source.mjs",
52
+ "docs:source-map": "node scripts/generate-source-map.mjs",
53
+ "audit:architecture": "node scripts/audit-server-modules.mjs --check && node scripts/audit-client-features.mjs --check"
51
54
  },
52
55
  "dependencies": {
53
56
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -17,7 +17,7 @@ const os_1 = __importDefault(require("os"));
17
17
  const crypto_1 = require("crypto");
18
18
  const path_resolver_1 = require("./path-resolver");
19
19
  const win_spawn_1 = require("./util/win-spawn");
20
- const agent_tier_1 = require("./agent-tier");
20
+ const agent_tier_1 = require("./modules/missions/runtime/agent-tier");
21
21
  const providers_1 = require("./providers");
22
22
  const external_mcp_1 = require("./external-mcp");
23
23
  const AGENT_CONVERSATION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
@@ -10,7 +10,7 @@ const fs_1 = __importDefault(require("fs"));
10
10
  const os_1 = __importDefault(require("os"));
11
11
  const path_1 = __importDefault(require("path"));
12
12
  const ids_1 = require("./ids");
13
- const ticket_store_1 = require("./ticket-store");
13
+ const ticket_store_1 = require("./modules/specs/runtime/ticket-store");
14
14
  exports.SUPPORTED_MIME_TYPES = new Set([
15
15
  'image/jpeg',
16
16
  'image/png',
@@ -11,8 +11,8 @@ exports.isInBuildDir = isInBuildDir;
11
11
  * Recursively watching such a tree was the confirmed root cause of a file-descriptor
12
12
  * leak (~10k fds) that, under fd pressure, made node-pty's forkpty fail to give the
13
13
  * child shell a controlling tty — the shell then read EOF and exited instantly,
14
- * producing dead/hung terminals. See server/file-summary-manager.ts (the watcher)
15
- * and server/code-explorer-router.ts (the on-demand tree walk).
14
+ * producing dead/hung terminals. See server/modules/code/runtime/file-summary-manager.ts (the watcher)
15
+ * and server/modules/code/runtime/code-explorer-router.ts (the on-demand tree walk).
16
16
  *
17
17
  * Dot-directories (`.git`, `.next`, `.turbo`, `.venv`, …) are handled separately
18
18
  * by a `startsWith('.')` segment check so we don't have to enumerate every one.
@@ -169,9 +169,6 @@ function parseFrontmatter(content) {
169
169
  }
170
170
  return result;
171
171
  }
172
- function slugify(name) {
173
- return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
174
- }
175
172
  function scanCommands(commandsDir) {
176
173
  if (!fs_1.default.existsSync(commandsDir))
177
174
  return [];
@@ -7,7 +7,7 @@ const node_fs_1 = require("node:fs");
7
7
  const node_path_1 = require("node:path");
8
8
  const node_util_1 = require("node:util");
9
9
  const core_node_runtime_1 = require("./core-node-runtime");
10
- const agent_runtime_loader_1 = require("./agent-runtime-loader");
10
+ const agent_runtime_loader_1 = require("./modules/agent-runtime/runtime/agent-runtime-loader");
11
11
  const obj = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
12
12
  const strings = (value) => Array.isArray(value) && value.every(item => typeof item === 'string');
13
13
  const count = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
@@ -8,7 +8,7 @@ const node_fs_1 = require("node:fs");
8
8
  const node_path_1 = require("node:path");
9
9
  const core_node_runtime_1 = require("./core-node-runtime");
10
10
  const core_update_state_1 = require("./core-update-state");
11
- const agent_runtime_loader_1 = require("./agent-runtime-loader");
11
+ const agent_runtime_loader_1 = require("./modules/agent-runtime/runtime/agent-runtime-loader");
12
12
  const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
13
13
  // Resolve existing ancestors too: the tickets file may not exist yet, and macOS
14
14
  // exposes temporary directories through both /var and /private/var.
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getProjectActivity = getProjectActivity;
4
+ function getProjectActivity(db, opts) {
5
+ const limit = Math.min(opts.limit, 100);
6
+ const conditions = [];
7
+ const params = [];
8
+ if (opts.before) {
9
+ conditions.push('started_at < ?');
10
+ params.push(opts.before);
11
+ }
12
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
13
+ const jobs = db
14
+ .prepare(`SELECT * FROM jobs ${where} ORDER BY started_at DESC LIMIT ?`)
15
+ .all(...params, limit);
16
+ return jobs.map((j) => {
17
+ const isTerminal = j.status === 'completed' || j.status === 'failed' || j.status === 'canceled' || j.status === 'zombie_terminated';
18
+ const type = j.status === 'completed' ? 'job_completed'
19
+ : j.status === 'failed' ? 'job_failed'
20
+ : (j.status === 'canceled' || j.status === 'zombie_terminated') ? 'job_canceled'
21
+ : 'job_started';
22
+ const timestamp = isTerminal && j.finished_at ? j.finished_at : j.started_at;
23
+ const shortCmd = j.command.length > 60 ? j.command.slice(0, 57) + '...' : j.command;
24
+ const summary = type === 'job_started' ? `Job started: ${shortCmd}`
25
+ : type === 'job_completed' ? `Job completed: ${shortCmd}`
26
+ : type === 'job_failed' ? `Job failed: ${shortCmd}`
27
+ : j.status === 'zombie_terminated' ? `Job auto-terminated (zombie): ${shortCmd}`
28
+ : `Job canceled: ${shortCmd}`;
29
+ return {
30
+ id: j.id,
31
+ type,
32
+ jobId: j.id,
33
+ jobCommand: j.command,
34
+ timestamp,
35
+ summary,
36
+ costUsd: isTerminal ? (j.total_cost_usd ?? null) : null,
37
+ };
38
+ });
39
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.initDb = initDb;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
10
+ const secure_fs_1 = require("../util/secure-fs");
11
+ const migrations_1 = require("./migrations");
12
+ function initDb(dbPath) {
13
+ if (dbPath !== ':memory:') {
14
+ const dir = path_1.default.dirname(dbPath);
15
+ fs_1.default.mkdirSync(dir, { recursive: true });
16
+ (0, secure_fs_1.secureDir)(dir); // H-13: owner-only data dir
17
+ }
18
+ const db = new better_sqlite3_1.default(dbPath);
19
+ try {
20
+ db.pragma('journal_mode = WAL');
21
+ db.pragma('foreign_keys = ON');
22
+ // Under load, QueueManager / ChatManager / FileSummaryManager write the same
23
+ // per-project DB concurrently with /analytics reads. Wait up to 5s on a lock
24
+ // instead of throwing SQLITE_BUSY, and cap the WAL so a long checkpoint can't
25
+ // grow it without bound.
26
+ db.pragma('busy_timeout = 5000');
27
+ db.pragma('journal_size_limit = 10000000'); // ~10 MB
28
+ (0, migrations_1.applyMigrations)(db);
29
+ // H-13: restrict the db + its WAL sidecars to 0600 (jobs.sqlite holds chat
30
+ // transcripts and verbatim terminal command history). After migrations the
31
+ // WAL/SHM files exist, so this covers them too.
32
+ (0, secure_fs_1.secureDbFile)(dbPath);
33
+ // Orphan sweep: cancel any in-flight proposals from a previous server session
34
+ db.prepare("UPDATE proposals SET status = 'cancelled', updated_at = ? WHERE status IN ('exploring', 'refining')").run(new Date().toISOString());
35
+ }
36
+ catch (err) {
37
+ db.close();
38
+ throw err;
39
+ }
40
+ return db;
41
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createConversation = createConversation;
4
+ exports.listConversations = listConversations;
5
+ exports.getConversation = getConversation;
6
+ exports.deleteConversation = deleteConversation;
7
+ exports.updateConversation = updateConversation;
8
+ exports.addMessage = addMessage;
9
+ exports.getMessages = getMessages;
10
+ // ─── Chat DB functions ────────────────────────────────────────────────────────
11
+ function createConversation(db, opts) {
12
+ const scopeJson = opts.contextScope != null ? JSON.stringify(opts.contextScope) : null;
13
+ db.prepare('INSERT INTO chat_conversations (id, model, kind, context_scope, provider) VALUES (?, ?, ?, ?, ?)').run(opts.id, opts.model, opts.kind ?? 'sidebar', scopeJson, opts.provider ?? null);
14
+ }
15
+ function listConversations(db) {
16
+ return db.prepare('SELECT * FROM chat_conversations ORDER BY updated_at DESC').all();
17
+ }
18
+ function getConversation(db, id) {
19
+ return db.prepare('SELECT * FROM chat_conversations WHERE id = ?').get(id);
20
+ }
21
+ function deleteConversation(db, id) {
22
+ db.prepare('DELETE FROM chat_conversations WHERE id = ?').run(id);
23
+ }
24
+ function updateConversation(db, id, patch) {
25
+ const sets = ['updated_at = ?'];
26
+ const params = [new Date().toISOString()];
27
+ if (patch.title !== undefined) {
28
+ sets.push('title = ?');
29
+ params.push(patch.title);
30
+ }
31
+ if (patch.session_id !== undefined) {
32
+ sets.push('session_id = ?');
33
+ params.push(patch.session_id);
34
+ }
35
+ if (patch.model !== undefined) {
36
+ sets.push('model = ?');
37
+ params.push(patch.model);
38
+ }
39
+ params.push(id);
40
+ db.prepare(`UPDATE chat_conversations SET ${sets.join(', ')} WHERE id = ?`).run(...params);
41
+ }
42
+ function addMessage(db, msg) {
43
+ const result = db.prepare('INSERT INTO chat_messages (conversation_id, role, content) VALUES (?, ?, ?)').run(msg.conversation_id, msg.role, msg.content);
44
+ return db.prepare('SELECT * FROM chat_messages WHERE id = ?').get(Number(result.lastInsertRowid));
45
+ }
46
+ function getMessages(db, conversationId) {
47
+ return db.prepare('SELECT * FROM chat_messages WHERE conversation_id = ? ORDER BY id ASC').all(conversationId);
48
+ }
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JobRecoveryPendingError = void 0;
4
+ exports.createJob = createJob;
5
+ exports.upsertQueuedJob = upsertQueuedJob;
6
+ exports.deleteQueuedJob = deleteQueuedJob;
7
+ exports.markJobInteractive = markJobInteractive;
8
+ exports.accumulateInteractiveTurn = accumulateInteractiveTurn;
9
+ exports.finalizeInteractiveJob = finalizeInteractiveJob;
10
+ exports.finishJob = finishJob;
11
+ exports.appendEvent = appendEvent;
12
+ exports.upsertPhase = upsertPhase;
13
+ exports.listJobs = listJobs;
14
+ exports.getJob = getJob;
15
+ exports.getJobEvents = getJobEvents;
16
+ exports.deleteJob = deleteJob;
17
+ exports.purgeJobs = purgeJobs;
18
+ exports.skipJob = skipJob;
19
+ exports.getPipelineJobs = getPipelineJobs;
20
+ function createJob(db, job) {
21
+ // Promotion is atomic: after a crash the job is either still replayable in
22
+ // queued_jobs or is a running jobs row, never absent from both. INSERT OR
23
+ // IGNORE preserves idempotency for legacy/restored jobs that already exist.
24
+ const promote = db.transaction(() => {
25
+ db.prepare('INSERT OR IGNORE INTO jobs (id, command, started_at, status, provider, owner, priority, depends_on_job_id, pipeline_id, interactive, causal_ownership) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(job.id, job.command, job.started_at, 'running', job.provider ?? null, job.owner ?? 'queue', job.priority ?? 'normal', job.depends_on_job_id ?? null, job.pipeline_id ?? null, job.interactive ? 1 : 0, job.causal_ownership ? 1 : 0);
26
+ db.prepare('UPDATE jobs SET status = ?, started_at = ?, provider = ?, owner = ?, interactive = ?, causal_ownership = ? WHERE id = ?').run('running', job.started_at, job.provider ?? null, job.owner ?? 'queue', job.interactive ? 1 : 0, job.causal_ownership ? 1 : 0, job.id);
27
+ db.prepare('DELETE FROM queued_jobs WHERE id = ?').run(job.id);
28
+ });
29
+ promote();
30
+ }
31
+ /** Idempotently persist a job that has been admitted but has not started. */
32
+ function upsertQueuedJob(db, job) {
33
+ db.prepare(`
34
+ INSERT INTO queued_jobs (
35
+ id, command, queue_position, priority, depends_on_job_id, pipeline_id,
36
+ provider, model, profile_name, profile_selection_set, interactive,
37
+ causal_ownership
38
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
39
+ ON CONFLICT(id) DO UPDATE SET
40
+ command = excluded.command,
41
+ queue_position = excluded.queue_position,
42
+ priority = excluded.priority,
43
+ depends_on_job_id = excluded.depends_on_job_id,
44
+ pipeline_id = excluded.pipeline_id,
45
+ provider = excluded.provider,
46
+ model = excluded.model,
47
+ profile_name = excluded.profile_name,
48
+ profile_selection_set = excluded.profile_selection_set,
49
+ interactive = excluded.interactive,
50
+ causal_ownership = excluded.causal_ownership
51
+ `).run(job.id, job.command, job.queue_position, job.priority, job.depends_on_job_id ?? null, job.pipeline_id ?? null, job.provider ?? null, job.model ?? null, job.profile_name ?? null, job.profile_selection_set ? 1 : 0, job.interactive == null ? null : (job.interactive ? 1 : 0), job.causal_ownership ? 1 : 0);
52
+ }
53
+ /** Remove a queued admission after cancellation/skip. Idempotent.
54
+ *
55
+ * Migration 43 deliberately keeps a read fallback for pre-migration builds
56
+ * that represented queued work in `jobs`. Remove that legacy representation
57
+ * too, but only while it is still queued: promotion changes the status to
58
+ * running before calling this helper, and terminal history must never be
59
+ * deleted here. */
60
+ function deleteQueuedJob(db, jobId) {
61
+ db.prepare('DELETE FROM queued_jobs WHERE id = ?').run(jobId);
62
+ db.prepare("DELETE FROM jobs WHERE id = ? AND status = 'queued'").run(jobId);
63
+ }
64
+ /**
65
+ * Flip a job row's `interactive` flag AFTER creation. Used by the loop engine
66
+ * when an ai-step upgrades the run's backing job to a resident interactive
67
+ * session — the row was created at run start (before the step's provider
68
+ * capability/kill-switch gate is consulted), so the flag lands lazily at the
69
+ * first interactive step spawn. Idempotent.
70
+ */
71
+ function markJobInteractive(db, jobId) {
72
+ db.prepare('UPDATE jobs SET interactive = 1 WHERE id = ?').run(jobId);
73
+ }
74
+ /**
75
+ * Add one completed interactive turn's REAL usage into the job row. Token/cost/
76
+ * turn columns accumulate (COALESCE so the first turn starts from a clean base);
77
+ * model + session_id are stamped from the first turn that reports them. The job
78
+ * stays 'running' — only finalizeInteractiveJob flips the terminal status. This
79
+ * keeps the live Job Detail totals honest (sum of completed turns, never an
80
+ * estimate) between turns.
81
+ */
82
+ function accumulateInteractiveTurn(db, jobId, turn) {
83
+ db.prepare(`
84
+ UPDATE jobs SET
85
+ tokens_in = COALESCE(tokens_in, 0) + ?,
86
+ tokens_out = COALESCE(tokens_out, 0) + ?,
87
+ tokens_cache_read = COALESCE(tokens_cache_read, 0) + ?,
88
+ tokens_cache_create = COALESCE(tokens_cache_create, 0) + ?,
89
+ total_cost_usd = COALESCE(total_cost_usd, 0) + ?,
90
+ num_turns = COALESCE(num_turns, 0) + ?,
91
+ total_cost_usd_estimated = CASE WHEN ? = 1 THEN 1 ELSE total_cost_usd_estimated END,
92
+ model = COALESCE(model, ?),
93
+ session_id = COALESCE(?, session_id)
94
+ WHERE id = ?
95
+ `).run(turn.tokens_in, turn.tokens_out, turn.tokens_cache_read, turn.tokens_cache_create, turn.total_cost_usd, turn.num_turns, turn.estimated ? 1 : 0, turn.model ?? null, turn.session_id ?? null, jobId);
96
+ }
97
+ /**
98
+ * Flip an interactive job to its terminal status (completed on finalize, failed
99
+ * on crash) and stamp finished_at. Token/cost/turn columns are left untouched —
100
+ * they were already accumulated turn-by-turn via accumulateInteractiveTurn.
101
+ */
102
+ function finalizeInteractiveJob(db, jobId, status) {
103
+ db.prepare('UPDATE jobs SET status = ?, finished_at = ? WHERE id = ?').run(status, new Date().toISOString(), jobId);
104
+ }
105
+ function finishJob(db, jobId, result) {
106
+ db.prepare(`
107
+ UPDATE jobs SET
108
+ status = ?,
109
+ exit_code = ?,
110
+ finished_at = ?,
111
+ tokens_in = ?,
112
+ tokens_out = ?,
113
+ tokens_cache_read = ?,
114
+ tokens_cache_create = ?,
115
+ total_cost_usd = ?,
116
+ total_cost_usd_estimated = ?,
117
+ num_turns = ?,
118
+ model = ?,
119
+ duration_ms = ?,
120
+ duration_api_ms = ?,
121
+ session_id = ?
122
+ WHERE id = ?
123
+ `).run(result.status, result.exit_code, new Date().toISOString(), result.tokens_in ?? null, result.tokens_out ?? null, result.tokens_cache_read ?? null, result.tokens_cache_create ?? null, result.total_cost_usd ?? null, result.total_cost_usd_estimated ? 1 : 0, result.num_turns ?? null, result.model ?? null, result.duration_ms ?? null, result.duration_api_ms ?? null, result.session_id ?? null, jobId);
124
+ }
125
+ function appendEvent(db, jobId, seq, event) {
126
+ db.prepare('INSERT INTO events (job_id, seq, event_type, source, payload) VALUES (?, ?, ?, ?, ?)').run(jobId, seq, event.event_type, event.source ?? null, event.payload);
127
+ }
128
+ function upsertPhase(db, jobId, phase, state) {
129
+ db.prepare('INSERT OR REPLACE INTO job_phases (job_id, phase, state, updated_at) VALUES (?, ?, ?, ?)').run(jobId, phase, state, new Date().toISOString());
130
+ }
131
+ function listJobs(db, opts) {
132
+ const limit = Math.min(opts.limit ?? 50, 200);
133
+ const offset = opts.offset ?? 0;
134
+ const conditions = [];
135
+ const params = [];
136
+ if (opts.status) {
137
+ conditions.push('status = ?');
138
+ params.push(opts.status);
139
+ }
140
+ if (opts.from) {
141
+ conditions.push('started_at >= ?');
142
+ params.push(opts.from);
143
+ }
144
+ if (opts.to) {
145
+ conditions.push('started_at <= ?');
146
+ params.push(opts.to);
147
+ }
148
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
149
+ const countRow = db
150
+ .prepare(`SELECT COUNT(*) as count FROM jobs ${where}`)
151
+ .get(...params);
152
+ const jobs = db
153
+ .prepare(`SELECT jobs.*, jp.profile_name AS profile_name
154
+ FROM jobs LEFT JOIN job_profiles jp ON jp.job_id = jobs.id
155
+ ${where}
156
+ ORDER BY started_at DESC LIMIT ? OFFSET ?`)
157
+ .all(...params, limit, offset);
158
+ return { jobs, total: countRow.count };
159
+ }
160
+ function getJob(db, jobId) {
161
+ return db
162
+ .prepare(`
163
+ SELECT jobs.*, jp.profile_name AS profile_name
164
+ FROM jobs
165
+ LEFT JOIN job_profiles jp ON jp.job_id = jobs.id
166
+ WHERE jobs.id = ?
167
+ `)
168
+ .get(jobId);
169
+ }
170
+ function getJobEvents(db, jobId) {
171
+ return db
172
+ .prepare('SELECT * FROM events WHERE job_id = ? ORDER BY seq ASC')
173
+ .all(jobId);
174
+ }
175
+ class JobRecoveryPendingError extends Error {
176
+ jobId;
177
+ constructor(jobId) {
178
+ super('Job recovery is still pending; retry deletion after recovery completes');
179
+ this.jobId = jobId;
180
+ this.name = 'JobRecoveryPendingError';
181
+ }
182
+ }
183
+ exports.JobRecoveryPendingError = JobRecoveryPendingError;
184
+ function deleteJob(db, jobId) {
185
+ // M7: jobs.depends_on_job_id REFERENCES jobs(id) with no ON DELETE action and
186
+ // foreign_keys=ON, so deleting a pipeline parent throws 'FOREIGN KEY
187
+ // constraint failed' and the job becomes undeletable from the UI. Clear inbound
188
+ // references first, in the same transaction as the delete, so it always
189
+ // succeeds (children keep running; they just lose the now-irrelevant pointer).
190
+ const tx = db.transaction((id) => {
191
+ const pendingLoopStep = db.prepare(`SELECT 1 FROM loop_step_recovery WHERE run_id = ? LIMIT 1`).get(id);
192
+ const pendingQueueRecovery = db.prepare(`SELECT 1 FROM orphan_job_recovery WHERE job_id = ? LIMIT 1`).get(id);
193
+ if (pendingLoopStep || pendingQueueRecovery) {
194
+ throw new JobRecoveryPendingError(id);
195
+ }
196
+ db.prepare('UPDATE jobs SET depends_on_job_id = NULL WHERE depends_on_job_id = ?').run(id);
197
+ // B41: events/job_phases cascade on the jobs FK, but telemetry_blobs/
198
+ // telemetry_summaries (keyed `jobId`), job_profiles and file_provenance
199
+ // (keyed `job_id`) have no FK — without these they accumulate forever. (The
200
+ // on-disk .ndjson.gz blob is reclaimed by the 7-day startup compactor.)
201
+ db.prepare('DELETE FROM telemetry_blobs WHERE jobId = ?').run(id);
202
+ db.prepare('DELETE FROM telemetry_summaries WHERE jobId = ?').run(id);
203
+ db.prepare('DELETE FROM job_profiles WHERE job_id = ?').run(id);
204
+ db.prepare('DELETE FROM file_provenance WHERE job_id = ?').run(id);
205
+ db.prepare('DELETE FROM jobs WHERE id = ?').run(id);
206
+ });
207
+ tx(jobId);
208
+ }
209
+ function purgeJobs(db, opts) {
210
+ const conditions = [
211
+ "status IN ('completed', 'failed', 'canceled', 'zombie_terminated', 'skipped')",
212
+ 'NOT EXISTS (SELECT 1 FROM loop_step_recovery WHERE loop_step_recovery.run_id = jobs.id)',
213
+ 'NOT EXISTS (SELECT 1 FROM orphan_job_recovery WHERE orphan_job_recovery.job_id = jobs.id)',
214
+ ];
215
+ const params = [];
216
+ if (opts?.from) {
217
+ conditions.push('started_at >= ?');
218
+ params.push(opts.from);
219
+ }
220
+ if (opts?.to) {
221
+ conditions.push('started_at <= ?');
222
+ params.push(opts.to);
223
+ }
224
+ const where = conditions.join(' AND ');
225
+ // M6: run the whole purge atomically. Previously these statements ran without a
226
+ // transaction, so when the final `DELETE FROM jobs` aborted on the
227
+ // depends_on_job_id FK (a purged job still referenced by a non-purged one), the
228
+ // events/phases deletes had already committed — destroying log history while
229
+ // deleting zero job rows, and a misleading 500. The transaction rolls back on
230
+ // any failure, and NULL-ing inbound references first makes the delete succeed.
231
+ const tx = db.transaction(() => {
232
+ const sel = `SELECT id FROM jobs WHERE ${where}`;
233
+ db.prepare(`DELETE FROM events WHERE job_id IN (${sel})`).run(...params);
234
+ db.prepare(`DELETE FROM job_phases WHERE job_id IN (${sel})`).run(...params);
235
+ // B41: also purge the no-FK orphan tables for the same jobs.
236
+ db.prepare(`DELETE FROM telemetry_blobs WHERE jobId IN (${sel})`).run(...params);
237
+ db.prepare(`DELETE FROM telemetry_summaries WHERE jobId IN (${sel})`).run(...params);
238
+ db.prepare(`DELETE FROM job_profiles WHERE job_id IN (${sel})`).run(...params);
239
+ db.prepare(`DELETE FROM file_provenance WHERE job_id IN (${sel})`).run(...params);
240
+ // Clear inbound FK references from NON-purged jobs to purged jobs.
241
+ db.prepare(`UPDATE jobs SET depends_on_job_id = NULL WHERE depends_on_job_id IN (${sel})`).run(...params);
242
+ return db.prepare(`DELETE FROM jobs WHERE ${where}`).run(...params).changes;
243
+ });
244
+ return tx();
245
+ }
246
+ function skipJob(db, jobId, reason) {
247
+ db.prepare(`UPDATE jobs SET status = 'skipped', skip_reason = ?, finished_at = ? WHERE id = ?`).run(reason, new Date().toISOString(), jobId);
248
+ }
249
+ function getPipelineJobs(db, pipelineId) {
250
+ return db.prepare('SELECT * FROM jobs WHERE pipeline_id = ? ORDER BY queue_position ASC, started_at ASC').all(pipelineId);
251
+ }