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,27 +1,28 @@
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.LoopRunManager = void 0;
7
4
  exports.truncate = truncate;
8
5
  exports.resolveRunVars = resolveRunVars;
9
6
  exports.extractChangeId = extractChangeId;
10
7
  exports.recoverOrphanLoopStepAccounting = recoverOrphanLoopStepAccounting;
11
- const tree_kill_1 = __importDefault(require("tree-kill"));
8
+ const multi_repo_execution_store_1 = require("./multi-repo-execution-store");
9
+ const win_spawn_1 = require("./util/win-spawn");
12
10
  const db_1 = require("./db");
13
11
  const interactive_job_session_1 = require("./interactive-job-session");
14
12
  const loop_graph_1 = require("./loop-graph");
15
13
  const loop_command_catalog_1 = require("./loop-command-catalog");
16
14
  const loop_constants_1 = require("./loop-constants");
15
+ const loop_step_idle_1 = require("./loop-step-idle");
17
16
  const loop_decider_1 = require("./loop-decider");
18
17
  const loop_runs_store_1 = require("./loop-runs-store");
19
18
  const ai_invocations_1 = require("./ai-invocations");
19
+ const provider_limit_1 = require("./provider-limit");
20
20
  const ids_1 = require("./ids");
21
21
  const providers_1 = require("./providers");
22
22
  const runtime_1 = require("./providers/runtime");
23
23
  const result_event_1 = require("./result-event");
24
24
  const rails_store_1 = require("./rails-store");
25
+ const verification_sentinel_1 = require("./verification-sentinel");
25
26
  const IMPLEMENT_CMD_TOKEN_RE = /\{\{\s*cmd:(?:implement|batch)\s*\}\}/;
26
27
  const IMPLEMENT_CMD_TEXT_RE = /(?:^|\s)(?:\/(?:specrails:|sr:)?(?:implement|batch-implement)|\$(?:implement|batch-implement))\b/;
27
28
  const LOOP_BLOCKED_LINE_RE = /(?:^|\n)\s*LOOP_BLOCKED:\s*(.+?)(?:\n|$)/i;
@@ -53,6 +54,44 @@ function aiStepBlockedReason(text) {
53
54
  }
54
55
  return null;
55
56
  }
57
+ /** The interactive step's timeout failure reason (matched by isResumeMiss). */
58
+ const AI_STEP_TIMEOUT_ERROR = 'AI step timed out';
59
+ /** Whole-step zero-work judgment. The interactive path reports it
60
+ * authoritatively (`zeroWork` set at session settle); the one-shot path is
61
+ * derived from the result's accumulated signals — one-shot `text` accumulates
62
+ * ONLY from assistant text-delta events, so non-empty text ⇒ assistant events
63
+ * were seen. A hard-failed one-shot (non-zero exit) is a crash, not zero-work. */
64
+ function aiStepZeroWork(res) {
65
+ if (res.zeroWork !== undefined)
66
+ return res.zeroWork;
67
+ if (res.failed)
68
+ return false;
69
+ return (0, interactive_job_session_1.isZeroWorkSettle)({
70
+ numTurns: res.numTurns ?? 0,
71
+ tokensIn: res.tokensIn ?? 0,
72
+ tokensOut: res.tokensOut ?? 0,
73
+ tokensCacheRead: res.tokensCacheRead ?? 0,
74
+ tokensCacheCreate: res.tokensCacheCreate ?? 0,
75
+ sawAssistantEvent: res.text.trim().length > 0,
76
+ resultText: res.text.trim() ? res.text : null,
77
+ });
78
+ }
79
+ /** A RESUMED step whose settle is zero-work for a reason other than an
80
+ * unresolvable command or the step timeout: the CLI answered with session
81
+ * bookkeeping (no model call — a transcript the resume could not load, a
82
+ * session torn down mid-flight) and the prompt never ran. Worth exactly one
83
+ * fresh retry; an `Unknown command:` would fail again identically. */
84
+ function isResumeMiss(res) {
85
+ if (res.stalled)
86
+ return false;
87
+ if (!aiStepZeroWork(res))
88
+ return false;
89
+ if (interactive_job_session_1.UNKNOWN_COMMAND_RE.test(res.text.trim()))
90
+ return false;
91
+ if (res.errorText === AI_STEP_TIMEOUT_ERROR)
92
+ return false;
93
+ return true;
94
+ }
56
95
  const HISTORY_MAX_CHARS = 1500;
57
96
  /** Caps for the `loop_step` detail fields (`template` / `command`) — the
58
97
  * payload persists per event, so a runaway prompt must not bloat the events
@@ -85,7 +124,7 @@ const RUN_TOKEN_RE = /\{\{\s*run\.(\w+)\s*\}\}/g;
85
124
  /** First ACTIVE `openspec/changes/<id>` path mentioned in a step's output.
86
125
  * Archive paths (`openspec/changes/archive/...`) are historical destinations,
87
126
  * not runnable change ids for later `{{run.changeId}}` commands. */
88
- const CHANGE_ID_RE = /openspec\/changes\/(?!archive(?:\/|$))([A-Za-z0-9._-]+)(?=\/|\s|$)/g;
127
+ const CHANGE_ID_RE = /openspec[\\/]+changes[\\/]+(?!archive(?:[\\/]|$))([A-Za-z0-9._-]+)(?=[\\/]|\s|$)/g;
89
128
  /** Replace `{{run.<name>}}` with the captured value; uncaptured → '' (never a
90
129
  * leaked literal token). Applied AFTER `{{cmd:*}}` and `{{spec.*}}`. */
