specrails-desktop 2.46.0 → 2.48.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 (259) hide show
  1. package/README.md +2 -0
  2. package/client/dist/assets/{ActivityFeedPage-CsHQPpvl.js → ActivityFeedPage-CGq6QhP5.js} +1 -1
  3. package/client/dist/assets/AgentBrowserCapture-C4GZ-kdf.js +1 -0
  4. package/client/dist/assets/AgentModeAnalyticsPane-_IPmL0ZS.js +2 -0
  5. package/client/dist/assets/AgentModeCodePane-B5WG5DOP.js +2 -0
  6. package/client/dist/assets/{AgentModeJobsPane-rnD-EPMd.js → AgentModeJobsPane-w1N_HIXh.js} +1 -1
  7. package/client/dist/assets/AgentsPage-CmwnnT46.js +87 -0
  8. package/client/dist/assets/AnalyticsPage-BUMsKtsJ.js +1 -0
  9. package/client/dist/assets/CodePage-PjgywJPS.js +3 -0
  10. package/client/dist/assets/DesktopAnalyticsPage-BXY1pH4y.js +1 -0
  11. package/client/dist/assets/DocsDialog-ccQETHKB.js +11 -0
  12. package/client/dist/assets/DocsPage-DzEgplB3.js +11 -0
  13. package/client/dist/assets/{ExportDropdown-pWZGYCv3.js → ExportDropdown-B8bBGopr.js} +1 -1
  14. package/client/dist/assets/InteractiveJobComposer-CEwGkJ8F.js +19 -0
  15. package/client/dist/assets/JobDetailModal-BYXmuTDe.js +1 -0
  16. package/client/dist/assets/JobDetailPage-CWp6MwJj.js +1 -0
  17. package/client/dist/assets/JobsPage-D1O1uyG8.js +1 -0
  18. package/client/dist/assets/{LoopBuilderPage-CtmOE4-b.js → LoopBuilderPage-DQE-7Vby.js} +2 -2
  19. package/client/dist/assets/{LoopPreviewModal-BYeLjQbr.js → LoopPreviewModal-Bk7zIXD5.js} +1 -1
  20. package/client/dist/assets/LoopsPage-CqG7mx56.js +1 -0
  21. package/client/dist/assets/MinimizedChatsContext-UpnqccPx.js +1 -0
  22. package/client/dist/assets/PluginsPage-CjgVvmpW.js +2 -0
  23. package/client/dist/assets/{ProjectSettingsDialog-DsCRIc4v.js → ProjectSettingsDialog-D6vBCoay.js} +1 -1
  24. package/client/dist/assets/{RepositoryDeliveries-BQm_3TO8.js → RepositoryDeliveries-C36ipKp_.js} +1 -1
  25. package/client/dist/assets/{RepositoryScopeSelector-DnAFtjTz.js → RepositoryScopeSelector-0Gz_fR1E.js} +1 -1
  26. package/client/dist/assets/ReviewPacketPage-Dr9RYs7q.js +1 -0
  27. package/client/dist/assets/TemplatePreviewModal-sCMHYFJo.js +1 -0
  28. package/client/dist/assets/TicketDetailModalContext-C5GgJlEt.js +14 -0
  29. package/client/dist/assets/{Trans-BwLgX6n7.js → Trans-9pHTy2fH.js} +1 -1
  30. package/client/dist/assets/{addspec-D2YIQ0GU.js → addspec-BOvgmcgk.js} +1 -1
  31. package/client/dist/assets/{addspec-B_TPJt-R.js → addspec-Clmyfn_R.js} +1 -1
  32. package/client/dist/assets/{addspec-A1lDQa4M.js → addspec-CsNaBpHp.js} +1 -1
  33. package/client/dist/assets/{addspec-CQBjzt24.js → addspec-D2tvWmmV.js} +1 -1
  34. package/client/dist/assets/{addspec-CLYLvaVP.js → addspec-D3nFwvD0.js} +1 -1
  35. package/client/dist/assets/{addspec-BPpZ_olp.js → addspec-DVLEhK3w.js} +1 -1
  36. package/client/dist/assets/{addspec-iFdQ6D99.js → addspec-DXbK-tb0.js} +1 -1
  37. package/client/dist/assets/{addspec-BHZd0N5F.js → addspec-OHG8DFEn.js} +1 -1
  38. package/client/dist/assets/agentRuntime-BUimiX__.js +1 -0
  39. package/client/dist/assets/agentRuntime-BcN97sAB.js +1 -0
  40. package/client/dist/assets/agentRuntime-Ckyg-ahT.js +1 -0
  41. package/client/dist/assets/agentRuntime-DJCwyX4t.js +1 -0
  42. package/client/dist/assets/agentRuntime-DVzPJOl0.js +1 -0
  43. package/client/dist/assets/agentRuntime-V9AZTG3c.js +1 -0
  44. package/client/dist/assets/agentRuntime-bAAjw1vw.js +1 -0
  45. package/client/dist/assets/agentRuntime-v4qKAYH-.js +1 -0
  46. package/client/dist/assets/{agents-45GbINfh.js → agents-CA5eT1d0.js} +1 -1
  47. package/client/dist/assets/{agents-DMKFStHL.js → agents-CAgy-5uS.js} +1 -1
  48. package/client/dist/assets/{agents-IUOgqT5t.js → agents-CNm6Qzlv.js} +1 -1
  49. package/client/dist/assets/{agents-ms7TwG16.js → agents-CTwbx-fb.js} +1 -1
  50. package/client/dist/assets/{agents-DaYcxAr1.js → agents-DEGV1Vao.js} +1 -1
  51. package/client/dist/assets/{agents-BHp0_FQt.js → agents-HsDF1aQU.js} +1 -1
  52. package/client/dist/assets/{agents-Bvyi3y6R.js → agents-IQjdA-z9.js} +1 -1
  53. package/client/dist/assets/{agents-rzPnqoBR.js → agents-RkGoteXh.js} +1 -1
  54. package/client/dist/assets/analytics-BNgZ1Nj7.js +1 -0
  55. package/client/dist/assets/analytics-BlypLLR6.js +1 -0
  56. package/client/dist/assets/analytics-CoEzWPx4.js +1 -0
  57. package/client/dist/assets/analytics-DFh5lSOl.js +1 -0
  58. package/client/dist/assets/analytics-DZp-DS2U.js +1 -0
  59. package/client/dist/assets/analytics-DjLlE7jS.js +1 -0
  60. package/client/dist/assets/analytics-jzCkOec3.js +1 -0
  61. package/client/dist/assets/analytics-nZIjjtCc.js +1 -0
  62. package/client/dist/assets/{dashboard-BK_mez-z.js → dashboard-C29Qwa-L.js} +1 -1
  63. package/client/dist/assets/{dashboard-BwkjxI6H.js → dashboard-CSJzf5OT.js} +1 -1
  64. package/client/dist/assets/{dashboard-C3bDKf7s.js → dashboard-CTfEtkqX.js} +1 -1
  65. package/client/dist/assets/{dashboard-Bt0cCzeG.js → dashboard-Cr4D8oMn.js} +1 -1
  66. package/client/dist/assets/{dashboard-fRrP0Kv0.js → dashboard-Csf1yGSN.js} +1 -1
  67. package/client/dist/assets/{dashboard-CdvjGfog.js → dashboard-D40HP2ks.js} +1 -1
  68. package/client/dist/assets/dashboard-DBAP2_SQ.js +1 -0
  69. package/client/dist/assets/{dashboard-B14uYXkG.js → dashboard-DMklepmz.js} +1 -1
  70. package/client/dist/assets/file-text-CFT-xXKN.js +1 -0
  71. package/client/dist/assets/{formatDistanceToNow-CeZv93iR.js → formatDistanceToNow-w_2U5pm0.js} +1 -1
  72. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-DxAkjvJb.js → getTimezoneOffsetInMilliseconds-BRymg1Kk.js} +1 -1
  73. package/client/dist/assets/index-D0ScFZvR.css +2 -0
  74. package/client/dist/assets/index-DORCrgbI.js +77 -0
  75. package/client/dist/assets/jira-api-BqHbSdxK.js +1 -0
  76. package/client/dist/assets/jobs-C6qmvUAi.js +1 -0
  77. package/client/dist/assets/jobs-C9j2PuwW.js +1 -0
  78. package/client/dist/assets/jobs-DLHdDkHc.js +1 -0
  79. package/client/dist/assets/jobs-DSp9DoD_.js +1 -0
  80. package/client/dist/assets/jobs-DTQO08nJ.js +1 -0
  81. package/client/dist/assets/jobs-DoFaEl9g.js +1 -0
  82. package/client/dist/assets/jobs-Dpw6AEOP.js +1 -0
  83. package/client/dist/assets/jobs-DrF8Do8s.js +1 -0
  84. package/client/dist/assets/{narration-BSldQfjd.js → narration--hUhjM5v.js} +1 -1
  85. package/client/dist/assets/{narration-DKLjXIFI.js → narration-CHI06czm.js} +1 -1
  86. package/client/dist/assets/{narration-4OSg0b5-.js → narration-DjKB3pQZ.js} +1 -1
  87. package/client/dist/assets/{narration-BPMOQPFe.js → narration-DtCYvRLM.js} +1 -1
  88. package/client/dist/assets/{narration-D-e9ssxA.js → narration-DvLQBW9U.js} +1 -1
  89. package/client/dist/assets/{narration-DnrKC3mF.js → narration-eSx3gR0W.js} +1 -1
  90. package/client/dist/assets/{narration-Dypjxeel.js → narration-n3hOA7LY.js} +1 -1
  91. package/client/dist/assets/{narration-CvQdWdR5.js → narration-thJOeT2-.js} +1 -1
  92. package/client/dist/assets/packet-BNW8ODQw.js +1 -0
  93. package/client/dist/assets/packet-CXYvfdjy.js +1 -0
  94. package/client/dist/assets/packet-CtIlB8YE.js +1 -0
  95. package/client/dist/assets/packet-DA1SJkMH.js +1 -0
  96. package/client/dist/assets/packet-DYcWfBip.js +1 -0
  97. package/client/dist/assets/packet-DseX9yKC.js +1 -0
  98. package/client/dist/assets/packet-_m5aOy_d.js +1 -0
  99. package/client/dist/assets/packet-qOemUAm3.js +1 -0
  100. package/client/dist/assets/{project-repositories-BvBj9XH9.js → project-repositories-BsCs1PPA.js} +1 -1
  101. package/client/dist/assets/provider-capabilities-Vi1ZGvAK.js +1 -0
  102. package/client/dist/assets/settings-BsehKpty.js +1 -0
  103. package/client/dist/assets/settings-CQWob3SB.js +1 -0
  104. package/client/dist/assets/settings-CgCl0hzy.js +1 -0
  105. package/client/dist/assets/settings-DLRLd22L.js +1 -0
  106. package/client/dist/assets/settings-Dn5prAFl.js +1 -0
  107. package/client/dist/assets/settings-HZSu-K4l.js +1 -0
  108. package/client/dist/assets/{settings-DEgcA0jd.js → settings-p4OVIyRh.js} +1 -1
  109. package/client/dist/assets/settings-rddaldUp.js +1 -0
  110. package/client/dist/assets/setup-B5FHuxvB.js +1 -0
  111. package/client/dist/assets/setup-BihzXadx.js +1 -0
  112. package/client/dist/assets/setup-BtC6Hg4K.js +1 -0
  113. package/client/dist/assets/setup-C9Fy9PKQ.js +1 -0
  114. package/client/dist/assets/setup-CROyqD5N.js +1 -0
  115. package/client/dist/assets/setup-DpqzaEEA.js +1 -0
  116. package/client/dist/assets/setup-Dwzt5nO1.js +1 -0
  117. package/client/dist/assets/setup-zLEDnG7N.js +1 -0
  118. package/client/dist/assets/{spending-DNM6NAoA.js → spending-hcgxHP4O.js} +1 -1
  119. package/client/dist/assets/{useDesktop-mxHVQOdx.js → useDesktop-CAisjKWC.js} +1 -1
  120. package/client/dist/assets/{useSharedWebSocket-DnxHj8jP.js → useSharedWebSocket-BHIfW4QT.js} +2 -2
  121. package/client/dist/assets/wrench-BfmNO9Vw.js +1 -0
  122. package/client/dist/index.html +27 -26
  123. package/docs/guide/de/integrations/7-local-engines.md +37 -0
  124. package/docs/guide/en/integrations/7-local-engines.md +37 -0
  125. package/docs/guide/es/integrations/7-local-engines.md +37 -0
  126. package/docs/guide/fr/integrations/7-local-engines.md +37 -0
  127. package/docs/guide/it/integrations/7-local-engines.md +37 -0
  128. package/docs/guide/ja/integrations/7-local-engines.md +37 -0
  129. package/docs/guide/pt/integrations/7-local-engines.md +37 -0
  130. package/docs/guide/zh/integrations/7-local-engines.md +37 -0
  131. package/docs/internals/adding-a-provider.md +12 -0
  132. package/docs/internals/local-agent-runner.md +153 -0
  133. package/docs/internals/review-packet.md +8 -0
  134. package/docs/local-providers.md +215 -0
  135. package/docs/plans/implementation-efficiency-artifact.json +18 -0
  136. package/docs/plans/implementation-efficiency-evaluation.json +1817 -0
  137. package/docs/plans/implementation-efficiency-evaluation.md +41 -0
  138. package/docs/plans/implementation-efficiency-progress.md +48 -0
  139. package/docs/plans/implementation-efficiency-release.md +41 -0
  140. package/docs/plans/implementation-efficiency.md +124 -0
  141. package/local-runner/dist/specrails-local-runner.js +15815 -0
  142. package/package.json +7 -5
  143. package/server/dist/agent-chat-manager.js +8 -1
  144. package/server/dist/agent-chat-router.js +5 -2
  145. package/server/dist/agent-fence-promotion.js +53 -0
  146. package/server/dist/agent-mcp-config.js +4 -1
  147. package/server/dist/agent-runtime-bridge.js +35 -26
  148. package/server/dist/agent-runtime-controls-router.js +20 -1
  149. package/server/dist/agent-runtime-controls.js +94 -12
  150. package/server/dist/agent-runtime-effective-config.js +53 -0
  151. package/server/dist/agent-runtime-events.js +46 -0
  152. package/server/dist/agent-runtime-history.js +47 -0
  153. package/server/dist/agent-runtime-loader.js +84 -5
  154. package/server/dist/agent-runtime-metrics.js +77 -1
  155. package/server/dist/agent-runtime-package.js +205 -0
  156. package/server/dist/agent-runtime-settings-router.js +96 -5
  157. package/server/dist/agent-runtime-settings.js +163 -7
  158. package/server/dist/agent-spec-framing.js +10 -4
  159. package/server/dist/binary-probe.js +12 -0
  160. package/server/dist/blueprint-router.js +3 -2
  161. package/server/dist/chat-manager.js +5 -1
  162. package/server/dist/desktop-router.js +87 -4
  163. package/server/dist/index.js +13 -0
  164. package/server/dist/interactive-job-session.js +3 -1
  165. package/server/dist/local-engine-detection.js +118 -0
  166. package/server/dist/loop-executors.js +4 -3
  167. package/server/dist/loop-role-engines.js +129 -0
  168. package/server/dist/loop-run-manager.js +14 -6
  169. package/server/dist/mcp/tools/app-settings.js +30 -4
  170. package/server/dist/mcp/tools/rails.js +2 -1
  171. package/server/dist/pr-body.js +11 -2
  172. package/server/dist/pricing.js +15 -0
  173. package/server/dist/provider-detection.js +63 -1
  174. package/server/dist/provider-selection.js +26 -1
  175. package/server/dist/providers/index.js +4 -1
  176. package/server/dist/providers/local-adapter-registry.js +56 -0
  177. package/server/dist/providers/local-adapter.js +426 -0
  178. package/server/dist/providers/registry.js +39 -4
  179. package/server/dist/rail-isolated-launch.js +4 -3
  180. package/server/dist/rails-router.js +90 -8
  181. package/server/dist/result-event.js +17 -0
  182. package/server/dist/review-packet.js +1 -0
  183. package/server/dist/schemas/agent-runtime.schema.json +28 -3
  184. package/server/dist/schemas/fixtures/runtime-efficiency-summary.v1.json +790 -0
  185. package/server/dist/setup-manager.js +5 -1
  186. package/server/dist/spec-models.js +14 -2
  187. package/server/dist/worktree-node-modules.js +97 -65
  188. package/client/dist/assets/AgentBrowserCapture-BVcCK7x0.js +0 -1
  189. package/client/dist/assets/AgentModeAnalyticsPane-C5F3m29G.js +0 -2
  190. package/client/dist/assets/AgentModeCodePane-C5QuKsu0.js +0 -2
  191. package/client/dist/assets/AgentsPage-DiyJWZcU.js +0 -87
  192. package/client/dist/assets/AnalyticsPage-Dkmb0dnd.js +0 -1
  193. package/client/dist/assets/CodePage-C0dH99-t.js +0 -3
  194. package/client/dist/assets/DesktopAnalyticsPage-DuTjr2n8.js +0 -1
  195. package/client/dist/assets/DocsDialog-BVZJZHVR.js +0 -11
  196. package/client/dist/assets/DocsPage-D6KHTv8V.js +0 -11
  197. package/client/dist/assets/InteractiveJobComposer-Ck3PQxjN.js +0 -19
  198. package/client/dist/assets/JobDetailModal-DkoMH3ie.js +0 -1
  199. package/client/dist/assets/JobDetailPage-CBVivcl2.js +0 -1
  200. package/client/dist/assets/JobsPage-CzxEeca9.js +0 -1
  201. package/client/dist/assets/LoopsPage-6iW7g25k.js +0 -1
  202. package/client/dist/assets/MinimizedChatsContext-B6wVevPq.js +0 -1
  203. package/client/dist/assets/PluginsPage-Bf1z3WPF.js +0 -2
  204. package/client/dist/assets/ReviewPacketPage-CaXAwtR7.js +0 -1
  205. package/client/dist/assets/TemplatePreviewModal-9aRFpJ_J.js +0 -1
  206. package/client/dist/assets/TicketDetailModalContext-B1X5glzV.js +0 -14
  207. package/client/dist/assets/agentRuntime-BUagD2I_.js +0 -1
  208. package/client/dist/assets/agentRuntime-BkL70WtX.js +0 -1
  209. package/client/dist/assets/agentRuntime-CWXVqgqE.js +0 -1
  210. package/client/dist/assets/agentRuntime-CvNBdUAe.js +0 -1
  211. package/client/dist/assets/agentRuntime-D4RmeuNY.js +0 -1
  212. package/client/dist/assets/agentRuntime-DTidBWKg.js +0 -1
  213. package/client/dist/assets/agentRuntime-DhKYfyaU.js +0 -1
  214. package/client/dist/assets/agentRuntime-LL8_GLx9.js +0 -1
  215. package/client/dist/assets/analytics-47pYygtV.js +0 -1
  216. package/client/dist/assets/analytics-B0vt1g5U.js +0 -1
  217. package/client/dist/assets/analytics-BMzCvrCU.js +0 -1
  218. package/client/dist/assets/analytics-BlFN3Zwx.js +0 -1
  219. package/client/dist/assets/analytics-CZYzL-nR.js +0 -1
  220. package/client/dist/assets/analytics-DAsb9MJ8.js +0 -1
  221. package/client/dist/assets/analytics-FXQlQ2Yt.js +0 -1
  222. package/client/dist/assets/analytics-RnFCNkah.js +0 -1
  223. package/client/dist/assets/dashboard-BfBir_6o.js +0 -1
  224. package/client/dist/assets/file-text-BcLCfcWZ.js +0 -1
  225. package/client/dist/assets/index-Cfh9aaep.css +0 -2
  226. package/client/dist/assets/index-DYoyDWim.js +0 -76
  227. package/client/dist/assets/jira-api-ChjVGJx_.js +0 -1
  228. package/client/dist/assets/jobs-5PQJbdOf.js +0 -1
  229. package/client/dist/assets/jobs-B2B08Oy9.js +0 -1
  230. package/client/dist/assets/jobs-BJZtBbwb.js +0 -1
  231. package/client/dist/assets/jobs-BlWEsQb7.js +0 -1
  232. package/client/dist/assets/jobs-ConfD8xD.js +0 -1
  233. package/client/dist/assets/jobs-DMPU64ms.js +0 -1
  234. package/client/dist/assets/jobs-DojASiTp.js +0 -1
  235. package/client/dist/assets/jobs-kVHkNpB-.js +0 -1
  236. package/client/dist/assets/packet-BQ2tXfJI.js +0 -1
  237. package/client/dist/assets/packet-BdpOJ1Cy.js +0 -1
  238. package/client/dist/assets/packet-C3Ievb9R.js +0 -1
  239. package/client/dist/assets/packet-C4zJIG0Y.js +0 -1
  240. package/client/dist/assets/packet-Cx-zRuWn.js +0 -1
  241. package/client/dist/assets/packet-DUTGX52M.js +0 -1
  242. package/client/dist/assets/packet-DhsKtUPB.js +0 -1
  243. package/client/dist/assets/packet-z3LJ8Zyy.js +0 -1
  244. package/client/dist/assets/provider-capabilities-CXipwrXJ.js +0 -1
  245. package/client/dist/assets/settings-B3rWEwOx.js +0 -1
  246. package/client/dist/assets/settings-BcVoiMyw.js +0 -1
  247. package/client/dist/assets/settings-BlYvaQ7w.js +0 -1
  248. package/client/dist/assets/settings-CD5uEBiL.js +0 -1
  249. package/client/dist/assets/settings-CyhS0DLJ.js +0 -1
  250. package/client/dist/assets/settings-DCMFUGDs.js +0 -1
  251. package/client/dist/assets/settings-DmbYKyMJ.js +0 -1
  252. package/client/dist/assets/setup-2xGgu4Sk.js +0 -1
  253. package/client/dist/assets/setup-Ba11l2JA.js +0 -1
  254. package/client/dist/assets/setup-BzW4kMok.js +0 -1
  255. package/client/dist/assets/setup-COzwYhkJ.js +0 -1
  256. package/client/dist/assets/setup-CRVULhtF.js +0 -1
  257. package/client/dist/assets/setup-DgvlxwpE.js +0 -1
  258. package/client/dist/assets/setup-Dw_wYeEY.js +0 -1
  259. package/client/dist/assets/setup-ZPtEeJPW.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.46.0",
