specrails-desktop 2.36.0 → 2.42.2

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 (510) hide show
  1. package/README.md +94 -247
  2. package/client/dist/assets/ActivityFeedPage-DoEMumQS.js +1 -0
  3. package/client/dist/assets/AgentBrowserCapture-DIh4K0gh.js +1 -0
  4. package/client/dist/assets/AgentModeAnalyticsPane-mbq4WEoO.js +2 -0
  5. package/client/dist/assets/AgentModeCodePane-CKh7zuA4.js +2 -0
  6. package/client/dist/assets/AgentModeJobsPane-BDokPiLJ.js +1 -0
  7. package/client/dist/assets/AgentsPage-BFVAZbBj.js +87 -0
  8. package/client/dist/assets/AnalyticsPage-3Y9LuWwz.js +1 -0
  9. package/client/dist/assets/CodePage-C9KyuV_C.js +3 -0
  10. package/client/dist/assets/DesktopAnalyticsPage-Bhi54bXQ.js +1 -0
  11. package/client/dist/assets/{DocsDialog-C76VdBJC.js → DocsDialog-Dj6mt4ga.js} +2 -2
  12. package/client/dist/assets/{DocsPage-ziMlj6B6.js → DocsPage-24yWglNI.js} +1 -1
  13. package/client/dist/assets/ExportDropdown-ophapHrT.js +1 -0
  14. package/client/dist/assets/InteractiveJobComposer-C1ucmxQh.js +19 -0
  15. package/client/dist/assets/JobDetailModal-DNWmgVHD.js +1 -0
  16. package/client/dist/assets/JobDetailPage-BcajZyuu.js +1 -0
  17. package/client/dist/assets/JobsPage-B9nTZw6e.js +1 -0
  18. package/client/dist/assets/LoopBuilderPage-Oc48iOFW.js +7 -0
  19. package/client/dist/assets/LoopPreviewModal-ae0Fr6SU.js +1 -0
  20. package/client/dist/assets/LoopsPage-CUEqzUBI.js +1 -0
  21. package/client/dist/assets/MinimizedChatsContext-Deg39peL.js +1 -0
  22. package/client/dist/assets/PluginsPage-C1xtARIx.js +2 -0
  23. package/client/dist/assets/ProjectSettingsDialog-JZzrXFBN.js +1 -0
  24. package/client/dist/assets/RepositoryDeliveries-CSDsiVUz.js +5 -0
  25. package/client/dist/assets/RepositoryScopeSelector-D-RCaknl.js +1 -0
  26. package/client/dist/assets/ReviewPacketPage-x5EfGTiL.js +1 -0
  27. package/client/dist/assets/TemplatePreviewModal-BSyuZxGq.js +1 -0
  28. package/client/dist/assets/TicketDetailModalContext-DqyF0so-.js +14 -0
  29. package/client/dist/assets/Trans-SnMX4MhW.js +1 -0
  30. package/client/dist/assets/agent-BdT0SB1D.js +1 -0
  31. package/client/dist/assets/agent-Bh9K7iEt.js +1 -0
  32. package/client/dist/assets/agent-C4ibdFk9.js +1 -0
  33. package/client/dist/assets/agent-CSFY22Zo.js +1 -0
  34. package/client/dist/assets/agent-DAnqxUn8.js +1 -0
  35. package/client/dist/assets/agent-DTQiu0Mi.js +1 -0
  36. package/client/dist/assets/agent-T54WuR_Y.js +1 -0
  37. package/client/dist/assets/agent-i75WDZOA.js +1 -0
  38. package/client/dist/assets/browser-05vRDg_y.js +1 -0
  39. package/client/dist/assets/browser-Bulb-cB1.js +1 -0
  40. package/client/dist/assets/browser-C7f-Jsg1.js +1 -0
  41. package/client/dist/assets/browser-CG0PE1j_.js +1 -0
  42. package/client/dist/assets/browser-CMzaqAjo.js +1 -0
  43. package/client/dist/assets/browser-DMajYdNX.js +1 -0
  44. package/client/dist/assets/browser-DxcOHrdP.js +1 -0
  45. package/client/dist/assets/browser-ObE7xVo3.js +1 -0
  46. package/client/dist/assets/builder-765NeYTp.js +1 -0
  47. package/client/dist/assets/builder-8MG3V6ea.js +1 -0
  48. package/client/dist/assets/builder-B-1UBs5K.js +1 -0
  49. package/client/dist/assets/builder-BTiGnPT0.js +1 -0
  50. package/client/dist/assets/builder-Cx9Rz-7u.js +1 -0
  51. package/client/dist/assets/builder-DKFMELBl.js +1 -0
  52. package/client/dist/assets/builder-OcmKrC0h.js +1 -0
  53. package/client/dist/assets/builder-P9qVBXFZ.js +1 -0
  54. package/client/dist/assets/{button-CJqFnUOL.js → button-DCBqEaZl.js} +1 -1
  55. package/client/dist/assets/{code-CkUuW4m9.js → code-B37GRg9C.js} +1 -1
  56. package/client/dist/assets/{code-B0O8-p8k.js → code-BBOEZbf9.js} +1 -1
  57. package/client/dist/assets/{code-B1VLZDhS.js → code-BRm-jcyx.js} +1 -1
  58. package/client/dist/assets/{code-Bpj4Ddml.js → code-CF7w62CR.js} +1 -1
  59. package/client/dist/assets/{code-BykBZ5-e.js → code-CLuvJt2r.js} +1 -1
  60. package/client/dist/assets/{code-B86IMAfz.js → code-D0RCm59f.js} +1 -1
  61. package/client/dist/assets/code-DaZ-ZwCT.js +1 -0
  62. package/client/dist/assets/{code-cAqaqMTx.js → code-iIHkGaCL.js} +1 -1
  63. package/client/dist/assets/commands-0XjAftla.js +1 -0
  64. package/client/dist/assets/commands-BAWSYHjQ.js +1 -0
  65. package/client/dist/assets/commands-BBwowgiE.js +1 -0
  66. package/client/dist/assets/commands-BacjIZLN.js +1 -0
  67. package/client/dist/assets/commands-Bs62IAZ3.js +1 -0
  68. package/client/dist/assets/commands-CLPdh-Hh.js +1 -0
  69. package/client/dist/assets/commands-DlbFzgrp.js +1 -0
  70. package/client/dist/assets/commands-LjPSsi5F.js +1 -0
  71. package/client/dist/assets/common-B5AZNYMH.js +1 -0
  72. package/client/dist/assets/common-BFZ0c_vN.js +1 -0
  73. package/client/dist/assets/common-BrlwEnL7.js +1 -0
  74. package/client/dist/assets/common-BttQx3z4.js +1 -0
  75. package/client/dist/assets/common-CM65k_-e.js +1 -0
  76. package/client/dist/assets/common-D4mvFFlu.js +1 -0
  77. package/client/dist/assets/common-TZ9Ny96S.js +1 -0
  78. package/client/dist/assets/common-nFpSF4GC.js +1 -0
  79. package/client/dist/assets/dashboard-B14uYXkG.js +1 -0
  80. package/client/dist/assets/dashboard-BK_mez-z.js +1 -0
  81. package/client/dist/assets/dashboard-BfBir_6o.js +1 -0
  82. package/client/dist/assets/dashboard-Bt0cCzeG.js +1 -0
  83. package/client/dist/assets/dashboard-BwkjxI6H.js +1 -0
  84. package/client/dist/assets/{dashboard-C8WOEiPu.js → dashboard-C3bDKf7s.js} +1 -1
  85. package/client/dist/assets/dashboard-CdvjGfog.js +1 -0
  86. package/client/dist/assets/dashboard-fRrP0Kv0.js +1 -0
  87. package/client/dist/assets/dialog-SRw01_QU.js +45 -0
  88. package/client/dist/assets/dist-js-CJ_2XX1Y.js +1 -0
  89. package/client/dist/assets/formatDistanceToNow-DSj5DprF.js +1 -0
  90. package/client/dist/assets/getTimezoneOffsetInMilliseconds-YhD8rIEs.js +1 -0
  91. package/client/dist/assets/index-DwAmL7m5.css +2 -0
  92. package/client/dist/assets/index-EzrkUb8B.js +76 -0
  93. package/client/dist/assets/{jira-api-B1-MUehg.js → jira-api-CSCqGr-Q.js} +1 -1
  94. package/client/dist/assets/{jobs-CN_zv4Gr.js → jobs-B8rrzfyZ.js} +1 -1
  95. package/client/dist/assets/{jobs-DUlWSYq2.js → jobs-BS6RTurf.js} +1 -1
  96. package/client/dist/assets/{jobs-Bpks6doU.js → jobs-BTT0Bsks.js} +1 -1
  97. package/client/dist/assets/{jobs-Bgq-PZkC.js → jobs-CezU69AE.js} +1 -1
  98. package/client/dist/assets/{jobs-SwYS2M-B.js → jobs-ClFJae0s.js} +1 -1
  99. package/client/dist/assets/{jobs-BAls3bWq.js → jobs-DJSFIFVy.js} +1 -1
  100. package/client/dist/assets/{jobs-DhxKgWFq.js → jobs-DhgGr8vm.js} +1 -1
  101. package/client/dist/assets/{jobs-B_TeSQxb.js → jobs-DzMjUcaN.js} +1 -1
  102. package/client/dist/assets/mcp-6rMoZfEO.js +1 -0
  103. package/client/dist/assets/mcp-BD8sL5mP.js +1 -0
  104. package/client/dist/assets/mcp-BkcA3I5s.js +1 -0
  105. package/client/dist/assets/mcp-CCpSelOh.js +1 -0
  106. package/client/dist/assets/mcp-CUAN6ORK.js +1 -0
  107. package/client/dist/assets/mcp-CqbRxRTE.js +1 -0
  108. package/client/dist/assets/mcp-DP2YfVhO.js +1 -0
  109. package/client/dist/assets/mcp-DUVQamDz.js +1 -0
  110. package/client/dist/assets/narration-B-ynWTpn.js +1 -0
  111. package/client/dist/assets/narration-Bj_laZwg.js +1 -0
  112. package/client/dist/assets/narration-BxHpmMv_.js +1 -0
  113. package/client/dist/assets/narration-C1Px0-0j.js +1 -0
  114. package/client/dist/assets/narration-CAXhGbzM.js +1 -0
  115. package/client/dist/assets/narration-DQ1l_Kq6.js +1 -0
  116. package/client/dist/assets/narration-XISXwqZd.js +1 -0
  117. package/client/dist/assets/narration-kwzfglQo.js +1 -0
  118. package/client/dist/assets/nav-1-Hh2kOz.js +1 -0
  119. package/client/dist/assets/nav-B34QkESm.js +1 -0
  120. package/client/dist/assets/nav-Bap_UXAq.js +1 -0
  121. package/client/dist/assets/{nav-pYuXq7sf.js → nav-BwxXIB24.js} +1 -1
  122. package/client/dist/assets/nav-Ch-zHBG5.js +1 -0
  123. package/client/dist/assets/nav-CygC_WN-.js +1 -0
  124. package/client/dist/assets/{nav-iwjCg5FR.js → nav-KnnLQtOQ.js} +1 -1
  125. package/client/dist/assets/nav-m00uxYUG.js +1 -0
  126. package/client/dist/assets/packet-BQ2tXfJI.js +1 -0
  127. package/client/dist/assets/packet-BdpOJ1Cy.js +1 -0
  128. package/client/dist/assets/packet-C3Ievb9R.js +1 -0
  129. package/client/dist/assets/packet-C4zJIG0Y.js +1 -0
  130. package/client/dist/assets/packet-Cx-zRuWn.js +1 -0
  131. package/client/dist/assets/packet-DUTGX52M.js +1 -0
  132. package/client/dist/assets/packet-DhsKtUPB.js +1 -0
  133. package/client/dist/assets/packet-z3LJ8Zyy.js +1 -0
  134. package/client/dist/assets/project-repositories-DYhTvwQ1.js +1 -0
  135. package/client/dist/assets/provider-capabilities-B-wOHfLP.js +1 -0
  136. package/client/dist/assets/settings-BBt2xtDQ.js +1 -0
  137. package/client/dist/assets/settings-BJB9RKJj.js +1 -0
  138. package/client/dist/assets/settings-CHwQR2rB.js +1 -0
  139. package/client/dist/assets/settings-CrMi3kSi.js +1 -0
  140. package/client/dist/assets/settings-DKyhDGD1.js +1 -0
  141. package/client/dist/assets/settings-Deupji9c.js +1 -0
  142. package/client/dist/assets/settings-DqRMmwAM.js +1 -0
  143. package/client/dist/assets/settings-J4rj6vo-.js +1 -0
  144. package/client/dist/assets/setup-2xGgu4Sk.js +1 -0
  145. package/client/dist/assets/setup-Ba11l2JA.js +1 -0
  146. package/client/dist/assets/setup-BzW4kMok.js +1 -0
  147. package/client/dist/assets/setup-COzwYhkJ.js +1 -0
  148. package/client/dist/assets/setup-CRVULhtF.js +1 -0
  149. package/client/dist/assets/setup-DgvlxwpE.js +1 -0
  150. package/client/dist/assets/setup-Dw_wYeEY.js +1 -0
  151. package/client/dist/assets/setup-ZPtEeJPW.js +1 -0
  152. package/client/dist/assets/spending-BqbdPm_4.js +1 -0
  153. package/client/dist/assets/square-Ci-PFcFe.js +1 -0
  154. package/client/dist/assets/{terminal-BFRZnMPP.js → terminal-BAzpHtpY.js} +1 -1
  155. package/client/dist/assets/{terminal-IahE4LW9.js → terminal-C7dHeR3x.js} +1 -1
  156. package/client/dist/assets/{terminal-tITa81iz.js → terminal-CMla5Qsz.js} +1 -1
  157. package/client/dist/assets/{terminal-BZiIu3rf.js → terminal-CdNXH-n9.js} +1 -1
  158. package/client/dist/assets/{terminal-ScjyXNxQ.js → terminal-Cw5bZsmV.js} +1 -1
  159. package/client/dist/assets/{terminal-C2NAd7BW.js → terminal-DFUP4eNv.js} +1 -1
  160. package/client/dist/assets/{terminal-d7GadXbN.js → terminal-DGqljeK_.js} +1 -1
  161. package/client/dist/assets/{terminal-QpXZU2Bn.js → terminal-cU70NodO.js} +1 -1
  162. package/client/dist/assets/useDesktop-RSMTyy7V.js +1 -0
  163. package/client/dist/assets/useSharedWebSocket-BOdS5_Iz.js +2 -0
  164. package/client/dist/index.html +34 -35
  165. package/docs/README.md +3 -0
  166. package/docs/agent-live-steering.md +57 -0
  167. package/docs/ci-cd.md +57 -0
  168. package/docs/code-explorer.md +48 -0
  169. package/docs/codex.md +12 -1
  170. package/docs/features/detachable-mission-windows.md +59 -0
  171. package/docs/guide/de/integrations/5-mcp-server.md +13 -9
  172. package/docs/guide/de/integrations/6-agent-chat.md +1 -0
  173. package/docs/guide/en/integrations/5-mcp-server.md +13 -9
  174. package/docs/guide/en/integrations/6-agent-chat.md +1 -0
  175. package/docs/guide/es/integrations/5-mcp-server.md +13 -9
  176. package/docs/guide/es/integrations/6-agent-chat.md +1 -0
  177. package/docs/guide/fr/integrations/5-mcp-server.md +13 -9
  178. package/docs/guide/fr/integrations/6-agent-chat.md +1 -0
  179. package/docs/guide/it/integrations/5-mcp-server.md +13 -9
  180. package/docs/guide/it/integrations/6-agent-chat.md +1 -0
  181. package/docs/guide/ja/integrations/5-mcp-server.md +13 -9
  182. package/docs/guide/ja/integrations/6-agent-chat.md +1 -0
  183. package/docs/guide/pt/integrations/5-mcp-server.md +13 -9
  184. package/docs/guide/pt/integrations/6-agent-chat.md +1 -0
  185. package/docs/guide/zh/integrations/5-mcp-server.md +13 -9
  186. package/docs/guide/zh/integrations/6-agent-chat.md +1 -0
  187. package/docs/internals/README.md +1 -0
  188. package/docs/internals/browser-capture-performance.md +55 -3
  189. package/docs/internals/browser-login-popups.md +45 -0
  190. package/docs/internals/browser-native-retina-audit.md +39 -0
  191. package/docs/internals/ci-cd-audit.md +39 -0
  192. package/docs/internals/core-runtime-updates.md +68 -0
  193. package/docs/internals/embedded-browser-native-webview-evaluation.md +2 -0
  194. package/docs/internals/implementation-reliability-audit.md +93 -0
  195. package/docs/internals/interactive-jobs.md +110 -6
  196. package/docs/internals/mcp-mission-audit.md +76 -0
  197. package/docs/internals/project-builder.md +404 -42
  198. package/docs/internals/safe-pr-review-flow.md +52 -10
  199. package/docs/internals/startup-recovery-audit.md +58 -0
  200. package/docs/mcp.md +74 -41
  201. package/docs/mission-processes.md +59 -0
  202. package/docs/multi-repo-projects.md +51 -0
  203. package/docs/platforms/windows-parity.md +70 -0
  204. package/docs/platforms/windows.md +13 -11
  205. package/docs/running-pipelines.md +14 -0
  206. package/mcp-bridge/dist/specrails-mcp.js +7186 -0
  207. package/package.json +9 -4
  208. package/server/dist/agent-chat-manager.js +537 -68
  209. package/server/dist/agent-chat-router.js +87 -6
  210. package/server/dist/agent-context-resolver.js +129 -25
  211. package/server/dist/agent-defaults.js +19 -11
  212. package/server/dist/agent-input-store.js +206 -0
  213. package/server/dist/agent-mcp-config.js +54 -15
  214. package/server/dist/agent-operator-prompt.js +255 -65
  215. package/server/dist/agent-spec-framing.js +202 -0
  216. package/server/dist/agent-steering.js +218 -0
  217. package/server/dist/agent-store.js +153 -0
  218. package/server/dist/api-not-found.js +8 -0
  219. package/server/dist/background-process-control.js +164 -0
  220. package/server/dist/background-process-service.js +61 -0
  221. package/server/dist/background-process-store.js +242 -0
  222. package/server/dist/background-windows-bootstrap.js +238 -0
  223. package/server/dist/blueprint-chat-manager.js +476 -62
  224. package/server/dist/blueprint-commit.js +21 -1
  225. package/server/dist/blueprint-draft-parser.js +122 -17
  226. package/server/dist/blueprint-generation.js +238 -0
  227. package/server/dist/blueprint-operator-prompt.js +91 -63
  228. package/server/dist/blueprint-router.js +55 -3
  229. package/server/dist/blueprint-spec-fixtures.js +85 -0
  230. package/server/dist/blueprint-spec-quality.js +57 -16
  231. package/server/dist/blueprint-store.js +92 -1
  232. package/server/dist/browser-capture-manager.js +86 -30
  233. package/server/dist/browser-playwright.js +168 -45
  234. package/server/dist/browser-viewport.js +26 -0
  235. package/server/dist/chat-manager.js +23 -15
  236. package/server/dist/chromium-archive.cjs +83 -0
  237. package/server/dist/chromium-resolver.js +72 -64
  238. package/server/dist/code-activity.js +145 -0
  239. package/server/dist/code-explorer-router.js +714 -114
  240. package/server/dist/core-compat.js +28 -1
  241. package/server/dist/core-execution.js +111 -0
  242. package/server/dist/core-package.js +4 -16
  243. package/server/dist/core-runtime.js +202 -0
  244. package/server/dist/core-update-manager.js +161 -60
  245. package/server/dist/core-update-state.js +29 -0
  246. package/server/dist/db.js +66 -35
  247. package/server/dist/delivery-evidence.js +142 -39
  248. package/server/dist/desktop-db.js +239 -43
  249. package/server/dist/desktop-router.js +109 -9
  250. package/server/dist/dev-ports.js +8 -3
  251. package/server/dist/file-provenance.js +34 -22
  252. package/server/dist/file-story-manager.js +62 -64
  253. package/server/dist/file-story.js +118 -35
  254. package/server/dist/file-summary-generator.js +46 -5
  255. package/server/dist/file-summary-manager.js +355 -106
  256. package/server/dist/framework-manager.js +98 -110
  257. package/server/dist/framework-reseed.js +76 -10
  258. package/server/dist/git-diagnostics.js +2 -2
  259. package/server/dist/headroom-manager.js +38 -3
  260. package/server/dist/host-control.js +52 -0
  261. package/server/dist/index.js +43 -2
  262. package/server/dist/interactive-job-session.js +149 -16
  263. package/server/dist/internal-api.js +48 -0
  264. package/server/dist/jira/jira-materializer.js +1 -0
  265. package/server/dist/json-tolerant.js +174 -0
  266. package/server/dist/legacy-migration.js +4 -2
  267. package/server/dist/loop-command-catalog.js +83 -21
  268. package/server/dist/loop-constants.js +6 -1
  269. package/server/dist/loop-decider.js +11 -1
  270. package/server/dist/loop-executors.js +194 -45
  271. package/server/dist/loop-factory.js +11 -4
  272. package/server/dist/loop-graph.js +90 -4
  273. package/server/dist/loop-run-manager.js +301 -69
  274. package/server/dist/loop-shell-invocation.js +21 -0
  275. package/server/dist/loop-step-idle.js +55 -0
  276. package/server/dist/loop-templates.js +31 -7
  277. package/server/dist/mcp/agent-capability.js +37 -4
  278. package/server/dist/mcp/guide.js +149 -36
  279. package/server/dist/mcp/mcp-admin-router.js +20 -4
  280. package/server/dist/mcp/mcp-server.js +89 -43
  281. package/server/dist/mcp/mcp-tiers.js +6 -5
  282. package/server/dist/mcp/mcp-token.js +3 -1
  283. package/server/dist/mcp/resources.js +5 -4
  284. package/server/dist/mcp/tools/catalog.js +4 -0
  285. package/server/dist/mcp/tools/code.js +125 -14
  286. package/server/dist/mcp/tools/context.js +153 -0
  287. package/server/dist/mcp/tools/git.js +15 -5
  288. package/server/dist/mcp/tools/jobs.js +84 -71
  289. package/server/dist/mcp/tools/loops.js +17 -6
  290. package/server/dist/mcp/tools/meta.js +74 -27
  291. package/server/dist/mcp/tools/mission.js +29 -0
  292. package/server/dist/mcp/tools/projects.js +22 -6
  293. package/server/dist/mcp/tools/rails.js +48 -11
  294. package/server/dist/mcp/tools/specs.js +86 -14
  295. package/server/dist/mcp/tools/support.js +1 -1
  296. package/server/dist/mcp/tools/types.js +117 -43
  297. package/server/dist/mcp/tools/watch.js +144 -51
  298. package/server/dist/milestone-chain-store.js +164 -0
  299. package/server/dist/milestone-chain.js +487 -0
  300. package/server/dist/milestone-progress.js +348 -0
  301. package/server/dist/mobile/mobile-devices.js +10 -7
  302. package/server/dist/mobile/mobile-gateway.js +2 -0
  303. package/server/dist/mobile/mobile-missions.js +214 -0
  304. package/server/dist/mobile/mobile-redact.js +17 -3
  305. package/server/dist/mobile/mobile-router.js +46 -4
  306. package/server/dist/mobile/mobile-ws.js +49 -4
  307. package/server/dist/multi-repo-bases.js +56 -0
  308. package/server/dist/multi-repo-checkout.js +101 -0
  309. package/server/dist/multi-repo-delivery.js +100 -0
  310. package/server/dist/multi-repo-execution-store.js +65 -0
  311. package/server/dist/multi-repo-execution.js +453 -0
  312. package/server/dist/offline-assemble.js +10 -8
  313. package/server/dist/openspec-runtime-plugin-commands.json +18 -0
  314. package/server/dist/openspec-runtime-plugin.js +120 -0
  315. package/server/dist/plugins/codex-spawn.js +49 -0
  316. package/server/dist/plugins/serena/manifest.js +2 -2
  317. package/server/dist/plugins/serena/templates/instructions.md +16 -0
  318. package/server/dist/pr-publisher.js +1 -1
  319. package/server/dist/profiles-router.js +3 -18
  320. package/server/dist/project-code-discovery.js +142 -0
  321. package/server/dist/project-git.js +19 -21
  322. package/server/dist/project-registry.js +272 -20
  323. package/server/dist/project-repositories.js +157 -0
  324. package/server/dist/project-repository-provenance.js +31 -0
  325. package/server/dist/project-router-background-processes.js +105 -0
  326. package/server/dist/project-router-git.js +75 -16
  327. package/server/dist/project-router-jobs.js +2 -126
  328. package/server/dist/project-router-loop-runs.js +72 -2
  329. package/server/dist/project-router-tickets.js +53 -8
  330. package/server/dist/project-router.js +239 -38
  331. package/server/dist/provider-detection.js +18 -6
  332. package/server/dist/provider-limit.js +64 -0
  333. package/server/dist/providers/claude-adapter.js +26 -1
  334. package/server/dist/providers/claude-background-tasks.js +18 -0
  335. package/server/dist/providers/claude-live-session.js +383 -0
  336. package/server/dist/providers/codex-adapter.js +19 -0
  337. package/server/dist/providers/codex-live-session.js +545 -0
  338. package/server/dist/providers/gemini-adapter.js +1 -0
  339. package/server/dist/providers/live-session-types.js +12 -0
  340. package/server/dist/providers/live-session.js +13 -0
  341. package/server/dist/providers/terminal-result.js +24 -0
  342. package/server/dist/queue-manager.js +2 -0
  343. package/server/dist/rail-isolated-launch.js +141 -16
  344. package/server/dist/rail-pr-decision.js +364 -99
  345. package/server/dist/rail-pr-store.js +24 -10
  346. package/server/dist/rail-worktree-release.js +34 -2
  347. package/server/dist/rail-worktrees-store.js +4 -4
  348. package/server/dist/rails-router.js +249 -55
  349. package/server/dist/repo-lock.js +92 -17
  350. package/server/dist/review-packet.js +23 -13
  351. package/server/dist/schemas/file-summary.v1.json +59 -59
  352. package/server/dist/schemas/profile.v1.json +137 -137
  353. package/server/dist/setup-manager.js +52 -8
  354. package/server/dist/shell-integration/bash-shim.bash +45 -0
  355. package/server/dist/shell-integration/fish-shim.fish +36 -0
  356. package/server/dist/shell-integration/powershell-shim.ps1 +48 -0
  357. package/server/dist/shell-integration/zsh-shim.zsh +47 -0
  358. package/server/dist/smash-runner.js +1 -0
  359. package/server/dist/spawn-lifecycle.js +44 -27
  360. package/server/dist/spec-contract-prompt.js +143 -0
  361. package/server/dist/spec-draft-parser.js +6 -1
  362. package/server/dist/spec-models.js +1 -0
  363. package/server/dist/stuck-run-detector.js +1 -0
  364. package/server/dist/terminal-manager.js +21 -1
  365. package/server/dist/terminal-shell-integration.js +4 -1
  366. package/server/dist/transient-children.js +619 -230
  367. package/server/dist/util/cli-prompt.js +78 -5
  368. package/server/dist/util/sqlite-migrations.js +20 -0
  369. package/server/dist/util/win-spawn.js +18 -3
  370. package/server/dist/util/windows-provider-shell.js +29 -0
  371. package/server/dist/verification-sentinel.js +14 -0
  372. package/server/dist/vitest-setup.js +2 -0
  373. package/server/dist/windows-job-supervisor.js +133 -0
  374. package/server/dist/workspace-manager.js +9 -30
  375. package/server/dist/worktree-manager.js +17 -2
  376. package/server/dist/worktree-overlay.js +3 -1
  377. package/client/dist/assets/ActivityFeedPage-tAQcWs1R.js +0 -1
  378. package/client/dist/assets/AgentBrowserCapture-BX8BNae5.js +0 -1
  379. package/client/dist/assets/AgentModeAnalyticsPane-BVAqCL0K.js +0 -2
  380. package/client/dist/assets/AgentModeCodePane-9XfqQc9c.js +0 -2
  381. package/client/dist/assets/AgentModeJobsPane-BCjLsPhc.js +0 -1
  382. package/client/dist/assets/AgentsPage-C8J_42sm.js +0 -87
  383. package/client/dist/assets/AnalyticsPage-BtQuyfJm.js +0 -1
  384. package/client/dist/assets/CodePage-BZ2MwdCu.js +0 -2
  385. package/client/dist/assets/DesktopAnalyticsPage-Bh_29V4g.js +0 -1
  386. package/client/dist/assets/ExportDropdown-BrtfWzEG.js +0 -1
  387. package/client/dist/assets/InteractiveJobComposer-BCZiSfzc.js +0 -19
  388. package/client/dist/assets/JobDetailModal-NAJmuYIT.js +0 -1
  389. package/client/dist/assets/JobDetailPage-fE0ZycUX.js +0 -1
  390. package/client/dist/assets/JobsPage-z5M_HVJG.js +0 -1
  391. package/client/dist/assets/LoopBuilderPage-j1FQS47c.js +0 -7
  392. package/client/dist/assets/LoopPreviewModal-DhDJjdCe.js +0 -1
  393. package/client/dist/assets/LoopsPage-Ll7zORZb.js +0 -1
  394. package/client/dist/assets/MinimizedChatsContext-Bd3OqR_S.js +0 -1
  395. package/client/dist/assets/PluginsPage-DN4kGvJt.js +0 -2
  396. package/client/dist/assets/ProjectSettingsDialog-OgsgxjzF.js +0 -1
  397. package/client/dist/assets/ReviewPacketPage-DFog4sXn.js +0 -1
  398. package/client/dist/assets/TemplatePreviewModal-L2XxqiOD.js +0 -1
  399. package/client/dist/assets/TicketDetailModal-CsF-AWZX.js +0 -14
  400. package/client/dist/assets/Trans-Bsz9_gyq.js +0 -1
  401. package/client/dist/assets/agent-BQgKv_2F.js +0 -1
  402. package/client/dist/assets/agent-C1NRDJqL.js +0 -1
  403. package/client/dist/assets/agent-CLNleiy6.js +0 -1
  404. package/client/dist/assets/agent-CmceArkQ.js +0 -1
  405. package/client/dist/assets/agent-D3UxQ3SR.js +0 -1
  406. package/client/dist/assets/agent-DB1JeE64.js +0 -1
  407. package/client/dist/assets/agent-DXkHcQ-D.js +0 -1
  408. package/client/dist/assets/agent-DjJJJ2M8.js +0 -1
  409. package/client/dist/assets/auth-D_5bQv5W.js +0 -1
  410. package/client/dist/assets/ban-E4EaZe3a.js +0 -1
  411. package/client/dist/assets/bot-DzuuuC5R.js +0 -1
  412. package/client/dist/assets/browser-BgqHDMXP.js +0 -1
  413. package/client/dist/assets/browser-CsKJtrpv.js +0 -1
  414. package/client/dist/assets/browser-Cu58EpZU.js +0 -1
  415. package/client/dist/assets/browser-CvXOJm-j.js +0 -1
  416. package/client/dist/assets/browser-DOrFh1c7.js +0 -1
  417. package/client/dist/assets/browser-DQv_Jo7x.js +0 -1
  418. package/client/dist/assets/browser-LPgxvJu2.js +0 -1
  419. package/client/dist/assets/browser-eL-3MIEI.js +0 -1
  420. package/client/dist/assets/builder-0IWtosUm.js +0 -1
  421. package/client/dist/assets/builder-Bf2N6XWn.js +0 -1
  422. package/client/dist/assets/builder-BnqaaBkT.js +0 -1
  423. package/client/dist/assets/builder-BxxfGgAO.js +0 -1
  424. package/client/dist/assets/builder-C5IIgWsq.js +0 -1
  425. package/client/dist/assets/builder-CHuahItu.js +0 -1
  426. package/client/dist/assets/builder-CjWosXL_.js +0 -1
  427. package/client/dist/assets/builder-CpCIOoKj.js +0 -1
  428. package/client/dist/assets/code-CHPkOkDS.js +0 -1
  429. package/client/dist/assets/commands-6tYQih5m.js +0 -1
  430. package/client/dist/assets/commands-BK2QinXN.js +0 -1
  431. package/client/dist/assets/commands-C2Nca76h.js +0 -1
  432. package/client/dist/assets/commands-CKdLihNX.js +0 -1
  433. package/client/dist/assets/commands-CYIBTAlc.js +0 -1
  434. package/client/dist/assets/commands-CjUtYJIM.js +0 -1
  435. package/client/dist/assets/commands-Cq_u_8y9.js +0 -1
  436. package/client/dist/assets/commands-D_ZHYQug.js +0 -1
  437. package/client/dist/assets/common-6MLbD-jM.js +0 -1
  438. package/client/dist/assets/common-BKGEu4rm.js +0 -1
  439. package/client/dist/assets/common-BNbnv8cE.js +0 -1
  440. package/client/dist/assets/common-BqGWtDbt.js +0 -1
  441. package/client/dist/assets/common-DBtTzeYm.js +0 -1
  442. package/client/dist/assets/common-DEW3Nr8n.js +0 -1
  443. package/client/dist/assets/common-DHsv9N5d.js +0 -1
  444. package/client/dist/assets/common-mkrEcW-Y.js +0 -1
  445. package/client/dist/assets/dashboard-B9GhC5ku.js +0 -1
  446. package/client/dist/assets/dashboard-CBnv6zOI.js +0 -1
  447. package/client/dist/assets/dashboard-CaP0fU87.js +0 -1
  448. package/client/dist/assets/dashboard-D4ZZ4x84.js +0 -1
  449. package/client/dist/assets/dashboard-DmuTQBX8.js +0 -1
  450. package/client/dist/assets/dashboard-DynLYj3d.js +0 -1
  451. package/client/dist/assets/dashboard-zj4gGFJ1.js +0 -1
  452. package/client/dist/assets/dialog-CdWCb02j.js +0 -45
  453. package/client/dist/assets/dist-bVqfYmby.js +0 -1
  454. package/client/dist/assets/dist-js-CWeUdf5Z.js +0 -1
  455. package/client/dist/assets/formatDistanceToNow-DQCvMgUc.js +0 -1
  456. package/client/dist/assets/getTimezoneOffsetInMilliseconds-C16cxSe1.js +0 -1
  457. package/client/dist/assets/git-refresh-wTW0rO0Z.js +0 -1
  458. package/client/dist/assets/i18n-hnX2DPmo.js +0 -2
  459. package/client/dist/assets/index-DIhMEAVw.js +0 -66
  460. package/client/dist/assets/index-DwygcU7s.css +0 -2
  461. package/client/dist/assets/mcp-B8X8wBLu.js +0 -1
  462. package/client/dist/assets/mcp-C2AojZZN.js +0 -1
  463. package/client/dist/assets/mcp-D8BT78_L.js +0 -1
  464. package/client/dist/assets/mcp-Df5RWBNk.js +0 -1
  465. package/client/dist/assets/mcp-Dj7BgMQW.js +0 -1
  466. package/client/dist/assets/mcp-Q7iT7rcz.js +0 -1
  467. package/client/dist/assets/mcp-YlvNvJbW.js +0 -1
  468. package/client/dist/assets/mcp-mKFpTSRb.js +0 -1
  469. package/client/dist/assets/narration-4Z2VUhZw.js +0 -1
  470. package/client/dist/assets/narration-BGuiEDj3.js +0 -1
  471. package/client/dist/assets/narration-BuSoqRmv.js +0 -1
  472. package/client/dist/assets/narration-CB9t29fy.js +0 -1
  473. package/client/dist/assets/narration-CU3x8FHD.js +0 -1
  474. package/client/dist/assets/narration-CWeXhfP5.js +0 -1
  475. package/client/dist/assets/narration-DRaDnhT5.js +0 -1
  476. package/client/dist/assets/narration-DtEbQgCw.js +0 -1
  477. package/client/dist/assets/nav-ApJVg3xu.js +0 -1
  478. package/client/dist/assets/nav-BKNky1Bg.js +0 -1
  479. package/client/dist/assets/nav-C6vWWKYx.js +0 -1
  480. package/client/dist/assets/nav-CXJ6Aw89.js +0 -1
  481. package/client/dist/assets/nav-CsmFBIDf.js +0 -1
  482. package/client/dist/assets/nav-DjkmQJjd.js +0 -1
  483. package/client/dist/assets/packet-B1IxaL5f.js +0 -1
  484. package/client/dist/assets/packet-Bh5kkbiB.js +0 -1
  485. package/client/dist/assets/packet-By8fqYxW.js +0 -1
  486. package/client/dist/assets/packet-CZdvA44E.js +0 -1
  487. package/client/dist/assets/packet-CrjHRQCP.js +0 -1
  488. package/client/dist/assets/packet-Dwi2pTtx.js +0 -1
  489. package/client/dist/assets/packet-_-ZIa0IR.js +0 -1
  490. package/client/dist/assets/packet-n0W6E1Hn.js +0 -1
  491. package/client/dist/assets/play-DNlrawbS.js +0 -1
  492. package/client/dist/assets/provider-capabilities-Doej9lfa.js +0 -1
  493. package/client/dist/assets/settings-Bd0x4TBa.js +0 -1
  494. package/client/dist/assets/settings-C1-7cXzC.js +0 -1
  495. package/client/dist/assets/settings-CAFaT38b.js +0 -1
  496. package/client/dist/assets/settings-Ct3v1nW2.js +0 -1
  497. package/client/dist/assets/settings-DHh7_IF5.js +0 -1
  498. package/client/dist/assets/settings-DQXAmdcv.js +0 -1
  499. package/client/dist/assets/settings-guzlhpup.js +0 -1
  500. package/client/dist/assets/settings-sZfTPMEH.js +0 -1
  501. package/client/dist/assets/setup-3jWaoVUO.js +0 -1
  502. package/client/dist/assets/setup-BjgRneb2.js +0 -1
  503. package/client/dist/assets/setup-BzFIjv-f.js +0 -1
  504. package/client/dist/assets/setup-C4VfQsxE.js +0 -1
  505. package/client/dist/assets/setup-DfUkpDbN.js +0 -1
  506. package/client/dist/assets/setup-ECe90Pmu.js +0 -1
  507. package/client/dist/assets/setup-dlqC4Jmm.js +0 -1
  508. package/client/dist/assets/setup-jwfLMiM2.js +0 -1
  509. package/client/dist/assets/spending-EC9xqio-.js +0 -1
  510. package/client/dist/assets/useDesktop-BQVt8lFd.js +0 -1
