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
@@ -3,7 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.AgentRuntimeConfigError = exports.REVIEW_THRESHOLD_FLOORS = void 0;
6
+ exports.AgentRuntimeConfigError = exports.REVIEW_THRESHOLD_FLOORS = exports.MAX_OUTPUT_TOKENS_MAX = exports.MAX_OUTPUT_TOKENS_MIN = exports.CONTEXT_WINDOW_TOKENS_MAX = exports.CONTEXT_WINDOW_TOKENS_MIN = void 0;
7
+ exports.stripDesktopConnectionFields = stripDesktopConnectionFields;
8
+ exports.coreConnectionFieldGates = coreConnectionFieldGates;
7
9
  exports.validateAgentRuntimeConfig = validateAgentRuntimeConfig;
8
10
  exports.defaultAgentRuntimeConfig = defaultAgentRuntimeConfig;
9
11
  exports.agentRuntimeConfigPath = agentRuntimeConfigPath;
@@ -17,6 +19,8 @@ exports.saveAgentRuntimeConfig = saveAgentRuntimeConfig;
17
19
  exports.validateRuntimeRolePrompts = validateRuntimeRolePrompts;
18
20
  exports.loadRuntimeRolePrompts = loadRuntimeRolePrompts;
19
21
  exports.saveRuntimeRolePrompts = saveRuntimeRolePrompts;
22
+ exports.validateRuntimeProviderOverride = validateRuntimeProviderOverride;
23
+ exports.forCoreRuntime = forCoreRuntime;
20
24
  const node_fs_1 = __importDefault(require("node:fs"));
21
25
  const node_path_1 = __importDefault(require("node:path"));
22
26
  const node_os_1 = __importDefault(require("node:os"));
@@ -24,6 +28,89 @@ const node_crypto_1 = require("node:crypto");
24
28
  const ajv_1 = __importDefault(require("ajv"));
25
29
  const agent_runtime_schema_json_1 = __importDefault(require("./schemas/agent-runtime.schema.json"));
26
30
  const workspace_resolution_1 = require("./workspace-resolution");