3
+ "version": "2.48.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,6 +17,7 @@
17
17
  "server/dist",
18
18
  "client/dist",
19
19
  "mcp-bridge/dist",
20
+ "local-runner/dist",
20
21
  "docs",
21
22
  ".claude/commands/specrails"
22
23
  ],
@@ -25,20 +26,21 @@
25
26
  "dev:server": "tsx watch server/index.ts",
26
27
  "dev:client": "cd client && npm run dev",
27
28
  "dev:desktop": "tauri dev",
28
- "dev:desktop:prepare": "npm run build:sidecar && npm run build:mcp-bridge && npm run dev:client",
29
+ "dev:desktop:prepare": "npm run build:sidecar && npm run build:mcp-bridge && npm run build:local-runner && npm run dev:client",
29
30
  "build:server": "node -e \"require('fs').rmSync('server/dist',{recursive:true,force:true})\" && tsc --project tsconfig.build.json --noCheck && node scripts/copy-server-assets.mjs",
30
31
  "build:cli": "tsc --project tsconfig.cli.json",
31
- "build": "npm run build:server && npm run build --prefix client && npm run build:cli && npm run build:mcp-bridge",
32
+ "build": "npm run build:server && npm run build --prefix client && npm run build:cli && npm run build:mcp-bridge && npm run build:local-runner",
32
33
  "prepack": "npm run build",