@@ -1,7 +1,4 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.createLoopExecutors = createLoopExecutors;
7
4
  /**
@@ -12,45 +9,62 @@ exports.createLoopExecutors = createLoopExecutors;
12
9
  * from coverage; the engine's traversal/decision logic is unit-tested against
13
10
  * fake executors in `loop-run-manager.test.ts`.
14
11
  */
12
+ const core_execution_1 = require("./core-execution");
13
+ const codex_spawn_1 = require("./plugins/codex-spawn");
15
14
  const node_child_process_1 = require("node:child_process");
16
15
  const node_crypto_1 = require("node:crypto");
17
- const tree_kill_1 = __importDefault(require("tree-kill"));
16
+ const node_fs_1 = require("node:fs");
17
+ const node_path_1 = require("node:path");
18
+ const win_spawn_1 = require("./util/win-spawn");
18
19
  const providers_1 = require("./providers");
19
20
  const workspace_manager_1 = require("./workspace-manager");
20
21
  const claude_trust_1 = require("./claude-trust");
21
22
  const spawn_lifecycle_1 = require("./spawn-lifecycle");
22
23
  const result_event_1 = require("./result-event");
24
+ const terminal_result_1 = require("./providers/terminal-result");
25
+ const claude_background_tasks_1 = require("./providers/claude-background-tasks");
26
+ const loop_constants_1 = require("./loop-constants");
23
27
  const loop_decider_1 = require("./loop-decider");