31
+ const registry_1 = require("./providers/registry");
32
+ const DESKTOP_CONNECTION_FIELDS = ['label', 'defaultModel', 'rates'];
33
+ /** Core-owned small-model fields: known only to cores advertising `capabilities.compactAgentLoop`
34
+ * (`supportsReasoningEffort` moved here: core forwards it as OpenAI `reasoning_effort`). */
35
+ const CORE_COMPACT_FIELDS = ['agentLoop', 'contextWindowTokens', 'supportsReasoningEffort'];
36
+ /** Known only to cores advertising `capabilities.compactOutputBudget`. */
37
+ const CORE_OUTPUT_BUDGET_FIELDS = ['maxOutputTokens'];
38
+ exports.CONTEXT_WINDOW_TOKENS_MIN = 4096;
39
+ exports.CONTEXT_WINDOW_TOKENS_MAX = 4_000_000;
40
+ exports.MAX_OUTPUT_TOKENS_MIN = 1024;
41
+ exports.MAX_OUTPUT_TOKENS_MAX = 1_000_000;
42
+ const MODEL_PATTERN = /^[^-\r\n\u0000][^\r\n\u0000]*$/;
43
+ /** Core-shaped providers: desktop-only connection fields removed. */
44
+ function stripDesktopConnectionFields(providers, opts) {
45
+ return providers.map((provider) => {
46
+ if (provider.kind !== 'openai-compatible')
47
+ return { ...provider };
48
+ const copy = { ...provider };
49
+ for (const field of DESKTOP_CONNECTION_FIELDS)
50
+ delete copy[field];
51
+ // Older cores reject unknown keys: only hand the compact-loop fields to a
52
+ // core that advertises them. Absent opts ⇒ conservative (strip).
53
+ if (opts?.coreCompactLoop !== true)
54
+ for (const field of CORE_COMPACT_FIELDS)
55
+ delete copy[field];
56
+ if (opts?.coreOutputBudget !== true)
57
+ for (const field of CORE_OUTPUT_BUDGET_FIELDS)
58
+ delete copy[field];
59
+ return copy;
60
+ });
61
+ }
62
+ function validateDesktopConnectionFields(provider, index) {
63
+ const at = `/providers/${index}`;
64
+ const extras = {};
65
+ if (provider.label !== undefined) {
66
+ if (typeof provider.label !== 'string' || !provider.label.trim() || provider.label.length > 64 || /[\r\n\u0000]/.test(provider.label))
67
+ throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/label: must be 1–64 characters`);
68
+ extras.label = provider.label;
69
+ }
70
+ if (provider.defaultModel !== undefined) {
71
+ if (typeof provider.defaultModel !== 'string' || !provider.defaultModel.trim() || provider.defaultModel.length > 256 || !MODEL_PATTERN.test(provider.defaultModel))
72
+ throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/defaultModel: must be a model id (no leading '-', no control characters)`);
73
+ extras.defaultModel = provider.defaultModel;
74
+ }
75
+ if (provider.rates !== undefined) {
76
+ const rates = provider.rates;
77
+ if (!rates || typeof rates !== 'object' || Array.isArray(rates) || Object.keys(rates).some((key) => key !== 'inputPer1M' && key !== 'outputPer1M'))
78
+ throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/rates: expected { inputPer1M, outputPer1M }`);
79
+ for (const key of ['inputPer1M', 'outputPer1M']) {
80
+ const value = rates[key];
81
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
82
+ throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/rates/${key}: must be a non-negative number`);
83
+ }
84
+ extras.rates = { inputPer1M: rates.inputPer1M, outputPer1M: rates.outputPer1M };
85
+ }
86
+ if (provider.supportsReasoningEffort !== undefined) {
87
+ if (typeof provider.supportsReasoningEffort !== 'boolean')
88
+ throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/supportsReasoningEffort: must be a boolean`);
89
+ extras.supportsReasoningEffort = provider.supportsReasoningEffort;
90
+ }
91
+ if (provider.agentLoop !== undefined) {
92
+ if (provider.agentLoop !== 'compact' && provider.agentLoop !== 'free')
93
+ throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/agentLoop: must be "compact" or "free"`);
94
+ extras.agentLoop = provider.agentLoop;
95
+ }
96
+ if (provider.contextWindowTokens !== undefined) {
97
+ const n = provider.contextWindowTokens;
98
+ if (typeof n !== 'number' || !Number.isSafeInteger(n) || n < exports.CONTEXT_WINDOW_TOKENS_MIN || n > exports.CONTEXT_WINDOW_TOKENS_MAX)
99
+ throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/contextWindowTokens: must be an integer between ${exports.CONTEXT_WINDOW_TOKENS_MIN} and ${exports.CONTEXT_WINDOW_TOKENS_MAX}`);
100
+ extras.contextWindowTokens = n;
101
+ }
102
+ if (provider.maxOutputTokens !== undefined) {
103
+ const n = provider.maxOutputTokens;
104
+ if (typeof n !== 'number' || !Number.isSafeInteger(n) || n < exports.MAX_OUTPUT_TOKENS_MIN || n > exports.MAX_OUTPUT_TOKENS_MAX)
105
+ throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/maxOutputTokens: must be an integer between ${exports.MAX_OUTPUT_TOKENS_MIN} and ${exports.MAX_OUTPUT_TOKENS_MAX}`);
106
+ extras.maxOutputTokens = n;
107
+ }
108
+ return extras;
109
+ }
110
+ /** The strip options for a loaded core: each core-owned field family travels only when the core advertises it. */
111
+ function coreConnectionFieldGates(capabilities) {
112
+ return { coreCompactLoop: capabilities?.compactAgentLoop === 1, coreOutputBudget: capabilities?.compactOutputBudget === 1 };
113
+ }
27
114
  /** Core's own review gate. Configured thresholds may only tighten it (mirrors Core's config.ts). */
