specrails-desktop 2.47.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 (248) hide show
  1. package/README.md +2 -0
  2. package/client/dist/assets/{ActivityFeedPage-DSCpSOGV.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-Dw3DNFkh.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-BwuvqS2Y.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-Byvq8Oqx.js → LoopBuilderPage-DQE-7Vby.js} +2 -2
  19. package/client/dist/assets/{LoopPreviewModal-BMC9Mgj1.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-nspqxH-M.js → ProjectSettingsDialog-D6vBCoay.js} +1 -1
  24. package/client/dist/assets/{RepositoryDeliveries-Dq9NJOWi.js → RepositoryDeliveries-C36ipKp_.js} +1 -1
  25. package/client/dist/assets/{RepositoryScopeSelector-C3jKG2z0.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-C0KyPo2O.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-DyI4rbHa.js → agents-CA5eT1d0.js} +1 -1
  47. package/client/dist/assets/{agents-_5mJMkAE.js → agents-CAgy-5uS.js} +1 -1
  48. package/client/dist/assets/{agents-BIT1UdTy.js → agents-CNm6Qzlv.js} +1 -1
  49. package/client/dist/assets/{agents-CST-lpYX.js → agents-CTwbx-fb.js} +1 -1
  50. package/client/dist/assets/{agents-C7fKfvm8.js → agents-DEGV1Vao.js} +1 -1
  51. package/client/dist/assets/{agents-DsgGMGwU.js → agents-HsDF1aQU.js} +1 -1
  52. package/client/dist/assets/{agents-Cm2JYrAa.js → agents-IQjdA-z9.js} +1 -1
  53. package/client/dist/assets/{agents-H6tVQUNP.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-D44phX1J.js → formatDistanceToNow-w_2U5pm0.js} +1 -1
  72. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-EAh9LFSx.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-UWi_xIZ-.js → narration--hUhjM5v.js} +1 -1
  85. package/client/dist/assets/{narration-DiPyj06T.js → narration-CHI06czm.js} +1 -1
  86. package/client/dist/assets/{narration-DrBAf8uv.js → narration-DjKB3pQZ.js} +1 -1
  87. package/client/dist/assets/{narration-CqNvgRi9.js → narration-DtCYvRLM.js} +1 -1
  88. package/client/dist/assets/{narration-B1uGfofH.js → narration-DvLQBW9U.js} +1 -1
  89. package/client/dist/assets/{narration-BKcL4Srl.js → narration-eSx3gR0W.js} +1 -1
  90. package/client/dist/assets/{narration-OyujFouL.js → narration-n3hOA7LY.js} +1 -1
  91. package/client/dist/assets/{narration-BtjKyHVM.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-GRYdjlKB.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-CN9PHM65.js → spending-hcgxHP4O.js} +1 -1
  119. package/client/dist/assets/{useDesktop-iius_s5d.js → useDesktop-CAisjKWC.js} +1 -1
  120. package/client/dist/assets/{useSharedWebSocket-D7hkZ3Ka.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/local-runner/dist/specrails-local-runner.js +15815 -0
  136. package/package.json +7 -5
  137. package/server/dist/agent-chat-manager.js +8 -1
  138. package/server/dist/agent-chat-router.js +5 -2
  139. package/server/dist/agent-fence-promotion.js +53 -0
  140. package/server/dist/agent-mcp-config.js +4 -1
  141. package/server/dist/agent-runtime-bridge.js +7 -2
  142. package/server/dist/agent-runtime-controls-router.js +12 -1
  143. package/server/dist/agent-runtime-controls.js +37 -2
  144. package/server/dist/agent-runtime-effective-config.js +28 -7
  145. package/server/dist/agent-runtime-metrics.js +1 -1
  146. package/server/dist/agent-runtime-settings-router.js +76 -6
  147. package/server/dist/agent-runtime-settings.js +137 -7
  148. package/server/dist/agent-spec-framing.js +10 -4
  149. package/server/dist/binary-probe.js +12 -0
  150. package/server/dist/blueprint-router.js +3 -2
  151. package/server/dist/chat-manager.js +5 -1
  152. package/server/dist/desktop-router.js +87 -4
  153. package/server/dist/index.js +13 -0
  154. package/server/dist/interactive-job-session.js +3 -1
  155. package/server/dist/local-engine-detection.js +118 -0
  156. package/server/dist/loop-executors.js +2 -1
  157. package/server/dist/loop-role-engines.js +129 -0
  158. package/server/dist/loop-run-manager.js +4 -4
  159. package/server/dist/mcp/tools/app-settings.js +30 -4
  160. package/server/dist/mcp/tools/rails.js +1 -1
  161. package/server/dist/pr-body.js +11 -2
  162. package/server/dist/pricing.js +15 -0
  163. package/server/dist/provider-detection.js +63 -1
  164. package/server/dist/provider-selection.js +26 -1
  165. package/server/dist/providers/index.js +4 -1
  166. package/server/dist/providers/local-adapter-registry.js +56 -0
  167. package/server/dist/providers/local-adapter.js +426 -0
  168. package/server/dist/providers/registry.js +39 -4
  169. package/server/dist/rail-isolated-launch.js +3 -3
  170. package/server/dist/rails-router.js +66 -10
  171. package/server/dist/result-event.js +17 -0
  172. package/server/dist/review-packet.js +1 -0
  173. package/server/dist/schemas/agent-runtime.schema.json +10 -2
  174. package/server/dist/setup-manager.js +5 -1
  175. package/server/dist/spec-models.js +14 -2
  176. package/server/dist/worktree-node-modules.js +97 -65
  177. package/client/dist/assets/AgentBrowserCapture-mc1c5pgS.js +0 -1
  178. package/client/dist/assets/AgentModeAnalyticsPane--UkamBTm.js +0 -2
  179. package/client/dist/assets/AgentModeCodePane-DJCswsvU.js +0 -2
  180. package/client/dist/assets/AgentsPage-ChElPUL9.js +0 -87
  181. package/client/dist/assets/AnalyticsPage-BGtkFHBp.js +0 -1
  182. package/client/dist/assets/CodePage-BBnw3VGn.js +0 -3
  183. package/client/dist/assets/DesktopAnalyticsPage-B85tNScc.js +0 -1
  184. package/client/dist/assets/DocsDialog-D4cXrOMl.js +0 -11
  185. package/client/dist/assets/DocsPage-BCAbC6qy.js +0 -11
  186. package/client/dist/assets/InteractiveJobComposer-Coq-Zhk0.js +0 -19
  187. package/client/dist/assets/JobDetailModal--Mjam0Nc.js +0 -1
  188. package/client/dist/assets/JobDetailPage-B6ShWSBh.js +0 -1
  189. package/client/dist/assets/JobsPage-DHU_ep_B.js +0 -1
  190. package/client/dist/assets/LoopsPage-CFGl-Nw8.js +0 -1
  191. package/client/dist/assets/MinimizedChatsContext-DHjHBYqa.js +0 -1
  192. package/client/dist/assets/PluginsPage-BHygRnW6.js +0 -2
  193. package/client/dist/assets/ReviewPacketPage-C9mqhi65.js +0 -1
  194. package/client/dist/assets/TemplatePreviewModal-BmwoAjOr.js +0 -1
  195. package/client/dist/assets/TicketDetailModalContext-lYmoONrd.js +0 -14
  196. package/client/dist/assets/agentRuntime-62GWdc26.js +0 -1
  197. package/client/dist/assets/agentRuntime-BN1mKAuY.js +0 -1
  198. package/client/dist/assets/agentRuntime-BNBCzQLM.js +0 -1
  199. package/client/dist/assets/agentRuntime-BQE_K1GG.js +0 -1
  200. package/client/dist/assets/agentRuntime-CCU3HCtS.js +0 -1
  201. package/client/dist/assets/agentRuntime-DT72Bpdv.js +0 -1
  202. package/client/dist/assets/agentRuntime-ZfMPeeiP.js +0 -1
  203. package/client/dist/assets/agentRuntime-g3W8Ry4d.js +0 -1
  204. package/client/dist/assets/analytics-47pYygtV.js +0 -1
  205. package/client/dist/assets/analytics-B0vt1g5U.js +0 -1
  206. package/client/dist/assets/analytics-BMzCvrCU.js +0 -1
  207. package/client/dist/assets/analytics-BlFN3Zwx.js +0 -1
  208. package/client/dist/assets/analytics-CZYzL-nR.js +0 -1
  209. package/client/dist/assets/analytics-DAsb9MJ8.js +0 -1
  210. package/client/dist/assets/analytics-FXQlQ2Yt.js +0 -1
  211. package/client/dist/assets/analytics-RnFCNkah.js +0 -1
  212. package/client/dist/assets/dashboard-BfBir_6o.js +0 -1
  213. package/client/dist/assets/file-text-BcLCfcWZ.js +0 -1
  214. package/client/dist/assets/index-Cfh9aaep.css +0 -2
  215. package/client/dist/assets/index-DgMBighL.js +0 -76
  216. package/client/dist/assets/jira-api-BQR6G0YY.js +0 -1
  217. package/client/dist/assets/jobs-5PQJbdOf.js +0 -1
  218. package/client/dist/assets/jobs-B2B08Oy9.js +0 -1
  219. package/client/dist/assets/jobs-BJZtBbwb.js +0 -1
  220. package/client/dist/assets/jobs-BlWEsQb7.js +0 -1
  221. package/client/dist/assets/jobs-ConfD8xD.js +0 -1
  222. package/client/dist/assets/jobs-DMPU64ms.js +0 -1
  223. package/client/dist/assets/jobs-DojASiTp.js +0 -1
  224. package/client/dist/assets/jobs-kVHkNpB-.js +0 -1
  225. package/client/dist/assets/packet-BQ2tXfJI.js +0 -1
  226. package/client/dist/assets/packet-BdpOJ1Cy.js +0 -1
  227. package/client/dist/assets/packet-C3Ievb9R.js +0 -1
  228. package/client/dist/assets/packet-C4zJIG0Y.js +0 -1
  229. package/client/dist/assets/packet-Cx-zRuWn.js +0 -1
  230. package/client/dist/assets/packet-DUTGX52M.js +0 -1
  231. package/client/dist/assets/packet-DhsKtUPB.js +0 -1
  232. package/client/dist/assets/packet-z3LJ8Zyy.js +0 -1
  233. package/client/dist/assets/provider-capabilities-CXipwrXJ.js +0 -1
  234. package/client/dist/assets/settings-B3rWEwOx.js +0 -1
  235. package/client/dist/assets/settings-BcVoiMyw.js +0 -1
  236. package/client/dist/assets/settings-BlYvaQ7w.js +0 -1
  237. package/client/dist/assets/settings-CD5uEBiL.js +0 -1
  238. package/client/dist/assets/settings-CyhS0DLJ.js +0 -1
  239. package/client/dist/assets/settings-DCMFUGDs.js +0 -1
  240. package/client/dist/assets/settings-DmbYKyMJ.js +0 -1
  241. package/client/dist/assets/setup-2xGgu4Sk.js +0 -1
  242. package/client/dist/assets/setup-Ba11l2JA.js +0 -1
  243. package/client/dist/assets/setup-BzW4kMok.js +0 -1
  244. package/client/dist/assets/setup-COzwYhkJ.js +0 -1
  245. package/client/dist/assets/setup-CRVULhtF.js +0 -1
  246. package/client/dist/assets/setup-DgvlxwpE.js +0 -1
  247. package/client/dist/assets/setup-Dw_wYeEY.js +0 -1
  248. 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.47.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.');
@@ -62,8 +62,13 @@ async function runAgentRuntimeInvocation(options) {
62
62
  repositoryIds: admittedContext.repositories.map(repo => repo.id), source, providerOverride: options.providerOverride,
63
63
  });