24
28
  const rail_isolation_1 = require("./rail-isolation");
25
29
  const repo_map_1 = require("./repo-map");
26
30
  const feature_flags_1 = require("./feature-flags");
31
+ const loop_step_idle_1 = require("./loop-step-idle");
27
32
  const runtime_1 = require("./providers/runtime");
33
+ const loop_shell_invocation_1 = require("./loop-shell-invocation");
28
34
  // Per-step wall-clock caps so a single hung step can't block the engine's
29
35
  // overall deadline check (which only runs between nodes).
30
36
  const AI_STEP_TIMEOUT_MS = 15 * 60_000;
31
37
  const DECIDER_TIMEOUT_MS = 3 * 60_000;
32
38
  const SHELL_TIMEOUT_MS = 10 * 60_000;
33
39
  const SHELL_OUTPUT_CAP = 256 * 1024;
40
+ // Match the queue's idle budget: a long implementation may keep running while
41
+ // producing output; a silent, wedged CLI must eventually release its rail.
42
+ const DEFAULT_INACTIVITY_TIMEOUT_MS = 30 * 60_000;
34
43
  function runShellCommand(command, cwd, env, timeoutMs, onLine, onSpawn) {
35
44
  return new Promise((resolve) => {
36
45
  const isWin = process.platform === 'win32';
37
- const shell = isWin ? process.env.ComSpec || 'cmd.exe' : '/bin/sh';
38
- const args = isWin ? ['/d', '/s', '/c', command] : ['-c', command];
46
+ const shellEnv = isWin ? (0, win_spawn_1.windowsSpawnEnv)(env) : env;
39
47
  const start = Date.now();
40
48
  let stdout = '';
41
49
  let stderr = '';
42
50
  let settled = false;
43
51
  let child;
44
52
  try {
45
- child = (0, node_child_process_1.spawn)(shell, args, {
53
+ const bundled = (0, loop_shell_invocation_1.bundledLoopShellInvocation)(command);
54
+ const options = {
46
55
  cwd,
47
- env,
56
+ env: shellEnv,
48
57
  stdio: ['ignore', 'pipe', 'pipe'],
49
- windowsVerbatimArguments: isWin,
50
- });
58
+ windowsHide: true,
59
+ };
60
+ // Node wraps /d /s /c with the required outer quotes. Passing a raw
61
+ // command ourselves with verbatim argv loses the first executable's quotes.
62
+ child = bundled ? (0, node_child_process_1.spawn)(bundled.binary, bundled.args, options) : isWin
63
+ ? (0, node_child_process_1.spawn)(command, { ...options, shell: shellEnv.ComSpec || 'cmd.exe' })
64
+ : (0, node_child_process_1.spawn)('/bin/sh', ['-c', command], options);
51
65
  }
52
- catch {
53
- resolve({ stdout: '', stderr: 'failed to spawn shell', exitCode: -1, durationMs: 0 });
66
+ catch (error) {
67
+ resolve({ stdout: '', stderr: `failed to spawn shell: ${error instanceof Error ? error.message : String(error)}`, exitCode: -1, durationMs: 0 });
54
68
  return;
55
69
  }
56
70
  onSpawn?.(child);
@@ -64,7 +78,7 @@ function runShellCommand(command, cwd, env, timeoutMs, onLine, onSpawn) {
64
78
  const timer = setTimeout(() => {
65
79
  if (child.pid) {
66
80
  try {
67
- (0, tree_kill_1.default)(child.pid, 'SIGKILL', () => { });
81
+ (0, win_spawn_1.treeKillSafe)(child.pid, 'SIGKILL', () => { });
68
82
  }
69
83
  catch { /* gone */ }
70
84
  }
@@ -94,12 +108,12 @@ function runShellCommand(command, cwd, env, timeoutMs, onLine, onSpawn) {
94
108
  * app's own draft-PR delivery. Default ON (flag=0/false/off disables). See
95
109
  * rail-isolation + the specrails-core SPECRAILS_GIT_AUTO contract.
96
110
  */
97
- function aiStepEnv(env, repoDir) {
98
- const base = repoDir ? { ...env, SPECRAILS_REPO_DIR: repoDir } : env;
111
+ function aiStepEnv(env, repoDir, manifest) {
112
+ const base = { ...env, ...(repoDir ? { SPECRAILS_REPO_DIR: repoDir } : {}), ...(manifest ? { SPECRAILS_EXECUTION_MANIFEST: JSON.stringify(manifest), SPECRAILS_REPOSITORIES: JSON.stringify(manifest.repositories.map((repo) => ({ id: repo.repositoryId, name: repo.name, path: repo.worktreePath }))) } : {}) };
99
113
  // Deterministic repo map (zero-AI) so the pipeline's exploration phase
100
114
  // starts oriented instead of spending its first turns on `ls`/`find`.
101
115
  const withMap = (0, repo_map_1.injectRepoMapEnv)(base, repoDir);
102
- return (0, rail_isolation_1.isRailPrDeliveryEnabled)() ? { ...withMap, SPECRAILS_GIT_AUTO: 'false' } : withMap;
116
+ return manifest || (0, rail_isolation_1.isRailPrDeliveryEnabled)() ? { ...withMap, SPECRAILS_GIT_AUTO: 'false' } : withMap;
103
117
  }
104
118
  /**
105
119
  * Relocated cwd is the workspace; the source repo is reached via the
@@ -113,27 +127,45 @@ function aiStepEnv(env, repoDir) {
113
127
  * (and cwd, defensively) to codex's sandbox writable_roots. Harmless
114
128
  * no-op under danger-full-access on iteration 1.
115
129
  */
116
- function aiStepExtraArgs(adapter, cwd, repoDir) {
130
+ function aiStepExtraArgs(adapter, cwd, repoDir, manifest) {
117
131
  if (!repoDir)
118
132
  return undefined;
119
- const args = (0, runtime_1.buildProviderRepoAccessArgs)(adapter, [repoDir, ...(adapter.id === 'codex' ? [cwd] : [])]);
133
+ const args = (0, runtime_1.buildProviderRepoAccessArgs)(adapter, [...new Set([repoDir, ...(manifest?.repositories.map((repo) => repo.worktreePath) ?? []), ...(adapter.id === 'codex' ? [cwd] : [])])]);
120
134
  return args.length > 0 ? args : undefined;
121
135
  }
122
136
  function createLoopExecutors(opts = {}) {
123
137
  const resolveEnv = () => typeof opts.env === 'function' ? opts.env() : opts.env ?? process.env;
138
+ const inactivityTimeoutMs = () => {
139
+ const env = resolveEnv();
140
+ const configuredIdle = env.SPECRAILS_LOOP_STEP_IDLE_TIMEOUT_MS ?? process.env.SPECRAILS_LOOP_STEP_IDLE_TIMEOUT_MS;
141
+ if (configuredIdle != null)
142
+ return (0, loop_step_idle_1.resolveLoopStepIdleTimeoutMs)({ ...env, SPECRAILS_LOOP_STEP_IDLE_TIMEOUT_MS: configuredIdle });
143
+ const raw = env.SPECRAILS_LOOP_INACTIVITY_MS ?? process.env.SPECRAILS_LOOP_INACTIVITY_MS;
144
+ const value = raw?.trim() ? Number(raw) : DEFAULT_INACTIVITY_TIMEOUT_MS;
145
+ return Number.isFinite(value) && value >= 0 ? value : DEFAULT_INACTIVITY_TIMEOUT_MS;
146
+ };
124
147
  // Deciders deliberately excluded: they run no pipeline agents.
125
- const withProfileEnv = (env, provider) => {
148
+ const withProfileEnv = (env, provider, profileName) => {
149
+ if (profileName === null) {
150
+ const legacyEnv = { ...env };
151
+ delete legacyEnv.SPECRAILS_PROFILE_PATH;
152
+ return legacyEnv;
153
+ }
126
154
  try {
127
- const profilePath = opts.profilePathFor?.(provider) ?? null;
155
+ const profilePath = opts.profilePathFor?.(provider, profileName) ?? null;
128
156
  return profilePath ? { ...env, SPECRAILS_PROFILE_PATH: profilePath } : env;
129
157
  }
130
- catch {
158
+ catch (error) {
159
+ if (profileName)
160
+ throw error;
131
161
  return env;
132
162
  }
133
163
  };
134
164
  return {
135
165
  // Cheap working-tree fingerprint for the engine's non-convergence guard:
136
- // HEAD sha + the porcelain dirty set, hashed. Two identical fingerprints
166
+ // HEAD, tracked diffs and untracked file contents, hashed. Status/path names
167
+ // alone stay identical while a fixer keeps improving the same files.
168
+ // Two identical fingerprints
137
169
  // across consecutive Decider 'continue' verdicts ⇒ the loop changed nothing
138
170
  // ⇒ abort `stalled`. Synchronous + best-effort: any git failure (no repo,
139
171
  // detached, etc.) returns null so the guard simply stays off. Never throws.
@@ -144,13 +176,34 @@ function createLoopExecutors(opts = {}) {
144
176
  // `-z` NUL-delimited + include untracked so a brand-new file counts as
145
177
  // a change; stable ordering makes the hash deterministic per tree state.
146
178
  const porcelain = (0, node_child_process_1.execFileSync)('git', ['status', '--porcelain', '-z', '--untracked-files=all'], opt);
147
- return (0, node_crypto_1.createHash)('sha256').update(head).update('\0').update(porcelain).digest('hex');
179
+ const hash = (0, node_crypto_1.createHash)('sha256').update(head).update('\0').update(porcelain);
180
+ // Separate index/worktree diffs preserve staged-only changes and staged
181
+ // content subsequently edited back to HEAD. Binary patches include
182
+ // actual bytes; disable external diff/textconv so this is read-only.
183
+ for (const args of [
184
+ ['diff', '--no-ext-diff', '--no-textconv', '--binary', '--'],
185
+ ['diff', '--cached', '--no-ext-diff', '--no-textconv', '--binary', '--'],
186
+ ])
187
+ hash.update('\0').update((0, node_child_process_1.execFileSync)('git', args, { ...opt, maxBuffer: 16 * 1024 * 1024 }));
188
+ const untracked = (0, node_child_process_1.execFileSync)('git', ['ls-files', '--others', '--exclude-standard', '-z'], opt);
189
+ for (const relative of untracked.split('\0').filter(Boolean).sort()) {
190
+ const file = (0, node_path_1.resolve)(dir, relative);
191
+ const stat = (0, node_fs_1.lstatSync)(file);
192
+ hash.update('\0').update(relative).update('\0').update(String(stat.mode));
193
+ // Never follow a symlink outside the worktree. If an unreadable file
194
+ // or a racing delete defeats the snapshot, disable this sample.
195
+ if (stat.isSymbolicLink())
196
+ hash.update((0, node_fs_1.readlinkSync)(file));
197
+ else if (stat.isFile())
198
+ hash.update((0, node_fs_1.readFileSync)(file));
199
+ }
200
+ return hash.digest('hex');
148
201
  }
149
202
  catch {
150
203
  return null;
151
204
  }
152
205
  },
153
- async runAiStep({ prompt, sessionId, provider, model, effort, cwd, repoDir, onLine, onRawLine, onSpawn, aiStepTimeoutMs }) {
206
+ async runAiStep({ coreRun, prompt, sessionId, provider, model, effort, profileName, cwd, repoDir, executionManifest, onLine, onRawLine, onSpawn, aiStepTimeoutMs, idleTimeoutMs }) {
154
207
  const adapter = (0, providers_1.getAdapter)(provider);
155
208
  // First iteration spawns headless (rail-job); subsequent iterations resume
156
209
  // the session so the agent keeps prior context across iterations.
@@ -160,14 +213,22 @@ function createLoopExecutors(opts = {}) {
160
213
  // for the pipeline's I/O exactly like QueueManager: SPECRAILS_REPO_DIR +
161
214
  // claude `--add-dir <repoDir>` (see the shared helpers above). Best-effort
162
215
  // agent self-heal on Windows.
163
- const stepEnv = withProfileEnv(aiStepEnv(resolveEnv(), repoDir), provider);
164
- const extraArgs = aiStepExtraArgs(adapter, cwd, repoDir);
165
- const effectivePrompt = (0, runtime_1.formatProviderCommand)(adapter, prompt, cwd, sessionId ?? undefined);
216
+ const baseStepEnv = withProfileEnv(aiStepEnv(resolveEnv(), repoDir, executionManifest), provider, profileName);
217
+ const core = coreRun ? (0, core_execution_1.prepareCoreExecution)({ run: coreRun, cwd, repoDir, manifest: executionManifest, env: baseStepEnv }) : undefined;
218
+ const stepEnv = core?.env ?? baseStepEnv;
219
+ let extraArgs = aiStepExtraArgs(adapter, cwd, repoDir, executionManifest);
220
+ const pluginScope = adapter.id === 'codex' ? opts.pluginScope?.() : undefined;
221
+ const pluginArgs = (0, codex_spawn_1.buildCodexPluginArgs)({ providerId: adapter.id, stateRoot: pluginScope?.stateRoot ?? stepEnv.SPECRAILS_WORKSPACE_DIR ?? cwd, repositoryPath: repoDir ?? cwd, legacyProviderId: pluginScope?.legacyProviderId });
222
+ if (pluginArgs.length)
223
+ extraArgs = [...(extraArgs ?? []), ...pluginArgs];
224
+ const effectivePrompt = (0, runtime_1.formatProviderCommand)(adapter, [core?.promptPrefix, prompt].filter(Boolean).join('\n\n'), cwd, sessionId ?? undefined);
166
225
  const buildOpts = {
167
226
  prompt: effectivePrompt,
227
+ ...(adapter.id === 'claude' ? { systemPrompt: loop_constants_1.FOREGROUND_RULE } : {}),
168
228
  model,
169
229
  sessionId: sessionId ?? undefined,
170
230
  reasoning_effort: effort,
231
+ ...(executionManifest ? { scopedWorkingDirectories: true } : {}),
171
232
  extraArgs,
172
233
  };
173
234
  if (repoDir) {
@@ -180,15 +241,32 @@ function createLoopExecutors(opts = {}) {
180
241
  // Pre-trust the spawn dir so headless claude honours the overlaid
181
242
  // `.claude/settings.json` permissions.allow (else "workspace not trusted").
182
243
  try {
183
- (0, claude_trust_1.ensureClaudeTrusted)(adapter.id, [cwd, repoDir]);
244
+ (0, claude_trust_1.ensureClaudeTrusted)(adapter.id, [cwd, repoDir, ...(executionManifest?.repositories.map((repo) => repo.worktreePath) ?? [])]);
184
245
  }
185
246
  catch { /* best-effort */ }
247
+ // Gemini acknowledges project agents before headless execution. Rails
248
+ // using the loop engine need the same adapter preparation as queue jobs.
249
+ try {
250
+ adapter.prepareHeadlessSpawn?.(cwd);
251
+ }
252
+ catch (err) {
253
+ console.warn(`[loop] provider preparation failed: ${err.message}`);
254
+ }
186
255
  let text = '';
187
256
  // The adapter parses provider failures (codex `turn.failed`, etc.) into a
188
257
  // structured `error` event on the stdout stream — capture its message so we
189
258
  // surface the REAL reason (e.g. "You've hit your usage limit") instead of
190
259
  // the `stderrTail`, which often holds only an informational line.
191
260
  let errorText;
261
+ // Idle watchdog (loop-step-idle): the child produced NO stdout/stderr for
262
+ // the idle budget — a wedge, not a slow step. Flagged separately from the
263
+ // wall-clock timeout so the engine can retry the step once by resume.
264
+ let stalled = false;
265
+ // The provider flagged the final `result` as an error (claude
266
+ // `is_error: true` — a usage/rate-limit notice returned AS the reply).
267
+ let resultIsError = false;
268
+ let liveBackgroundTasks = [];
269
+ const effectiveIdleTimeoutMs = idleTimeoutMs ?? inactivityTimeoutMs();
192
270
  const wallStartedAt = Date.now();
193
271
  const res = await (0, spawn_lifecycle_1.runAiCliInvocation)({
194
272
  adapter,
@@ -199,13 +277,25 @@ function createLoopExecutors(opts = {}) {
199
277
  // 0 ⇒ watchdog disabled (factory loops run untimed; the loop's
200
278
  // maxIterations / cost cap remain the runaway guards).
201
279
  timeoutMs: (aiStepTimeoutMs ?? AI_STEP_TIMEOUT_MS) > 0 ? (aiStepTimeoutMs ?? AI_STEP_TIMEOUT_MS) : undefined,
280
+ // The engine's explicit bound wins, including 0. Direct callers retain
281
+ // the existing env-resolved watchdog when no bound was supplied.
282
+ inactivityTimeoutMs: effectiveIdleTimeoutMs > 0 ? effectiveIdleTimeoutMs : undefined,
283
+ onInactivityTimeout: () => { stalled = true; },
202
284
  onSpawn,
203
285
  // Two complementary streams, mirroring QueueManager's contract:
204
286
  // • RAW JSONL via onStdoutLine → engine emits parsed `event`s that drive
205
287
  // JobStatusPanel activity (LogViewer SKIPS these to avoid dupes).
206
288
  // • display text/tools via onEvent → engine emits `log` lines that
207
289
  // LogViewer actually RENDERS (the visible transcript).
208
- onStdoutLine: onRawLine,
290
+ onStdoutLine: (line) => {
291
+ if (adapter.id === 'claude') {
292
+ try {
293
+ liveBackgroundTasks = (0, claude_background_tasks_1.readClaudeBackgroundTasks)(JSON.parse(line)) ?? liveBackgroundTasks;
294
+ }
295
+ catch { /* ordinary output */ }
296
+ }
297
+ onRawLine?.(line);
298
+ },
209
299
  onEvent: (ev) => {
210
300
  if (ev.kind === 'text-delta') {
211
301
  text += ev.text;
@@ -215,18 +305,39 @@ function createLoopExecutors(opts = {}) {
215
305
  onLine?.(`🔧 ${ev.name} ${ev.inputPreview ?? ''}`.trim());
216
306
  else if (ev.kind === 'error' && ev.message)
217
307
  errorText = ev.message;
308
+ else if (ev.kind === 'result') {
309
+ const terminalError = (0, terminal_result_1.terminalResultError)(ev.payload);
310
+ errorText = terminalError ?? errorText;
311
+ if (ev.isError || terminalError)
312
+ resultIsError = true;
313
+ }
218
314
  },
219
315
  });
316
+ // The kill-switch one-shot transport cannot continue a resident parent.
317
+ // Never certify a clean CLI exit as completed work if its workers were
318
+ // still pending. Interactive mode can join them in the same session.
319
+ if (liveBackgroundTasks.length > 0 && !errorText) {
320
+ errorText = `Agent exited with ${liveBackgroundTasks.length} unfinished background task(s); implementation step is incomplete.`;
321
+ }
220
322
  const failed = res.spawnFailed ||
221
323
  res.timedOut ||
222
- (res.code != null && res.code !== 0) ||
223
- errorText !== undefined;
324
+ res.code !== 0 ||
325
+ errorText !== undefined ||
326
+ resultIsError;
224
327
  if (res.spawnFailed) {
225
328
  errorText = errorText ?? `failed to spawn "${adapter.binary}" (on PATH?)`;
226
329
  const msg = `AI step: ${errorText}`;
227
330
  console.error(`[loop] ${msg}`);
228
331
  onLine?.(msg, 'stderr');
229
332
  }
333
+ else if (stalled) {
334
+ // The inactivity watchdog tore the child down (settles timedOut too, so
335
+ // this branch must precede the wall-clock one). The engine retries once.
336
+ errorText = loop_step_idle_1.AI_STEP_STALLED_ERROR;
337
+ const msg = `AI step: ${adapter.binary} produced no output for ${Math.round(effectiveIdleTimeoutMs / 1000)}s — stalled`;
338
+ console.error(`[loop] ${msg}`);
339
+ onLine?.(msg, 'stderr');
340
+ }
230
341
  else if (res.timedOut) {
231
342
  // A wedged/unresponsive provider settles with code=null + timedOut. Count
232
343
  // it as a failure so the engine's fail-fast can bail instead of paying the
@@ -236,11 +347,20 @@ function createLoopExecutors(opts = {}) {
236
347
  console.error(`[loop] ${msg}`);
237
348
  onLine?.(msg, 'stderr');
238
349
  }
239
- else if (res.code != null && res.code !== 0) {
350
+ else if (resultIsError) {
351
+ // The provider returned an error AS the reply (claude `is_error: true`
352
+ // — typically a usage/rate-limit notice). The text IS the reason.
353
+ errorText = errorText ?? (text.trim().slice(-300) || 'the provider returned an error result');
354
+ const msg = `AI step: provider returned an error result — ${errorText}`;
355
+ console.error(`[loop] ${msg}`);
356
+ onLine?.(msg, 'stderr');
357
+ }
358
+ else if (res.code !== 0) {
240
359
  // Prefer the adapter's structured error (e.g. codex `turn.failed`:
241
360
  // "You've hit your usage limit") over `stderrTail`, which for codex holds
242
361
  // only the informational "Reading additional input from stdin…" line.
243
362
  const reason = errorText ?? (res.stderrTail ? res.stderrTail.slice(0, 300) : '');
363
+ errorText = reason || `${adapter.binary} terminated without a successful exit (code=${res.code})`;
244
364
  const msg = `AI step: ${adapter.binary} exited code=${res.code}${reason ? ` — ${reason}` : ''}`;
245
365
  console.error(`[loop] ${msg}`);
246
366
  onLine?.(msg, 'stderr');
@@ -286,6 +406,8 @@ function createLoopExecutors(opts = {}) {
286
406
  estimated,
287
407
  failed,
288
408
  errorText,
409
+ ...(stalled ? { stalled: true } : {}),
410
+ ...(resultIsError ? { resultIsError: true } : {}),
289
411
  };
290
412
  },
291
413
  /**
@@ -298,14 +420,16 @@ function createLoopExecutors(opts = {}) {
298
420
  * spike-verified on claude 2.1.198). Null ⇒ the engine one-shots the step
299
421
  * (non-claude providers, or the kill-switch) — byte-identical legacy.
300
422
  */
301
- planInteractiveAiStep({ provider, model, effort, cwd, repoDir, sessionId, aiStepTimeoutMs }) {
423
+ planInteractiveAiStep({ coreRun, provider, model, effort, profileName, cwd, repoDir, executionManifest, sessionId, aiStepTimeoutMs, idleTimeoutMs }) {
302
424
  if (!(0, feature_flags_1.isInteractiveJobsEnabled)())
303
425
  return null;
304
426
  const adapter = (0, providers_1.getAdapter)(provider);
305
427
  if (!adapter.capabilities.persistentStdin)
306
428
  return null;
307
- const stepEnv = withProfileEnv(aiStepEnv(resolveEnv(), repoDir), provider);
308
- const extraArgs = aiStepExtraArgs(adapter, cwd, repoDir);
429
+ const baseStepEnv = withProfileEnv(aiStepEnv(resolveEnv(), repoDir, executionManifest), provider, profileName);
430
+ const core = coreRun ? (0, core_execution_1.prepareCoreExecution)({ run: coreRun, cwd, repoDir, manifest: executionManifest, env: baseStepEnv }) : undefined;
431
+ const stepEnv = core?.env ?? baseStepEnv;
432
+ const extraArgs = aiStepExtraArgs(adapter, cwd, repoDir, executionManifest);
309
433
  if (repoDir) {
310
434
  try {
311
435
  (0, workspace_manager_1.ensureFrameworkAgents)(cwd, adapter.projectDirName);
@@ -316,9 +440,15 @@ function createLoopExecutors(opts = {}) {
316
440
  // Pre-trust the spawn dir so headless claude honours the overlaid
317
441
  // `.claude/settings.json` permissions.allow (else "workspace not trusted").
318
442
  try {
319
- (0, claude_trust_1.ensureClaudeTrusted)(adapter.id, [cwd, repoDir]);
443
+ (0, claude_trust_1.ensureClaudeTrusted)(adapter.id, [cwd, repoDir, ...(executionManifest?.repositories.map((repo) => repo.worktreePath) ?? [])]);
320
444
  }
321
445
  catch { /* best-effort */ }
446
+ try {
447
+ adapter.prepareHeadlessSpawn?.(cwd);
448
+ }
449
+ catch (err) {
450
+ console.warn(`[loop] provider preparation failed: ${err.message}`);
451
+ }
322
452
  const buildOpts = {
323
453
  // chat-stream feeds the prompt over stdin per-turn, so the argv `prompt`
324
454
  // is unused — pass empty to satisfy the shared SpawnOptions shape.
@@ -329,28 +459,36 @@ function createLoopExecutors(opts = {}) {
329
459
  // drops the session id at an iterate-loop's loop-back).
330
460
  sessionId: sessionId ?? undefined,
331
461
  reasoning_effort: effort,
462
+ ...(executionManifest ? { scopedWorkingDirectories: true } : {}),
332
463
  extraArgs,
333
464
  };
334
465
  const args = adapter.buildArgs('chat-stream', buildOpts);
466
+ const effectiveIdleTimeoutMs = idleTimeoutMs ?? inactivityTimeoutMs();
335
467
  return {
336
468
  adapter,
337
469
  spec: { binary: adapter.binary, args, cwd, env: (0, runtime_1.buildProviderEnv)(adapter, buildOpts, stepEnv) },
338
470
  // The loop's ai-step timeout bounds the WHOLE step, interactive
339
471
  // included. 0 ⇒ unbounded (the engine skips arming the step timer).
472
+ ...(core?.promptPrefix ? { promptPrefix: core.promptPrefix } : {}),
340
473
  stepTimeoutMs: aiStepTimeoutMs ?? AI_STEP_TIMEOUT_MS,
474
+ // Keep the old field for callers that already inspect the plan.
475
+ inactivityTimeoutMs: effectiveIdleTimeoutMs,
476
+ ...(effectiveIdleTimeoutMs > 0 ? { idleTimeoutMs: effectiveIdleTimeoutMs } : {}),
341
477
  };
342
478
  },
343
- async runShell({ command, cwd, onLine, onSpawn }) {
344
- return runShellCommand(command, cwd, resolveEnv(), SHELL_TIMEOUT_MS, onLine, onSpawn);
479
+ async runShell({ command, cwd, repoDir, onLine, onSpawn, timeoutMs }) {
480
+ const env = resolveEnv();
481
+ return runShellCommand(command, repoDir ?? cwd, repoDir ? { ...env, SPECRAILS_REPO_DIR: repoDir } : env, timeoutMs ?? SHELL_TIMEOUT_MS, onLine, onSpawn);
345
482
  },
346
- async runDecider({ systemPrompt, userPrompt, provider, model, effort, cwd, repoDir, onRawLine, onSpawn }) {
483
+ async runDecider({ systemPrompt, userPrompt, provider, model, effort, cwd, repoDir, executionManifest, onRawLine, onSpawn, timeoutMs }) {
347
484
  const adapter = (0, providers_1.getAdapter)(provider);
348
485
  let text = '';
486
+ let errorText;
349
487
  const baseEnv = resolveEnv();
350
- const decEnv = repoDir ? { ...baseEnv, SPECRAILS_REPO_DIR: repoDir } : baseEnv;
488
+ const decEnv = aiStepEnv(baseEnv, repoDir, executionManifest);
351
489
  // spec-gen is a one-shot, system-prompted invocation (workspace-write on
352
490
  // codex, not full-access) — appropriate for a read-only judgment.
353
- const buildOpts = { prompt: userPrompt, systemPrompt, model, maxTurns: 1, reasoning_effort: effort, toolPolicy: 'read-only' };
491
+ const buildOpts = { prompt: userPrompt, systemPrompt, model, maxTurns: 1, reasoning_effort: effort, toolPolicy: 'read-only', ...(executionManifest ? { extraArgs: aiStepExtraArgs(adapter, cwd, repoDir, executionManifest) } : {}) };
354
492
  const wallStartedAt = Date.now();
355
493
  const res = await (0, spawn_lifecycle_1.runAiCliInvocation)({
356
494
  adapter,
@@ -358,13 +496,24 @@ function createLoopExecutors(opts = {}) {
358
496
  buildOpts,
359
497
  cwd,
360
498
  env: (0, runtime_1.buildProviderEnv)(adapter, buildOpts, decEnv),
361
- timeoutMs: DECIDER_TIMEOUT_MS,
499
+ timeoutMs: timeoutMs ?? DECIDER_TIMEOUT_MS,
362
500
  onSpawn,
363
501
  onStdoutLine: onRawLine,
364
- onEvent: (ev) => { if (ev.kind === 'text-delta')
365
- text += ev.text; },
502
+ onEvent: (ev) => {
503
+ if (ev.kind === 'text-delta')
504
+ text += ev.text;
505
+ else if (ev.kind === 'error' && ev.message)
506
+ errorText = ev.message;
507
+ else if (ev.kind === 'result')
508
+ errorText = (0, terminal_result_1.terminalResultError)(ev.payload) ?? errorText;
509
+ },
366
510
  });
367
- const decision = (0, loop_decider_1.parseDeciderDecision)(text);
511
+ // A streamed verdict from an interrupted/failed invocation is not an
512
+ // authoritative completion gate, even when the partial JSON parses.
513
+ const failed = res.spawnFailed || res.timedOut || res.code !== 0 || errorText !== undefined;
514
+ const decision = failed
515
+ ? { continue: true, blocked: false, parsed: false, reasoning: errorText ?? (res.timedOut ? 'Decider timed out; verification is incomplete.' : res.stderrTail || 'Decider failed; verification is incomplete.') }
516
+ : (0, loop_decider_1.parseDeciderDecision)(text);
368
517
  const { result, estimated } = (0, result_event_1.finaliseInvocationResult)(adapter, res.events, {
369
518
  fallbackModel: model,
370
519
  durationMs: Math.max(0, Date.now() - wallStartedAt),
@@ -6,7 +6,7 @@ exports.isFactoryLoopId = isFactoryLoopId;
6
6
  exports.factoryLoopMode = factoryLoopMode;
7
7
  exports.factoryLoopForMode = factoryLoopForMode;
8
8
  const loop_templates_1 = require("./loop-templates");
9
- const GREEN_GOAL = 'Stop only when the latest verification step reports {{const:VERIFICATION_PASS}} and the history proves the spec is implemented with all required tests/build checks passing.';
9
+ const GREEN_GOAL = 'Stop only when the latest verification step reports {{const:VERIFICATION_PASS}} and the history proves every acceptance criterion is implemented across all required tickets and repositories, with the relevant behavioral checks and configured tests/build passing. Green baseline checks, setup, or planning alone do not prove completion.';
10
10
  // Factory loops run the WHOLE architect→developer→reviewer pipeline inside a
11
11
  // single AI step (`/specrails:implement` etc.), so no fixed wall-clock budget is
12
12
  // honest — a legit implement outran the old 60-min step cap and got killed
@@ -29,7 +29,7 @@ exports.FACTORY_LOOPS = [
29
29
  {
30
30
  id: 'factory:implement',
31
31
  name: 'Implement',
32
- description: 'Fully autonomous: implement the spec, verify, and refine (fix) on failure — looping until all tests pass.',
32
+ description: 'Implement the spec, verify its acceptance criteria and project checks, then complete missing work or fix defects until the full change passes.',
33
33
  mode: 'implement',
34
34
  graph: (0, loop_templates_1.fixLoopGraph)(['{{cmd:implement}}'], GREEN_GOAL, FACTORY_MAX_ITERATIONS, FACTORY_LOOP_TIMEOUT_MIN, FACTORY_AI_STEP_TIMEOUT_MIN),
35
35
  },
@@ -60,9 +60,16 @@ exports.FACTORY_LOOPS = [
60
60
  // renders as an EMPTY string, so a hand-launched run would have a blank
61
61
  // central instruction. The launch route refuses it; the prompt guards too.
62
62
  launchable: false,
63
- description: "Apply the ONE change the user asked for on top of work already delivered, re-grade it with the reviewer, then verify + refine until green. No re-planning. Runs automatically when you ask for changes on a review — it is not started by hand.",
63
+ description: "Apply the ONE change the user asked for on top of work already delivered, then run one independent reviewer-owned verification gate and refine until green. No re-planning. Runs automatically when you ask for changes on a review — it is not started by hand.",
64
64
  mode: 'loop',
65
- graph: (0, loop_templates_1.fixLoopGraph)(['{{cmd:revise}}'], GREEN_GOAL, FACTORY_MAX_ITERATIONS, FACTORY_LOOP_TIMEOUT_MIN, FACTORY_AI_STEP_TIMEOUT_MIN),
65
+ // Revision is the ONLY factory loop that swaps the generic `{{cmd:verify}}`
66
+ // gate: `{{cmd:revision-verify}}` owns reviewer re-grading AND the full pass
67
+ // of record in ONE read-only step, so the mutator no longer runs sr-reviewer
68
+ // itself and no second repository-wide gate follows it. The trailing `true`
69
+ // makes verify/fix run in a FRESH provider session — the gate's verdict and
70
+ // its confidence artifact then describe the candidate on disk rather than
71
+ // the mutator's own account of it.
72
+ graph: (0, loop_templates_1.fixLoopGraph)(['{{cmd:revise}}'], GREEN_GOAL, FACTORY_MAX_ITERATIONS, FACTORY_LOOP_TIMEOUT_MIN, FACTORY_AI_STEP_TIMEOUT_MIN, '{{cmd:revision-verify}}', true),
66
73
  },
67
74
  SDD_QUICK_OPENSPEC_FACTORY,
68
75
  ];
@@ -10,6 +10,7 @@
10
10
  * trivially unit-testable and reused verbatim by both the store and the engine.
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.assertLoopShellRepositoryScope = assertLoopShellRepositoryScope;
13
14
  exports.emptyLoopGraph = emptyLoopGraph;
14
15
  exports.validateLoopGraph = validateLoopGraph;
15
16
  exports.nodesById = nodesById;
@@ -17,6 +18,15 @@ exports.findStartNode = findStartNode;
17
18
  exports.successors = successors;
18
19
  exports.loopNeedsTicket = loopNeedsTicket;
19
20
  exports.interpolateSpec = interpolateSpec;
21
+ /** A named shell target must never silently fall back to another checkout. */
22
+ function assertLoopShellRepositoryScope(graph, repositoryIds) {
23
+ for (const node of graph.nodes) {
24
+ const target = node.type === 'shell' ? node.data?.repositoryId : undefined;
25
+ if (target !== undefined && (typeof target !== 'string' || !repositoryIds.includes(target))) {
26
+ throw new Error(`Shell step ${node.id} targets a repository outside this launch`);
27
+ }
28
+ }
29
+ }
20
30
  /** A minimal, structurally-valid empty graph (used to seed a new Draft). */
21
31
  function emptyLoopGraph() {
22
32
  return { nodes: [], edges: [], config: { maxIterations: 10, timeoutMinutes: 30 } };
@@ -34,8 +44,32 @@ function emptyLoopGraph() {
34
44
  */
35
45
  function validateLoopGraph(graph) {
36
46
  const errors = [];
37
- const nodes = graph.nodes ?? [];
38
- const edges = graph.edges ?? [];
47
+ if (!graph || !Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
48
+ return { valid: false, errors: [{ code: 'INVALID_CONFIG', message: 'A loop graph must contain nodes and edges arrays.' }] };
49
+ }
50
+ const nodes = graph.nodes;
51
+ const edges = graph.edges;
52
+ const kinds = new Set(['start', 'ai-step', 'shell', 'decider', 'condition', 'end']);
53
+ if (nodes.some((node) => !node || typeof node.id !== 'string' || !node.id.trim() || !kinds.has(node.type))) {
54
+ return { valid: false, errors: [{ code: 'INVALID_NODE', message: 'Every node needs a non-empty id and a supported node type.' }] };
55
+ }
56
+ if (edges.some((edge) => !edge || typeof edge.id !== 'string' || !edge.id.trim() || typeof edge.source !== 'string' || typeof edge.target !== 'string')) {
57
+ return { valid: false, errors: [{ code: 'DANGLING_EDGE', message: 'Every edge needs a non-empty id, source and target.' }] };
58
+ }
59
+ for (const [items, code] of [[nodes, 'DUPLICATE_NODE'], [edges, 'DUPLICATE_EDGE']]) {
60
+ const ids = new Set();
61
+ for (const item of items) {
62
+ if (ids.has(item.id))
63
+ errors.push({ code, message: `Duplicate ${code === 'DUPLICATE_NODE' ? 'node' : 'edge'} id "${item.id}".` });
64
+ ids.add(item.id);
65
+ }
66
+ }
67
+ for (const node of nodes) {
68
+ if (node.type === 'shell' && node.data?.repositoryId !== undefined &&
69
+ (typeof node.data.repositoryId !== 'string' || !node.data.repositoryId.trim())) {
70
+ errors.push({ code: 'INVALID_NODE', nodeId: node.id, message: 'A shell repositoryId must be a non-empty registered repository id.' });
71
+ }
72
+ }
39
73
  // ── Start cardinality ──────────────────────────────────────────────────────
40
74
  const starts = nodes.filter((n) => n.type === 'start');
41
75
  if (starts.length === 0) {
@@ -61,10 +95,12 @@ function validateLoopGraph(graph) {
61
95
  cfg.maxIterations < 1 ||
62
96
  typeof cfg.timeoutMinutes !== 'number' ||
63
97
  !Number.isFinite(cfg.timeoutMinutes) ||
64
- cfg.timeoutMinutes < 0) {
98
+ cfg.timeoutMinutes < 0 ||
99
+ (cfg.aiStepTimeoutMinutes !== undefined && (!Number.isFinite(cfg.aiStepTimeoutMinutes) || cfg.aiStepTimeoutMinutes < 0)) ||
100
+ (cfg.maxCostUsd !== undefined && !Number.isFinite(cfg.maxCostUsd))) {
65
101
  errors.push({
66
102
  code: 'INVALID_CONFIG',
67
- message: 'maxIterations must be ≥ 1 and timeoutMinutes ≥ 0 (0 = no timeout).',
103
+ message: 'maxIterations must be ≥ 1; timeouts must be finite and ≥ 0 (0 = no timeout); maxCostUsd must be finite.',
68
104
  });
69
105
  }
70
106
  // ── Dangling edges ────────────────────────────────────────────────────────────
@@ -78,6 +114,33 @@ function validateLoopGraph(graph) {
78
114
  });
79
115
  }
80
116
  }
117
+ // The executor follows one successor at a time. Publishing a branching AI,
118
+ // shell or condition node would silently skip all but its first branch.
119
+ for (const node of nodes) {
120
+ const out = edges.filter((edge) => edge.source === node.id);
121
+ if (node.type === 'end') {
122
+ if (out.length)
123
+ errors.push({ code: 'INVALID_BRANCH', nodeId: node.id, message: 'End nodes cannot have outgoing edges.' });
124
+ }
125
+ else if (node.type === 'decider') {
126
+ const labeled = out.some((edge) => edge.branch !== undefined);
127
+ const validBranches = labeled
128
+ ? out.length === 2 && out.filter((edge) => edge.branch === 'continue').length === 1 && out.filter((edge) => edge.branch === 'stop').length === 1
129
+ : out.length === 2 && out.filter((edge) => nodes.find((candidate) => candidate.id === edge.target)?.type === 'end').length === 1;
130
+ if (!validBranches)
131
+ errors.push({ code: 'INVALID_BRANCH', nodeId: node.id, message: 'A Decider needs exactly one continue branch and one stop branch.' });
132
+ const cont = out.find((edge) => edge.branch === 'continue');
133
+ if (cont && nodes.find((candidate) => candidate.id === cont.target)?.type === 'end') {
134
+ errors.push({ code: 'INVALID_BRANCH', nodeId: node.id, message: 'The continue branch must lead to another step, not End.' });
135
+ }
136
+ }
137
+ else if (!out.length) {
138
+ errors.push({ code: 'DEAD_END', nodeId: node.id, message: `Node "${node.id}" needs a next step or an End.` });
139
+ }
140
+ else if (out.length !== 1) {
141
+ errors.push({ code: 'UNSUPPORTED_BRANCHING', nodeId: node.id, message: `Node "${node.id}" has multiple successors; use sequential steps or a Decider with explicit branches.` });
142
+ }
143
+ }
81
144
  // ── Reachability from Start (orphan detection) ──────────────────────────────
82
145
  // Only meaningful with exactly one Start; otherwise the start-cardinality
83
146
  // errors above already explain the shape problem.
@@ -151,6 +214,29 @@ function loopNeedsTicket(graph) {
151
214
  }
152
215
  function interpolateSpec(text, spec) {
153
216
  return text.replace(/\{\{\s*spec\.(\w+)\s*\}\}/g, (_match, key) => {
217
+ if (key === 'scope') {
218
+ if (!spec)
219
+ return '';
220
+ const changeName = typeof spec.openspecChangeName === 'string' ? spec.openspecChangeName.trim()
221
+ : typeof spec.metadata?.openspecChangeName === 'string' ? spec.metadata.openspecChangeName.trim() : undefined;
222
+ // Explicit task data, not arbitrary metadata or injected template code.
223
+ // Preserve all batch descriptions so later fresh verification/repair
224
+ // sessions do not have to reconstruct acceptance scope from short logs.
225
+ const scope = JSON.stringify({
226
+ id: spec.id,
227
+ ticketIds: spec.ticketIds,
228
+ title: spec.title,
229
+ description: spec.description,
230
+ repositoryIds: spec.repositoryIds,
231
+ openspecChangeName: changeName || undefined,
232
+ tickets: spec.tickets?.map(ticket => ({ id: ticket.id, title: ticket.title, description: ticket.description, repositoryIds: ticket.repositoryIds })),
233
+ }, null, 2);
234
+ if (scope === '{}')
235
+ return '';
236
+ // Constants/run-vars expand after spec data. Escape their delimiters
237
+ // inside JSON strings, along with the surrounding data block delimiter.
238
+ return scope.replace(/\{\{/g, '\\u007b\\u007b').replace(/\}\}/g, '\\u007d\\u007d').replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
239
+ }
154
240
  // `{{spec.ids}}` → all rail ticket ids as `#1 #2 #3` (used by implement/batch).
155
241
  if (key === 'ids') {
156
242
  const ids = spec?.ticketIds ?? (spec?.id != null ? [spec.id] : []);