28
115
  exports.REVIEW_THRESHOLD_FLOORS = {
29
116
  minScore: 70,
@@ -39,6 +126,19 @@ exports.AgentRuntimeConfigError = AgentRuntimeConfigError;
39
126
  /** Vendored from Core's schema; parity tests keep offline configuration aligned.
40
127
  * Semantic checks mirror Core because JSON Schema cannot check provider refs. */
41
128
  function validateAgentRuntimeConfig(input) {
129
+ // Desktop-only connection fields ride the openai-compatible entries; the
130
+ // vendored (byte-identical to Core) schema forbids unknown keys, so they are
131
+ // validated here and re-attached after the schema pass.
132
+ const extrasByIndex = new Map();
133
+ if (input && typeof input === 'object' && !Array.isArray(input) && Array.isArray(input.providers)) {
134
+ const rawProviders = input.providers;
135
+ rawProviders.forEach((provider, index) => {
136
+ if (!provider || typeof provider !== 'object' || provider.kind !== 'openai-compatible')
137
+ return;
138
+ extrasByIndex.set(index, validateDesktopConnectionFields(provider, index));
139
+ });
140
+ input = { ...input, providers: stripDesktopConnectionFields(rawProviders) };
141
+ }
42
142
  if (!schemaValidator(input)) {
43
143
  const error = schemaValidator.errors?.[0];
44
144
  throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${error?.instancePath || '/'}: ${error?.message ?? 'invalid value'}`);
@@ -51,6 +151,10 @@ function validateAgentRuntimeConfig(input) {
51
151
  for (const provider of config.providers) {
52
152
  if (provider.kind !== 'openai-compatible')
53
153
  continue;
154
+ // A connection id may never shadow a registered CLI adapter (claude/codex/
155
+ // gemini/kimi): local adapters register under the connection id.
156
+ if ((0, registry_1.hasAdapter)(provider.id) && (0, registry_1.adapterKind)(provider.id) !== 'local')
157
+ throw new AgentRuntimeConfigError(`Provider id '${provider.id}' is reserved for the ${provider.id} CLI adapter`);
54
158
  let url;
55
159
  try {
56
160
  url = new URL(provider.baseUrl);
@@ -61,23 +165,43 @@ function validateAgentRuntimeConfig(input) {
61
165
  if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || provider.baseUrl.includes('\0'))
62
166
  throw new AgentRuntimeConfigError('Provider URLs must use HTTP(S) without credentials, query parameters or fragments');
63
167
  }
64
- for (const role of ROLES) {
65
- const agent = config.agents[role];
168
+ for (const role of [...ROLES, ...(config.fixer ? ['fixer'] : [])]) {
169
+ const agent = role === 'fixer' ? config.fixer : config.agents[role];
66
170
  const provider = config.providers.find(({ id }) => id === agent.provider);
67
171
  if (!provider)
68
172
  throw new AgentRuntimeConfigError(`Role ${role} references a provider that is not configured`);
69
- if (provider.kind === 'openai-compatible' && !agent.model)
173
+ // Core requires a model on API providers, but a LOCAL engine (a registered
174
+ // connection with a discovered/stored default) may be saved as "provider
175
+ // default": the launch path fills the connection's CURRENT default
176
+ // (`fillDefaultRoleModels`), so changing the connection's default later
177
+ // applies to every role that did not pin one. Persisting the filled value
178
+ // used to freeze gpt-oss-fast into the architect after the user switched
179
+ // the connection to qwen3-coder.
180
+ if (provider.kind === 'openai-compatible' && !agent.model && !((0, registry_1.hasAdapter)(provider.id) && (0, registry_1.isLocalAdapterId)(provider.id)))
70
181
  throw new AgentRuntimeConfigError(`Role ${role} requires a model for its API provider`);
71
182
  if (agent.model !== undefined && !agent.model.trim())
72
183
  throw new AgentRuntimeConfigError(`Role ${role} requires a nonempty model identifier`);
184
+ for (const value of [agent.model, agent.effort, agent.escalation?.model, agent.escalation?.effort])
185
+ if (value !== undefined && (!value.trim() || value.includes('\0')))
186
+ throw new AgentRuntimeConfigError(`Role ${role} has an invalid model or effort`);
187
+ if (agent.escalation && (!agent.model || (agent.escalation.model === agent.model && agent.escalation.effort === agent.effort)))
188
+ throw new AgentRuntimeConfigError(`Role ${role} escalation requires an explicit base and a different model or effort`);
73
189
  if (agent.maxTurns !== undefined && !Number.isSafeInteger(agent.maxTurns))
74
190
  throw new AgentRuntimeConfigError(`Role ${role} maxTurns must be a safe integer`);
191
+ if (agent.thinking !== undefined && agent.thinking !== 'on' && agent.thinking !== 'off')
192
+ throw new AgentRuntimeConfigError(`Role ${role} thinking must be "on" or "off"`);
75
193
  }
76
194
  for (const [key, value] of Object.entries(config.limits ?? {})) {
77
195
  if (!Number.isFinite(value) || (key !== 'maxCostUsd' && !Number.isSafeInteger(value)))
78
196
  throw new AgentRuntimeConfigError('Workflow limits must be finite, safe numbers');
79
197
  }
198
+ const keys = config.verification.flatMap(command => command.key ? [command.key] : []);
199
+ if (new Set(keys).size !== keys.length)
200
+ throw new AgentRuntimeConfigError('Verification check keys must be unique');
80
201
  for (const command of config.verification) {
202
+ for (const value of [command.label, ...(command.policy?.inputs ?? []), ...(command.policy?.toolchainInputs ?? []), ...(command.policy?.resources ?? [])])
203
+ if (value !== undefined && (!value.trim() || value.includes('\0')))
204
+ throw new AgentRuntimeConfigError('Invalid verification label or policy input');
81
205
  if (![command.command, ...command.args, ...(command.cwd === undefined ? [] : [command.cwd])].every((value) => !value.includes('\0')) || !command.command.trim() || (command.cwd !== undefined && !command.cwd.trim()))
82
206
  throw new AgentRuntimeConfigError('Verification command contains an empty or invalid value');
83
207
  if (command.timeoutMs !== undefined && !Number.isSafeInteger(command.timeoutMs))
@@ -95,7 +219,10 @@ function validateAgentRuntimeConfig(input) {
95
219
  if (value < exports.REVIEW_THRESHOLD_FLOORS.aspects[aspect])
96
220
  throw new AgentRuntimeConfigError(`Review threshold review.aspects.${aspect} must be at least ${exports.REVIEW_THRESHOLD_FLOORS.aspects[aspect]} (Core's own review gate)`);
97
221
  }
98
- return structuredClone(config);
222
+ const cloned = structuredClone(config);
223
+ for (const [index, extras] of extrasByIndex)
224
+ cloned.providers[index] = { ...cloned.providers[index], ...extras };
225
+ return cloned;
99
226
  }
100
227
  function defaultAgentRuntimeConfig(project) {
101
228
  const provider = CLI_PROVIDERS.includes(project.provider) ? project.provider : 'claude';
@@ -211,13 +338,14 @@ function saveAgentRuntimeConfig(project, input) {
211
338
  atomicJson(file, settings);
212
339
  return config;
213
340
  }
341
+ const PROMPT_ROLES = [...ROLES, 'fixer'];
214
342
  function validateRuntimeRolePrompts(input) {
215
343
  if (!input || typeof input !== 'object' || Array.isArray(input))
216
344
  throw new AgentRuntimeConfigError('Role prompts must be an object');
217
345
  const prompts = {};
218
346
  for (const [role, text] of Object.entries(input)) {
219
- if (!ROLES.includes(role) || typeof text !== 'string' || !text.trim() || text.length > 20000 || text.includes('\0'))
220
- throw new AgentRuntimeConfigError('Role prompts must use architect, developer or reviewer with 1–20000 nonblank characters and no null bytes');
347
+ if (!PROMPT_ROLES.includes(role) || typeof text !== 'string' || !text.trim() || text.length > 20000 || text.includes('\0'))
348
+ throw new AgentRuntimeConfigError('Role prompts must use architect, developer, reviewer or fixer with 1–20000 nonblank characters and no null bytes');
221
349
  prompts[role] = text;
222
350
  }
223
351
  return prompts;
@@ -237,3 +365,31 @@ function saveRuntimeRolePrompts(input) {
237
365
  atomicJson(node_path_1.default.join(node_os_1.default.homedir(), '.specrails', 'runtime-role-prompts.json'), prompts);
238
366
  return prompts;
239
367
  }
368
+ function validateRuntimeProviderOverride(value) {
369
+ if (value === undefined)
370
+ return undefined;
371
+ if (!value || typeof value !== 'object' || Array.isArray(value))
372
+ throw new AgentRuntimeConfigError('Invalid runtime provider override');
373
+ const raw = value;
374
+ if (Object.keys(raw).some(key => !['provider', 'model', 'effort'].includes(key)) || typeof raw.provider !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(raw.provider))
375
+ throw new AgentRuntimeConfigError('Invalid runtime provider override provider');
376
+ if (raw.model !== undefined && (typeof raw.model !== 'string' || !raw.model.trim() || raw.model.length > 256 || /^-|[\r\n\0]/.test(raw.model)))
377
+ throw new AgentRuntimeConfigError('Invalid runtime provider override model');
378
+ if (raw.effort !== undefined && (typeof raw.effort !== 'string' || !/^[a-z][a-z0-9_-]{0,31}$/.test(raw.effort)))
379
+ throw new AgentRuntimeConfigError('Invalid runtime provider override effort');
380
+ return structuredClone(raw);
381
+ }
382
+ /** Older Core runtimes reject unknown top-level keys: drop `guardrails` unless the loaded Core advertises `configurableGuardrails`. */
383
+ function forCoreRuntime(config, capabilities) {
384
+ let out = config;
385
+ if (capabilities?.configurableGuardrails !== 1 && out.guardrails !== undefined) {
386
+ const { guardrails: _dropped, ...rest } = out;
387
+ out = rest;
388
+ }
389
+ // The per-role thinking switch is known only to cores advertising `roleThinkingControl`.
390
+ if (capabilities?.roleThinkingControl !== 1 && (Object.values(out.agents ?? {}).some(agent => agent?.thinking !== undefined) || out.fixer?.thinking !== undefined)) {
391
+ const strip = (agent) => { const { thinking: _t, ...rest } = agent; return rest; };
392
+ out = { ...out, ...(out.agents ? { agents: Object.fromEntries(Object.entries(out.agents).map(([role, agent]) => [role, strip(agent)])) } : {}), ...(out.fixer ? { fixer: strip(out.fixer) } : {}) };
393
+ }
394
+ return out;
395
+ }
@@ -33,6 +33,8 @@ exports.framingRefusalMessage = framingRefusalMessage;
33
33
  exports.checkSpecFraming = checkSpecFraming;
34
34
  exports.recordSpecCommitted = recordSpecCommitted;
35
35
  const agent_store_1 = require("./agent-store");
36
+ const agent_fence_promotion_1 = require("./agent-fence-promotion");
37
+ const registry_1 = require("./providers/registry");
36
38
  /** User-typed command word that switches framing off for the conversation. */
37
39
  exports.FRAMING_WAIVER_TOKEN = '#noframe';
38
40
  /** User-typed command word that switches it back on. */
@@ -78,7 +80,10 @@ function payloadIsValidFrame(raw) {
78
80
  return true;
79
81
  }
80
82
  /** True when an assistant message carries at least one VALID frame block. */
81
- function hasValidProblemFrame(content) {
83
+ function hasValidProblemFrame(rawContent, opts) {
84
+ // Local (OpenAI-compatible) engines only — mirrors the client: a ```json
85
+ // fence with a frame-shaped body counts. CLI providers stay strict.
86
+ const content = opts?.tolerantFences ? (0, agent_fence_promotion_1.promoteAgentProtocolFences)(rawContent) : rawContent;
82
87
  if (!content.includes(OPEN_FENCE))
83
88
  return false;
84
89
  FENCE_RE.lastIndex = 0;
@@ -125,7 +130,7 @@ function isFramingMarker(content) {
125
130
  * rather than a series. A waiver is sticky for the rest of the conversation and
126
131
  * is cleared only by the restore token.
127
132
  */
128
- function evaluateFramingState(messages) {
133
+ function evaluateFramingState(messages, opts) {
129
134
  let waived = false;
130
135
  let pendingFrame = false; // emitted, awaiting the user's answer
131
136
  let answeredFrame = false; // answered and not yet spent on a spec
@@ -137,7 +142,7 @@ function evaluateFramingState(messages) {
137
142
  continue;
138
143
  }
139
144
  if (msg.role === 'assistant') {
140
- if (hasValidProblemFrame(msg.content)) {
145
+ if (hasValidProblemFrame(msg.content, opts)) {
141
146
  pendingFrame = true;
142
147
  sawFrame = true;
143
148
  }
@@ -182,7 +187,8 @@ function framingRefusalMessage(reason) {
182
187
  * MCP client cannot render the card and holds no conversation here.
183
188
  */
184
189
  function checkSpecFraming(db, conversationId) {
185
- const state = evaluateFramingState((0, agent_store_1.listAgentMessages)(db, conversationId));
190
+ const tolerantFences = (0, registry_1.isLocalAdapterId)((0, agent_store_1.getAgentConversation)(db, conversationId)?.provider);
191
+ const state = evaluateFramingState((0, agent_store_1.listAgentMessages)(db, conversationId), { tolerantFences });
186
192
  if (state.satisfied)
187
193
  return null;
188
194
  return framingRefusalMessage(state.reason ?? 'no_frame');
@@ -1,7 +1,12 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.binaryOnPath = binaryOnPath;
4
7
  exports.__resetBinaryProbeCacheForTest = __resetBinaryProbeCacheForTest;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
5
10
  const child_process_1 = require("child_process");
6
11
  const win_spawn_1 = require("./util/win-spawn");
7
12
  // Windows has no `which`; probe via `where` instead. Both exit non-zero
@@ -20,6 +25,13 @@ function binaryOnPath(binary) {
20
25
  if (hit && now - hit.at < PROBE_TTL_MS)
21
26
  return hit.onPath;
22
27
  let onPath;
28
+ // An absolute binary (the bundled Node that runs the local AI runner) is
29
+ // probed by existence: `where C:\\…\\node.exe` does not resolve a path.
30
+ if (node_path_1.default.isAbsolute(binary)) {
31
+ onPath = node_fs_1.default.existsSync(binary);
32
+ _cache.set(binary, { at: now, onPath });
33
+ return onPath;
34
+ }
23
35
  try {
24
36
  // `where` runs via cmd.exe; pass a SystemRoot-backfilled env so the probe
25
37
  // (a HARD gate before job/chat spawns) can't be falsely cached as missing
@@ -8,6 +8,7 @@ const providers_1 = require("./providers");
8
8
  const runtime_1 = require("./providers/runtime");
9
9
  const blueprint_store_1 = require("./blueprint-store");
10
10
  const blueprint_spec_quality_1 = require("./blueprint-spec-quality");
11
+ const provider_selection_1 = require("./provider-selection");
11
12
  function isProjectBuilderEnabled() {
12
13
  return process.env.SPECRAILS_PROJECT_BUILDER !== 'false';
13
14
  }
@@ -33,7 +34,7 @@ function createBlueprintRouter(deps) {
33
34
  next();
34
35
  });
35
36
  router.get('/models', (req, res) => {
36
- const provider = validProvider(req.query.provider) ?? 'claude';
37
+ const provider = validProvider(req.query.provider) ?? (0, provider_selection_1.defaultMachineProvider)();
37
38
  const adapter = (0, providers_1.getAdapter)(provider);
38
39
  const toolPolicy = (0, runtime_1.pureOutputToolPolicy)(adapter);
39
40
  const requestedModel = typeof req.query.model === 'string' && req.query.model
@@ -63,7 +64,7 @@ function createBlueprintRouter(deps) {
63
64
  });
64
65
  router.post('/conversations', (req, res) => {
65
66
  const body = (req.body ?? {});
66
- const provider = validProvider(body.provider) ?? 'claude';
67
+ const provider = validProvider(body.provider) ?? (0, provider_selection_1.defaultMachineProvider)();
67
68
  if (!(0, runtime_1.pureOutputToolPolicy)((0, providers_1.getAdapter)(provider))) {
68
69
  res.status(409).json({ error: 'provider_tool_policy_unsupported', provider });
69
70
  return;
@@ -21,6 +21,7 @@ const providers_1 = require("./providers");
21
21
  const runtime_1 = require("./providers/runtime");
22
22
  const context_scope_1 = require("./context-scope");
23
23
  const user_mcp_config_1 = require("./user-mcp-config");
24
+ const registry_1 = require("./providers/registry");
24
25
  const binary_probe_1 = require("./binary-probe");
25
26
  const explore_stdin_session_1 = require("./explore-stdin-session");
26
27
  const workspace_resolution_1 = require("./workspace-resolution");
@@ -937,7 +938,10 @@ class ChatManager {
937
938
  // gating is therefore claude-only today — codex inherits its sandbox
938
939
  // and approval policy from the project's `.codex/config.toml` (or the
939
940
  // `-c sandbox_mode=` override the adapter already attaches on resume).
940
- const scopeFlags = conversationScope && adapter.id === 'claude'
941
+ // Local (OpenAI-compatible) runners accept the SAME `--tools` /
942
+ // `--disallowedTools` shape (the adapter's extraArgs allowlist drops any
943
+ // claude-only flag), so the scope tiers apply to them too.
944
+ const scopeFlags = conversationScope && (adapter.id === 'claude' || (0, registry_1.isLocalAdapterId)(adapter.id))
941
945
  ? (0, context_scope_1.toolFlagsForScope)(conversationScope).args
942
946
  : [];
943
947
  if (this._cwd && conversation.kind !== 'milestone' && (!conversationScope || conversationScope.mcp)) {
@@ -21,6 +21,9 @@ const specrails_tech_client_1 = require("./specrails-tech-client");
21
21
  const core_compat_1 = require("./core-compat");
22
22
  const providers_1 = require("./providers");
23
23
  const provider_detection_1 = require("./provider-detection");
24
+ const local_engine_detection_1 = require("./local-engine-detection");
25
+ const local_adapter_registry_1 = require("./providers/local-adapter-registry");
26
+ const local_adapter_1 = require("./providers/local-adapter");
24
27
  const agent_defaults_1 = require("./agent-defaults");
25
28
  const external_mcp_1 = require("./external-mcp");
26
29
  const workspace_manager_1 = require("./workspace-manager");
@@ -397,6 +400,17 @@ function createDesktopRouter(registry, broadcast) {
397
400
  // provider surfaces here with no edit. Apply per-provider beta gates: codex
398
401
  // is forced unavailable when SPECRAILS_CODEX_BETA=0 (emergency rollback).
399
402
  const gated = { ...providers };
403
+ // Local (OpenAI-compatible) engines have no CLI to `which`: their binary is
404
+ // the bundled node, so the PATH probe above always says "installed" even
405
+ // when the endpoint is down. Their availability is the bounded HTTP probe
406
+ // from the detection cycle (refreshed here so a first call after boot is
407
+ // not blind); an id the snapshot does not know stays unavailable.
408
+ const localIds = (0, providers_1.listAdapters)().filter(local_adapter_1.isLocalAdapter).map((adapter) => adapter.id);
409
+ if (localIds.length > 0) {
410
+ const snapshot = await (0, provider_detection_1.getDetectionSnapshot)();
411
+ for (const id of localIds)
412
+ gated[id] = snapshot.providers[id]?.usable === true;
413
+ }
400
414
  if ((0, provider_detection_1.isCodexBetaDisabled)())
401
415
  gated.codex = false;
402
416
  // Gemini: enabled by default; forced unavailable only when
@@ -430,7 +444,10 @@ function createDesktopRouter(registry, broadcast) {
430
444
  adapter.id,
431
445
  { command: adapter.binary, args: [] },
432
446
  ]));
433
- res.json({ ...gated, tiers, providerIssues, launchDescriptors });
447
+ // Display names so app-level selectors (Agent composer, Builder) can label
448
+ // a local engine with its connection label instead of its raw id.
449
+ const labels = Object.fromEntries((0, providers_1.listAdapters)().map((adapter) => [adapter.id, adapter.displayName]));
450
+ res.json({ ...gated, tiers, providerIssues, launchDescriptors, labels });
434
451
  });
435
452
  router.get('/setup-prerequisites', (req, res) => {
436
453
  const status = (0, setup_prerequisites_1.getSetupPrerequisitesStatus)();
@@ -1230,28 +1247,94 @@ function createDesktopRouter(registry, broadcast) {
1230
1247
  res.json({ language, monthlyBudgetUsd });
1231
1248
  });
1232
1249
  (0, runtime_role_prompts_router_1.registerRuntimeRolePromptRoutes)(router);
1250
+ // ─── Runtime provider connections (local AI engines) ──────────────────────
1251
+ // GET returns every connection plus a per-id status: local ids read the
1252
+ // detection cache (reachable/authState/models), CLI ids the detection
1253
+ // snapshot. PUT saves, re-syncs the local adapters, re-probes them (awaited)
1254
+ // and broadcasts `providers.detected_changed` when the usable set changed so
1255
+ // open engine selectors update live. POST /test probes an UNSAVED draft.
1256
+ const runtimeProviderStatus = (providers) => {
1257
+ const snapshot = (0, provider_detection_1.getSnapshotSync)();
1258
+ const status = {};
1259
+ for (const provider of providers) {
1260
+ if (provider.kind === 'openai-compatible') {
1261
+ const probe = (0, local_engine_detection_1.getCachedProbe)(provider.id);
1262
+ status[provider.id] = probe
1263
+ ? {
1264
+ kind: 'local', reachable: probe.reachable, authState: probe.authState, models: probe.models,
1265
+ latencyMs: probe.latencyMs, ...(probe.error ? { error: probe.error } : {}),
1266
+ ...(probe.apiKeyEnvMissing ? { apiKeyEnvMissing: true } : {}),
1267
+ }
1268
+ : { kind: 'local', reachable: false, authState: 'unknown', models: [], probing: true };
1269
+ continue;
1270
+ }
1271
+ const row = snapshot?.providers[provider.cli];
1272
+ status[provider.id] = row
1273
+ ? { kind: 'cli', reachable: row.usable, installed: row.installed, executable: row.executable, authState: row.authState, models: [], ...(row.version ? { version: row.version } : {}), ...(row.error ? { error: row.error } : {}) }
1274
+ : { kind: 'cli', reachable: false, installed: false, executable: false, authState: 'unknown', models: [], probing: true };
1275
+ }
1276
+ return status;
1277
+ };
1233
1278
  router.get('/runtime-providers', (_req, res) => {
1234
1279
  try {
1235
1280
  for (const project of (0, desktop_db_1.listProjects)(registry.desktopDb))
1236
1281
  (0, agent_runtime_settings_1.loadAgentRuntimeConfig)(project);
1237
- res.json({ providers: (0, agent_runtime_settings_1.loadRuntimeProviders)() });
1282
+ const providers = (0, agent_runtime_settings_1.loadRuntimeProviders)();
1283
+ res.json({ providers, status: runtimeProviderStatus(providers), localEnginesEnabled: (0, local_adapter_registry_1.isLocalEnginesEnabled)() });
1238
1284
  }
1239
1285
  catch (error) {
1240
1286
  res.status(422).json({ message: error.message });
1241
1287
  }
1242
1288
  });
1243
- router.put('/runtime-providers', (req, res) => {
1289
+ router.put('/runtime-providers', async (req, res) => {
1290
+ let saved;
1244
1291
  try {
1245
1292
  const projects = (0, desktop_db_1.listProjects)(registry.desktopDb).map(project => (0, agent_runtime_settings_1.loadAgentRuntimeConfig)(project));
1246
1293
  const providers = (0, agent_runtime_settings_1.validateRuntimeProviders)(req.body.providers);
1247
1294
  for (const config of projects)
1248
1295
  if (config)
1249
1296
  (0, agent_runtime_settings_1.validateAgentRuntimeConfig)({ ...config, providers });
1250
- res.json({ providers: (0, agent_runtime_settings_1.saveRuntimeProviders)(providers) });
1297
+ saved = (0, agent_runtime_settings_1.saveRuntimeProviders)(providers);
1251
1298
  }
1252
1299
  catch (error) {
1253
1300
  res.status(error instanceof agent_runtime_settings_1.AgentRuntimeConfigError ? 400 : 500).json({ message: error.message });
1301
+ return;
1302
+ }
1303
+ try {
1304
+ (0, local_adapter_registry_1.syncLocalAdapters)(saved);
1305
+ const { snapshot, changed } = await (0, provider_detection_1.refreshLocalDetection)();
1306
+ if (changed) {
1307
+ broadcast({
1308
+ type: 'providers.detected_changed',
1309
+ detected: snapshot.detected,
1310
+ providers: snapshot.providers,
1311
+ timestamp: new Date().toISOString(),
1312
+ });
1313
+ }
1254
1314
  }
1315
+ catch (error) {
1316
+ console.warn('[runtime-providers] local engine re-sync failed (non-fatal):', error.message);
1317
+ }
1318
+ res.json({ providers: saved, status: runtimeProviderStatus(saved), localEnginesEnabled: (0, local_adapter_registry_1.isLocalEnginesEnabled)() });
1319
+ });
1320
+ router.post('/runtime-providers/test', async (req, res) => {
1321
+ const body = (req.body ?? {});
1322
+ // Same shape rules as the connections schema (absolute http(s), no
1323
+ // credentials/query/fragment; env NAME only, never a key value).
1324
+ if (typeof body.baseUrl !== 'string' || !/^https?:\/\/[^/?#@]+(?:\/[^?#]*)?$/.test(body.baseUrl)) {
1325
+ res.status(400).json({ error: 'invalid_base_url', message: 'baseUrl must be an absolute HTTP(S) URL without credentials, query or fragment' });
1326
+ return;
1327
+ }
1328
+ if (body.apiKeyEnv !== undefined && body.apiKeyEnv !== null && body.apiKeyEnv !== '' && (typeof body.apiKeyEnv !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(body.apiKeyEnv))) {
1329
+ res.status(400).json({ error: 'invalid_api_key_env', message: 'apiKeyEnv must be an environment variable name' });
1330
+ return;
1331
+ }
1332
+ const apiKeyEnv = typeof body.apiKeyEnv === 'string' && body.apiKeyEnv ? body.apiKeyEnv : undefined;
1333
+ const probe = await (0, local_engine_detection_1.probeConnection)({ baseUrl: body.baseUrl, apiKeyEnv });
1334
+ res.json({
1335
+ reachable: probe.reachable, authState: probe.authState, models: probe.models, latencyMs: probe.latencyMs,
1336
+ ...(probe.error ? { error: probe.error } : {}), ...(probe.apiKeyEnvMissing ? { apiKeyEnvMissing: true } : {}),
1337
+ });
1255
1338
  });
1256
1339
  // ─── Specrails Agents defaults (global per-provider agent model/effort) ───
1257
1340
  // App-level layer read AT SPAWN TIME by QueueManager / rails-router / loop
@@ -53,6 +53,19 @@ const transient_children_1 = require("./transient-children");
53
53
  // before any manager constructs a project context. See
54
54
  // openspec/changes/add-multi-provider-support/specs/multi-provider-architecture/spec.md.
55
55
  require("./providers");
56
+ const local_adapter_registry_1 = require("./providers/local-adapter-registry");
57
+ const agent_runtime_settings_1 = require("./agent-runtime-settings");
58
+ // Local AI engines: every OpenAI-compatible connection in
59
+ // ~/.specrails/runtime-providers.json becomes a registered adapter BEFORE the
60
+ // first detection cycle (kill switch SPECRAILS_LOCAL_ENGINES=false ⇒ no-op).
61
+ try {
62
+ const sync = (0, local_adapter_registry_1.syncLocalAdapters)((0, agent_runtime_settings_1.loadRuntimeProviders)());
63
+ if (sync.registered.length)
64
+ console.log(`[local-engines] registered: ${sync.registered.join(', ')}`);
65
+ }
66
+ catch (err) {
67
+ console.warn('[local-engines] could not load runtime provider connections (non-fatal):', err.message);
68
+ }
56
69
  const hostControlToken = (0, host_control_1.consumeHostControlToken)();
57
70
  const inheritedPathBeforeResolve = (process.env.PATH ?? '').split(process.platform === 'win32' ? ';' : ':').filter(Boolean).length;
58
71
  // Backfill SystemRoot/ComSpec/etc into process.env BEFORE anything spawns, so a
@@ -39,6 +39,7 @@ const explore_stdin_session_1 = require("./explore-stdin-session");
39
39
  const result_event_1 = require("./result-event");
40
40
  const db_1 = require("./db");
41
41
  const stream_display_1 = require("./util/stream-display");
42
+ const registry_1 = require("./providers/registry");
42
43
  const runtime_1 = require("./providers/runtime");
43
44
  const terminal_result_1 = require("./providers/terminal-result");
44
45
  const claude_background_tasks_1 = require("./providers/claude-background-tasks");
@@ -227,7 +228,8 @@ class InteractiveJobSession {
227
228
  // system context, not in the command's argument string. Preserve any
228
229
  // existing appended system prompt supplied by the queue/loop owner.
229
230
  const args = [...spec.args];
230
- if (this._adapter.id === 'claude' && this._settleMode === 'auto') {
231
+ // Local runners share claude's argv shape for the appended system prompt.
232
+ if ((this._adapter.id === 'claude' || (0, registry_1.isLocalAdapterId)(this._adapter.id)) && this._settleMode === 'auto') {
231
233
  const index = args.indexOf('--append-system-prompt');
232
234
  if (index >= 0)
233
235
  args[index + 1] = `${args[index + 1] ?? ''}\n\n${loop_constants_1.FOREGROUND_RULE}`;