64
64
  config.rolePrompts = { ...(await (0, agent_runtime_loader_1.loadCoreAgentRuntime)()).rolePromptDefaults(), ...(0, agent_runtime_settings_1.loadRuntimeRolePrompts)() };
65
- const override = options.providerOverride;
65
+ // Core rejects unknown connection keys: drop the desktop-only local-engine
66
+ // fields (label/defaultModel/rates/supportsReasoningEffort) before Core sees it.
66
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;
67
72
  runtime.validateRuntimeConfig(JSON.parse(JSON.stringify(config)));
68
73
  (0, agent_runtime_loader_1.validateRequestedRoleEfforts)(runtime, config);
69
74
  cli = (0, agent_runtime_package_1.retainAgentRuntime)(cli, options.contextPath);
@@ -94,7 +99,7 @@ async function runAgentRuntimeInvocation(options) {
94
99
  const frozenPath = (0, node_path_1.join)((0, node_path_1.dirname)(options.contextPath), 'desktop-runtime-config.json');
95
100
  if ((0, node_fs_1.existsSync)(frozenPath)) {
96
101
  const frozen = JSON.parse((0, node_fs_1.readFileSync)(frozenPath, 'utf8'));
97
- for (const [role, assignment] of Object.entries(frozen.agents)) {
102
+ for (const [role, assignment] of [...Object.entries(frozen.agents), ...(frozen.fixer ? [['fixer', frozen.fixer]] : [])]) {
98
103
  try {
99
104
  options.onLine?.(`[runtime] ${role}: ${assignment.provider}/${assignment.model ?? 'provider default'}\n`);
100
105
  }
@@ -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() });
@@ -82,6 +84,15 @@ function registerAgentRuntimeControlRoutes({ router, ctx }) {
82
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' });
83
85
  }
84
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
+ });
85
96
  router.post('/:projectId/agent-runtime/runs/:runId/cancel', (req, res) => {
86
97
  try {
87
98
  controls(req).cancel(String(req.params.runId));
@@ -4,6 +4,7 @@ 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;
9
10
  const agent_runtime_events_1 = require("./agent-runtime-events");
@@ -25,8 +26,23 @@ const multi_repo_execution_store_1 = require("./multi-repo-execution-store");
25
26
  const db_1 = require("./db");
26
27
  const loop_run_manager_1 = require("./loop-run-manager");
27
28
  const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
28
- const STEP_IDS = ['architect', 'developer', 'verify', 'reviewer', 'archive'];
29
+ const STEP_IDS = ['architect', 'developer', 'fixer', 'verify', 'reviewer', 'archive'];
29
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
+ }
30
46
  class RuntimeControlError extends Error {
31
47
  statusCode;
32
48
  code;
@@ -189,7 +205,8 @@ class AgentRuntimeControls {
189
205
  traceId: state.traceId, pendingQuestion: openQuestion(state),
190
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),
191
207
  canSettle: !superseding && !active && state.status === 'succeeded' && this.ctx.db.prepare('SELECT status FROM jobs WHERE id = ?').get(runId)?.status !== 'completed',
192
- recoverableSteps, active, canCancel: this.active.has(runId), canResume: !active && parent?.status === 'completed' && state.status !== 'succeeded' };
208
+ recoverableSteps, active, canCancel: this.active.has(runId), canResume: !active && parent?.status === 'completed' && state.status !== 'succeeded',
209
+ canDismiss: !active, dismissed: this.isDismissed(runId) };
193
210
  }
