specrails-desktop 2.12.0 → 2.13.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 (401) hide show
  1. package/README.md +3 -2
  2. package/client/dist/assets/ActivityFeedPage-D0-P3PsD.js +1 -0
  3. package/client/dist/assets/AgentsPage-BZa3lhQM.js +86 -0
  4. package/client/dist/assets/AnalyticsPage-CgaEg-lx.js +1 -0
  5. package/client/dist/assets/BarChart-D9vfeilz.js +33 -0
  6. package/client/dist/assets/CodePage-2GUsF_JS.js +2 -0
  7. package/client/dist/assets/DesktopAnalyticsPage-C6Qccm5E.js +1 -0
  8. package/client/dist/assets/DocsDialog-BAmtk9Jc.js +11 -0
  9. package/client/dist/assets/DocsPage-CJwnpB57.js +11 -0
  10. package/client/dist/assets/{ExportDropdown-I_8R48Yq.js → ExportDropdown-DMJ__Yui.js} +1 -1
  11. package/client/dist/assets/IntegrationsPage-CP2VcOpr.js +3 -0
  12. package/client/dist/assets/JobDetailPage-Bk6tQTew.js +16 -0
  13. package/client/dist/assets/JobsPage-BvbeqWjS.js +1 -0
  14. package/client/dist/assets/LoopBuilderPage-BS3enu9N.js +7 -0
  15. package/client/dist/assets/LoopBuilderPage-DtxX27Jz.css +1 -0
  16. package/client/dist/assets/LoopsPage-B0pgvoky.js +2 -0
  17. package/client/dist/assets/analytics-BHApBnUn.js +1 -0
  18. package/client/dist/assets/analytics-BUxrug9J.js +1 -0
  19. package/client/dist/assets/analytics-Cy6DTfO-.js +1 -0
  20. package/client/dist/assets/analytics-DMJxD9FD.js +1 -0
  21. package/client/dist/assets/analytics-DgrBuxla.js +1 -0
  22. package/client/dist/assets/analytics-DwxT1CS8.js +1 -0
  23. package/client/dist/assets/analytics-GrLc-SMr.js +1 -0
  24. package/client/dist/assets/analytics-qvPq4T4h.js +1 -0
  25. package/client/dist/assets/atom-one-dark-DrWoEEbZ.js +1 -0
  26. package/client/dist/assets/auth-Cvsb7-Ek.js +1 -0
  27. package/client/dist/assets/{cssMode-DzNPAYFh.js → cssMode-DSHXamUH.js} +1 -1
  28. package/client/dist/assets/dashboard-8FSyxKGV.js +1 -0
  29. package/client/dist/assets/dashboard-A9XQUfdS.js +1 -0
  30. package/client/dist/assets/dashboard-BFktSDsV.js +1 -0
  31. package/client/dist/assets/dashboard-C1PKCXLs.js +1 -0
  32. package/client/dist/assets/dashboard-DNf0czU-.js +1 -0
  33. package/client/dist/assets/dashboard-DQQCOVfu.js +1 -0
  34. package/client/dist/assets/dashboard-DxFqeHZk.js +1 -0
  35. package/client/dist/assets/dashboard-WvPGhbbi.js +1 -0
  36. package/client/dist/assets/{dist-js-DXfIbU9U.js → dist-js-_oLAsZQ4.js} +1 -1
  37. package/client/dist/assets/{dist-js-CtdZvl0R.js → dist-js-o4iwbCuN.js} +1 -1
  38. package/client/dist/assets/{editor.main-C7Rmw-hR.js → editor.main-Cd8qBRAT.js} +2 -2
  39. package/client/dist/assets/{freemarker2-Cszs4SVo.js → freemarker2-CkEKTBm2.js} +1 -1
  40. package/client/dist/assets/{handlebars-Dp7Lsuym.js → handlebars-BRGx0JpL.js} +1 -1
  41. package/client/dist/assets/{html-BURidrEm.js → html-BfRIaGgN.js} +1 -1
  42. package/client/dist/assets/{htmlMode--k5M7GjZ.js → htmlMode-BTpSnsDE.js} +1 -1
  43. package/client/dist/assets/index-DrFJinsI.css +2 -0
  44. package/client/dist/assets/index-QEeqwz4v.js +143 -0
  45. package/client/dist/assets/{javascript-kJQz__44.js → javascript-DMmE2kKN.js} +1 -1
  46. package/client/dist/assets/{jsonMode-v5JYPpnz.js → jsonMode-hYuaUleY.js} +1 -1
  47. package/client/dist/assets/{lib-BBoUS-5a.js → lib-CVrpo1ac.js} +2 -2
  48. package/client/dist/assets/{liquid-Dl9I6gWt.js → liquid-C-VbuTr7.js} +1 -1
  49. package/client/dist/assets/loops-B1ZQCKEk.js +1 -0
  50. package/client/dist/assets/loops-Bp5yTSic.js +1 -0
  51. package/client/dist/assets/loops-BvoL_LAc.js +1 -0
  52. package/client/dist/assets/loops-BwgehdXY.js +1 -0
  53. package/client/dist/assets/loops-CMLUqUpc.js +1 -0
  54. package/client/dist/assets/loops-CW69VvTI.js +1 -0
  55. package/client/dist/assets/loops-DkV2RhEb.js +1 -0
  56. package/client/dist/assets/loops-arQ602Zm.js +1 -0
  57. package/client/dist/assets/{lspLanguageFeatures-CPlEe0NK.js → lspLanguageFeatures-BALx-vjn.js} +1 -1
  58. package/client/dist/assets/{mdx-Byl7TtzQ.js → mdx-Cs8MNCzn.js} +1 -1
  59. package/client/dist/assets/{monaco.contribution-YMAkHQcQ.js → monaco.contribution-CIvCAJs4.js} +2 -2
  60. package/client/dist/assets/nav-B5W9IMnf.js +1 -0
  61. package/client/dist/assets/nav-CCz4krPP.js +1 -0
  62. package/client/dist/assets/nav-CrG_mYRr.js +1 -0
  63. package/client/dist/assets/nav-Cw3_40fA.js +1 -0
  64. package/client/dist/assets/nav-DNDJ3H-_.js +1 -0
  65. package/client/dist/assets/nav-DVp1nEsi.js +1 -0
  66. package/client/dist/assets/nav-DbAbG5_k.js +1 -0
  67. package/client/dist/assets/nav-_0vAgZ_k.js +1 -0
  68. package/client/dist/assets/{python-jWQwT6j2.js → python-CGShLxuI.js} +1 -1
  69. package/client/dist/assets/{razor-BWS3sP-E.js → razor-CvOBQGd5.js} +1 -1
  70. package/client/dist/assets/{setup-Cpu17hJv.js → setup-BOmQ4vh2.js} +1 -1
  71. package/client/dist/assets/setup-BwO2-kuK.js +1 -0
  72. package/client/dist/assets/{setup-D-1r0uSx.js → setup-CUTNWz-0.js} +1 -1
  73. package/client/dist/assets/{setup-BHroXlke.js → setup-Cf3hjChU.js} +1 -1
  74. package/client/dist/assets/{setup-BIXsWUp1.js → setup-CsuXrbJ2.js} +1 -1
  75. package/client/dist/assets/{setup-B6egeeTM.js → setup-Df4cJOqu.js} +1 -1
  76. package/client/dist/assets/{setup-C0rVGnCy.js → setup-Do0fWx6W.js} +1 -1
  77. package/client/dist/assets/{setup-BJRdg1iE.js → setup-GlkDhBlJ.js} +1 -1
  78. package/client/dist/assets/{tsMode-BbOGOuSV.js → tsMode-04uOpiiP.js} +1 -1
  79. package/client/dist/assets/{typescript-eBtFQJLs.js → typescript-CX0CwyIZ.js} +1 -1
  80. package/client/dist/assets/upload-Dn9N1ESQ.js +1 -0
  81. package/client/dist/assets/value-Bd3Cf9Sr.js +1 -0
  82. package/client/dist/assets/{workers-BvicOoDf.js → workers-yr2u4c3C.js} +1 -1
  83. package/client/dist/assets/{xml-BJepAPyM.js → xml-C0OFlt5X.js} +1 -1
  84. package/client/dist/assets/{yaml-DabgV-eA.js → yaml-DgabaPs5.js} +1 -1
  85. package/client/dist/index.html +28 -26
  86. package/docs/getting-started.md +3 -2
  87. package/docs/guide/de/pipeline/1-rails-and-jobs.md +27 -22
  88. package/docs/guide/de/pipeline/5-the-loop-builder.md +91 -0
  89. package/docs/guide/en/pipeline/1-rails-and-jobs.md +11 -6
  90. package/docs/guide/en/pipeline/5-the-loop-builder.md +91 -0
  91. package/docs/guide/es/pipeline/1-rails-and-jobs.md +11 -6
  92. package/docs/guide/es/pipeline/5-the-loop-builder.md +91 -0
  93. package/docs/guide/fr/pipeline/1-rails-and-jobs.md +40 -35
  94. package/docs/guide/fr/pipeline/5-the-loop-builder.md +91 -0
  95. package/docs/guide/it/pipeline/1-rails-and-jobs.md +11 -6
  96. package/docs/guide/it/pipeline/5-the-loop-builder.md +91 -0
  97. package/docs/guide/ja/pipeline/1-rails-and-jobs.md +11 -6
  98. package/docs/guide/ja/pipeline/5-the-loop-builder.md +91 -0
  99. package/docs/guide/pt/pipeline/1-rails-and-jobs.md +50 -45
  100. package/docs/guide/pt/pipeline/5-the-loop-builder.md +91 -0
  101. package/docs/guide/zh/pipeline/1-rails-and-jobs.md +13 -8
  102. package/docs/guide/zh/pipeline/5-the-loop-builder.md +91 -0
  103. package/docs/internals/companion-rails-as-loops-contract.md +199 -0
  104. package/docs/internals/global-artifacts-alignment-contract.md +2 -0
  105. package/docs/running-pipelines.md +25 -14
  106. package/package.json +1 -1
  107. package/server/dist/ai-invocations.js +5 -3
  108. package/server/dist/db.js +45 -0
  109. package/server/dist/desktop-db.js +48 -0
  110. package/server/dist/desktop-router.js +10 -0
  111. package/server/dist/feature-flags.js +13 -0
  112. package/server/dist/loop-command-catalog.js +145 -0
  113. package/server/dist/loop-constants.js +82 -0
  114. package/server/dist/loop-decider.js +66 -0
  115. package/server/dist/loop-executors.js +193 -0
  116. package/server/dist/loop-factory.js +56 -0
  117. package/server/dist/loop-graph.js +144 -0
  118. package/server/dist/loop-preview.js +69 -0
  119. package/server/dist/loop-run-manager.js +407 -0
  120. package/server/dist/loop-runs-store.js +69 -0
  121. package/server/dist/loop-templates.js +137 -0
  122. package/server/dist/loops-router.js +302 -0
  123. package/server/dist/loops-store.js +137 -0
  124. package/server/dist/mobile/mobile-router.js +22 -0
  125. package/server/dist/mobile/mobile-ws.js +17 -1
  126. package/server/dist/project-registry.js +103 -1
  127. package/server/dist/project-router-jobs.js +11 -1
  128. package/server/dist/project-router-loop-runs.js +102 -0
  129. package/server/dist/project-router.js +2 -0
  130. package/server/dist/providers/claude-adapter.js +4 -0
  131. package/server/dist/providers/codex-adapter.js +21 -0
  132. package/server/dist/providers/gemini-adapter.js +4 -0
  133. package/server/dist/rails-router.js +175 -6
  134. package/server/dist/spending.js +5 -2
  135. package/server/dist/util/cli-prompt.js +6 -3
  136. package/client/dist/assets/ActivityFeedPage-B9-UAGS7.js +0 -1
  137. package/client/dist/assets/AgentsPage-Bz9rophT.js +0 -86
  138. package/client/dist/assets/AnalyticsPage-D3ynQwGd.js +0 -1
  139. package/client/dist/assets/BarChart-Cwp6VuED.js +0 -33
  140. package/client/dist/assets/CodePage-CO_TLOvi.js +0 -2
  141. package/client/dist/assets/DesktopAnalyticsPage-BMDqiAbw.js +0 -1
  142. package/client/dist/assets/DocsDialog-B-chJn-f.js +0 -11
  143. package/client/dist/assets/DocsPage-BcJwYo20.js +0 -11
  144. package/client/dist/assets/IntegrationsPage-Bd4l3L6u.js +0 -3
  145. package/client/dist/assets/JobDetailPage-Dt3jLjS4.js +0 -16
  146. package/client/dist/assets/JobsPage-DcWpJbBc.js +0 -1
  147. package/client/dist/assets/analytics-BeTyviO8.js +0 -1
  148. package/client/dist/assets/analytics-C4eEO260.js +0 -1
  149. package/client/dist/assets/analytics-C67cIA1b.js +0 -1
  150. package/client/dist/assets/analytics-CAguvW28.js +0 -1
  151. package/client/dist/assets/analytics-DBtt8Mgk.js +0 -1
  152. package/client/dist/assets/analytics-DMCto-TF.js +0 -1
  153. package/client/dist/assets/analytics-DUPtODxX.js +0 -1
  154. package/client/dist/assets/analytics-YIpQvPAc.js +0 -1
  155. package/client/dist/assets/dashboard--Ahnvfr3.js +0 -1
  156. package/client/dist/assets/dashboard-BN1C2pEh.js +0 -1
  157. package/client/dist/assets/dashboard-BZs_EzAn.js +0 -1
  158. package/client/dist/assets/dashboard-Bsw44L8_.js +0 -1
  159. package/client/dist/assets/dashboard-Bw3VECgY.js +0 -1
  160. package/client/dist/assets/dashboard-CuOshSHn.js +0 -1
  161. package/client/dist/assets/dashboard-DfouCM3_.js +0 -1
  162. package/client/dist/assets/dashboard-Pp5hwnZB.js +0 -1
  163. package/client/dist/assets/index-BEGU2TCx.js +0 -143
  164. package/client/dist/assets/index-B_Zqeoa5.css +0 -2
  165. package/client/dist/assets/nav-B05EYB0b.js +0 -1
  166. package/client/dist/assets/nav-BNGCq-0y.js +0 -1
  167. package/client/dist/assets/nav-BRInPX8a.js +0 -1
  168. package/client/dist/assets/nav-Bf87DRHD.js +0 -1
  169. package/client/dist/assets/nav-BkVzzFpc.js +0 -1
  170. package/client/dist/assets/nav-BzFLtS1W.js +0 -1
  171. package/client/dist/assets/nav-DBDbQOYn.js +0 -1
  172. package/client/dist/assets/nav-X9sVtUWC.js +0 -1
  173. package/client/dist/assets/setup-Dn2-veYO.js +0 -1
  174. /package/client/dist/assets/{abap-s65oMlhi.js → abap-CK68cr9-.js} +0 -0
  175. /package/client/dist/assets/{activity-Cyy07Tgo.js → activity-B2MvEH54.js} +0 -0
  176. /package/client/dist/assets/{activity-qFTcMyW9.js → activity-BbNkUmW4.js} +0 -0
  177. /package/client/dist/assets/{activity-DlbWCa4y.js → activity-C7VdmeGb.js} +0 -0
  178. /package/client/dist/assets/{activity-Dv6H7wEr.js → activity-Ca2XoBi-.js} +0 -0
  179. /package/client/dist/assets/{activity-C8qqEIoP.js → activity-D4dhNEwO.js} +0 -0
  180. /package/client/dist/assets/{activity-Dwq0heud.js → activity-DHjfmAyT.js} +0 -0
  181. /package/client/dist/assets/{activity-CZVM4nlJ.js → activity-Di2f50_Z.js} +0 -0
  182. /package/client/dist/assets/{activity-BqqwnH_h.js → activity-usnNa7FX.js} +0 -0
  183. /package/client/dist/assets/{addon-image-CpF0L0jM.js → addon-image-biWX2UUm.js} +0 -0
  184. /package/client/dist/assets/{addon-ligatures-hXysGZrA.js → addon-ligatures-D4EiDb5r.js} +0 -0
  185. /package/client/dist/assets/{addon-webgl-Cn1slavz.js → addon-webgl-CDCIZQ03.js} +0 -0
  186. /package/client/dist/assets/{addspec-RuL8Zd7w.js → addspec-1HEtmLv1.js} +0 -0
  187. /package/client/dist/assets/{addspec-DDvvnE6N.js → addspec-BxMn39wn.js} +0 -0
  188. /package/client/dist/assets/{addspec-xjDbYZWL.js → addspec-CNyAUGz_.js} +0 -0
  189. /package/client/dist/assets/{addspec-CIGb34PS.js → addspec-CO1xv6BL.js} +0 -0
  190. /package/client/dist/assets/{addspec-BErjOdNK.js → addspec-DAveBiCE.js} +0 -0
  191. /package/client/dist/assets/{addspec-C_3NBarY.js → addspec-DE_VfIyP.js} +0 -0
  192. /package/client/dist/assets/{addspec-B-BKlvDj.js → addspec-DfuR6EAl.js} +0 -0
  193. /package/client/dist/assets/{addspec-rmhOaH7N.js → addspec-IozoiiGs.js} +0 -0
  194. /package/client/dist/assets/{agents-BDx1RXcl.js → agents-1lhqEcXH.js} +0 -0
  195. /package/client/dist/assets/{agents-BFr3kUhK.js → agents-BYjUOrLD.js} +0 -0
  196. /package/client/dist/assets/{agents-CMxtJMLD.js → agents-CIhRSOna.js} +0 -0
  197. /package/client/dist/assets/{agents-DcxZHzNr.js → agents-CQlu1nOm.js} +0 -0
  198. /package/client/dist/assets/{agents-BlPnx-mz.js → agents-DCX9D3-Q.js} +0 -0
  199. /package/client/dist/assets/{agents-23iPejcA.js → agents-DVYpVGur.js} +0 -0
  200. /package/client/dist/assets/{agents-B_1L9xRg.js → agents-DcoUUFZl.js} +0 -0
  201. /package/client/dist/assets/{agents-G3shOewU.js → agents-Jc-CjBDB.js} +0 -0
  202. /package/client/dist/assets/{agentstudio-kk9RB7Se.js → agentstudio-1E-bag77.js} +0 -0
  203. /package/client/dist/assets/{agentstudio-Bk1eZcv4.js → agentstudio-9NbsoGXk.js} +0 -0
  204. /package/client/dist/assets/{agentstudio-Y3G0ddJ2.js → agentstudio-B-jQgYV-.js} +0 -0
  205. /package/client/dist/assets/{agentstudio-B-CMAQqy.js → agentstudio-B1y0JWY-.js} +0 -0
  206. /package/client/dist/assets/{agentstudio-DpP9caEE.js → agentstudio-Bwa72b1S.js} +0 -0
  207. /package/client/dist/assets/{agentstudio-CxlUllqI.js → agentstudio-DCTpIqOx.js} +0 -0
  208. /package/client/dist/assets/{agentstudio-DNlme601.js → agentstudio-DCyDW8sy.js} +0 -0
  209. /package/client/dist/assets/{agentstudio-ChxNuGAu.js → agentstudio-Q0Sq_jTg.js} +0 -0
  210. /package/client/dist/assets/{aiedit-DrfzQroF.js → aiedit-CGxeml8D.js} +0 -0
  211. /package/client/dist/assets/{aiedit-De0SOH6S.js → aiedit-CfBNanHE.js} +0 -0
  212. /package/client/dist/assets/{aiedit-BBCrOpHq.js → aiedit-CvSspQmE.js} +0 -0
  213. /package/client/dist/assets/{aiedit-fMltW101.js → aiedit-DSLtsYIu.js} +0 -0
  214. /package/client/dist/assets/{aiedit-BMtcGYNE.js → aiedit-DUpe7NxI.js} +0 -0
  215. /package/client/dist/assets/{aiedit-TTwzL1TS.js → aiedit-Dirqf3Bm.js} +0 -0
  216. /package/client/dist/assets/{aiedit-5ETerMK1.js → aiedit-kAU7KADg.js} +0 -0
  217. /package/client/dist/assets/{aiedit-D9ddlgkM.js → aiedit-qTGI_PGs.js} +0 -0
  218. /package/client/dist/assets/{apex-BLUBIldB.js → apex-BHHyAVNb.js} +0 -0
  219. /package/client/dist/assets/{attachments-CHaDUfjB.js → attachments-3CuBBRFu.js} +0 -0
  220. /package/client/dist/assets/{attachments-DazTVJbH.js → attachments-BLqqPGyy.js} +0 -0
  221. /package/client/dist/assets/{attachments-CCWasu-P.js → attachments-BNgH6Tcu.js} +0 -0
  222. /package/client/dist/assets/{attachments-BIsSSnHJ.js → attachments-C2JPppoP.js} +0 -0
  223. /package/client/dist/assets/{attachments-CVSAbGNl.js → attachments-CppM7de8.js} +0 -0
  224. /package/client/dist/assets/{attachments-Chg5poG1.js → attachments-D5fVsMgb.js} +0 -0
  225. /package/client/dist/assets/{attachments-Dn-JImAK.js → attachments-UJtEq892.js} +0 -0
  226. /package/client/dist/assets/{attachments-LDA9kp2X.js → attachments-pW3zm9nu.js} +0 -0
  227. /package/client/dist/assets/{azcli-DuWxh9mO.js → azcli-C56lfIEP.js} +0 -0
  228. /package/client/dist/assets/{bat-UKoTejQm.js → bat-BLamSaf-.js} +0 -0
  229. /package/client/dist/assets/{bicep-4sTT4B3D.js → bicep-lrOcEItS.js} +0 -0
  230. /package/client/dist/assets/{browser-D2Y_UAKA.js → browser-B7i7Ysc8.js} +0 -0
  231. /package/client/dist/assets/{browser-BDd1dbFa.js → browser-BVQgd1_1.js} +0 -0
  232. /package/client/dist/assets/{browser-Dxc_VIRK.js → browser-C91MQBxx.js} +0 -0
  233. /package/client/dist/assets/{browser-DH9SGVfM.js → browser-C__Vz7KR.js} +0 -0
  234. /package/client/dist/assets/{browser-lTQwcDCI.js → browser-DhwTc0ry.js} +0 -0
  235. /package/client/dist/assets/{browser-Qya9cARy.js → browser-Dpo_ug4c.js} +0 -0
  236. /package/client/dist/assets/{browser-BWSgbfdX.js → browser-DwE289IS.js} +0 -0
  237. /package/client/dist/assets/{browser-DWOVYMlg.js → browser-EFmPpkvO.js} +0 -0
  238. /package/client/dist/assets/{cameligo-CAAryRYO.js → cameligo-BK_p0bgU.js} +0 -0
  239. /package/client/dist/assets/{chat-UVVZqA57.js → chat-9RcD5Lcn.js} +0 -0
  240. /package/client/dist/assets/{chat-CUrG1eVg.js → chat-BXVx6rb5.js} +0 -0
  241. /package/client/dist/assets/{chat-CPgmgZOj.js → chat-BnJPyeUK.js} +0 -0
  242. /package/client/dist/assets/{chat-CvOOKB2s.js → chat-C5nJF6L0.js} +0 -0
  243. /package/client/dist/assets/{chat-BO9MvVID.js → chat-CE8bx6Rz.js} +0 -0
  244. /package/client/dist/assets/{chat-DIh3hr6y.js → chat-PVakYp92.js} +0 -0
  245. /package/client/dist/assets/{chat-mPn3UlMl.js → chat-UMl6H55K.js} +0 -0
  246. /package/client/dist/assets/{chat-BsbNGPW9.js → chat-mq1x9cxZ.js} +0 -0
  247. /package/client/dist/assets/{clojure-BlMERO1w.js → clojure-BQ7JOykv.js} +0 -0
  248. /package/client/dist/assets/{clsx-CnH-HMk3.js → clsx-CjLeUJfG.js} +0 -0
  249. /package/client/dist/assets/{code-BwIz8agY.js → code-BPMR8_Vp.js} +0 -0
  250. /package/client/dist/assets/{code-CDFlxUFC.js → code-BsOBm1Fb.js} +0 -0
  251. /package/client/dist/assets/{code-CY85RXZU.js → code-DtIatVES.js} +0 -0
  252. /package/client/dist/assets/{code-DtZBQTi9.js → code-Lg4awa35.js} +0 -0
  253. /package/client/dist/assets/{code-CD7yNSK0.js → code-OoLqhjoI.js} +0 -0
  254. /package/client/dist/assets/{code-D24e1Crx.js → code-aCUqkVN_.js} +0 -0
  255. /package/client/dist/assets/{code-Cp3Fdng-.js → code-eh4vO2-_.js} +0 -0
  256. /package/client/dist/assets/{code-nKa0fkm_.js → code-u9Sg9ohM.js} +0 -0
  257. /package/client/dist/assets/{coffee-Cj8D-Wl1.js → coffee-BD3DiL5s.js} +0 -0
  258. /package/client/dist/assets/{commands-t4frzhB0.js → commands-B9XEyMj-.js} +0 -0
  259. /package/client/dist/assets/{commands-BR1kDkHQ.js → commands-BI1Ta-E7.js} +0 -0
  260. /package/client/dist/assets/{commands-DWgp-8W1.js → commands-BdI0CtOL.js} +0 -0
  261. /package/client/dist/assets/{commands-ddsl1V91.js → commands-C2ewlyXo.js} +0 -0
  262. /package/client/dist/assets/{commands-IXMOKBYt.js → commands-CwypztRT.js} +0 -0
  263. /package/client/dist/assets/{commands-Cb21pDlG.js → commands-DSX3yJyr.js} +0 -0
  264. /package/client/dist/assets/{commands-B-MVT-2F.js → commands-Ic1DJDxm.js} +0 -0
  265. /package/client/dist/assets/{commands-B0yFTp7e.js → commands-yNn4IA4C.js} +0 -0
  266. /package/client/dist/assets/{common-CTEbWVZS.js → common-BhFasU8N.js} +0 -0
  267. /package/client/dist/assets/{common-CALKUpYm.js → common-CnrZ9gCZ.js} +0 -0
  268. /package/client/dist/assets/{common-B4sqsKp7.js → common-CwhHWbTD.js} +0 -0
  269. /package/client/dist/assets/{common-wA36jmj1.js → common-D9GAPxeN.js} +0 -0
  270. /package/client/dist/assets/{common-5ilvMOcH.js → common-DI4jE5y-.js} +0 -0
  271. /package/client/dist/assets/{common-DQiza2Xp.js → common-DS8zKLQT.js} +0 -0
  272. /package/client/dist/assets/{common-BKpVwUIf.js → common-DTHzyfYC.js} +0 -0
  273. /package/client/dist/assets/{common-BzEC3kJU.js → common-DY4nzbWC.js} +0 -0
  274. /package/client/dist/assets/{cpp-BPfKnaj_.js → cpp-splnCwCh.js} +0 -0
  275. /package/client/dist/assets/{csharp-gX-x5uD6.js → csharp-BCpsl-o3.js} +0 -0
  276. /package/client/dist/assets/{csp-DKGVt8SM.js → csp-tkCWrZmb.js} +0 -0
  277. /package/client/dist/assets/{css-CPMdnAVq.js → css-B_xdnReq.js} +0 -0
  278. /package/client/dist/assets/{cypher-ClMDrj9S.js → cypher-OL1zq-y3.js} +0 -0
  279. /package/client/dist/assets/{dart-C4zbzpVv.js → dart-CtTlOkrm.js} +0 -0
  280. /package/client/dist/assets/{dockerfile-D9xw73D1.js → dockerfile-DcQ5CNiW.js} +0 -0
  281. /package/client/dist/assets/{ecl-gqO8tIR9.js → ecl-DZpiDRt6.js} +0 -0
  282. /package/client/dist/assets/{editor.api2-BPnIxMjz.js → editor.api2-Cwla1nvV.js} +0 -0
  283. /package/client/dist/assets/{elixir-DSAhVF3_.js → elixir-Dzj2lqtK.js} +0 -0
  284. /package/client/dist/assets/{explore-8gMBVmkp.js → explore-4SD1DxR0.js} +0 -0
  285. /package/client/dist/assets/{explore-vdBm_Re8.js → explore-B2n8r9Ye.js} +0 -0
  286. /package/client/dist/assets/{explore-BkWTnLi3.js → explore-BxR9q8Hq.js} +0 -0
  287. /package/client/dist/assets/{explore-CQUCw9ci.js → explore-CBYU76JD.js} +0 -0
  288. /package/client/dist/assets/{explore-Dbac0ErD.js → explore-CCcPXrHg.js} +0 -0
  289. /package/client/dist/assets/{explore-DUU36tcH.js → explore-CZt4Ncpa.js} +0 -0
  290. /package/client/dist/assets/{explore-DlKgN7D-.js → explore-DQ0XEjed.js} +0 -0
  291. /package/client/dist/assets/{explore-COaUKI0t.js → explore-xQoaak6e.js} +0 -0
  292. /package/client/dist/assets/{flow9-DeQCSPOd.js → flow9-CFENCHIZ.js} +0 -0
  293. /package/client/dist/assets/{format-command-CZsy-vHq.js → format-command-CMsBTOPN.js} +0 -0
  294. /package/client/dist/assets/{fsharp-CEfaXL-S.js → fsharp-D-4bv7FB.js} +0 -0
  295. /package/client/dist/assets/{go-Xp1OkZCh.js → go-djxbTJq7.js} +0 -0
  296. /package/client/dist/assets/{graphql-BwRXrUwe.js → graphql-DHIFrkZ6.js} +0 -0
  297. /package/client/dist/assets/{hcl-u06DtVFk.js → hcl-CXVoowNb.js} +0 -0
  298. /package/client/dist/assets/{ini-AmeIpFND.js → ini-Crzc5kly.js} +0 -0
  299. /package/client/dist/assets/{integrations-B9CEpNF0.js → integrations-9qxUNhPD.js} +0 -0
  300. /package/client/dist/assets/{integrations-DWz1eU_K.js → integrations-A9f08E0o.js} +0 -0
  301. /package/client/dist/assets/{integrations-BlvAdewo.js → integrations-B17KyA-i.js} +0 -0
  302. /package/client/dist/assets/{integrations-Bw8UM9Xd.js → integrations-B46ac3sf.js} +0 -0
  303. /package/client/dist/assets/{integrations-7YyTBuU9.js → integrations-C7u7SInG.js} +0 -0
  304. /package/client/dist/assets/{integrations-DiPR8Fzp.js → integrations-DkBi-Gsh.js} +0 -0
  305. /package/client/dist/assets/{integrations-C5SxNKnG.js → integrations-DwSVdCpf.js} +0 -0
  306. /package/client/dist/assets/{integrations-CJQKMmdW.js → integrations-jJGaKwff.js} +0 -0
  307. /package/client/dist/assets/{java-CyDbRQjX.js → java-CYLaJrdE.js} +0 -0
  308. /package/client/dist/assets/{jira-DOw8bkIR.js → jira-01H1RfQj.js} +0 -0
  309. /package/client/dist/assets/{jira-DKImM1YH.js → jira-0ldfMByG.js} +0 -0
  310. /package/client/dist/assets/{jira-Fob8EGxN.js → jira-4Fin3fu7.js} +0 -0
  311. /package/client/dist/assets/{jira-xZA2lixb.js → jira-BpnpUFlb.js} +0 -0
  312. /package/client/dist/assets/{jira-C-ATCti0.js → jira-BwACz875.js} +0 -0
  313. /package/client/dist/assets/{jira-DlA-wGp-.js → jira-CBU0200d.js} +0 -0
  314. /package/client/dist/assets/{jira-D7bkKAX8.js → jira-Dht8qNck.js} +0 -0
  315. /package/client/dist/assets/{jira-CmVfRM-b.js → jira-DzIVvNmy.js} +0 -0
  316. /package/client/dist/assets/{jobs-DAF8AGy5.js → jobs-BF9raUkL.js} +0 -0
  317. /package/client/dist/assets/{jobs-D93lG6If.js → jobs-Bw4hqEro.js} +0 -0
  318. /package/client/dist/assets/{jobs-F5PGJwbW.js → jobs-DM9owF1j.js} +0 -0
  319. /package/client/dist/assets/{jobs-Db3xrsp_.js → jobs-DRInW0AE.js} +0 -0
  320. /package/client/dist/assets/{jobs-fYWWxCUV.js → jobs-DcCONARH.js} +0 -0
  321. /package/client/dist/assets/{jobs-Brp44JDd.js → jobs-Oz3UtLQn.js} +0 -0
  322. /package/client/dist/assets/{jobs-Do4Ltqdj.js → jobs-jEQidSIb.js} +0 -0
  323. /package/client/dist/assets/{jobs-BGkI19S_.js → jobs-ym72SIR9.js} +0 -0
  324. /package/client/dist/assets/{julia-BqialFRG.js → julia-BJ7tUU-T.js} +0 -0
  325. /package/client/dist/assets/{kotlin-Dzz8TWAt.js → kotlin-CeWcU3lC.js} +0 -0
  326. /package/client/dist/assets/{less-DHRJD3TR.js → less-CviwWNG4.js} +0 -0
  327. /package/client/dist/assets/{lexon-5Y3QgTmT.js → lexon-Dt28jqDS.js} +0 -0
  328. /package/client/dist/assets/{lua-sKvhfPn5.js → lua-B6AOaJSi.js} +0 -0
  329. /package/client/dist/assets/{m3-DWDVwkFG.js → m3-VnMkHJcy.js} +0 -0
  330. /package/client/dist/assets/{markdown-CD_aSBxW.js → markdown-8vKNdvIM.js} +0 -0
  331. /package/client/dist/assets/{mips-687T03hg.js → mips-DoEcuzan.js} +0 -0
  332. /package/client/dist/assets/{msdax-C1St-dIV.js → msdax-CNzvHyEp.js} +0 -0
  333. /package/client/dist/assets/{mysql-BG7r8oBS.js → mysql-Buk3vUtL.js} +0 -0
  334. /package/client/dist/assets/{objective-c-Ds1-m05L.js → objective-c-DfOmda-5.js} +0 -0
  335. /package/client/dist/assets/{pascal-BKK9FpIi.js → pascal-DoAA2yjy.js} +0 -0
  336. /package/client/dist/assets/{pascaligo-SRS3nwtO.js → pascaligo-C65Vn1fu.js} +0 -0
  337. /package/client/dist/assets/{perl-B2hTOlrF.js → perl-INlfQ_ro.js} +0 -0
  338. /package/client/dist/assets/{pgsql-DIQJYNpL.js → pgsql-DtwRzglx.js} +0 -0
  339. /package/client/dist/assets/{php-BEaVe8X2.js → php-DfHqVNet.js} +0 -0
  340. /package/client/dist/assets/{pla-oPLHpZ-Q.js → pla-D3dNVRYk.js} +0 -0
  341. /package/client/dist/assets/{postiats-D_vzrAzD.js → postiats-UKXD1rM4.js} +0 -0
  342. /package/client/dist/assets/{powerquery-BKG6w-FH.js → powerquery-zE3E3O7s.js} +0 -0
  343. /package/client/dist/assets/{powershell-B3dLhDt4.js → powershell-Df9b3zQl.js} +0 -0
  344. /package/client/dist/assets/{protobuf-DC8SGjcl.js → protobuf-BF0nUVrW.js} +0 -0
  345. /package/client/dist/assets/{pug-D5E-4fI0.js → pug-Bj6Yikv_.js} +0 -0
  346. /package/client/dist/assets/{qsharp-6vJAWv0x.js → qsharp-BI4h-raZ.js} +0 -0
  347. /package/client/dist/assets/{r-CDwsEcbM.js → r-DhpnmaXx.js} +0 -0
  348. /package/client/dist/assets/{redis-CuQbbESS.js → redis-DwYYPr4d.js} +0 -0
  349. /package/client/dist/assets/{redshift-B9e1k-qI.js → redshift-D2f3Bc-4.js} +0 -0
  350. /package/client/dist/assets/{restructuredtext-BiJ5IwaU.js → restructuredtext-Dkiif6fe.js} +0 -0
  351. /package/client/dist/assets/{ruby-B0UAHY9b.js → ruby-C6hLmHy0.js} +0 -0
  352. /package/client/dist/assets/{rust-Dg_spmFr.js → rust-X8WAGtxB.js} +0 -0
  353. /package/client/dist/assets/{sb-DjU66I8Q.js → sb-4M2la0Kr.js} +0 -0
  354. /package/client/dist/assets/{scala-qvStIdfG.js → scala-CaCXz58E.js} +0 -0
  355. /package/client/dist/assets/{scheme-FstEk5Rh.js → scheme-CMdjpmP7.js} +0 -0
  356. /package/client/dist/assets/{scss-w0U3rQLK.js → scss-QopmvPyp.js} +0 -0
  357. /package/client/dist/assets/{settings-CewqtD9V.js → settings-0m2iuTzd.js} +0 -0
  358. /package/client/dist/assets/{settings-Puea5c8v.js → settings-BA1V6KdK.js} +0 -0
  359. /package/client/dist/assets/{settings-Cx97tHs5.js → settings-BN7OusER.js} +0 -0
  360. /package/client/dist/assets/{settings-BwMQ1lx5.js → settings-Bi5FFkIw.js} +0 -0
  361. /package/client/dist/assets/{settings-EMaKBwE0.js → settings-CRJxyTNN.js} +0 -0
  362. /package/client/dist/assets/{settings-DkB9Wh-f.js → settings-DyBxunCh.js} +0 -0
  363. /package/client/dist/assets/{settings-CqCY8gZq.js → settings-fk-WlnUt.js} +0 -0
  364. /package/client/dist/assets/{settings-CyixAx0X.js → settings-iQe5Dim-.js} +0 -0
  365. /package/client/dist/assets/{shell-DJ78wREd.js → shell-CDBchS00.js} +0 -0
  366. /package/client/dist/assets/{solidity-1aGIVsdX.js → solidity-DvBfi5mC.js} +0 -0
  367. /package/client/dist/assets/{sophia-40LqcGjB.js → sophia-HSnXR17u.js} +0 -0
  368. /package/client/dist/assets/{sparql-Cz5dqG_g.js → sparql-DhG_ppup.js} +0 -0
  369. /package/client/dist/assets/{specs-DfwDeADE.js → specs-Bku8SypT.js} +0 -0
  370. /package/client/dist/assets/{specs-B4GuOzuZ.js → specs-BvsFNKTs.js} +0 -0
  371. /package/client/dist/assets/{specs-ghyBMnib.js → specs-C2wW5bq8.js} +0 -0
  372. /package/client/dist/assets/{specs-VK-zXv7x.js → specs-CUE3xSFM.js} +0 -0
  373. /package/client/dist/assets/{specs-D-Sb6dre.js → specs-C_8j4ELZ.js} +0 -0
  374. /package/client/dist/assets/{specs-C62F2CDv.js → specs-CnH7dOiF.js} +0 -0
  375. /package/client/dist/assets/{specs-DFSkAeK8.js → specs-D4ORogcT.js} +0 -0
  376. /package/client/dist/assets/{specs-BVLKe2n5.js → specs-D6Y76Bgi.js} +0 -0
  377. /package/client/dist/assets/{sql-64f62Ni4.js → sql-DHeIS-tf.js} +0 -0
  378. /package/client/dist/assets/{st-gJe2yG8J.js → st-CMWchiRU.js} +0 -0
  379. /package/client/dist/assets/{swift-C6ME22mv.js → swift-CDXvBefj.js} +0 -0
  380. /package/client/dist/assets/{systemverilog-CEWz259w.js → systemverilog-0RTukYUE.js} +0 -0
  381. /package/client/dist/assets/{tcl-CcLVIi3m.js → tcl-CbBF_Ttl.js} +0 -0
  382. /package/client/dist/assets/{terminal-CPpK58RC.js → terminal-BLh00ouM.js} +0 -0
  383. /package/client/dist/assets/{terminal-CdxkpafL.js → terminal-BTanJIt_.js} +0 -0
  384. /package/client/dist/assets/{terminal-DHIkiWcs.js → terminal-CB33IsO1.js} +0 -0
  385. /package/client/dist/assets/{terminal-Ciia0wh2.js → terminal-CR6t-Oey.js} +0 -0
  386. /package/client/dist/assets/{terminal-DY42QANg.js → terminal-CY_sPfdD.js} +0 -0
  387. /package/client/dist/assets/{terminal-C0xx0SjA.js → terminal-Cj4HzP3p.js} +0 -0
  388. /package/client/dist/assets/{terminal-BYtreaaF.js → terminal-Cz_Puz0q.js} +0 -0
  389. /package/client/dist/assets/{terminal-DoxtVdma.js → terminal-DFEH9XTQ.js} +0 -0
  390. /package/client/dist/assets/{tickets-D5MSAPe_.js → tickets-BFrVcXAI.js} +0 -0
  391. /package/client/dist/assets/{tickets-DBV3wgQZ.js → tickets-C3gG--yT.js} +0 -0
  392. /package/client/dist/assets/{tickets-r4-oNV0R.js → tickets-Cfhwbdxj.js} +0 -0
  393. /package/client/dist/assets/{tickets-RZ0LyeQe.js → tickets-DcggvP3k.js} +0 -0
  394. /package/client/dist/assets/{tickets-d1A6EOHa.js → tickets-EnP6m_Ao.js} +0 -0
  395. /package/client/dist/assets/{tickets-Q0_pONEh.js → tickets-IbYhlr-6.js} +0 -0
  396. /package/client/dist/assets/{tickets-CVJQ-vRm.js → tickets-ZS5EsDxn.js} +0 -0
  397. /package/client/dist/assets/{tickets-CG_mo-Bg.js → tickets-lJR7pWNI.js} +0 -0
  398. /package/client/dist/assets/{twig-DvsO-WjW.js → twig-DCcGd8FE.js} +0 -0
  399. /package/client/dist/assets/{typespec-Brkt3IAA.js → typespec-HTphp-Uh.js} +0 -0
  400. /package/client/dist/assets/{vb-r121Uzxt.js → vb-Cb_yUcVd.js} +0 -0
  401. /package/client/dist/assets/{wgsl-BRX8uYh4.js → wgsl-DrKU3YXj.js} +0 -0