91
130
  function resolveRunVars(text, vars) {
@@ -318,6 +357,7 @@ class LoopRunManager {
318
357
  broadcast;
319
358
  executors;
320
359
  now;
360
+ options;
321
361
  _disposed = false;
322
362
  _cancelled = new Set();
323
363
  /** The currently-spawned child per run, so cancel/stop can actually KILL a
@@ -331,11 +371,15 @@ class LoopRunManager {
331
371
  _interactiveSteps = new Map();
332
372
  _activeStepRecovery = new Map();
333
373
  _pausedHumanDecisions = new Map();
334
- constructor(db, broadcast, executors, now = () => Date.now()) {
374
+ constructor(db, broadcast, executors, now = () => Date.now(),
375
+ /** `idleTimeoutMs`: override the env-resolved loop-step idle watchdog
376
+ * (tests). Undefined ⇒ resolveLoopStepIdleTimeoutMs() per run. */
377
+ options = {}) {
335
378
  this.db = db;
336
379
  this.broadcast = broadcast;
337
380
  this.executors = executors;
338
381
  this.now = now;
382
+ this.options = options;
339
383
  }
340
384
  /** WebSocket delivery is advisory. A disconnected/misbehaving listener must
341
385
  * never change launch, accounting, traversal, or terminal outcomes. */
@@ -365,7 +409,7 @@ class LoopRunManager {
365
409
  const child = this._activeChild.get(runId);
366
410
  if (child?.pid) {
367
411
  try {
368
- (0, tree_kill_1.default)(child.pid, 'SIGKILL', () => { });
412
+ (0, win_spawn_1.treeKillSafe)(child.pid, 'SIGKILL', () => { });
369
413
  }
370
414
  catch { /* already gone */ }
371
415
  }
@@ -473,7 +517,7 @@ class LoopRunManager {
473
517
  for (const child of this._activeChild.values()) {
474
518
  if (child?.pid) {
475
519
  try {
476
- (0, tree_kill_1.default)(child.pid, 'SIGKILL', () => { });
520
+ (0, win_spawn_1.treeKillSafe)(child.pid, 'SIGKILL', () => { });
477
521
  }
478
522
  catch { /* gone */ }
479
523
  }
@@ -489,6 +533,24 @@ class LoopRunManager {
489
533
  this._pausedHumanDecisions.clear();
490
534
  }
491
535
  async run(req) {
536
+ // Later fresh steps must verify the admitted spec, even if its caller or
537
+ // backlog changes while the implementation is running.
538
+ req = { ...req, graph: structuredClone(req.graph) };
539
+ if (req.spec)
540
+ req = { ...req, spec: structuredClone(req.spec) };
541
+ if (req.executionManifest)
542
+ req = { ...req, executionManifest: structuredClone(req.executionManifest) };
543
+ (0, loop_graph_1.assertLoopShellRepositoryScope)(req.graph, req.executionManifest?.selectedRepositoryIds ?? (req.repositoryId ? [req.repositoryId] : []));
544
+ if (req.executionManifest) {
545
+ for (const node of req.graph.nodes) {
546
+ if (node.type !== 'shell')
547
+ continue;
548
+ const repositoryId = node.data?.repositoryId;
549
+ if (typeof repositoryId !== 'string' || !req.executionManifest.repositories.some((repo) => repo.repositoryId === repositoryId)) {
550
+ throw new Error(`Shell step ${node.id} requires an explicit selected repositoryId for this execution`);
551
+ }
552
+ }
553
+ }
492
554
  if (this._disposed)
493
555
  throw new Error('LoopRunManager is shut down');
494
556
  const adapter = (0, providers_1.getAdapter)(req.provider);
@@ -512,6 +574,14 @@ class LoopRunManager {
512
574
  const aiStepTimeoutMs = req.graph.config.aiStepTimeoutMinutes != null
513
575
  ? req.graph.config.aiStepTimeoutMinutes * 60_000
514
576
  : undefined;
577
+ // Inactivity bound for every AI step (loop-step-idle): an UNTIMED step is
578
+ // still bounded by silence, so a wedged provider never leaves the run
579
+ // `running` forever. 0 ⇒ disabled.
580
+ const idleTimeoutMs = this.options.idleTimeoutMs ?? (0, loop_step_idle_1.resolveLoopStepIdleTimeoutMs)({
581
+ ...process.env,
582
+ // Preserve installations configured with the previous watchdog name.
583
+ SPECRAILS_LOOP_STEP_IDLE_TIMEOUT_MS: process.env.SPECRAILS_LOOP_STEP_IDLE_TIMEOUT_MS ?? process.env.SPECRAILS_LOOP_INACTIVITY_MS,
584
+ });
515
585
  // Optional cost cap (USD). Like maxIterations/timeout, it's a BETWEEN-STEPS
516
586
  // guard: per-step cost is only known when that step's process exits, so the
517
587
  // loop is stopped before the NEXT step once the accumulated total crosses the
@@ -549,6 +619,8 @@ class LoopRunManager {
549
619
  iterationLimit: maxIterations,
550
620
  startedAt: launchStartedAt,
551
621
  });
622
+ if (req.executionManifest)
623
+ this.db.prepare('UPDATE loop_runs SET execution_manifest = ? WHERE id = ?').run(JSON.stringify(req.executionManifest), runId);
552
624
  (0, db_1.createJob)(this.db, {
553
625
  id: runId,
554
626
  command: jobCommand,
@@ -598,8 +670,9 @@ class LoopRunManager {
598
670
  // emission (see LoopStepEventPayload).
599
671
  const emitStep = (kind, title, nodeId, iteration, detail) => {
600
672
  stepNum += 1;
601
- openStep = { index: stepNum, nodeId, startMs: this.now() };
602
- const payload = { index: stepNum, kind, title, nodeId, iteration, ...(detail ?? {}) };
673
+ const startedAtMs = this.now();
674
+ openStep = { index: stepNum, nodeId, startMs: startedAtMs };
675
+ const payload = { index: stepNum, kind, title, nodeId, iteration, startedAtMs, ...(detail ?? {}) };
603
676
  emitRunEvent('loop_step', payload);
604
677
  logLine(`\n━━━━━━ Step ${stepNum} · ${title} ━━━━━━`);
605
678
  };
@@ -619,6 +692,8 @@ class LoopRunManager {
619
692
  exitCode: input.exitCode ?? null,
620
693
  durationMs: input.durationMs ?? Math.max(0, this.now() - openStep.startMs),
621
694
  ...(input.decision ? { decision: input.decision } : {}),
695
+ ...(input.reason ? { reason: input.reason } : {}),
696
+ ...(input.idleMs != null ? { idleMs: input.idleMs } : {}),
622
697
  };
623
698
  openStep = null;
624
699
  emitRunEvent('loop_step_end', payload);
@@ -691,6 +766,8 @@ class LoopRunManager {
691
766
  const byId = (0, loop_graph_1.nodesById)(req.graph);
692
767
  const start = (0, loop_graph_1.findStartNode)(req.graph);
693
768
  let outcome = 'failed';
769
+ let stallReason;
770
+ let providerLimitHit;
694
771
  let iteration = 0;
695
772
  let totalCost = 0;
696
773
  let totalTokens = 0;
@@ -720,6 +797,10 @@ class LoopRunManager {
720
797
  // RESUMED steps already carry prior context in their session, so re-appending
721
798
  // the history there is redundant tokens; this flag keeps it off for them.
722
799
  let justContinued = false;
800
+ // Transport/test failures are engine facts. A model's STOP cannot turn a
801
+ // failed pass (or a failed final archive command) into a successful run.
802
+ // A continue starts a fresh repair pass which can establish success again.
803
+ let passFailed = false;
723
804
  // Fail-open honesty: flips true the first time a cost-bearing step yields no
724
805
  // priced cost while a cap is set (non-Claude estimate / unknown model / a
725
806
  // failed step) → the cap can under-count, so we warn once in the run log.
@@ -930,7 +1011,7 @@ class LoopRunManager {
930
1011
  outcome = 'stopped';
931
1012
  break;
932
1013
  }
933
- if (this.now() > deadline) {
1014
+ if (this.now() >= deadline) {
934
1015
  outcome = 'failed';
935
1016
  break;
936
1017
  }
@@ -962,17 +1043,23 @@ class LoopRunManager {
962
1043
  nodeId = succs[0]?.id;
963
1044
  break;
964
1045
  case 'end':
965
- outcome = node.data?.outcome === 'failure' ? 'failed' : 'success';
1046
+ outcome = node.data?.outcome === 'failure' || passFailed ? 'failed' : 'success';
966
1047
  settled = true;
967
1048
  break;
968
1049
  case 'ai-step': {
1050
+ // Some roles must inspect the previous step without inheriting its
1051
+ // provider conversation (notably Revision's mutator → reviewer
1052
+ // boundary). The node still receives durable disk state, its fully
1053
+ // rendered prompt, and bounded prior-step history.
1054
+ if (node.data?.freshSession === true)
1055
+ aiSessionId = undefined;
969
1056
  // Expand magic commands FIRST — `{{cmd:implement}}` becomes the
970
1057
  // NATIVE per-provider invocation (claude `/specrails:implement #<id>
971
1058
  // --yes`, codex `$implement #<id> --yes`) — then resolve `{{spec.*}}`
972
1059
  // data tokens and finally `{{const:*}}` library constants.
973
1060
  const rawTemplate = String(node.data?.prompt ?? '');
974
1061
  const expanded = (0, loop_constants_1.resolveConstants)(resolveRunVars((0, loop_graph_1.interpolateSpec)((0, loop_command_catalog_1.expandCommands)(rawTemplate, { provider: nodeProvider, ticketIds: req.spec?.ticketIds, specId: req.spec?.id }), req.spec), runVars), constMap);
975
- const base = withReviewContinuationContext(expanded, rawTemplate, req.spec);
1062
+ const base = [(0, multi_repo_execution_store_1.executionManifestPrompt)(req.executionManifest), withReviewContinuationContext(expanded, rawTemplate, req.spec)].filter(Boolean).join('\n\n');
976
1063
  // Inject the cross-iteration history only when there's no live session
977
1064
  // to carry it (a fresh pass) OR right after a Decider 'continue' (so the
978
1065
  // step sees the verdict). A mid-body resumed step already has it.
@@ -984,69 +1071,171 @@ class LoopRunManager {
984
1071
  // were noise once the step explorer landed; the log now opens on real
985
1072
  // output). `template` is omitted when rendering changed nothing (a
986
1073
  // plain free-text prompt would just duplicate `command`).
987
- emitStep('ai-step', `🤖 ${nodeLabel || 'AI Step'} (${nodeProvider}/${nodeModel}${nodeEffort ? `, effort: ${nodeEffort}` : ''})`, node.id, iteration + 1, {
1074
+ const stepTitle = `🤖 ${nodeLabel || 'AI Step'} (${nodeProvider}/${nodeModel}${nodeEffort ? `, effort: ${nodeEffort}` : ''})`;
1075
+ const stepDetail = {
988
1076
  ...(rawTemplate && rawTemplate !== base ? { template: capText(rawTemplate, STEP_TEMPLATE_CAP) } : {}),
989
1077
  ...(base ? { command: capText(base, STEP_COMMAND_CAP) } : {}),
990
- });
1078
+ };
1079
+ emitStep('ai-step', stepTitle, node.id, iteration + 1, stepDetail);
991
1080
  // BUG-32: capture the REAL start instant BEFORE the await — the row is
992
1081
  // bucketed/ordered by started_at, which must be the turn-start, not the
993
1082
  // finish time (this.now() evaluated after the await would be the finish).
994
- const aiStepStart = new Date(this.now()).toISOString();
995
- const aiRecoveryKey = stageAiAccounting('ai', node.id, `loop:${runId}`, aiStepStart);
1083
+ let aiStepStart = new Date(this.now()).toISOString();
1084
+ let aiRecoveryKey = stageAiAccounting('ai', node.id, `loop:${runId}`, aiStepStart);
996
1085
  // Interactive upgrade (default for persistent-stdin providers): the
997
1086
  // executors return a resident-session plan when the kill-switch is on
998
1087
  // and the provider is capable (claude); null falls through to the
999
1088
  // byte-identical one-shot spawn. The plan mirrors the one-shot's
1000
1089
  // cwd/env/argv derivation; the engine owns the session lifecycle.
1001
- const interactivePlan = this.executors.planInteractiveAiStep?.({
1002
- provider: nodeProvider,
1003
- model: nodeModel,
1004
- effort: nodeEffort,
1005
- cwd: req.cwd,
1006
- repoDir: req.repoDir,
1007
- sessionId: aiSessionId,
1008
- aiStepTimeoutMs,
1009
- }) ?? null;
1010
- const res = interactivePlan
1011
- ? await this._runInteractiveAiStep({
1012
- runId,
1013
- projectId: req.projectId,
1014
- plan: interactivePlan,
1015
- prompt,
1016
- fallbackModel: nodeModel,
1017
- nextEventSeq: takeSeq,
1018
- recoveryStepKey: aiRecoveryKey,
1019
- })
1020
- : await this.executors.runAiStep({ prompt, sessionId: aiSessionId, provider: nodeProvider, model: nodeModel, effort: nodeEffort, cwd: req.cwd, repoDir: req.repoDir, onLine: logLine, onRawLine, onSpawn: (c) => this._activeChild.set(runId, c), aiStepTimeoutMs });
1090
+ // One attempt of this step. `sessionId` is the ONLY thing that
1091
+ // differs between the resumed attempt and its fresh retry below.
1092
+ const runAttempt = async (sessionId) => {
1093
+ const remainingMs = deadline - this.now();
1094
+ const effectiveTimeoutMs = Number.isFinite(remainingMs)
1095
+ ? Math.max(1, Math.min(aiStepTimeoutMs === 0 ? Infinity : (aiStepTimeoutMs ?? 15 * 60_000), remainingMs))
1096
+ : aiStepTimeoutMs;
1097
+ const coreRun = { runId, spec: req.spec, goal: req.spec ? undefined : JSON.stringify({ loop: req.loopName, graph: req.graph }), repositoryId: req.repositoryId, verificationStep: node.data?.requireVerificationPass === true || /\{\{\s*cmd:(?:verify|revision-verify|opsx:verify)\s*\}\}/.test(rawTemplate) };
1098
+ const interactivePlan = this.executors.planInteractiveAiStep?.({
1099
+ coreRun,
1100
+ provider: nodeProvider,
1101
+ model: nodeModel,
1102
+ effort: nodeEffort,
1103
+ profileName: req.profileName,
1104
+ cwd: req.cwd,
1105
+ repoDir: req.repoDir, executionManifest: req.executionManifest,
1106
+ sessionId,
1107
+ aiStepTimeoutMs: effectiveTimeoutMs,
1108
+ idleTimeoutMs,
1109
+ }) ?? null;
1110
+ return interactivePlan
1111
+ ? this._runInteractiveAiStep({
1112
+ runId,
1113
+ projectId: req.projectId,
1114
+ plan: interactivePlan,
1115
+ prompt: [interactivePlan.promptPrefix, prompt].filter(Boolean).join('\n\n'),
1116
+ fallbackModel: nodeModel,
1117
+ nextEventSeq: takeSeq,
1118
+ recoveryStepKey: aiRecoveryKey,
1119
+ })
1120
+ : this.executors.runAiStep({ coreRun, prompt, sessionId, provider: nodeProvider, model: nodeModel, effort: nodeEffort, profileName: req.profileName, cwd: req.cwd, repoDir: req.repoDir, executionManifest: req.executionManifest, onLine: logLine, onRawLine, onSpawn: (c) => this._activeChild.set(runId, c), aiStepTimeoutMs: effectiveTimeoutMs, idleTimeoutMs });
1121
+ };
1122
+ let res = await runAttempt(aiSessionId);
1021
1123
  if (this._disposed)
1022
1124
  return neverAfterDispose();
1023
1125
  this._activeChild.delete(runId);
1126
+ // Idle stall (loop-step-idle): the provider produced no stream
1127
+ // activity for the idle budget and the watchdog tore the step down.
1128
+ // Close this attempt as `stalled`, then retry the SAME step ONCE by
1129
+ // resuming its session (the worktree + transcript carry the partial
1130
+ // work). A second stall fails the step and settles the run `stalled`
1131
+ // instead of leaving it `running` forever. A user cancel is not a
1132
+ // stall — the loop head settles it 'stopped' below.
1133
+ if (res.stalled && !this._cancelled.has(runId)) {
1134
+ const stalledSession = res.sessionId ?? aiSessionId;
1135
+ const idleMin = Math.max(1, Math.round(idleTimeoutMs / 60_000));
1136
+ emitStepEnd({ status: 'stalled', reason: 'idle_timeout', idleMs: idleTimeoutMs, durationMs: res.durationMs });
1137
+ // Each provider process owns independent accounting and recovery
1138
+ // frontiers. Settle the first attempt before staging its retry.
1139
+ record(`loop:${runId}`, { ...res, failed: true }, aiStepStart, aiRecoveryKey);
1140
+ this._activeStepRecovery.delete(runId);
1141
+ if (this.now() >= deadline || (maxCostUsd !== undefined && totalCost >= maxCostUsd)) {
1142
+ outcome = this.now() >= deadline ? 'failed' : 'max-cost';
1143
+ logLine('■ Step stalled; the run deadline or cost cap prevents another attempt.', 'stderr');
1144
+ settled = true;
1145
+ break;
1146
+ }
1147
+ logLine(`↻ Step stalled — no provider output for ${idleMin} min. Retrying this step once${stalledSession ? ` by resuming session ${stalledSession}` : ''}.`, 'stderr');
1148
+ emitStep('ai-step', stepTitle, node.id, iteration + 1, { ...stepDetail, attempt: 2 });
1149
+ aiStepStart = new Date(this.now()).toISOString();
1150
+ aiRecoveryKey = stageAiAccounting('ai', node.id, `loop:${runId}`, aiStepStart);
1151
+ res = await runAttempt(stalledSession);
1152
+ if (this._disposed)
1153
+ return neverAfterDispose();
1154
+ this._activeChild.delete(runId);
1155
+ if (res.stalled && !this._cancelled.has(runId)) {
1156
+ emitStepEnd({ status: 'stalled', reason: 'idle_timeout', idleMs: idleTimeoutMs, durationMs: res.durationMs });
1157
+ logLine(`■ Step stalled twice — no provider output for ${idleMin} min on the resumed attempt either; stopping the run.`, 'stderr');
1158
+ history.push(`AI Step: ${truncate(res.text)}`);
1159
+ record(`loop:${runId}`, { ...res, failed: true }, aiStepStart, aiRecoveryKey);
1160
+ this._activeStepRecovery.delete(runId);
1161
+ stallReason = 'idle_timeout';
1162
+ outcome = 'stalled';
1163
+ settled = true;
1164
+ break;
1165
+ }
1166
+ }
1167
+ // Resume miss (loop run 5c958db2): a step that `--resume`d the
1168
+ // previous step's session and settled with ZERO work — no model
1169
+ // call, no `Unknown command:` text — was answered by the CLI's
1170
+ // session bookkeeping (a transcript the resume could not load, a
1171
+ // session torn down mid-flight), never by the model. The prompt did
1172
+ // not run, so retry it ONCE in a fresh session instead of failing
1173
+ // the step and spinning the loop through verify/fix on nothing.
1174
+ // Unknown commands, timeouts and cancels are NOT resume misses.
1175
+ if (aiSessionId && !this._cancelled.has(runId) && isResumeMiss(res)) {
1176
+ logLine(`↻ Resuming session ${aiSessionId} produced no work — retrying this step in a fresh session.`, 'stderr');
1177
+ aiSessionId = undefined;
1178
+ res = await runAttempt(undefined);
1179
+ if (this._disposed)
1180
+ return neverAfterDispose();
1181
+ this._activeChild.delete(runId);
1182
+ }
1024
1183
  // Zero-work strictness (run 01f41203): a settle that consumed NO
1025
1184
  // model work — the claude CLI's synthetic `Unknown command:` result
1026
1185
  // frame (num_turns 0, no assistant events, zero usage tokens) —
1027
1186
  // means the step's command never actually ran. A step that didn't
1028
1187
  // run is FAILED, never 'ok'. The interactive path evaluates the
1029
1188
  // predicate at session settle (res.zeroWork set); for the one-shot
1030
- // path the engine derives it here from the result's accumulated
1031
- // signals (text accumulates ONLY from assistant text-delta events,
1032
- // so non-empty text ⇒ assistant events were seen).
1033
- const oneShotZeroWork = res.zeroWork === undefined && !res.failed && (0, interactive_job_session_1.isZeroWorkSettle)({
1034
- numTurns: res.numTurns ?? 0,
1035
- tokensIn: res.tokensIn ?? 0,
1036
- tokensOut: res.tokensOut ?? 0,
1037
- tokensCacheRead: res.tokensCacheRead ?? 0,
1038
- tokensCacheCreate: res.tokensCacheCreate ?? 0,
1039
- sawAssistantEvent: res.text.trim().length > 0,
1040
- resultText: res.text.trim() ? res.text : null,
1041
- });
1042
- const zeroWork = res.zeroWork === true || oneShotZeroWork;
1189
+ // path the engine derives it from the result's accumulated signals
1190
+ // (aiStepZeroWork).
1191
+ const zeroWork = aiStepZeroWork(res);
1192
+ const oneShotZeroWork = res.zeroWork === undefined && zeroWork;
1193
+ // Provider usage/rate limit (run 52124009): the CLI answers with the
1194
+ // limit notice as the reply. Iterating on it burns cycles and hides
1195
+ // the real cause — stop the run at once with the provider's own
1196
+ // words and the reset hint, and tell every surface.
1197
+ const providerLimit = !this._cancelled.has(runId)
1198
+ ? ((0, provider_limit_1.classifyProviderLimit)(res.errorText) ?? (0, provider_limit_1.classifyProviderLimit)(res.text))
1199
+ : null;
1200
+ if (providerLimit) {
1201
+ const described = (0, provider_limit_1.describeProviderLimit)(providerLimit);
1202
+ emitStepEnd({ status: 'failed', reason: 'provider_limit', durationMs: res.durationMs });
1203
+ logLine(`■ Provider limit reached (${req.provider}): ${described}. Stopping the loop now — relaunch or Recover & retry after the reset.`, 'stderr');
1204
+ history.push(`AI Step: ${truncate(res.text)}`);
1205
+ record(`loop:${runId}`, { ...res, failed: true }, aiStepStart, aiRecoveryKey);
1206
+ this._activeStepRecovery.delete(runId);
1207
+ this._emit({
1208
+ type: 'loop.provider_limit',
1209
+ projectId: req.projectId,
1210
+ runId,
1211
+ provider: req.provider,
1212
+ kind: providerLimit.kind,
1213
+ message: providerLimit.message,
1214
+ resetsAt: providerLimit.resetsAt,
1215
+ timestamp: new Date().toISOString(),
1216
+ });
1217
+ stallReason = 'provider_limit';
1218
+ providerLimitHit = providerLimit;
1219
+ outcome = 'stalled';
1220
+ settled = true;
1221
+ break;
1222
+ }
1043
1223
  const blockedReason = aiStepBlockedReason(res.text);
1044
- const stepFailed = res.failed === true || zeroWork || blockedReason !== null;
1224
+ const requiresVerification = node.data?.requireVerificationPass === true
1225
+ || /\{\{cmd:(?:verify|revision-verify|opsx:verify)\}\}/.test(rawTemplate);
1226
+ const verificationFailed = requiresVerification
1227
+ && (0, verification_sentinel_1.parseVerificationSentinel)(res.text).verdict !== 'pass';
1228
+ const stepFailed = res.failed === true || zeroWork || blockedReason !== null || verificationFailed;
1229
+ passFailed ||= res.failed === true || zeroWork || (verificationFailed && !blockedReason);
1045
1230
  const stepErrorText = res.errorText ?? (zeroWork
1046
1231
  ? `zero work performed — the command never ran${res.text.trim() ? `: ${res.text.trim()}` : ''}`
1047
1232
  : blockedReason
1048
1233
  ? `blocked — ${blockedReason}`
1049
- : undefined);
1234
+ : verificationFailed
1235
+ ? 'verification did not finish with VERIFICATION: PASS'
1236
+ : undefined);
1237
+ if (verificationFailed && !blockedReason)
1238
+ logLine(`✖ ${stepErrorText}`, 'stderr');
1050
1239
  // Make the failure reason land visibly INSIDE the step's log
1051
1240
  // segment (the interactive session already surfaced its own note at
1052
1241
  // settle; the Template/Command flat-log lines are gone, so without
@@ -1067,7 +1256,9 @@ class LoopRunManager {
1067
1256
  // would otherwise make the next step `--resume` a dead session.
1068
1257
  if (res.sessionId && (!stepFailed || (blockedReason !== null && !res.failed && !zeroWork)))
1069
1258
  aiSessionId = res.sessionId;
1070
- history.push(`AI Step: ${truncate(res.text)}`);
1259
+ else if (stepFailed)
1260
+ aiSessionId = undefined;
1261
+ history.push(`AI Step${stepFailed ? ` FAILED (${stepErrorText ?? 'provider invocation failed'})` : ''}: ${truncate(res.text)}`);
1071
1262
  record(`loop:${runId}`, { ...res, failed: stepFailed }, aiStepStart, aiRecoveryKey);
1072
1263
  this._activeStepRecovery.delete(runId);
1073
1264
  if (blockedReason) {
@@ -1101,7 +1292,11 @@ class LoopRunManager {
1101
1292
  // (its `Unknown command:` text is a CLI synthetic, not model output),
1102
1293
  // so a persistently-unresolvable command aborts the run identically
1103
1294
  // instead of grinding to the cap.
1104
- if (stepFailed && (zeroWork || !res.text.trim())) {
1295
+ if (this._cancelled.has(runId)) {
1296
+ // A user cancel tears the step down with no output — that is not
1297
+ // a provider failure; the loop head settles the run 'stopped'.
1298
+ }
1299
+ else if (stepFailed && (zeroWork || res.resultIsError || !res.text.trim())) {
1105
1300
  consecutiveAiFailures += 1;
1106
1301
  if (consecutiveAiFailures >= AI_FAILFAST_THRESHOLD) {
1107
1302
  logLine(`Loop aborted: provider appears down — ${consecutiveAiFailures} AI steps failed with no output and no successful AI call in between${stepErrorText ? ` — ${stepErrorText}` : ''}`, 'stderr');
@@ -1138,12 +1333,14 @@ class LoopRunManager {
1138
1333
  const command = (0, loop_constants_1.resolveConstants)(resolveRunVars((0, loop_graph_1.interpolateSpec)(String(node.data?.command ?? ''), req.spec), runVars), constMap);
1139
1334
  emitStep('shell', `⚡ ${nodeLabel || 'Shell'}`, node.id, iteration + 1);
1140
1335
  logLine(`$ ${command}`);
1141
- const sh = await this.executors.runShell({ command, cwd: req.cwd, onLine: logLine, onSpawn: (c) => this._activeChild.set(runId, c) });
1336
+ const shellRepo = req.executionManifest?.repositories.find((repo) => repo.repositoryId === node.data?.repositoryId)?.worktreePath ?? req.repoDir;
1337
+ const sh = await this.executors.runShell({ command, cwd: req.executionManifest ? shellRepo ?? req.cwd : req.cwd, repoDir: shellRepo, timeoutMs: Math.min(10 * 60_000, Math.max(1, deadline - this.now())), onLine: logLine, onSpawn: (c) => this._activeChild.set(runId, c) });
1142
1338
  if (this._disposed)
1143
1339
  return neverAfterDispose();
1144
1340
  this._activeChild.delete(runId);
1145
1341
  logLine(`(exit ${sh.exitCode})`);
1146
1342
  emitStepEnd({ status: sh.exitCode === 0 ? 'ok' : 'failed', exitCode: sh.exitCode, durationMs: sh.durationMs });
1343
+ passFailed ||= sh.exitCode !== 0;
1147
1344
  totalDuration += sh.durationMs ?? 0;
1148
1345
  (0, loop_runs_store_1.updateLoopRunCounters)(this.db, runId, {
1149
1346
  iterationCount: iteration,
@@ -1169,23 +1366,27 @@ class LoopRunManager {
1169
1366
  // BUG-32: capture the real Decider start BEFORE the await (see AI step).
1170
1367
  const deciderStart = new Date(this.now()).toISOString();
1171
1368
  const deciderRecoveryKey = stageAiAccounting('decider', node.id, `loop:${runId}:decider`, deciderStart);
1172
- const dec = await this.executors.runDecider({
1369
+ let dec = await this.executors.runDecider({
1173
1370
  systemPrompt: (0, loop_decider_1.buildDeciderSystemPrompt)(),
1174
1371
  // Give the Decider the spec so it can verify completeness against the
1175
1372
  // FULL scope instead of trusting a step's self-reported success.
1176
- userPrompt: (0, loop_decider_1.buildDeciderUserPrompt)({ goal, history, spec: req.spec ? { title: req.spec.title, description: req.spec.description } : undefined }),
1373
+ userPrompt: [(0, multi_repo_execution_store_1.executionManifestPrompt)(req.executionManifest), (0, loop_decider_1.buildDeciderUserPrompt)({ goal, history, spec: req.spec })].filter(Boolean).join('\n\n'),
1177
1374
  provider: nodeProvider,
1178
1375
  model: nodeModel,
1179
1376
  effort: nodeEffort,
1180
1377
  cwd: req.cwd,
1181
- repoDir: req.repoDir,
1378
+ repoDir: req.repoDir, executionManifest: req.executionManifest,
1182
1379
  onLine: logLine,
1183
1380
  onRawLine,
1184
1381
  onSpawn: (c) => this._activeChild.set(runId, c),
1382
+ timeoutMs: Math.min(3 * 60_000, Math.max(1, deadline - this.now())),
1185
1383
  });
1186
1384
  if (this._disposed)
1187
1385
  return neverAfterDispose();
1188
1386
  this._activeChild.delete(runId);
1387
+ if ((!dec.parsed || passFailed) && !dec.blocked && !dec.continue) {
1388
+ dec = { ...dec, continue: true, reasoning: `A required step failed in this pass; repair and verify before completion. ${dec.reasoning}` };
1389
+ }
1189
1390
  // A parseable Decider verdict means this AI invocation (SAME
1190
1391
  // provider/model) succeeded → the provider is alive, so clear any
1191
1392
  // ai-step failure streak. This stops a false fail-fast abort when an
@@ -1257,14 +1458,16 @@ class LoopRunManager {
1257
1458
  // (e.g. verify keeps passing a baseline while the feature is never
1258
1459
  // written) — abort `stalled` rather than cycle to the cap.
1259
1460
  if (dec.continue && this.executors.repoStateHash) {
1260
- const dir = req.repoDir ?? req.cwd;
1261
- const hash = this.executors.repoStateHash(dir);
1461
+ const dirs = req.executionManifest?.repositories.map((repo) => repo.worktreePath) ?? [req.repoDir ?? req.cwd];
1462
+ const hashes = dirs.map((dir) => this.executors.repoStateHash(dir));
1463
+ const hash = hashes.some((value) => value === null) ? null : JSON.stringify(hashes);
1262
1464
  // null = couldn't read the tree → skip (never let null match null).
1263
1465
  if (hash !== null) {
1264
1466
  if (hash === lastRepoHash) {
1265
1467
  consecutiveNoProgress += 1;
1266
1468
  if (consecutiveNoProgress >= NO_PROGRESS_LIMIT) {
1267
1469
  logLine(`\n■ Loop stalled — ${consecutiveNoProgress + 1} iterations changed nothing in the working tree; stopping instead of cycling.`, 'stderr');
1470
+ stallReason = 'no_progress';
1268
1471
  outcome = 'stalled';
1269
1472
  settled = true;
1270
1473
  break;
@@ -1277,9 +1480,17 @@ class LoopRunManager {
1277
1480
  }
1278
1481
  }
1279
1482
  if (dec.continue) {
1483
+ // Enforce the cap before another expensive implementation pass,
1484
+ // not after that extra pass reaches the next decider.
1485
+ if (iteration >= maxIterations) {
1486
+ outcome = 'max-iterations';
1487
+ settled = true;
1488
+ break;
1489
+ }
1280
1490
  const target = branchTarget('continue') ?? conts[0]?.id;
1281
- if (target) {
1491
+ if (target && byId.get(target)?.type !== 'end') {
1282
1492
  nodeId = target;
1493
+ passFailed = false;
1283
1494
  // The next step acts on this verdict → let it see the history.
1284
1495
  justContinued = true;
1285
1496
  // New iteration of an iterate-per-item loop → drop the resumed AI
@@ -1291,7 +1502,7 @@ class LoopRunManager {
1291
1502
  aiSessionId = undefined;
1292
1503
  }
1293
1504
  else {
1294
- outcome = 'success';
1505
+ outcome = 'failed';
1295
1506
  settled = true;
1296
1507
  }
1297
1508
  }
@@ -1307,7 +1518,14 @@ class LoopRunManager {
1307
1518
  break;
1308
1519
  }
1309
1520
  case 'condition':
1310
- // v1: AND/OR branching is not yet evaluated — follow the first edge.
1521
+ // Multiple branches have no implemented condition evaluator. Never
1522
+ // silently skip required branches and report a successful run.
1523
+ if (succs.length !== 1) {
1524
+ logLine('Condition branching is unsupported; replace it with explicit sequential steps or a Decider.', 'stderr');
1525
+ outcome = 'failed';
1526
+ settled = true;
1527
+ break;
1528
+ }
1311
1529
  nodeId = succs[0]?.id;
1312
1530
  break;
1313
1531
  default:
@@ -1315,7 +1533,7 @@ class LoopRunManager {
1315
1533
  }
1316
1534
  // Ran off the end of the graph without an explicit End node.
1317
1535
  if (nodeId === undefined && !settled) {
1318
- outcome = 'success';
1536
+ outcome = 'failed';
1319
1537
  settled = true;
1320
1538
  }
1321
1539
  }
@@ -1455,6 +1673,8 @@ class LoopRunManager {
1455
1673
  outcome,
1456
1674
  iterations: iteration,
1457
1675
  totalCostUsd: usageTelemetryAvailable ? totalCost : null,
1676
+ ...(outcome === 'stalled' && stallReason ? { stallReason } : {}),
1677
+ ...(providerLimitHit ? { providerLimit: providerLimitHit } : {}),
1458
1678
  };
1459
1679
  }
1460
1680
  /**
@@ -1486,6 +1706,9 @@ class LoopRunManager {
1486
1706
  return new Promise((resolve) => {
1487
1707
  let timer = null;
1488
1708
  let timedOut = false;
1709
+ // Set by the session's wedge detector BEFORE it aborts: tells a stall
1710
+ // (no raw output for the idle budget) apart from any other 'crashed'.
1711
+ let stalled = false;
1489
1712
  const session = new interactive_job_session_1.InteractiveJobSession({
1490
1713
  jobId: input.runId,
1491
1714
  projectId: input.projectId,
@@ -1493,8 +1716,13 @@ class LoopRunManager {
1493
1716
  adapter: input.plan.adapter,
1494
1717
  broadcast: this.broadcast,
1495
1718
  settleMode: 'auto',
1496
- // The step timeout below is the sole watchdog — the one-shot loop path
1497
- // has no zombie detector either (byte-parity), so none is armed here.
1719
+ // Inactivity bound (loop-step-idle): the session's zombie timer IS the
1720
+ // step's idle watchdog — reset on any raw child output, silence for the
1721
+ // whole budget folds the in-flight turn and settles 'crashed' with the
1722
+ // `stalled` flag set so the engine retries the step once by resume.
1723
+ // Absent (0) ⇒ the step timeout below is the only watchdog.
1724
+ zombieTimeoutMs: input.plan.idleTimeoutMs ?? input.plan.inactivityTimeoutMs ?? 0,
1725
+ onZombieTimeout: () => { stalled = true; },
1498
1726
  nextEventSeq: input.nextEventSeq,
1499
1727
  persistTurnUsage: (usage, completedEventSeq, checkpoint) => {
1500
1728
  const checkpointTurn = this.db.transaction(() => {
@@ -1529,7 +1757,7 @@ class LoopRunManager {
1529
1757
  // command:` frame) is a FAILED step — the command never ran. Judged
1530
1758
  // by the session itself over the accumulated totals (whole-session,
1531
1759
  // so a multi-turn step ending on one synthetic frame is unaffected).
1532
- const failed = info.reason === 'crashed' || info.zeroWork;
1760
+ const failed = info.reason === 'crashed' || info.zeroWork || info.resultIsError;
1533
1761
  resolve({
1534
1762
  // The last turn's `result` payload — the same terminal text the
1535
1763
  // one-shot path captures (feeds history/changeId/fail-fast).
@@ -1554,9 +1782,13 @@ class LoopRunManager {
1554
1782
  zeroWork: info.zeroWork,
1555
1783
  errorText: failed
1556
1784
  ? (info.reason === 'crashed'
1557
- ? (timedOut ? 'AI step timed out' : 'interactive step session crashed')
1558
- : `zero work performed — the command never ran${info.resultText ? `: ${info.resultText}` : ''}`)
1785
+ ? (stalled ? loop_step_idle_1.AI_STEP_STALLED_ERROR : timedOut ? AI_STEP_TIMEOUT_ERROR : info.resultIsError && info.resultText ? info.resultText : 'interactive step session crashed')
1786
+ : info.zeroWork
1787
+ ? `zero work performed — the command never ran${info.resultText ? `: ${info.resultText}` : ''}`
1788
+ : `provider returned an error result${info.resultText ? ` — ${info.resultText}` : ''}`)
1559
1789
  : undefined,
1790
+ ...(stalled && info.reason === 'crashed' ? { stalled: true } : {}),
1791
+ ...(info.resultIsError ? { resultIsError: true } : {}),
1560
1792
  });
1561
1793
  },
1562
1794
  });