194
211
  catch (error) {
195
212
  try {
@@ -362,6 +379,24 @@ class AgentRuntimeControls {
362
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 });
363
380
  }
364
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
+ }
365
400
  cancel(runId) {
366
401
  const active = this.active.get(runId);
367
402
  if (!active)
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveEffectiveRuntimeConfig = resolveEffectiveRuntimeConfig;
4
+ exports.fillDefaultRoleModels = fillDefaultRoleModels;
4
5
  const providers_1 = require("./providers");
5
6
  /** A pure admission resolver; only deliberate launch intent overrides a project role. */
6
7
  function resolveEffectiveRuntimeConfig(input, options) {
@@ -10,23 +11,43 @@ function resolveEffectiveRuntimeConfig(input, options) {
10
11
  const provider = config.providers.find(entry => entry.id === override.provider);
11
12
  if (!provider)
12
13
  throw new Error(`Selected runtime provider is not configured: ${override.provider}`);
13
- for (const role of ['architect', 'developer', 'reviewer']) {
14
- const current = config.agents[role];
14
+ for (const role of ['architect', 'developer', 'reviewer', 'fixer']) {
15
+ const current = role === 'fixer' ? config.fixer : config.agents[role];
16
+ if (!current)
17
+ continue;
15
18
  const changedProvider = current.provider !== provider.id;
16
19
  const changedModel = override.model !== undefined && override.model !== current.model;
17
- config.agents[role] = {
20
+ const next = {
18
21
  ...current, provider: provider.id,
19
22
  model: override.model ?? (changedProvider ? undefined : current.model),
20
23
  effort: override.effort ?? (changedProvider || changedModel ? undefined : current.effort),
21
24
  escalation: changedProvider || changedModel ? undefined : current.escalation,
22
25
  };
26
+ if (role === 'fixer')
27
+ config.fixer = next;
28
+ else
29
+ config.agents[role] = next;
23
30
  }
24
31
  }
25
- for (const agent of Object.values(config.agents)) {
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] : [])]) {
26
46
  const provider = config.providers.find(entry => entry.id === agent.provider);
27
- if (!agent.model && provider?.kind === 'cli')
47
+ if (!agent.model && provider?.kind === 'cli' && !options.localOnly)
28
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();
29
51
  }
30
- config.verification = config.verification.filter(check => options.repositoryIds.includes(check.repositoryId));
31
- return { config, origins: { architect: override ? 'explicit-launch-override' : options.source, developer: override ? 'explicit-launch-override' : options.source, reviewer: override ? 'explicit-launch-override' : options.source } };
52
+ return config;
32
53
  }