33
34
  "test:scripts": "node scripts/test-scripts.mjs",
34
35
  "check:package": "node scripts/check-package.mjs",
35
36
  "build:sidecar": "node scripts/build-sidecar.mjs",
36
37
  "build:mcp-bridge": "node scripts/build-mcp-bridge.mjs",
37
- "build:desktop": "npm run build:server && cd client && npm run build && cd .. && npm run build:sidecar && npm run build:mcp-bridge && tauri build",
38
+ "build:local-runner": "node scripts/build-local-runner.mjs",
39
+ "build:desktop": "npm run build:server && cd client && npm run build && cd .. && npm run build:sidecar && npm run build:mcp-bridge && npm run build:local-runner && tauri build",
38
40
  "assemble-runtimes": "node scripts/assemble-runtimes-local.mjs",
39
41
  "build:desktop:local": "npm run assemble-runtimes && npm run build:desktop",
40
42
  "generate-icons": "tauri icon src-tauri/icons/icon.svg",
41
- "typecheck": "tsc --noEmit && tsc -p tsconfig.cli.json --noEmit && tsc -p mcp-bridge/tsconfig.json --noEmit && cd client && tsc --noEmit",
43
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.cli.json --noEmit && tsc -p mcp-bridge/tsconfig.json --noEmit && tsc -p local-runner/tsconfig.json --noEmit && cd client && tsc --noEmit",
42
44
  "check-core-compat": "tsx scripts/check-core-compat.ts",