@@ -134,6 +134,17 @@ function createMobileRouter(deps) {
134
134
  return;
135
135
  void forward(res, 'GET', `/api/projects/${encodeURIComponent(pid)}/jobs/${encodeURIComponent(jid)}`, '');
136
136
  });
137
+ // rails-as-loops reads: the global Loop catalog (for the mobile loop picker)
138
+ // and per-project loop-run state (the running surface — a loop run has no jobId
139
+ // so it can't be tailed via /jobs/:id). 404s downstream when Loops are disabled.
140
+ v1.get('/loops', (_req, res) => { void forward(res, 'GET', '/api/loops', ''); });
141
+ v1.get('/loops/factory', (_req, res) => { void forward(res, 'GET', '/api/loops/factory', ''); });
142
+ v1.get('/projects/:pid/loop-runs/:id', (req, res) => {
143
+ const pid = seg(req.params.pid), rid = seg(req.params.id);
144
+ if (!validate(res, [pid, PID_RE], [rid, JOBID_RE]))
145
+ return;
146
+ void forward(res, 'GET', `/api/projects/${encodeURIComponent(pid)}/loop-runs/${encodeURIComponent(rid)}`, '');
147
+ });
137
148
  v1.get('/projects/:pid/tickets/:tid/spending-summary', (req, res) => {
138
149
  const pid = seg(req.params.pid), tid = seg(req.params.tid);
139
150
  if (!validate(res, [pid, PID_RE], [tid, NUM_RE]))
@@ -183,6 +194,14 @@ function createMobileRouter(deps) {
183
194
  narrowed.aiEngine = b.aiEngine;
184
195
  if (typeof b.model === 'string')
185
196
  narrowed.model = b.model; // ultracode model
197
+ // rails-as-loops: let a mobile client launch a chosen Loop (factory or custom).
198
+ // Additive — the frozen bare-`mode` path is untouched (a v1 client sends no loopId).
199
+ if (typeof b.loopId === 'string')
200
+ narrowed.loopId = b.loopId;
201
+ if (b.reasoning_effort === 'low' || b.reasoning_effort === 'medium' || b.reasoning_effort === 'high')
202
+ narrowed.reasoning_effort = b.reasoning_effort;
203
+ if (typeof b.interactive === 'boolean')
204
+ narrowed.interactive = b.interactive;
186
205
  void forward(res, 'POST', `/api/projects/${encodeURIComponent(pid)}/rails/${encodeURIComponent(i)}/launch`, '', narrowed);
187
206
  });
188
207
  v1.put('/projects/:pid/rails/:i/engine', (req, res) => {
@@ -375,6 +394,9 @@ exports.MOBILE_ALLOWLIST = [
375
394
  { method: 'GET', path: '/v1/projects/:pid/tickets/:tid' },
376
395
  { method: 'GET', path: '/v1/projects/:pid/jobs' },
377
396
  { method: 'GET', path: '/v1/projects/:pid/jobs/:jid' },
397
+ { method: 'GET', path: '/v1/loops' },
398
+ { method: 'GET', path: '/v1/loops/factory' },
399
+ { method: 'GET', path: '/v1/projects/:pid/loop-runs/:id' },
378
400
  { method: 'GET', path: '/v1/projects/:pid/queue' },
379
401
  { method: 'GET', path: '/v1/projects/:pid/rails' },
380
402
  { method: 'GET', path: '/v1/projects/:pid/activity' },
@@ -27,7 +27,15 @@ function topicFor(type) {
27
27
  case 'rail.job_started':
28
28
  case 'rail.job_stopped':
29
29
  case 'rail.job_completed':
30
- case 'rail.updated': return 'rails';
30
+ case 'rail.updated':
31
+ // rails-as-loops: loop runs (no queue job) report their lifecycle via the
32
+ // loop.run_* family. Map them onto the EXISTING 'rails' topic (frozen-safe —
33
+ // the companion's subscribe set is unchanged; an old v1 client ignores the
34
+ // unknown `loop.run_*` types harmlessly, a new client mirrors the run).
35
+ case 'loop.run_started':
36
+ case 'loop.run_progress':
37
+ case 'loop.run_stopped':
38
+ case 'loop.run_completed': return 'rails';
31
39
  case 'spending.invalidated': return 'spending';
32
40
  case 'chat_stream':
33
41
  case 'chat_done':
@@ -59,6 +67,14 @@ const LEGACY_WIRE_TYPES = {
59
67
  desktop_daily_budget_exceeded: 'hub_daily_budget_exceeded',
60
68
  };
61
69
  function toMobileWire(msg) {
70
+ // SECURITY: loop.run_progress carries `reasoning` — free-text LLM output that
71
+ // the path-only `redact()` does NOT scrub for repo content. Drop it at the
72
+ // mobile boundary (forward only iteration/activeNode); it's the one field that
73
+ // would leak more than any existing rail.* frame. Forward everything else.
74
+ if (msg.type === 'loop.run_progress') {
75
+ const { reasoning: _drop, ...rest } = msg;
76
+ return rest;
77
+ }
62
78
  const legacyType = LEGACY_WIRE_TYPES[msg.type];
63
79
  if (!legacyType)
64
80
  return msg;
@@ -32,6 +32,9 @@ const workspace_resolution_1 = require("./workspace-resolution");
32
32
  const workspace_manager_1 = require("./workspace-manager");
33
33
  const ticket_store_1 = require("./ticket-store");
34
34
  const jira_sync_manager_1 = require("./jira/jira-sync-manager");
35
+ const loop_run_manager_1 = require("./loop-run-manager");
36
+ const loop_executors_1 = require("./loop-executors");
37
+ const loop_runs_store_1 = require("./loop-runs-store");
35
38
  const rails_store_1 = require("./rails-store");
36
39
  const desktop_db_1 = require("./desktop-db");
37
40
  const config_1 = require("./config");
@@ -626,7 +629,106 @@ class ProjectRegistry {
626
629
  // projects instead of a per-project profile.
627
630
  contextPool: this._browserContextPool,
628
631
  });
629
- const ctx = { project, db, queueManager, chatManager, setupManager, proposalManager, agentRefineManager, fileSummaryManager, specLauncherManager, ticketWatcher, browserCaptureManager, jiraSyncManager, broadcast: boundBroadcast, railJobs };
632
+ // ── Loops engine (the Loops feature) ──────────────────────────────────────
633
+ const railLoopRuns = new Map();
634
+ // A restart kills any in-flight loop process, so reconcile orphan 'running'
635
+ // rows to terminal — otherwise they keep the loop un-editable (isRunning 409).
636
+ try {
637
+ const orphans = (0, loop_runs_store_1.reconcileOrphanLoopRuns)(db, new Date().toISOString());
638
+ if (orphans > 0)
639
+ console.log(`[loops] reconciled ${orphans} orphan loop run(s) for ${project.slug}`);
640
+ }
641
+ catch { /* non-fatal */ }
642
+ const loopRunManager = new loop_run_manager_1.LoopRunManager(db, boundBroadcast, (0, loop_executors_1.createLoopExecutors)());
643
+ const getTicketSpec = (ticketId) => {
644
+ try {
645
+ const exec = (0, workspace_resolution_1.resolveProjectExecution)({ slug: project.slug, path: project.path });
646
+ const ticketFile = exec.relocated ? exec.ticketsPath : (0, ticket_store_1.resolveTicketStoragePath)(project.path);
647
+ const t = (0, ticket_store_1.readStore)(ticketFile).tickets[String(ticketId)];
648
+ if (!t)
649
+ return undefined;
650
+ // Expose every field a loop prompt can reference via `{{spec.*}}`.
651
+ return {
652
+ id: t.id,
653
+ title: t.title,
654
+ description: t.description,
655
+ status: t.status,
656
+ priority: t.priority,
657
+ labels: t.labels,
658
+ jira_key: t.jira_key ?? null,
659
+ jira_url: t.jira_url ?? null,
660
+ };
661
+ }
662
+ catch {
663
+ return undefined;
664
+ }
665
+ };
666
+ // Releases a finished loop run's tickets + rail slots. The engine already
667
+ // emitted loop.run_completed; this mirrors onJobFinished's ticket/rail
668
+ // release (kept separate so the critical job path is untouched).
669
+ const onLoopRunFinished = (runId, outcome) => {
670
+ const meta = railLoopRuns.get(runId);
671
+ if (!meta)
672
+ return;
673
+ railLoopRuns.delete(runId);
674
+ const ticketIds = meta.ticketIds;
675
+ if (ticketIds.length === 0)
676
+ return;
677
+ const status = outcome === 'success' ? 'completed' : outcome === 'stopped' ? 'canceled' : 'failed';
678
+ try {
679
+ const exec = (0, workspace_resolution_1.resolveProjectExecution)({ slug: project.slug, path: project.path });
680
+ const ticketFile = exec.relocated ? exec.ticketsPath : (0, ticket_store_1.resolveTicketStoragePath)(project.path);
681
+ const now = new Date().toISOString();
682
+ let changedIds = [];
683
+ const store = (0, ticket_store_1.mutateStore)(ticketFile, (s) => {
684
+ changedIds = (0, ticket_store_1.applyJobOutcomeToTickets)(s, ticketIds, status, now);
685
+ });
686
+ for (const tid of changedIds) {
687
+ const ticket = store.tickets[String(tid)];
688
+ if (!ticket)
689
+ continue;
690
+ boundBroadcast({
691
+ type: 'ticket_updated',
692
+ ticket: ticket,
693
+ projectId: project.id,
694
+ timestamp: ticket.updated_at,
695
+ });
696
+ }
697
+ try {
698
+ const needsReviewIds = ticketIds.filter((tid) => store.tickets[String(tid)]?.needs_review === true);
699
+ jiraSyncManager.onJobOutcome({ ticketIds, status, jobId: runId, costUsd: null, durationMs: null, needsReviewIds });
700
+ }
701
+ catch (err) {
702
+ console.error('[project-registry] jira loop onJobOutcome failed:', err);
703
+ }
704
+ }
705
+ catch (err) {
706
+ console.error('[project-registry] failed to apply loop outcome to tickets:', err);
707
+ }
708
+ try {
709
+ for (const rail of (0, rails_store_1.getRails)(db)) {
710
+ const remaining = rail.ticketIds.filter((id) => !ticketIds.includes(id));
711
+ if (remaining.length === rail.ticketIds.length)
712
+ continue;
713
+ (0, rails_store_1.setRailTickets)(db, rail.railIndex, remaining, rail.mode, rail.profileName, rail.aiEngine);
714
+ boundBroadcast({
715
+ type: 'rail.updated',
716
+ projectId: project.id,
717
+ railIndex: rail.railIndex,
718
+ changed: 'tickets',
719
+ ticketIds: remaining,
720
+ name: rail.name ?? null,
721
+ mode: rail.mode,
722
+ profileName: rail.profileName ?? null,
723
+ aiEngine: rail.aiEngine ?? null,
724
+ });
725
+ }
726
+ }
727
+ catch (err) {
728
+ console.error('[project-registry] failed to release rail tickets after loop run:', err);
729
+ }
730
+ };
731
+ const ctx = { project, db, queueManager, chatManager, setupManager, proposalManager, agentRefineManager, fileSummaryManager, specLauncherManager, ticketWatcher, browserCaptureManager, jiraSyncManager, broadcast: boundBroadcast, railJobs, loopRunManager, railLoopRuns, onLoopRunFinished, getTicketSpec, desktopDb: this._desktopDb };
630
732
  this._contexts.set(project.id, ctx);
631
733
  return ctx;
632
734
  }
@@ -101,8 +101,18 @@ function registerJobsRoutes(deps) {
101
101
  res.json({ model, provider, allowed, providers: project.providers });
102
102
  });
103
103
  router.delete('/:projectId/jobs/:id', (req, res) => {
104
+ const id = req.params.id;
105
+ const c = ctx(req);
106
+ // A loop run is backed by a job row but is NOT a QueueManager job — it is
107
+ // driven by the LoopRunManager. Cancel it through the engine (which settles
108
+ // it 'stopped' → finishJob('canceled') + releases the rail's tickets).
109
+ if (c.railLoopRuns?.has(id)) {
110
+ c.loopRunManager?.cancel(id);
111
+ res.json({ ok: true, status: 'canceling' });
112
+ return;
113
+ }
104
114
  try {
105
- const result = ctx(req).queueManager.cancel(req.params.id);
115
+ const result = c.queueManager.cancel(id);
106
116
  res.json({ ok: true, status: result });
107
117
  }
108
118
  catch (err) {
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerLoopRunRoutes = registerLoopRunRoutes;
4
+ const feature_flags_1 = require("./feature-flags");
5
+ const loops_store_1 = require("./loops-store");
6
+ const loop_runs_store_1 = require("./loop-runs-store");
7
+ const loop_constants_1 = require("./loop-constants");
8
+ const providers_1 = require("./providers");
9
+ const provider_selection_1 = require("./provider-selection");
10
+ const workspace_resolution_1 = require("./workspace-resolution");
11
+ const loop_command_catalog_1 = require("./loop-command-catalog");
12
+ const ids_1 = require("./ids");
13
+ const VALID_REASONING_EFFORTS = new Set(['low', 'medium', 'high']);
14
+ function registerLoopRunRoutes(deps) {
15
+ const { router, ctx } = deps;
16
+ // GET a single loop run's live/terminal state. Backs the companion's running
17
+ // surface (a loop run has no jobId, so it can't be tailed via /jobs/:id).
18
+ router.get('/:projectId/loop-runs/:id', (req, res) => {
19
+ if (!(0, feature_flags_1.isLoopsEnabled)()) {
20
+ res.status(404).json({ error: 'Not Found' });
21
+ return;
22
+ }
23
+ const c = ctx(req);
24
+ const run = (0, loop_runs_store_1.getLoopRun)(c.db, req.params.id);
25
+ if (!run || run.project_id !== c.project.id) {
26
+ res.status(404).json({ error: 'Loop run not found' });
27
+ return;
28
+ }
29
+ res.json({ loopRun: run });
30
+ });
31
+ router.post('/:projectId/loop-runs', (req, res) => {
32
+ if (!(0, feature_flags_1.isLoopsEnabled)()) {
33
+ res.status(404).json({ error: 'Not Found' });
34
+ return;
35
+ }
36
+ const c = ctx(req);
37
+ const body = req.body ?? {};
38
+ const { loopId, aiEngine, provider: providerAlias, reasoning_effort } = body;
39
+ if (typeof loopId !== 'string' || !loopId) {
40
+ res.status(400).json({ error: 'loopId is required' });
41
+ return;
42
+ }
43
+ const loop = (0, loops_store_1.getLoop)(c.desktopDb, loopId);
44
+ if (!loop) {
45
+ res.status(404).json({ error: 'Loop not found' });
46
+ return;
47
+ }
48
+ if (loop.status !== 'published') {
49
+ res.status(400).json({ error: 'Loop must be published before it can run' });
50
+ return;
51
+ }
52
+ let effort;
53
+ if (reasoning_effort !== undefined && reasoning_effort !== null) {
54
+ if (typeof reasoning_effort !== 'string' || !VALID_REASONING_EFFORTS.has(reasoning_effort)) {
55
+ res.status(400).json({ error: 'reasoning_effort must be one of: low, medium, high' });
56
+ return;
57
+ }
58
+ effort = reasoning_effort;
59
+ }
60
+ const check = (0, provider_selection_1.validateRequestedProvider)(c.project, aiEngine ?? providerAlias);
61
+ if (!check.ok) {
62
+ res.status(400).json({ error: check.error });
63
+ return;
64
+ }
65
+ const provider = check.provider;
66
+ // claude-only guard (e.g. a loop that uses {{cmd:ultracode}}).
67
+ const promptsText = loop.graph.nodes
68
+ .filter((n) => n.type === 'ai-step')
69
+ .map((n) => String(n.data?.prompt ?? ''))
70
+ .join('\n');
71
+ if ((0, loop_command_catalog_1.referencesClaudeOnlyCommand)(promptsText) && provider !== 'claude') {
72
+ res.status(400).json({ error: 'This loop uses a Claude-only command and requires the Claude provider' });
73
+ return;
74
+ }
75
+ const model = (0, providers_1.getAdapter)(provider).defaultModel();
76
+ const exec = (0, workspace_resolution_1.resolveProjectExecution)({ slug: c.project.slug, path: c.project.path });
77
+ const runId = (0, ids_1.newId)();
78
+ c.loopRunManager
79
+ .run({
80
+ runId,
81
+ loopId,
82
+ loopName: loop.name,
83
+ graph: loop.graph,
84
+ projectId: c.project.id,
85
+ cwd: exec.cwd,
86
+ repoDir: exec.relocated ? exec.repoDir : undefined,
87
+ railIndex: null,
88
+ ticketId: null,
89
+ spec: undefined,
90
+ constants: (0, loop_constants_1.loadConstantMap)(c.desktopDb),
91
+ provider,
92
+ model,
93
+ effort,
94
+ })
95
+ .then((r) => c.onLoopRunFinished(r.runId, r.outcome))
96
+ .catch((err) => {
97
+ console.error('[loop-runs] standalone run failed:', err);
98
+ c.onLoopRunFinished(runId, 'failed');
99
+ });
100
+ res.status(202).json({ loopRunId: runId });
101
+ });
102
+ }
@@ -18,6 +18,7 @@ const project_router_setup_1 = require("./project-router-setup");
18
18
  const project_router_tickets_1 = require("./project-router-tickets");
19
19
  const project_router_terminals_1 = require("./project-router-terminals");
20
20
  const project_router_settings_1 = require("./project-router-settings");
21
+ const project_router_loop_runs_1 = require("./project-router-loop-runs");
21
22
  // Re-export the spec helpers from their new home so existing importers
22
23
  // (`import { ... } from './project-router'`) keep working unchanged.
23
24
  var project_router_helpers_1 = require("./project-router-helpers");
@@ -115,5 +116,6 @@ function createProjectRouter(registry) {
115
116
  (0, project_router_tickets_1.registerTicketsRoutes)(deps);
116
117
  (0, project_router_terminals_1.registerTerminalsRoutes)(deps);
117
118
  (0, project_router_settings_1.registerSettingsRoutes)(deps);
119
+ (0, project_router_loop_runs_1.registerLoopRunRoutes)(deps);
118
120
  return router;
119
121
  }
@@ -68,6 +68,9 @@ function commonFlagsFor(opts) {
68
68
  ...COMMON_FLAGS,
69
69
  '--setting-sources',
70
70
  opts.loadUserEnv ? 'user,project,local' : 'project,local',
71
+ // Native reasoning-effort flag (claude CLI accepts low|medium|high|xhigh|max).
72
+ // Emitted only when requested; absent ⇒ the CLI's default effort.
73
+ ...(opts.reasoning_effort ? ['--effort', opts.reasoning_effort] : []),
71
74
  ];
72
75
  }
73
76
  function buildClaudeArgs(action, opts) {
@@ -313,6 +316,7 @@ exports.claudeAdapter = {
313
316
  profileEnvSupport: true,
314
317
  systemPromptArg: true,
315
318
  persistentStdin: true,
319
+ supportsReasoningEffort: true,
316
320
  },
317
321
  modelCatalog: () => CLAUDE_MODELS,
318
322
  defaultModel: () => 'sonnet',
@@ -66,6 +66,10 @@ function buildCodexArgs(action, opts) {
66
66
  args.push('exec', '--json', ...SANDBOX_FLAGS, SKIP_GIT_CHECK);
67
67
  args.push(opts.prompt);
68
68
  args.push('--model', opts.model);
69
+ // Native reasoning effort (codex 0.139+). String value is quoted to match
70
+ // the `-c key="value"` override form used elsewhere in this adapter.
71
+ if (opts.reasoning_effort)
72
+ args.push('-c', `model_reasoning_effort="${opts.reasoning_effort}"`);
69
73
  if (opts.extraArgs)
70
74
  args.push(...opts.extraArgs);
71
75
  return args;
@@ -77,6 +81,10 @@ function buildCodexArgs(action, opts) {
77
81
  args.push('exec', '--json', ...SANDBOX_FLAGS, SKIP_GIT_CHECK);
78
82
  args.push(fold(opts.systemPrompt, opts.prompt));
79
83
  args.push('--model', opts.model);
84
+ // Native reasoning effort (codex 0.139+). String value is quoted to match
85
+ // the `-c key="value"` override form used elsewhere in this adapter.
86
+ if (opts.reasoning_effort)
87
+ args.push('-c', `model_reasoning_effort="${opts.reasoning_effort}"`);
80
88
  if (opts.extraArgs)
81
89
  args.push(...opts.extraArgs);
82
90
  return args;
@@ -92,6 +100,10 @@ function buildCodexArgs(action, opts) {
92
100
  args.push(opts.sessionId);
93
101
  args.push(opts.prompt);
94
102
  args.push('--model', opts.model);
103
+ // Native reasoning effort (codex 0.139+). String value is quoted to match
104
+ // the `-c key="value"` override form used elsewhere in this adapter.
105
+ if (opts.reasoning_effort)
106
+ args.push('-c', `model_reasoning_effort="${opts.reasoning_effort}"`);
95
107
  if (opts.extraArgs)
96
108
  args.push(...opts.extraArgs);
97
109
  return args;
@@ -104,6 +116,10 @@ function buildCodexArgs(action, opts) {
104
116
  args.push(opts.sessionId);
105
117
  args.push(fold(opts.systemPrompt, opts.prompt));
106
118
  args.push('--model', opts.model);
119
+ // Native reasoning effort (codex 0.139+). String value is quoted to match
120
+ // the `-c key="value"` override form used elsewhere in this adapter.
121
+ if (opts.reasoning_effort)
122
+ args.push('-c', `model_reasoning_effort="${opts.reasoning_effort}"`);
107
123
  if (opts.extraArgs)
108
124
  args.push(...opts.extraArgs);
109
125
  return args;
@@ -123,6 +139,10 @@ function buildCodexArgs(action, opts) {
123
139
  args.push('exec', '--json', ...RAIL_SANDBOX_FLAGS, SKIP_GIT_CHECK);
124
140
  args.push(fold(opts.systemPrompt, opts.prompt));
125
141
  args.push('--model', opts.model);
142
+ // Native reasoning effort (codex 0.139+). String value is quoted to match
143
+ // the `-c key="value"` override form used elsewhere in this adapter.
144
+ if (opts.reasoning_effort)
145
+ args.push('-c', `model_reasoning_effort="${opts.reasoning_effort}"`);
126
146
  if (opts.extraArgs)
127
147
  args.push(...opts.extraArgs);
128
148
  return args;
@@ -288,6 +308,7 @@ exports.codexAdapter = {
288
308
  nativeOtelEnv: false,
289
309
  profileEnvSupport: true,
290
310
  systemPromptArg: false,
311
+ supportsReasoningEffort: true,
291
312
  },
292
313
  modelCatalog: () => CODEX_MODELS,
293
314
  defaultModel: () => 'gpt-5.5',
@@ -229,6 +229,10 @@ exports.geminiAdapter = {
229
229
  nativeOtelEnv: true,
230
230
  profileEnvSupport: true,
231
231
  systemPromptArg: false,
232
+ // gemini-cli has NO per-invocation reasoning-effort flag (thinking levels
233
+ // require settings.json `thinkingConfig` + custom model aliases, not a
234
+ // per-spawn arg), so effort is not offered and the adapter ignores it.
235
+ supportsReasoningEffort: false,
232
236
  },
233
237
  modelCatalog: () => GEMINI_MODELS,
234
238
  defaultModel: () => 'gemini-3.5-flash',
@@ -6,10 +6,21 @@ const rails_store_1 = require("./rails-store");
6
6
  const queue_manager_1 = require("./queue-manager");
7
7
  const provider_selection_1 = require("./provider-selection");
8
8
  const feature_flags_1 = require("./feature-flags");
9
- const VALID_MODES = new Set(['implement', 'batch-implement', 'ultracode']);
9
+ const loops_store_1 = require("./loops-store");
10
+ const loop_runs_store_1 = require("./loop-runs-store");
11
+ const providers_1 = require("./providers");
12
+ const workspace_resolution_1 = require("./workspace-resolution");
13
+ const loop_factory_1 = require("./loop-factory");
14
+ const loop_constants_1 = require("./loop-constants");
15
+ const loop_command_catalog_1 = require("./loop-command-catalog");
16
+ const ids_1 = require("./ids");
17
+ const VALID_MODES = new Set(['implement', 'batch-implement', 'ultracode', 'loop']);
10
18
  // Models the ultracode picker exposes (Claude aliases). Mirrors the client
11
19
  // RailModelSelector options and the project-router orchestrator-model allow-list.
12
20
  const VALID_ULTRACODE_MODELS = new Set(['haiku', 'sonnet', 'opus']);
21
+ // Reasoning-effort tiers a loop launch may request (mirrors the client selector
22
+ // + the provider adapters' supported values).
23
+ const VALID_REASONING_EFFORTS = new Set(['low', 'medium', 'high']);
13
24
  function createRailsRouter() {
14
25
  const router = (0, express_1.Router)({ mergeParams: true });
15
26
  function ctx(req) {
@@ -46,7 +57,19 @@ function createRailsRouter() {
46
57
  for (const [jobId, meta] of c.railJobs.entries()) {
47
58
  activeJobs[meta.railIndex] = { jobId, mode: meta.mode };
48
59
  }
49
- res.json({ rails, activeJobs });
60
+ // Active loop runs (Loops mode) so clients can reconcile a 'running' rail
61
+ // that's executing a loop rather than a queue job. Enriched from the
62
+ // loop_runs row with the loop identity + resolved provider/model so a
63
+ // mirror (e.g. the mobile companion) can label the run instead of showing a
64
+ // stale `mode`/null engine — these never land on the rail row itself.
65
+ const activeLoopRuns = {};
66
+ for (const [runId, meta] of c.railLoopRuns.entries()) {
67
+ const run = (0, loop_runs_store_1.getLoopRun)(c.db, runId);
68
+ activeLoopRuns[meta.railIndex] = run
69
+ ? { loopRunId: runId, loopId: run.loop_id, loopName: run.loop_name, provider: run.provider, model: run.model, iteration: run.iteration_count }
70
+ : { loopRunId: runId };
71
+ }
72
+ res.json({ rails, activeJobs, activeLoopRuns });
50
73
  }
51
74
  catch (err) {
52
75
  console.error('[rails-router] get rails error:', err);
@@ -190,9 +213,26 @@ function createRailsRouter() {
190
213
  res.status(400).json({ error: 'Invalid rail index' });
191
214
  return;
192
215
  }
193
- const { mode = 'implement', profileName, aiEngine, model, interactive } = req.body ?? {};
216
+ let { mode = 'implement' } = req.body ?? {};
217
+ const { profileName, aiEngine, model, interactive, loopId, reasoning_effort } = req.body ?? {};
218
+ // rails-as-loops: a FACTORY loop id (`factory:implement` etc.) maps to its
219
+ // legacy mode and runs through the EXISTING engine (QueueManager) — the rail
220
+ // "picks a Loop", the plumbing is unchanged. A CUSTOM loop id keeps mode='loop'
221
+ // (LoopRunManager). Legacy callers that still send a bare `mode` are unchanged.
222
+ if (typeof loopId === 'string' && (0, loop_factory_1.isFactoryLoopId)(loopId)) {
223
+ const fmode = (0, loop_factory_1.factoryLoopMode)(loopId);
224
+ if (!fmode) {
225
+ res.status(404).json({ error: 'Factory loop not found' });
226
+ return;
227
+ }
228
+ mode = fmode;
229
+ }
194
230
  if (!VALID_MODES.has(mode)) {
195
- res.status(400).json({ error: 'mode must be "implement", "batch-implement" or "ultracode"' });
231
+ res.status(400).json({ error: 'mode must be "implement", "batch-implement", "ultracode" or "loop"' });
232
+ return;
233
+ }
234
+ if (mode === 'loop' && !(0, feature_flags_1.isLoopsEnabled)()) {
235
+ res.status(403).json({ error: 'Loops are disabled on this server' });
196
236
  return;
197
237
  }
198
238
  // Ultracode model picker: optional, validated against the allow-list.
@@ -262,6 +302,113 @@ function createRailsRouter() {
262
302
  resolvedProfile = undefined; // fall through to QueueManager default resolution
263
303
  }
264
304
  try {
305
+ // Loop mode: run a published loop (the Loops feature) against each spec,
306
+ // one app-driven LoopRun per ticket (mirrors ultracode's per-ticket model).
307
+ // Drives raw prompts via the LoopRunManager — NO queue job, NO slash command.
308
+ // rails-as-loops: a chosen loop (factory OR custom) runs through the
309
+ // LoopRunManager when Loops are enabled — so factory loops get their
310
+ // autonomous verify→fix loop too. (Loops off / no loopId → the legacy
311
+ // bare-mode QueueManager path below; `mode` is the derived factory mode.)
312
+ if ((0, feature_flags_1.isLoopsEnabled)() && typeof loopId === 'string' && loopId) {
313
+ let loopGraph;
314
+ let loopName;
315
+ if ((0, loop_factory_1.isFactoryLoopId)(loopId)) {
316
+ const f = (0, loop_factory_1.getFactoryLoop)(loopId);
317
+ if (!f) {
318
+ res.status(404).json({ error: 'Factory loop not found' });
319
+ return;
320
+ }
321
+ loopGraph = f.graph;
322
+ loopName = f.name;
323
+ }
324
+ else {
325
+ const loop = (0, loops_store_1.getLoop)(c.desktopDb, loopId);
326
+ if (!loop) {
327
+ res.status(404).json({ error: 'Loop not found' });
328
+ return;
329
+ }
330
+ if (loop.status !== 'published') {
331
+ res.status(400).json({ error: 'Loop must be published before it can run on a rail' });
332
+ return;
333
+ }
334
+ loopGraph = loop.graph;
335
+ loopName = loop.name;
336
+ }
337
+ let effort;
338
+ if (reasoning_effort !== undefined && reasoning_effort !== null) {
339
+ if (typeof reasoning_effort !== 'string' || !VALID_REASONING_EFFORTS.has(reasoning_effort)) {
340
+ res.status(400).json({ error: 'reasoning_effort must be one of: low, medium, high' });
341
+ return;
342
+ }
343
+ effort = reasoning_effort;
344
+ }
345
+ const loopProvider = railProvider ?? c.project.provider ?? 'claude';
346
+ const loopModel = typeof model === 'string' && model ? model : (0, providers_1.getAdapter)(loopProvider).defaultModel();
347
+ // The loop's command(s) declare ticket scope + claude-only-ness.
348
+ const promptsText = loopGraph.nodes
349
+ .filter((n) => n.type === 'ai-step')
350
+ .map((n) => String(n.data?.prompt ?? ''))
351
+ .join('\n');
352
+ if ((0, loop_command_catalog_1.referencesClaudeOnlyCommand)(promptsText) && loopProvider !== 'claude') {
353
+ res.status(400).json({ error: 'This loop uses a Claude-only command and requires the Claude provider' });
354
+ return;
355
+ }
356
+ const scope = (0, loop_command_catalog_1.dominantTicketScope)(promptsText);
357
+ // Spawn from the SAME cwd a rail uses (workspace when relocated, else the
358
+ // repo) so native `{{cmd:*}}` slash commands resolve — and surface the repo
359
+ // via SPECRAILS_REPO_DIR + `--add-dir` exactly like QueueManager.
360
+ const loopExec = (0, workspace_resolution_1.resolveProjectExecution)({ slug: c.project.slug, path: c.project.path });
361
+ const loopRunIds = [];
362
+ const launchLoopRun = (runId, ticketIds, spec) => {
363
+ c.railLoopRuns.set(runId, { railIndex, ticketIds });
364
+ c.loopRunManager
365
+ .run({
366
+ runId,
367
+ loopId,
368
+ loopName,
369
+ graph: loopGraph,
370
+ projectId: c.project.id,
371
+ cwd: loopExec.cwd,
372
+ repoDir: loopExec.relocated ? loopExec.repoDir : undefined,
373
+ railIndex,
374
+ ticketId: ticketIds[0],
375
+ spec: spec ? { ...spec, ticketIds } : { ticketIds },
376
+ constants: (0, loop_constants_1.loadConstantMap)(c.desktopDb),
377
+ provider: loopProvider,
378
+ model: loopModel,
379
+ effort,
380
+ })
381
+ .then((r) => c.onLoopRunFinished(r.runId, r.outcome))
382
+ .catch((err) => {
383
+ console.error('[rails-router] loop run failed:', err);
384
+ c.onLoopRunFinished(runId, 'failed');
385
+ });
386
+ loopRunIds.push(runId);
387
+ try {
388
+ c.jiraSyncManager.onRailLaunch(ticketIds, runId);
389
+ }
390
+ catch { /* non-fatal */ }
391
+ };
392
+ if (scope === 'all') {
393
+ // ONE run over ALL the rail's tickets ({{spec.ids}} = #1 #2 #3).
394
+ const allIds = [...rail.ticketIds];
395
+ launchLoopRun((0, ids_1.newId)(), allIds, c.getTicketSpec(allIds[0]));
396
+ }
397
+ else {
398
+ // One run per ticket.
399
+ for (const ticketId of rail.ticketIds) {
400
+ launchLoopRun((0, ids_1.newId)(), [ticketId], c.getTicketSpec(ticketId));
401
+ }
402
+ }
403
+ res.status(202).json({ loopRunIds, railIndex, mode });
404
+ return;
405
+ }
406
+ // A custom-loop launch (mode='loop') with no loopId is invalid — there is
407
+ // no factory loop to fall back to (only implement/batch/ultracode do).
408
+ if (mode === 'loop') {
409
+ res.status(400).json({ error: 'loopId is required for loop mode' });
410
+ return;
411
+ }
265
412
  let jobId;
266
413
  if (mode === 'ultracode') {
267
414
  // Ultracode launches ONE independent Claude job per ticket — each gets
@@ -355,7 +502,10 @@ function createRailsRouter() {
355
502
  const targetJobIds = Array.from(c.railJobs.entries())
356
503
  .filter(([, meta]) => meta.railIndex === railIndex)
357
504
  .map(([jobId]) => jobId);
358
- if (targetJobIds.length === 0) {
505
+ const targetLoopRunIds = Array.from(c.railLoopRuns.entries())
506
+ .filter(([, meta]) => meta.railIndex === railIndex)
507
+ .map(([runId]) => runId);
508
+ if (targetJobIds.length === 0 && targetLoopRunIds.length === 0) {
359
509
  res.status(404).json({ error: 'No active rail job found for this rail' });
360
510
  return;
361
511
  }
@@ -382,7 +532,26 @@ function createRailsRouter() {
382
532
  };
383
533
  c.broadcast(stopMsg);
384
534
  }
385
- res.json({ ok: true, jobIds: targetJobIds, canceled: canceledCount });
535
+ // Loop runs (Loops mode): request cancellation. The engine settles 'stopped'
536
+ // at the next node boundary and onLoopRunFinished releases the rail's tickets
537
+ // (so we deliberately do NOT delete railLoopRuns here).
538
+ for (const runId of targetLoopRunIds) {
539
+ try {
540
+ c.loopRunManager.cancel(runId);
541
+ canceledCount++;
542
+ }
543
+ catch (err) {
544
+ console.warn(`[rails-router] stop: loop cancel(${runId}) failed: ${err.message}`);
545
+ }
546
+ const stopMsg = {
547
+ type: 'loop.run_stopped',
548
+ projectId: c.project.id,
549
+ loopRunId: runId,
550
+ railIndex,
551
+ };
552
+ c.broadcast(stopMsg);
553
+ }
554
+ res.json({ ok: true, jobIds: targetJobIds, loopRunIds: targetLoopRunIds, canceled: canceledCount });
386
555
  });
387
556
  return router;
388
557
  }