@@ -5,7 +5,7 @@ exports.readRuntimeEfficiencySummary = readRuntimeEfficiencySummary;
5
5
  exports.applyRuntimeSelectionOrigins = applyRuntimeSelectionOrigins;
6
6
  const COUNTS = ['attempts', 'measuredAttempts', 'providerCalls', 'toolCalls', 'inputTokens', 'outputTokens', 'uncachedInputTokens', 'cacheReadInputTokens', 'cacheWriteInputTokens'];
7
7
  const KEYS = [...COUNTS, 'durationMs', 'agentDurationMs', 'costUsd'];
8
- const PHASES = ['architect', 'developer', 'verify', 'reviewer', 'archive'];
8
+ const PHASES = ['architect', 'developer', 'fixer', 'verify', 'reviewer', 'archive'];
9
9
  function object(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null; }
10
10
  function counters(value) {
11
11
  const input = object(value);
@@ -4,9 +4,59 @@ exports.registerAgentRuntimeSettingsRoutes = registerAgentRuntimeSettingsRoutes;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const agent_runtime_loader_1 = require("./agent-runtime-loader");
6
6
  const agent_runtime_settings_1 = require("./agent-runtime-settings");
7
+ const agent_runtime_settings_2 = require("./agent-runtime-settings");
7
8
  const agent_runtime_verification_suggestions_1 = require("./agent-runtime-verification-suggestions");
8
9
  const project_repositories_1 = require("./project-repositories");
10
+ const agent_runtime_effective_config_1 = require("./agent-runtime-effective-config");
11
+ /** Raw request body → same body with every local role's missing model filled (validation then passes). */
12
+ function withDefaultRoleModels(body) {
13
+ const agents = body.agents;
14
+ const providers = body.providers;
15
+ if (!agents || typeof agents !== 'object' || !Array.isArray(providers))
16
+ return body;
17
+ // Only LOCAL roles: a CLI role saved without a model keeps meaning "provider default" (never frozen into the file).
18
+ const fixer = body.fixer && typeof body.fixer === 'object' ? structuredClone(body.fixer) : undefined;
19
+ const filled = (0, agent_runtime_effective_config_1.fillDefaultRoleModels)({ agents: structuredClone(agents), providers: providers, ...(fixer ? { fixer: fixer } : {}) }, { localOnly: true });
20
+ return { ...body, agents: filled.agents, ...(filled.fixer ? { fixer: filled.fixer } : {}) };
21
+ }
22
+ const loop_role_engines_1 = require("./loop-role-engines");
9
23
  function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
24
+ /** Guardrail catalog of the loaded Core (configurable-guardrails): ids + phase; the UI owns labels. `supported:false` on older cores. */
25
+ router.get('/:projectId/agent-runtime/guardrails', async (_req, res) => {
26
+ try {
27
+ const runtime = await (0, agent_runtime_loader_1.loadCoreAgentRuntime)();
28
+ const supported = runtime.api?.capabilities?.configurableGuardrails === 1;
29
+ const catalog = supported && Array.isArray(runtime.api?.guardrails) ? runtime.api.guardrails.filter((item) => item && typeof item.id === 'string' && typeof item.phase === 'string') : [];
30
+ res.json({ supported, catalog });
31
+ }
32
+ catch {
33
+ res.json({ supported: false, catalog: [] });
34
+ }
35
+ });
36
+ /** Loop-side role engines (verifier / decider) for `roles` rail launches — hybrid-role-engines. */
37
+ router.get('/:projectId/agent-runtime/loop-roles', (req, res) => {
38
+ try {
39
+ res.json({ roles: (0, loop_role_engines_1.loadLoopRoleEngines)(ctx(req).project) });
40
+ }
41
+ catch {
42
+ res.status(500).json({ error: 'loop_roles_read_failed' });
43
+ }
44
+ });
45
+ router.put('/:projectId/agent-runtime/loop-roles', (req, res) => {
46
+ try {
47
+ const project = ctx(req).project;
48
+ const roles = (0, loop_role_engines_1.validateLoopRoleEngines)(req.body?.roles ?? req.body ?? {}, project);
49
+ (0, loop_role_engines_1.saveLoopRoleEngines)(project, roles);
50
+ res.json({ roles });
51
+ }
52
+ catch (error) {
53
+ if (error instanceof loop_role_engines_1.LoopRoleEnginesError) {
54
+ res.status(400).json({ error: 'invalid_loop_roles', message: error.message });
55
+ return;
56
+ }
57
+ res.status(500).json({ error: 'loop_roles_write_failed' });
58
+ }
59
+ });
10
60
  router.get('/:projectId/agent-runtime/config', async (_req, res) => {
11
61
  try {
12
62
  const project = ctx(_req).project;
@@ -39,19 +89,38 @@ function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
39
89
  });
40
90
  router.post('/:projectId/agent-runtime/capabilities', async (req, res) => {
41
91
  try {
42
- const config = (0, agent_runtime_settings_1.validateAgentRuntimeConfig)({ ...req.body, providers: (0, agent_runtime_settings_1.loadRuntimeProviders)(), enabled: true });
92
+ const config = (0, agent_runtime_settings_1.validateAgentRuntimeConfig)(withDefaultRoleModels({ ...req.body, providers: (0, agent_runtime_settings_1.loadRuntimeProviders)(), enabled: true }));
43
93
  const runtime = await (0, agent_runtime_loader_1.loadCoreAgentRuntime)();
44
94
  if (!runtime.capabilities)
45
95
  throw new Error('Installed Core does not support capability introspection');
46
- res.json(await runtime.capabilities(config));
96
+ // Core's schema is additionalProperties:false — strip the desktop-only
97
+ // connection fields (label/defaultModel/rates/supportsReasoningEffort) first.
98
+ res.json(await runtime.capabilities({ ...(0, agent_runtime_settings_2.forCoreRuntime)(config, runtime.api?.capabilities), providers: (0, agent_runtime_settings_2.stripDesktopConnectionFields)(config.providers, (0, agent_runtime_settings_1.coreConnectionFieldGates)(runtime.api?.capabilities)) }));
47
99
  }
48
100
  catch (error) {
49
- res.status(error instanceof agent_runtime_settings_1.AgentRuntimeConfigError ? 400 : 503).json({ error: 'runtime_capabilities_unavailable', message: error instanceof agent_runtime_settings_1.AgentRuntimeConfigError ? error.message : 'Update the paired Core runtime to inspect model capabilities' });
101
+ // Core prints its verdict (`{"type":"runtime-result","status":"failed","error":…}`) before exiting non-zero: show that, not the generic "update Core" line.
102
+ const coreMessage = (() => { const out = error.stdout; if (typeof out !== 'string')
103
+ return undefined; try {
104
+ const parsed = JSON.parse(out.trim().split('\n').pop() ?? '');
105
+ return typeof parsed.error === 'string' ? parsed.error : undefined;
106
+ }
107
+ catch {
108
+ return undefined;
109
+ } })();
110
+ res.status(error instanceof agent_runtime_settings_1.AgentRuntimeConfigError || coreMessage ? 400 : 503).json({ error: 'runtime_capabilities_unavailable', message: error instanceof agent_runtime_settings_1.AgentRuntimeConfigError ? error.message : coreMessage ?? 'Update the paired Core runtime to inspect model capabilities' });
50
111
  }
51
112
  });
52
113
  router.put('/:projectId/agent-runtime/config', async (req, res) => {
53
114
  try {
54
- (0, agent_runtime_settings_1.validateAgentRuntimeConfig)(req.body);
115
+ // A local role saved as "provider default" carries no model; fill the
116
+ // connection default (the launch rule) before the model-required check.
117
+ // The raw body is validated first (its own providers included — a
118
+ // credential in a submitted URL must be refused), then the stored
119
+ // connections replace them and the missing local-role models are filled
120
+ // from each connection's default before the model-required check.
121
+ // Saved WITHOUT filling local-role models: "provider default" stays a
122
+ // live reference to the connection's default (resolved at launch).
123
+ (0, agent_runtime_settings_1.validateAgentRuntimeConfig)({ ...req.body, providers: Array.isArray(req.body?.providers) ? req.body.providers : (0, agent_runtime_settings_1.loadRuntimeProviders)() });
55
124
  const config = (0, agent_runtime_settings_1.validateAgentRuntimeConfig)({ ...req.body, enabled: true, providers: (0, agent_runtime_settings_1.loadRuntimeProviders)() });
56
125
  const runtimeAvailable = (0, agent_runtime_loader_1.findCoreAgentRuntimeEntry)() !== null;
57
126
  if (config.enabled && !runtimeAvailable) {
@@ -62,8 +131,9 @@ function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
62
131
  if (config.enabled && runtimeAvailable) {
63
132
  try {
64
133
  const runtime = await (0, agent_runtime_loader_1.loadCoreAgentRuntime)();
65
- runtime.validateRuntimeConfig(config);
66
- (0, agent_runtime_loader_1.validateRequestedRoleEfforts)(runtime, config);
134
+ const coreConfig = { ...(0, agent_runtime_settings_2.forCoreRuntime)((0, agent_runtime_effective_config_1.fillDefaultRoleModels)(structuredClone(config), { localOnly: true }), runtime.api?.capabilities), providers: (0, agent_runtime_settings_2.stripDesktopConnectionFields)(config.providers, (0, agent_runtime_settings_1.coreConnectionFieldGates)(runtime.api?.capabilities)) };
135
+ runtime.validateRuntimeConfig(coreConfig);
136
+ (0, agent_runtime_loader_1.validateRequestedRoleEfforts)(runtime, coreConfig);
67
137
  efficiencyAvailable = runtime.api?.capabilities?.efficientRoleExecution === 1;
68
138
  }
69
139
  catch {