43
45
  "test": "vitest run && tsx scripts/check-core-compat.ts",
44
46
  "test:watch": "vitest",
@@ -13,6 +13,7 @@ const promises_1 = require("node:fs/promises");
13
13
  const win_spawn_1 = require("./util/win-spawn");
14
14
  const providers_1 = require("./providers");
15
15
  const runtime_1 = require("./providers/runtime");
16
+ const registry_1 = require("./providers/registry");
16
17
  const spawn_lifecycle_1 = require("./spawn-lifecycle");
17
18
  const live_session_1 = require("./providers/live-session");
18
19
  const live_session_types_1 = require("./providers/live-session-types");
@@ -415,7 +416,13 @@ class AgentChatManager {
415
416
  // Per-turn dynamic context (pinned project, permission level, provider) rides
416
417
  // the USER turn, never the system prompt — byte-stability contract (see
417
418
  // agent-operator-prompt.ts).
418
- const turnSettings = `Permission level: ${tierLevel} (${['observe', 'edit', 'operate', 'autonomous'][tierLevel]}) | Provider: ${adapter.id} | Model: ${model}${reasoningEffort ? ` | Reasoning effort: ${reasoningEffort}` : ''}`;
419
+ // Local (OpenAI-compatible) engines are usually small models that drop the
420
+ // exact fence tags and ask the user for the #noframe waiver — restate the
421
+ // two rules they break most, on the USER turn (system prompt stays byte-stable).
422
+ const localProtocolNote = (0, registry_1.isLocalAdapterId)(adapter.id)
423
+ ? ' | Protocol: fenced protocol blocks MUST be tagged exactly problem-frame, spec-draft or options (never json); never ask the user to type #noframe'
424
+ : '';
425
+ const turnSettings = `Permission level: ${tierLevel} (${['observe', 'edit', 'operate', 'autonomous'][tierLevel]}) | Provider: ${adapter.id} | Model: ${model}${reasoningEffort ? ` | Reasoning effort: ${reasoningEffort}` : ''}${localProtocolNote}`;
419
426
  const contextPrefix = conversation.pinned_project_id
420
427
  ? `[Active project: projectId="${conversation.pinned_project_id}" | ${turnSettings}. Use this project for project-scoped tools; changing the execution target requires changing the conversation pin.]`
421
428
  : `[No project pinned (Home) | ${turnSettings}]`;
@@ -9,6 +9,7 @@ const fs_1 = __importDefault(require("fs"));
9
9
  const multer_1 = __importDefault(require("multer"));
10
10
  const express_1 = require("express");
11
11
  const providers_1 = require("./providers");
12
+ const provider_selection_1 = require("./provider-selection");
12
13
  const agent_tier_1 = require("./agent-tier");
13
14
  const attachment_manager_1 = require("./attachment-manager");
14
15
  const agent_store_1 = require("./agent-store");
@@ -121,7 +122,7 @@ function createAgentChatRouter(deps) {
121
122
  });
122
123
  // Per-provider model catalog for the header model selector.
123
124
  router.get('/models', (req, res) => {
124
- const provider = validProvider(req.query.provider) ?? 'claude';
125
+ const provider = validProvider(req.query.provider) ?? (0, provider_selection_1.defaultMachineProvider)();
125
126
  const adapter = (0, providers_1.getAdapter)(provider);
126
127
  const requestedModel = (0, providers_1.isModelAvailableForAdapter)(adapter, req.query.model)
127
128
  ? req.query.model
@@ -171,7 +172,9 @@ function createAgentChatRouter(deps) {
171
172
  });
172
173
  router.post('/conversations', (req, res) => {
173
174
  const body = (req.body ?? {});
174
- let provider = 'claude';
175
+ // No explicit provider ⇒ the machine's primary (a local-only machine lands
176
+ // on its local engine, never on an uninstalled claude).
177
+ let provider = (0, provider_selection_1.defaultMachineProvider)();
175
178
  if (body.provider !== undefined) {
176
179
  const v = validProvider(body.provider);
177
180
  if (!v) {
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ // ─── Protocol fence promotion (small-model tolerance) ─────────────────────────
3
+ // The operator protocol asks for fenced blocks tagged EXACTLY `options`,
4
+ // `problem-frame` and `spec-draft`. Small / local models routinely emit the
5
+ // right JSON under a generic ```json (or bare ```) fence instead, which the
6
+ // strict parsers ignore — so no chips, no framing card, and commit_draft then
7
+ // refuses because no frame was ever registered. This pass re-tags a CLOSED
8
+ // generic fence whose body unambiguously has one protocol shape. Anything
9
+ // ambiguous is left untouched; fences already carrying a protocol tag are
10
+ // never rewritten. Mirror: server/agent-fence-promotion.ts (keep in sync).
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.detectProtocolShape = detectProtocolShape;
13
+ exports.promoteAgentProtocolFences = promoteAgentProtocolFences;
14
+ const GENERIC_FENCE_RE = /```(?:json|jsonc|JSON)?[^\S\n]*\n([\s\S]*?)\n[^\S\n]*```/g;
15
+ function isRecord(v) {
16
+ return !!v && typeof v === 'object' && !Array.isArray(v);
17
+ }
18
+ /** Detect which protocol block a parsed JSON body is, or null. */
19
+ function detectProtocolShape(value) {
20
+ if (Array.isArray(value)) {
21
+ if (value.length < 2 || value.length > 8)
22
+ return null;
23
+ return value.every((s) => typeof s === 'string' && s.trim() && s.trim().length <= 80) ? 'options' : null;
24
+ }
25
+ if (!isRecord(value))
26
+ return null;
27
+ if (isRecord(value.restated) && isRecord(value.alternative) && typeof value.discriminator === 'string')
28
+ return 'problem-frame';
29
+ if (typeof value.title === 'string' && typeof value.description === 'string'
30
+ && ('acceptanceCriteria' in value || 'labels' in value || 'priority' in value))
31
+ return 'spec-draft';
32
+ return null;
33
+ }
34
+ /** Re-tag generic fences that carry a protocol-shaped JSON body. */
35
+ function promoteAgentProtocolFences(text) {
36
+ if (!text || !text.includes('```'))
37
+ return text ?? '';
38
+ GENERIC_FENCE_RE.lastIndex = 0;
39
+ return text.replace(GENERIC_FENCE_RE, (whole, body, offset) => {
40
+ // A fence preceded by another backtick is a quoted/long fence — never touch.
41
+ if (offset > 0 && text[offset - 1] === '`')
42
+ return whole;
43
+ let parsed;
44
+ try {
45
+ parsed = JSON.parse(body);
46
+ }
47
+ catch {
48
+ return whole;
49
+ }
50
+ const shape = detectProtocolShape(parsed);
51
+ return shape ? '```' + shape + '\n' + body + '\n```' : whole;
52
+ });
53
+ }
@@ -306,7 +306,10 @@ function codexExternalOverrides(server) {
306
306
  function prepareAgentMcp(opts) {
307
307
  const adapter = (0, providers_1.getAdapter)(opts.adapterId);
308
308
  const external = (opts.external ?? []).filter((s) => s.name !== 'specrails');
309
- if (adapter.id === 'claude') {
309
+ // claude AND local (OpenAI-compatible) runners take the per-conversation
310
+ // `--mcp-config <file>` (design D8): the bundled runner starts the same
311
+ // stdio servers and exposes their tools as `mcp__<server>__<tool>`.
312
+ if (adapter.id === 'claude' || (0, providers_1.isLocalAdapterId)(adapter.id)) {
310
313
  const extraArgs = buildAgentMcpArgs(opts);
311
314
  if (!extraArgs.length)
312
315
  throw new Error('The bundled Specrails MCP bridge is unavailable.');
@@ -3,15 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RUNTIME_HOST_ENV_KEYS = void 0;
4
4
  exports.runtimeChangeName = runtimeChangeName;
5
5
  exports.runAgentRuntimeInvocation = runAgentRuntimeInvocation;
6
+ const agent_runtime_history_1 = require("./agent-runtime-history");
6
7
  const agent_runtime_repositories_1 = require("./agent-runtime-repositories");
7
8
  const node_util_1 = require("node:util");
8
- const providers_1 = require("./providers");
9
+ const agent_runtime_effective_config_1 = require("./agent-runtime-effective-config");
9
10
  const node_child_process_1 = require("node:child_process");
10
11
  const node_crypto_1 = require("node:crypto");
11
12
  const node_fs_1 = require("node:fs");
12
13
  const node_path_1 = require("node:path");
13
14
  const node_readline_1 = require("node:readline");
14
15
  const agent_runtime_loader_1 = require("./agent-runtime-loader");
16
+ const agent_runtime_package_1 = require("./agent-runtime-package");
15
17
  const agent_runtime_settings_1 = require("./agent-runtime-settings");
16
18
  const core_node_runtime_1 = require("./core-node-runtime");
17
19
  const win_spawn_1 = require("./util/win-spawn");
@@ -42,7 +44,8 @@ function saveHostContext(options) {
42
44
  /** Core owns the complete agent workflow in one managed process. The existing
43
45
  * rail's cancellation and worktree ownership remain in Desktop. */
44
46
  async function runAgentRuntimeInvocation(options) {
45
- const cli = (0, agent_runtime_loader_1.findCoreAgentRuntimeCli)();
47
+ const selectedCli = options.resume ? (0, agent_runtime_package_1.resolveRetainedAgentRuntime)(options.contextPath) : (0, agent_runtime_loader_1.findCoreAgentRuntimeCli)();
48
+ let cli = selectedCli;
46
49
  if (!cli)
47
50
  throw new Error('Programmatic agent runtime is enabled but its Core CLI is unavailable. Build or bundle the compatible Core runtime.');
48
51
  if (!options.resume && (!options.configPath || !options.change))
@@ -50,35 +53,36 @@ async function runAgentRuntimeInvocation(options) {
50
53
  const admittedContext = JSON.parse((0, node_fs_1.readFileSync)(options.contextPath, 'utf8'));
51
54
  if (typeof admittedContext.runId !== 'string')
52
55
  throw new Error('Core context is missing its run identity');
53
- if (!options.resume)
54
- saveHostContext(options);
55
56
  const args = [cli, options.resume ? 'resume' : 'run', '--context', options.contextPath];
56
57
  if (!options.resume) {
57
58
  if (!Array.isArray(admittedContext.repositories) || !admittedContext.repositories.length || admittedContext.repositories.some(repo => !repo || typeof repo.id !== 'string' || !repo.id))
58
59
  throw new Error('Core context is missing its repository scope');
59
- const config = (0, agent_runtime_settings_1.loadRuntimeConfigFile)(options.configPath, options.defaultProvider);
60
+ const source = (0, node_fs_1.existsSync)(options.configPath) ? 'project-role' : 'default';
61
+ const { config, origins } = (0, agent_runtime_effective_config_1.resolveEffectiveRuntimeConfig)((0, agent_runtime_settings_1.loadRuntimeConfigFile)(options.configPath, options.defaultProvider), {
62
+ repositoryIds: admittedContext.repositories.map(repo => repo.id), source, providerOverride: options.providerOverride,
63
+ });
60
64
  config.rolePrompts = { ...(await (0, agent_runtime_loader_1.loadCoreAgentRuntime)()).rolePromptDefaults(), ...(0, agent_runtime_settings_1.loadRuntimeRolePrompts)() };
61
- if (options.defaultProvider) {
62
- const provider = config.providers.find(entry => entry.id === options.defaultProvider);
63
- if (!provider)
64
- throw new Error(`Selected runtime provider is not configured: ${options.defaultProvider}`);
65
- for (const role of Object.values(config.agents)) {
66
- if (role.provider !== provider.id) {
67
- role.provider = provider.id;
68
- role.model = options.selectedModel ?? (provider.kind === 'cli' ? (0, providers_1.getAdapter)(provider.cli).defaultModel() : undefined);
69
- if (!role.model)
70
- throw new Error(`Selected runtime provider requires a model: ${provider.id}`);
71
- }
72
- if (!role.model && provider.kind === 'cli')
73
- role.model = (0, providers_1.getAdapter)(provider.cli).defaultModel();
74
- }
75
- if (options.selectedModel)
76
- config.agents.developer.model = options.selectedModel;
65
+ // Core rejects unknown connection keys: drop the desktop-only local-engine
66
+ // fields (label/defaultModel/rates/supportsReasoningEffort) before Core sees it.
67
+ const runtime = await (0, agent_runtime_loader_1.loadCoreAgentRuntime)();
68
+ config.providers = (0, agent_runtime_settings_1.stripDesktopConnectionFields)(config.providers, (0, agent_runtime_settings_1.coreConnectionFieldGates)(runtime.api?.capabilities));
69
+ if (runtime.api?.capabilities?.configurableGuardrails !== 1)
70
+ delete config.guardrails;
71
+ const override = options.providerOverride;
72
+ runtime.validateRuntimeConfig(JSON.parse(JSON.stringify(config)));
73
+ (0, agent_runtime_loader_1.validateRequestedRoleEfforts)(runtime, config);
74
+ cli = (0, agent_runtime_package_1.retainAgentRuntime)(cli, options.contextPath);
75
+ args[0] = cli;
76
+ saveHostContext(options);
77
+ const selectionFile = (0, node_path_1.join)((0, node_path_1.dirname)(options.contextPath), 'desktop-runtime-selection.json');
78
+ const selection = JSON.stringify({ schemaVersion: 1, runId: admittedContext.runId, providerOverride: override ?? null, origins }) + '\n';
79
+ try {
80
+ (0, node_fs_1.writeFileSync)(selectionFile, selection, { flag: 'wx', mode: 0o600 });
81
+ }
82
+ catch (error) {
83
+ if (error.code !== 'EEXIST' || (0, node_fs_1.readFileSync)(selectionFile, 'utf8') !== selection)
84
+ throw new Error('Runtime selection provenance changed; start a new run');
77
85
  }
78
- const selected = new Set(admittedContext.repositories.map(repo => repo.id));
79
- // Project settings cover every repository; a ticket may select only a subset.
80
- // Freeze that subset for admission without changing the project settings.
81
- config.verification = config.verification.filter(command => selected.has(command.repositoryId));
82
86
  const scopedPath = (0, node_path_1.join)((0, node_path_1.dirname)(options.contextPath), 'desktop-runtime-config.json');
83
87
  const serialized = JSON.stringify(config, null, 2) + '\n';
84
88
  try {
@@ -95,7 +99,7 @@ async function runAgentRuntimeInvocation(options) {
95
99
  const frozenPath = (0, node_path_1.join)((0, node_path_1.dirname)(options.contextPath), 'desktop-runtime-config.json');
96
100
  if ((0, node_fs_1.existsSync)(frozenPath)) {
97
101
  const frozen = JSON.parse((0, node_fs_1.readFileSync)(frozenPath, 'utf8'));
98
- for (const [role, assignment] of Object.entries(frozen.agents)) {
102
+ for (const [role, assignment] of [...Object.entries(frozen.agents), ...(frozen.fixer ? [['fixer', frozen.fixer]] : [])]) {
99
103
  try {
100
104
  options.onLine?.(`[runtime] ${role}: ${assignment.provider}/${assignment.model ?? 'provider default'}\n`);
101
105
  }
@@ -112,6 +116,7 @@ async function runAgentRuntimeInvocation(options) {
112
116
  args.push('--answer', options.answer);
113
117
  }
114
118
  const started = Date.now();
119
+ (0, agent_runtime_history_1.writeRuntimeHistory)(options.contextPath, { status: 'running' });
115
120
  return new Promise((resolve) => {
116
121
  let result;
117
122
  let stderr = '', summary = '', invalidProtocol = false, timedOut = false;
@@ -220,6 +225,10 @@ async function runAgentRuntimeInvocation(options) {
220
225
  ? `Workflow awaits an answer in Agent Runtime settings: ${result.pendingQuestion.question.trim().slice(0, 500)}`
221
226
  : 'Workflow awaits approval in Agent Runtime settings')
222
227
  : failed ? stderr || 'Core exited without a successful programmatic workflow result' : undefined));
228
+ try {
229
+ (0, agent_runtime_history_1.writeRuntimeHistory)(options.contextPath, invalidProtocol || timedOut || observerError || !result ? { status: 'failed', error: errorText } : { ...result });
230
+ }
231
+ catch { /* Projection failure cannot replay a completed execution. */ }
223
232
  resolve({
224
233
  text: summary || (failed ? errorText ?? '' : 'Programmatic implementation verified and archived.'),
225
234
  provider: 'agent-runtime', model: 'per-role', failed, errorText,
@@ -38,7 +38,9 @@ function registerAgentRuntimeControlRoutes({ router, ctx }) {
38
38
  }
39
39
  const context = ctx(req);
40
40
  const latest = context.db.prepare('SELECT id FROM loop_runs WHERE rail_index = ? ORDER BY started_at DESC, rowid DESC LIMIT 1').get(railIndex);
41
- res.json({ runs: latest && (0, agent_runtime_paths_1.hasAgentRuntimeRequest)(context.project, latest.id) ? [await controls(req).summary(latest.id)] : [] });
41
+ // A dismissed continuation — or a green one with nothing left to decide — leaves the rail card (the run stays in the history list below).
42
+ const summary = latest && (0, agent_runtime_paths_1.hasAgentRuntimeRequest)(context.project, latest.id) ? await controls(req).summary(latest.id) : undefined;
43
+ res.json({ runs: summary && (0, agent_runtime_controls_1.pinsRailCard)(summary) ? [summary] : [] });
42
44
  return;
43
45
  }
44
46
  res.json({ runs: await controls(req).list() });
@@ -56,6 +58,14 @@ function registerAgentRuntimeControlRoutes({ router, ctx }) {
56
58
  res.status(500).json({ error: 'runtime_status_failed' });
57
59
  }
58
60
  });
61
+ router.get('/:projectId/agent-runtime/runs/:runId/evidence', async (req, res) => {
62
+ try {
63
+ res.json(await controls(req).evidence(String(req.params.runId), req.query));
64
+ }
65
+ catch (error) {
66
+ res.status(error instanceof agent_runtime_controls_1.RuntimeControlError ? error.statusCode : 503).json({ error: 'evidence_unavailable', message: error instanceof agent_runtime_controls_1.RuntimeControlError ? error.message : 'The original runtime evidence is unavailable' });
67
+ }
68
+ });
59
69
  router.post('/:projectId/agent-runtime/runs/:runId/resume', async (req, res) => {
60
70
  try {
61
71
  await controls(req).resume(String(req.params.runId), (0, agent_runtime_controls_1.validateRuntimeResumeInput)(req.body));
@@ -74,6 +84,15 @@ function registerAgentRuntimeControlRoutes({ router, ctx }) {
74
84
  res.status(error instanceof agent_runtime_controls_1.RuntimeControlError ? error.statusCode : 500).json({ message: error instanceof agent_runtime_controls_1.RuntimeControlError ? error.message : 'Could not prepare delivery' });
75
85
  }
76
86
  });
87
+ router.post('/:projectId/agent-runtime/runs/:runId/dismiss', (req, res) => {
88
+ try {
89
+ controls(req).dismiss(String(req.params.runId));
90
+ res.json({ dismissed: true });
91
+ }
92
+ catch (error) {
93
+ res.status(error instanceof agent_runtime_controls_1.RuntimeControlError ? error.statusCode : 500).json({ error: error instanceof agent_runtime_controls_1.RuntimeControlError ? error.code : 'runtime_dismiss_failed', message: error instanceof Error ? error.message : 'Could not dismiss the execution' });
94
+ }
95
+ });
77
96
  router.post('/:projectId/agent-runtime/runs/:runId/cancel', (req, res) => {
78
97
  try {
79
98
  controls(req).cancel(String(req.params.runId));
@@ -4,8 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AgentRuntimeControls = exports.RuntimeControlError = void 0;
7
+ exports.pinsRailCard = pinsRailCard;
7
8
  exports.validateRuntimeResumeInput = validateRuntimeResumeInput;
8
9
  exports.readAgentRuntimeStatus = readAgentRuntimeStatus;
10
+ const agent_runtime_events_1 = require("./agent-runtime-events");
11
+ const agent_runtime_history_1 = require("./agent-runtime-history");
9
12
  const agent_runtime_settlement_1 = require("./agent-runtime-settlement");
10
13
  const agent_runtime_metrics_1 = require("./agent-runtime-metrics");
11
14
  const node_child_process_1 = require("node:child_process");
@@ -13,7 +16,7 @@ const node_crypto_1 = require("node:crypto");
13
16
  const node_fs_1 = __importDefault(require("node:fs"));
14
17
  const node_path_1 = __importDefault(require("node:path"));
15
18
  const node_util_1 = require("node:util");
16
- const agent_runtime_loader_1 = require("./agent-runtime-loader");
19
+ const agent_runtime_package_1 = require("./agent-runtime-package");
17
20
  const agent_runtime_bridge_1 = require("./agent-runtime-bridge");
18
21
  const core_node_runtime_1 = require("./core-node-runtime");
19
22
  const win_spawn_1 = require("./util/win-spawn");
@@ -23,8 +26,23 @@ const multi_repo_execution_store_1 = require("./multi-repo-execution-store");
23
26
  const db_1 = require("./db");
24
27
  const loop_run_manager_1 = require("./loop-run-manager");
25
28
  const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
26
- const STEP_IDS = ['architect', 'developer', 'verify', 'reviewer', 'archive'];
29
+ const STEP_IDS = ['architect', 'developer', 'fixer', 'verify', 'reviewer', 'archive'];
27
30
  const ANSWER_LIMIT = 20_000;
31
+ /**
32
+ * Whether a continuation still deserves the rail card. Anything that needs a
33
+ * human (blocked, failed, interrupted, a pending question or approval, a
34
+ * settle still owed) pins the rail; a run that SUCCEEDED and offers no action
35
+ * does not — its evidence lives in the job log and the Settings history, and
36
+ * the delivery decision (when one exists) has its own strip. Observed: a rail
37
+ * kept a "Completed" card with only Dismiss on it after every green run.
38
+ */
39
+ function pinsRailCard(summary) {
40
+ if (summary.dismissed)
41
+ return false;
42
+ if (summary.active || summary.canResume || summary.canSettle || summary.canCancel || summary.pendingQuestion || summary.pendingApproval || summary.recoverableSteps.length)
43
+ return true;
44
+ return summary.status !== 'succeeded';
45
+ }
28
46
  class RuntimeControlError extends Error {
29
47
  statusCode;
30
48
  code;
@@ -56,7 +74,13 @@ function openQuestion(state) {
56
74
  }
57
75
  /** Status is a read-only Core CLI operation. It never invokes a provider. */
58
76
  async function readAgentRuntimeStatus(contextPath, cwd, env) {
59
- const cli = (0, agent_runtime_loader_1.findCoreAgentRuntimeCli)();
77
+ let cli;
78
+ try {
79
+ cli = (0, agent_runtime_package_1.resolveRetainedAgentRuntime)(contextPath);
80
+ }
81
+ catch (error) {
82
+ throw new RuntimeControlError(409, 'original_runtime_unavailable', error instanceof Error ? error.message : 'The original runtime package is unavailable');
83
+ }
60
84
  if (!cli)
61
85
  throw new RuntimeControlError(503, 'runtime_unavailable', 'Update Core to inspect or resume agent runtime executions');
62
86
  const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)((0, core_node_runtime_1.resolveCoreNodeRuntime)(), [cli, 'status', '--context', contextPath, '--compact'], {
@@ -65,7 +89,12 @@ async function readAgentRuntimeStatus(contextPath, cwd, env) {
65
89
  const result = JSON.parse(stdout);
66
90
  if (result.type !== 'runtime-status' || result.state === undefined)
67
91
  throw new Error('Core returned an invalid runtime status');
68
- return result.state;
92
+ let selection;
93
+ try {
94
+ selection = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(node_path_1.default.dirname(contextPath), 'desktop-runtime-selection.json'), 'utf8'));
95
+ }
96
+ catch { /* Original hosts may not record selection origins. */ }
97
+ return result.state ? { ...result.state, efficiencySummary: (0, agent_runtime_metrics_1.applyRuntimeSelectionOrigins)((0, agent_runtime_metrics_1.readRuntimeEfficiencySummary)(result.efficiencySummary), selection, result.state.runId) } : null;
69
98
  }
70
99
  /** One controller per ProjectContext; Core's durable lease remains the final
71
100
  * cross-process guard. Resume never constructs a new context or worktree. */
@@ -82,7 +111,7 @@ class AgentRuntimeControls {
82
111
  this.ctx = ctx;
83
112
  this.dependencies = dependencies;
84
113
  }
85
- context(runId) {
114
+ context(runId, historical = false) {
86
115
  if (!SAFE_ID.test(runId))
87
116
  throw new RuntimeControlError(400, 'invalid_run_id', 'Invalid runtime run ID');
88
117
  const execution = (0, workspace_resolution_1.resolveProjectExecution)(this.ctx.project);
@@ -98,7 +127,7 @@ class AgentRuntimeControls {
98
127
  if (frozen.runId !== runId || node_fs_1.default.realpathSync(frozen.backlogRoot) !== backlogRoot || !Array.isArray(frozen.repositories) || !frozen.repositories.length || !frozen.repositories.some((repository) => repository.path === frozen.artifactRoot))
99
128
  throw new Error();
100
129
  for (const root of [frozen.artifactRoot, ...frozen.repositories.map((repository) => repository.path)]) {
101
- if (!node_path_1.default.isAbsolute(root) || node_fs_1.default.realpathSync(root) !== root || !node_fs_1.default.statSync(root).isDirectory())
130
+ if (!node_path_1.default.isAbsolute(root) || (!historical && (node_fs_1.default.realpathSync(root) !== root || !node_fs_1.default.statSync(root).isDirectory())))
102
131
  throw new Error();
103
132
  }
104
133
  }
@@ -121,6 +150,21 @@ class AgentRuntimeControls {
121
150
  Object.assign(env, host.env, { SPECRAILS_EXECUTION_CONTEXT: file });
122
151
  return { file, frozen, cwd: host.cwd, env };
123
152
  }
153
+ async evidence(runId, query) {
154
+ const { file, frozen, env } = this.context(runId, true);
155
+ const flags = [];
156
+ for (const [key, value] of Object.entries(query)) {
157
+ if (!['id', 'section', 'sourceId', 'cursor', 'limit'].includes(key) || typeof value !== 'string' || value.length > 1024)
158
+ throw new RuntimeControlError(400, 'invalid_evidence_query', 'Invalid evidence query');
159
+ flags.push('--' + (key === 'sourceId' ? 'source-id' : key), value);
160
+ }
161
+ const cli = (0, agent_runtime_package_1.resolveRetainedAgentRuntime)(file);
162
+ const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)((0, core_node_runtime_1.resolveCoreNodeRuntime)(), [cli, 'evidence', '--context', file, ...flags], { cwd: frozen.backlogRoot, env: (0, win_spawn_1.windowsSpawnEnv)(env), windowsHide: true, timeout: 15000, maxBuffer: 4 * 1024 * 1024, encoding: 'utf8' });
163
+ const result = JSON.parse(stdout);
164
+ if (result.schemaVersion !== 1 || typeof result.available !== 'boolean' || typeof result.truncated !== 'boolean')
165
+ throw new RuntimeControlError(503, 'evidence_unavailable', 'Core evidence is unavailable or incompatible');
166
+ return result;
167
+ }
124
168
  async list() {
125
169
  const directory = node_path_1.default.join((0, workspace_resolution_1.resolveProjectExecution)(this.ctx.project).specrailsDir, 'pipeline');
126
170
  if (!node_fs_1.default.existsSync(directory))
@@ -140,7 +184,7 @@ class AgentRuntimeControls {
140
184
  // key, keeping idle settings panes from spawning CLI processes repeatedly.
141
185
  const checkpoint = node_path_1.default.join(node_path_1.default.dirname(file), 'agent-workflow', runId, 'checkpoint.json');
142
186
  const stat = node_fs_1.default.existsSync(checkpoint) ? node_fs_1.default.statSync(checkpoint) : null;
143
- const fingerprint = stat ? `${stat.mtimeMs}:${stat.size}` : null;
187
+ const fingerprint = stat ? (0, node_crypto_1.createHash)('sha256').update(node_fs_1.default.readFileSync(checkpoint)).digest('hex') : null;
144
188
  const cached = this.statusCache.get(runId);
145
189
  const state = fingerprint && cached?.fingerprint === fingerprint ? cached.state : await this.dependencies.status(file, cwd, env);
146
190
  if (!state || state.runId !== runId)
@@ -149,15 +193,29 @@ class AgentRuntimeControls {
149
193
  this.statusCache.set(runId, { fingerprint, state });
150
194
  const parent = (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId);
151
195
  const active = this.active.has(runId) || parent?.status === 'running' || parent?.status === 'paused';
196
+ let projection = null;
197
+ try {
198
+ projection = (0, agent_runtime_history_1.readRuntimeHistory)(file);
199
+ }
200
+ catch { /* Invalid advisory history cannot replace live state. */ }
201
+ const superseding = !active && projection && ['failed', 'cancelled', 'running'].includes(projection.status) && Date.parse(projection.updatedAt) > Date.parse(state.updatedAt ?? '1970-01-01') ? projection : null;
152
202
  const recoverableSteps = Object.entries(state.steps).filter(([, step]) => ['running', 'interrupted'].includes(step.status)).map(([id]) => id);
153
- return { runId, status: state.status === 'running' && !active ? 'interrupted' : state.status, nextStep: state.nextStep,
154
- updatedAt: state.updatedAt, error: this.errors.get(runId) ?? state.error, pendingApproval: state.pendingApproval,
203
+ return { runId, status: superseding ? (superseding.status === 'running' ? 'interrupted' : superseding.status) : state.status === 'running' && !active ? 'interrupted' : state.status, nextStep: state.nextStep,
204
+ updatedAt: superseding?.updatedAt ?? state.updatedAt, error: this.errors.get(runId) ?? superseding?.error ?? state.error, pendingApproval: state.pendingApproval,
155
205
  traceId: state.traceId, pendingQuestion: openQuestion(state),
156
- metrics: (0, agent_runtime_metrics_1.readRuntimeEfficiency)(state.metrics),
157
- canSettle: !active && state.status === 'succeeded' && this.ctx.db.prepare('SELECT status FROM jobs WHERE id = ?').get(runId)?.status !== 'completed',
158
- recoverableSteps, active, canCancel: this.active.has(runId), canResume: !active && parent?.status === 'completed' && state.status !== 'succeeded' };
206
+ metrics: (0, agent_runtime_metrics_1.readRuntimeEfficiency)(superseding ? superseding.metrics : state.metrics), efficiencySummary: (0, agent_runtime_metrics_1.readRuntimeEfficiencySummary)(superseding ? superseding.efficiencySummary : state.efficiencySummary),
207
+ canSettle: !superseding && !active && state.status === 'succeeded' && this.ctx.db.prepare('SELECT status FROM jobs WHERE id = ?').get(runId)?.status !== 'completed',
208
+ recoverableSteps, active, canCancel: this.active.has(runId), canResume: !active && parent?.status === 'completed' && state.status !== 'succeeded',
209
+ canDismiss: !active, dismissed: this.isDismissed(runId) };
159
210
  }
160
211
  catch (error) {
212
+ try {
213
+ const { file } = this.context(runId, true);
214
+ const historical = (0, agent_runtime_history_1.readRuntimeHistory)(file);
215
+ if (historical)
216
+ return { ...historical, runId, status: historical.status === 'running' ? 'interrupted' : historical.status, historical: true, active: this.active.has(runId), canResume: false, canSettle: false, canCancel: this.active.has(runId), recoverableSteps: [], error: error instanceof RuntimeControlError ? error.message : 'Historical result; live runtime verification is unavailable' };
217
+ }
218
+ catch { /* No trustworthy historical projection is available. */ }
161
219
  return { runId, status: 'unavailable', nextStep: null, active: this.active.has(runId), canResume: false, canCancel: this.active.has(runId), recoverableSteps: [], error: error instanceof RuntimeControlError ? error.message : 'Could not inspect the saved runtime execution' };
162
220
  }
163
221
  }
@@ -246,6 +304,12 @@ class AgentRuntimeControls {
246
304
  eventType = parsed.type;
247
305
  }
248
306
  catch { /* Keep raw diagnostics available to recovery. */ }
307
+ if (eventType === 'runtime-efficiency-event') {
308
+ const validated = (0, agent_runtime_events_1.runtimeEfficiencyEventLine)(line, runId);
309
+ if (!validated || (0, agent_runtime_events_1.isRecordedRuntimeEfficiencyEvent)(this.ctx.db, runId, validated))
310
+ return;
311
+ line = validated;
312
+ }
249
313
  this.ctx.db.transaction(() => {
250
314
  (0, db_1.appendEvent)(this.ctx.db, runId, ++sequence, { event_type: eventType, source: 'stdout', payload: line });
251
315
  (0, loop_runs_store_1.updateLoopStepActivityCheckpoint)(this.ctx.db, runId, stepKey, undefined, Date.now());
@@ -315,6 +379,24 @@ class AgentRuntimeControls {
315
379
  this.ctx.broadcast?.({ type: 'runtime.continuation', projectId: this.ctx.project.id, jobId: runId, railIndex: (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId)?.rail_index ?? null, active: false });
316
380
  }
317
381
  }
382
+ /** Marker next to the frozen context: the rail card stops showing this run; history/log stay intact. */
383
+ dismissMarker(runId) { return node_path_1.default.join(node_path_1.default.dirname(this.context(runId).file), 'desktop-runtime-dismissed'); }
384
+ isDismissed(runId) { try {
385
+ return node_fs_1.default.existsSync(this.dismissMarker(runId));
386
+ }
387
+ catch {
388
+ return false;
389
+ } }
390
+ dismiss(runId) {
391
+ if (this.active.has(runId))
392
+ throw new RuntimeControlError(409, 'runtime_run_active', 'Stop the execution before dismissing it');
393
+ const parent = (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId);
394
+ if (parent && (parent.status === 'running' || parent.status === 'paused'))
395
+ throw new RuntimeControlError(409, 'runtime_run_active', 'Stop the rail run before dismissing it');
396
+ node_fs_1.default.writeFileSync(this.dismissMarker(runId), new Date().toISOString() + '\n', { mode: 0o600 });
397
+ this.statusCache.delete(runId);
398
+ this.ctx.broadcast?.({ type: 'runtime.continuation', projectId: this.ctx.project.id, jobId: runId, railIndex: parent?.rail_index ?? null, active: false });
399
+ }
318
400
  cancel(runId) {
319
401
  const active = this.active.get(runId);
320
402
  if (!active)
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveEffectiveRuntimeConfig = resolveEffectiveRuntimeConfig;
4
+ exports.fillDefaultRoleModels = fillDefaultRoleModels;
5
+ const providers_1 = require("./providers");
6
+ /** A pure admission resolver; only deliberate launch intent overrides a project role. */
7
+ function resolveEffectiveRuntimeConfig(input, options) {
8
+ const config = structuredClone(input);
9
+ const override = options.providerOverride;
10
+ if (override) {
11
+ const provider = config.providers.find(entry => entry.id === override.provider);
12
+ if (!provider)
13
+ throw new Error(`Selected runtime provider is not configured: ${override.provider}`);
14
+ for (const role of ['architect', 'developer', 'reviewer', 'fixer']) {
15
+ const current = role === 'fixer' ? config.fixer : config.agents[role];
16
+ if (!current)
17
+ continue;
18
+ const changedProvider = current.provider !== provider.id;
19
+ const changedModel = override.model !== undefined && override.model !== current.model;
20
+ const next = {
21
+ ...current, provider: provider.id,
22
+ model: override.model ?? (changedProvider ? undefined : current.model),
23
+ effort: override.effort ?? (changedProvider || changedModel ? undefined : current.effort),
24
+ escalation: changedProvider || changedModel ? undefined : current.escalation,
25
+ };
26
+ if (role === 'fixer')
27
+ config.fixer = next;
28
+ else
29
+ config.agents[role] = next;
30
+ }
31
+ }
32
+ fillDefaultRoleModels(config);
33
+ config.verification = config.verification.filter(check => options.repositoryIds.includes(check.repositoryId));
34
+ return { config, origins: { architect: override ? 'explicit-launch-override' : options.source, developer: override ? 'explicit-launch-override' : options.source, reviewer: override ? 'explicit-launch-override' : options.source } };
35
+ }
36
+ /**
37
+ * Every role gets a concrete model: CLI roles the adapter default, local
38
+ * (OpenAI-compatible) roles the connection's default (stored `defaultModel`,
39
+ * else the first discovered model). Core requires a model on API providers, so
40
+ * this runs before launch AND before the compatibility check — otherwise
41
+ * picking a local engine with "provider default" failed the check with
42
+ * "Role architect requires a model for its API provider".
43
+ */
44
+ function fillDefaultRoleModels(config, options = {}) {
45
+ for (const agent of [...Object.values(config.agents), ...(config.fixer ? [config.fixer] : [])]) {
46
+ const provider = config.providers.find(entry => entry.id === agent.provider);
47
+ if (!agent.model && provider?.kind === 'cli' && !options.localOnly)
48
+ agent.model = (0, providers_1.getAdapter)(provider.cli).defaultModel();
49
+ if (!agent.model && provider?.kind === 'openai-compatible' && (0, providers_1.hasAdapter)(provider.id) && (0, providers_1.isLocalAdapterId)(provider.id))
50
+ agent.model = (0, providers_1.getAdapter)(provider.id).defaultModel();
51
+ }
52
+ return config;
53
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runtimeEfficiencyEventLine = runtimeEfficiencyEventLine;
4
+ exports.isRecordedRuntimeEfficiencyEvent = isRecordedRuntimeEfficiencyEvent;
5
+ /** Efficiency events are advisory measurements, never workflow or delivery authority. */
6
+ function runtimeEfficiencyEventLine(line, runId) {
7
+ if (line.length > 16384)
8
+ return null;
9
+ let event;
10
+ try {
11
+ event = JSON.parse(line);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ if (event?.type !== 'runtime-efficiency-event' || event.schemaVersion !== 1 || event.runId !== runId || typeof event.eventId !== 'string' || !event.eventId.startsWith(runId + ':') || event.eventId.length > 256 || typeof event.attemptId !== 'string' || event.attemptId.length > 256 || !['role-context', 'role-route', 'check-started', 'check-finished', 'check-reused', 'check-invalidated'].includes(String(event.kind)))
17
+ return null;
18
+ if (!event.payload || typeof event.payload !== 'object' || Array.isArray(event.payload))
19
+ return null;
20
+ const raw = event.payload, payload = {};
21
+ for (const key of ['invocationId', 'provider', 'model', 'requestedEffort', 'kind', 'tier', 'routeReason', 'contextMode', 'status', 'executionId', 'repositoryId', 'checkId', 'label', 'reason']) {
22
+ const value = raw[key];
23
+ if (value === undefined)
24
+ continue;
25
+ if (value !== null && (typeof value !== 'string' || value.length > 256))
26
+ return null;
27
+ payload[key] = value;
28
+ }
29
+ for (const key of ['ordinal', 'promptBytes', 'contextBytes', 'handoffBytes', 'durationMs', 'toolCalls']) {
30
+ if (raw[key] === undefined)
31
+ continue;
32
+ if (typeof raw[key] !== 'number' || !Number.isFinite(raw[key]) || raw[key] < 0)
33
+ return null;
34
+ payload[key] = raw[key];
35
+ }
36
+ if (raw.exitCode !== undefined) {
37
+ if (typeof raw.exitCode !== 'number' || !Number.isSafeInteger(raw.exitCode))
38
+ return null;
39
+ payload.exitCode = raw.exitCode;
40
+ }
41
+ return JSON.stringify({ type: event.type, schemaVersion: 1, eventId: event.eventId, runId, attemptId: event.attemptId, kind: event.kind, payload });
42
+ }
43
+ function isRecordedRuntimeEfficiencyEvent(db, runId, line) {
44
+ const event = JSON.parse(line);
45
+ return Boolean(db.prepare("SELECT 1 FROM events WHERE job_id = ? AND event_type = 'runtime-efficiency-event' AND CASE WHEN json_valid(payload) THEN json_extract(payload, '$.eventId') END = ? LIMIT 1").get(runId, event.eventId));
46
+ }