specrails-desktop 2.29.1 → 2.29.3

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 (548) hide show
  1. package/client/dist/assets/ActivityFeedPage-CFB4UBhT.js +1 -0
  2. package/client/dist/assets/AgentBrowserCapture-C-T0r16B.js +1 -0
  3. package/client/dist/assets/AgentModeCodePane-IljV5FOQ.js +2 -0
  4. package/client/dist/assets/AgentModeJobsPane-B5miH10_.js +1 -0
  5. package/client/dist/assets/AgentsPage-BgYQrAcH.js +86 -0
  6. package/client/dist/assets/AnalyticsPage-BtVCuNjL.js +1 -0
  7. package/client/dist/assets/BarChart-nBOMNfiy.js +33 -0
  8. package/client/dist/assets/CodePage-Cb_jAjSV.js +2 -0
  9. package/client/dist/assets/DesktopAnalyticsPage-s_Cc_JL1.js +1 -0
  10. package/client/dist/assets/DocsDialog-Cr47QI9L.js +11 -0
  11. package/client/dist/assets/DocsPage-C_WPRuG_.js +11 -0
  12. package/client/dist/assets/ExportDropdown-BrtfWzEG.js +1 -0
  13. package/client/dist/assets/IntegrationsPage-Bs00_tl4.js +1 -0
  14. package/client/dist/assets/InteractiveJobComposer-NfFOciRB.js +19 -0
  15. package/client/dist/assets/JobDetailModal-DqEW7Z7K.js +1 -0
  16. package/client/dist/assets/JobDetailPage-D4ppEpDE.js +1 -0
  17. package/client/dist/assets/JobsPage-DStSoMyq.js +1 -0
  18. package/client/dist/assets/LoopBuilderPage-DltCwS4P.js +7 -0
  19. package/client/dist/assets/LoopPreviewModal-BWaiAbUq.js +1 -0
  20. package/client/dist/assets/LoopsPage-BqjbXuBZ.js +1 -0
  21. package/client/dist/assets/MinimizedChatsContext--o3JAM9F.js +1 -0
  22. package/client/dist/assets/PluginsPage-B-UOmzXw.js +2 -0
  23. package/client/dist/assets/ProjectSettingsDialog-DkGajIxE.js +1 -0
  24. package/client/dist/assets/TemplatePreviewModal-5CUXlkw7.js +1 -0
  25. package/client/dist/assets/TicketDetailModal-B_igqmti.js +14 -0
  26. package/client/dist/assets/Trans-DDMIBBUd.js +1 -0
  27. package/client/dist/assets/activity-DiyjsgQV.js +1 -0
  28. package/client/dist/assets/addon-image-BRmmRHgQ.js +1 -0
  29. package/client/dist/assets/addon-ligatures-qIZUPvRV.js +2 -0
  30. package/client/dist/assets/{addon-webgl-DMbGeI_u.js → addon-webgl-C2d7lriQ.js} +3 -3
  31. package/client/dist/assets/addspec-Rw8uVBC1.js +1 -0
  32. package/client/dist/assets/agent-BXc59ARK2.js +1 -0
  33. package/client/dist/assets/agent-BbIFXdDq.js +1 -0
  34. package/client/dist/assets/agent-BqEfA5u5.js +1 -0
  35. package/client/dist/assets/agent-BtGzgTC4.js +1 -0
  36. package/client/dist/assets/agent-CIhys_5d.js +1 -0
  37. package/client/dist/assets/agent-CJPaqsVR.js +1 -0
  38. package/client/dist/assets/agent-n-hY4GuL.js +1 -0
  39. package/client/dist/assets/agent-tJ-efa3_2.js +1 -0
  40. package/client/dist/assets/agents-DhG7mg6-.js +1 -0
  41. package/client/dist/assets/agentstudio-BIUe1Im2.js +1 -0
  42. package/client/dist/assets/aiedit-iIjz2_Qk.js +1 -0
  43. package/client/dist/assets/analytics-DZ8Aoq3I.js +1 -0
  44. package/client/dist/assets/api-B2M7CtDw.js +1 -0
  45. package/client/dist/assets/arrow-left-CpIR4j7G.js +1 -0
  46. package/client/dist/assets/arrow-up-right-9DoMn7fD.js +1 -0
  47. package/client/dist/assets/attachments-B53PG733.js +1 -0
  48. package/client/dist/assets/brain-DI9lHkKY.js +1 -0
  49. package/client/dist/assets/browser-CXUcnTMI.js +1 -0
  50. package/client/dist/assets/button-BUIIBQPT.js +1 -0
  51. package/client/dist/assets/chat-BGD7MXAy.js +1 -0
  52. package/client/dist/assets/chevron-right-CViMxucY.js +1 -0
  53. package/client/dist/assets/chunk-KS7C4IRE-Cx5sqopX.js +3 -0
  54. package/client/dist/assets/circle-x-CMYsN4ZH.js +1 -0
  55. package/client/dist/assets/code-BDXEM4ml.js +1 -0
  56. package/client/dist/assets/commands-DyUZQ1FI.js +1 -0
  57. package/client/dist/assets/common-6MLbD-jM.js +1 -0
  58. package/client/dist/assets/common-BKGEu4rm.js +1 -0
  59. package/client/dist/assets/common-BNbnv8cE2.js +1 -0
  60. package/client/dist/assets/common-BqGWtDbt.js +1 -0
  61. package/client/dist/assets/common-DBtTzeYm2.js +1 -0
  62. package/client/dist/assets/common-DEW3Nr8n.js +1 -0
  63. package/client/dist/assets/common-DHsv9N5d.js +1 -0
  64. package/client/dist/assets/common-mkrEcW-Y.js +1 -0
  65. package/client/dist/assets/createLucideIcon-Bjbbp_KS.js +1 -0
  66. package/client/dist/assets/{css.worker-Wv5dxAWO.js → css.worker-yKcq13f2.js} +13 -13
  67. package/client/dist/assets/{cssMode-DBHbkBvI.js → cssMode-BM4z9OX0.js} +1 -1
  68. package/client/dist/assets/dashboard-BYzxom0L.js +1 -0
  69. package/client/dist/assets/dashboard-BmDZmHyY.js +1 -0
  70. package/client/dist/assets/dashboard-CnNtJBQ5.js +1 -0
  71. package/client/dist/assets/dashboard-D4Uj-XWc2.js +1 -0
  72. package/client/dist/assets/dashboard-D57uRaGw2.js +1 -0
  73. package/client/dist/assets/dashboard-DAr6F9et.js +1 -0
  74. package/client/dist/assets/dashboard-vAcsn0L3.js +1 -0
  75. package/client/dist/assets/dashboard-vrjZvxWy.js +1 -0
  76. package/client/dist/assets/dialog-CzIekCw0.js +45 -0
  77. package/client/dist/assets/dist-Cez5CRkX.js +1 -0
  78. package/client/dist/assets/dist-bVqfYmby.js +1 -0
  79. package/client/dist/assets/{dist-js-CgpP6tKI.js → dist-js-BCUhGUXS.js} +1 -1
  80. package/client/dist/assets/{dist-js-DHiN-A_E.js → dist-js-DcoXTnEI.js} +1 -1
  81. package/client/dist/assets/download-CFHyyHcw.js +1 -0
  82. package/client/dist/assets/editor.api2-4ndOupAA.js +872 -0
  83. package/client/dist/assets/{editor.main-C9J2ikJK.js → editor.main-DxrmGfoq.js} +2 -2
  84. package/client/dist/assets/editor.worker-tytyn86m.js +26 -0
  85. package/client/dist/assets/explore-DgBACn39.js +1 -0
  86. package/client/dist/assets/external-link-BcU5EWZp.js +1 -0
  87. package/client/dist/assets/file-text-CFT-xXKN.js +1 -0
  88. package/client/dist/assets/formatDistanceToNow-DP3Ab6fX.js +1 -0
  89. package/client/dist/assets/{freemarker2-J5iIIIK-.js → freemarker2-CELlTDJ2.js} +1 -1
  90. package/client/dist/assets/getTimezoneOffsetInMilliseconds-DdKKL8Fx.js +1 -0
  91. package/client/dist/assets/git-branch-BRM0F5GN.js +1 -0
  92. package/client/dist/assets/{handlebars-DSw1l_tw.js → handlebars-NEjHJ7o_.js} +1 -1
  93. package/client/dist/assets/{html-h2-wE1Lj.js → html-CA10zN5A.js} +1 -1
  94. package/client/dist/assets/{html.worker-CQP8QQsS.js → html.worker-DSJ9zmd7.js} +12 -12
  95. package/client/dist/assets/{htmlMode-Cc_ij7lW.js → htmlMode-89rPYiyq.js} +1 -1
  96. package/client/dist/assets/i18n-DREQCE2F.js +2 -0
  97. package/client/dist/assets/index-B784m8RT.js +76 -0
  98. package/client/dist/assets/index-CQBIemiz.css +2 -0
  99. package/client/dist/assets/integrations-mB-7zoyT.js +1 -0
  100. package/client/dist/assets/{javascript-C1q3xv6N.js → javascript-B1gDmwzS.js} +1 -1
  101. package/client/dist/assets/jira-CmKWERE0.js +1 -0
  102. package/client/dist/assets/jira-api-BgFIr8eD.js +1 -0
  103. package/client/dist/assets/job-time-BCL7wee9.js +1 -0
  104. package/client/dist/assets/jobs-B-sDHEay2.js +1 -0
  105. package/client/dist/assets/jobs-BCtGfgUA.js +1 -0
  106. package/client/dist/assets/jobs-BHR_EnDI.js +1 -0
  107. package/client/dist/assets/jobs-BMbZJjJJ.js +1 -0
  108. package/client/dist/assets/jobs-CVFD8rar.js +1 -0
  109. package/client/dist/assets/jobs-DoSaFzAa.js +1 -0
  110. package/client/dist/assets/jobs-DzSIR8V62.js +1 -0
  111. package/client/dist/assets/jobs-lk1_Dq1U.js +1 -0
  112. package/client/dist/assets/json.worker-Oa9UrkKE.js +58 -0
  113. package/client/dist/assets/{jsonMode-CeaCdEhr.js → jsonMode-xHLMfzhF.js} +1 -1
  114. package/client/dist/assets/jsx-runtime-zu2_FqZY.js +1 -0
  115. package/client/dist/assets/lib-C7Mhm-gT.js +7 -0
  116. package/client/dist/assets/lib-CGdyxIWg.js +29 -0
  117. package/client/dist/assets/{liquid-eFsVK0V-.js → liquid-BY6hCUXG.js} +1 -1
  118. package/client/dist/assets/loader-circle-DcygOQpC.js +1 -0
  119. package/client/dist/assets/loops-BzfXWEwO.js +1 -0
  120. package/client/dist/assets/{lspLanguageFeatures-DT3-bbhn.js → lspLanguageFeatures-ira2aEJh.js} +1 -1
  121. package/client/dist/assets/mcp-uDkXYu4L.js +1 -0
  122. package/client/dist/assets/{mdx-Fuwnh1cX.js → mdx-BNx0xr3N.js} +1 -1
  123. package/client/dist/assets/monaco.contribution-Bt92R3Vw.js +2 -0
  124. package/client/dist/assets/nav-myFaXRHD.js +1 -0
  125. package/client/dist/assets/pencil-C9RkrIPZ.js +1 -0
  126. package/client/dist/assets/play-DNlrawbS.js +1 -0
  127. package/client/dist/assets/plus-CsnjfkmX.js +1 -0
  128. package/client/dist/assets/preload-helper-Czpn1I53.js +1 -0
  129. package/client/dist/assets/{python-BgoEs6Cj.js → python-DLdbtmNw.js} +1 -1
  130. package/client/dist/assets/{razor-CM1sPMMq.js → razor-B9gdhAbR.js} +1 -1
  131. package/client/dist/assets/react-dom-DiWzBDP9.js +1 -0
  132. package/client/dist/assets/rolldown-runtime-CNC7AqOf.js +1 -0
  133. package/client/dist/assets/save-28sIlCdr.js +1 -0
  134. package/client/dist/assets/search-Cq_gE3Ik.js +1 -0
  135. package/client/dist/assets/settings-B0ypw9uw.js +1 -0
  136. package/client/dist/assets/setup-CmK87LuS.js +1 -0
  137. package/client/dist/assets/sparkles-DrY8hb0X.js +1 -0
  138. package/client/dist/assets/specs-DII0C8gw.js +1 -0
  139. package/client/dist/assets/spending-CtY7yYes.js +1 -0
  140. package/client/dist/assets/terminal-BFRZnMPP.js +1 -0
  141. package/client/dist/assets/tickets-B8BuAaj5.js +1 -0
  142. package/client/dist/assets/tickets-B_yh4qQk.js +1 -0
  143. package/client/dist/assets/tickets-Bs37scwY2.js +1 -0
  144. package/client/dist/assets/tickets-CFk2n2Or.js +1 -0
  145. package/client/dist/assets/tickets-DQp7JS2R2.js +1 -0
  146. package/client/dist/assets/tickets-D_yDquEw.js +1 -0
  147. package/client/dist/assets/tickets-DpmdE8tv2.js +1 -0
  148. package/client/dist/assets/tickets-pAUJkDTu.js +1 -0
  149. package/client/dist/assets/trash-2-BOU3WdZZ.js +1 -0
  150. package/client/dist/assets/triangle-alert-H9T4jAmh.js +1 -0
  151. package/client/dist/assets/{ts.worker-METxwbDZ.js → ts.worker-BtjBFNHX.js} +48 -33
  152. package/client/dist/assets/{tsMode-BmT8WogI.js → tsMode-DEjSThU9.js} +1 -1
  153. package/client/dist/assets/{typescript-CQrQPsoW.js → typescript-CRRnD2Ts.js} +1 -1
  154. package/client/dist/assets/upload-DYMjAQ6M.js +1 -0
  155. package/client/dist/assets/useBrowserCaptureSession-BFBdW59q.js +1 -0
  156. package/client/dist/assets/useDesktop-uWVZ8eia.js +1 -0
  157. package/client/dist/assets/useSharedWebSocket-DaPh7QBL.js +1 -0
  158. package/client/dist/assets/utils-BC4DHlz2.js +1 -0
  159. package/client/dist/assets/value-s-gzBeyF.js +1 -0
  160. package/client/dist/assets/{workers-U0UXyLUf.js → workers-uOo7WLaa.js} +1 -1
  161. package/client/dist/assets/x-CBMI9ejn.js +1 -0
  162. package/client/dist/assets/{xml-WRxJs8_I.js → xml-EDREH2X7.js} +1 -1
  163. package/client/dist/assets/{yaml-CjpSTjJU.js → yaml-BjaDVX1E.js} +1 -1
  164. package/client/dist/assets/zap-CHo3aCD1.js +1 -0
  165. package/client/dist/index.html +73 -32
  166. package/docs/guide/en/integrations/5-mcp-server.md +2 -2
  167. package/docs/internals/safe-pr-review-flow.md +382 -141
  168. package/docs/mcp.md +15 -5
  169. package/package.json +11 -7
  170. package/server/dist/active-pr-continuation.js +250 -50
  171. package/server/dist/agent-chat-manager.js +387 -191
  172. package/server/dist/agent-cwd-manager.js +52 -8
  173. package/server/dist/agent-generator.js +90 -48
  174. package/server/dist/agent-mcp-config.js +69 -36
  175. package/server/dist/agent-operator-prompt.js +3 -2
  176. package/server/dist/agent-refine-manager.js +47 -6
  177. package/server/dist/agent-store.js +19 -4
  178. package/server/dist/agent-tier.js +13 -11
  179. package/server/dist/artifact-registry.js +33 -2
  180. package/server/dist/auth.js +5 -4
  181. package/server/dist/browser-capture-manager.js +92 -29
  182. package/server/dist/chat-manager.js +302 -46
  183. package/server/dist/contract-refine-runner.js +19 -6
  184. package/server/dist/db.js +686 -8
  185. package/server/dist/desktop-router.js +1 -2
  186. package/server/dist/headroom-manager.js +1310 -121
  187. package/server/dist/headroom-routing.js +120 -2
  188. package/server/dist/index.js +48 -13
  189. package/server/dist/interactive-job-session.js +237 -82
  190. package/server/dist/jira/jira-adf.js +10 -0
  191. package/server/dist/jira/jira-db.js +15 -3
  192. package/server/dist/jira/jira-sync-manager.js +182 -13
  193. package/server/dist/job-listing.js +140 -0
  194. package/server/dist/job-spawn-idempotency.js +67 -0
  195. package/server/dist/loop-run-manager.js +508 -98
  196. package/server/dist/loop-runs-store.js +266 -13
  197. package/server/dist/mcp/agent-capability.js +71 -0
  198. package/server/dist/mcp/guide.js +5 -2
  199. package/server/dist/mcp/mcp-server.js +5 -10
  200. package/server/dist/mcp/mcp-token.js +5 -5
  201. package/server/dist/mcp/tools/jobs.js +20 -13
  202. package/server/dist/mcp/tools/types.js +20 -42
  203. package/server/dist/mobile/mobile-router.js +1 -1
  204. package/server/dist/pr-lifecycle.js +169 -0
  205. package/server/dist/pr-publisher.js +94 -27
  206. package/server/dist/process-admission.js +82 -0
  207. package/server/dist/project-git.js +62 -2
  208. package/server/dist/project-registry.js +572 -174
  209. package/server/dist/project-router-git.js +13 -2
  210. package/server/dist/project-router-jobs.js +111 -55
  211. package/server/dist/project-router-tickets.js +32 -2
  212. package/server/dist/providers/claude-adapter.js +21 -13
  213. package/server/dist/queue-manager.js +2332 -623
  214. package/server/dist/rail-isolated-launch.js +1679 -296
  215. package/server/dist/rail-isolation.js +4 -2
  216. package/server/dist/rail-pr-decision.js +1665 -307
  217. package/server/dist/rail-pr-delivery.js +2 -1
  218. package/server/dist/rail-pr-recovery-git.js +235 -0
  219. package/server/dist/rail-pr-store.js +429 -19
  220. package/server/dist/rail-pr-ticket-effects.js +328 -0
  221. package/server/dist/rail-worktree-release.js +305 -3
  222. package/server/dist/rails-router.js +238 -120
  223. package/server/dist/rails-store.js +106 -3
  224. package/server/dist/ticket-store.js +23 -1
  225. package/server/dist/transient-children.js +13 -0
  226. package/server/dist/util/cli-prompt.js +42 -8
  227. package/server/dist/util/secure-token-file.js +84 -0
  228. package/server/dist/worktree-manager.js +126 -21
  229. package/server/dist/worktree-overlay.js +166 -4
  230. package/client/dist/assets/ActivityFeedPage-D8RKRRoM.js +0 -1
  231. package/client/dist/assets/AgentBrowserCapture-BUHPHNSN.js +0 -1
  232. package/client/dist/assets/AgentModeCodePane-GZc8bNGT.js +0 -2
  233. package/client/dist/assets/AgentModeJobsPane-DWtQqj3k.js +0 -1
  234. package/client/dist/assets/AgentsPage-CKPtHkRP.js +0 -86
  235. package/client/dist/assets/AnalyticsPage-CZW8c77k.js +0 -1
  236. package/client/dist/assets/BarChart-DugEoe2q.js +0 -33
  237. package/client/dist/assets/CodePage-CQ-uKETG.js +0 -2
  238. package/client/dist/assets/DesktopAnalyticsPage-Bgo4SyCH.js +0 -1
  239. package/client/dist/assets/DocsDialog-DxpWw_La.js +0 -11
  240. package/client/dist/assets/DocsPage-CmcOkK19.js +0 -11
  241. package/client/dist/assets/ExportDropdown-DvTS2wc7.js +0 -1
  242. package/client/dist/assets/IntegrationsPage-B72WxJ3Z.js +0 -1
  243. package/client/dist/assets/InteractiveJobComposer-B5gh4Qoy.js +0 -19
  244. package/client/dist/assets/JobDetailModal-D-BeqsJ6.js +0 -1
  245. package/client/dist/assets/JobDetailPage-DwdozwLO.js +0 -1
  246. package/client/dist/assets/JobsPage-BDkinklt.js +0 -1
  247. package/client/dist/assets/LoopBuilderPage-CclJ3VZA.js +0 -7
  248. package/client/dist/assets/LoopPreviewModal-Dw4UgoLu.js +0 -1
  249. package/client/dist/assets/LoopsPage-D7ac4M24.js +0 -1
  250. package/client/dist/assets/PluginsPage-wdC-qa40.js +0 -2
  251. package/client/dist/assets/ProjectSettingsDialog-BlTlRtGI.js +0 -1
  252. package/client/dist/assets/TemplatePreviewModal-CeMx2Red.js +0 -1
  253. package/client/dist/assets/activity-Dbl71S-E.js +0 -1
  254. package/client/dist/assets/addon-image-CwoZj17F.js +0 -1
  255. package/client/dist/assets/addon-ligatures-CWDrED80.js +0 -2
  256. package/client/dist/assets/addspec-DrXqP8ip.js +0 -1
  257. package/client/dist/assets/agent-B8BXmlUQ2.js +0 -1
  258. package/client/dist/assets/agent-BEXYxDbs.js +0 -1
  259. package/client/dist/assets/agent-BYc3-6Ln2.js +0 -1
  260. package/client/dist/assets/agent-BbJorPdT.js +0 -1
  261. package/client/dist/assets/agent-CewFImgL.js +0 -1
  262. package/client/dist/assets/agent-DfhYoUmp.js +0 -1
  263. package/client/dist/assets/agent-TK_rrK4d.js +0 -1
  264. package/client/dist/assets/agent-bHjA9R_j2.js +0 -1
  265. package/client/dist/assets/agents-CGDn_3kB.js +0 -1
  266. package/client/dist/assets/agentstudio-Dxe_94PK.js +0 -1
  267. package/client/dist/assets/aiedit-DrfHQnlz.js +0 -1
  268. package/client/dist/assets/analytics-CxDYcBl9.js +0 -1
  269. package/client/dist/assets/attachments-DvxQbBTC.js +0 -1
  270. package/client/dist/assets/brain-C92rjjYP.js +0 -1
  271. package/client/dist/assets/browser-DM8G151q.js +0 -1
  272. package/client/dist/assets/chat-0dg5hOo_.js +0 -1
  273. package/client/dist/assets/chunk-BNv3lrIs.js +0 -1
  274. package/client/dist/assets/code-B62fClk-.js +0 -1
  275. package/client/dist/assets/commands-DCJ-R9E4.js +0 -1
  276. package/client/dist/assets/common-BM3LFLCE2.js +0 -1
  277. package/client/dist/assets/common-Bwafujnc2.js +0 -1
  278. package/client/dist/assets/common-C-vU-j-I.js +0 -1
  279. package/client/dist/assets/common-C5HKSbbz.js +0 -1
  280. package/client/dist/assets/common-DXrmj6WD.js +0 -1
  281. package/client/dist/assets/common-DhWTkocg.js +0 -1
  282. package/client/dist/assets/common-DiPTT7V_2.js +0 -1
  283. package/client/dist/assets/common-DmqBPwZG.js +0 -1
  284. package/client/dist/assets/dashboard-B5ang-ZS.js +0 -1
  285. package/client/dist/assets/dashboard-CHHcOWVL2.js +0 -1
  286. package/client/dist/assets/dashboard-DBD4wPGY2.js +0 -1
  287. package/client/dist/assets/dashboard-DBuLfWo9.js +0 -1
  288. package/client/dist/assets/dashboard-DSnztwWH.js +0 -1
  289. package/client/dist/assets/dashboard-D_aP_InU.js +0 -1
  290. package/client/dist/assets/dashboard-DmBcdl1B.js +0 -1
  291. package/client/dist/assets/dashboard-RIcUKY1Y2.js +0 -1
  292. package/client/dist/assets/editor.api2-C3B0tp2x.js +0 -872
  293. package/client/dist/assets/editor.worker-Bd9IXS8d.js +0 -26
  294. package/client/dist/assets/explore-_XyU5e7R.js +0 -1
  295. package/client/dist/assets/index-BVbgKXUu.css +0 -2
  296. package/client/dist/assets/index-DyY1QyIn.js +0 -158
  297. package/client/dist/assets/integrations-By80EBkf.js +0 -1
  298. package/client/dist/assets/jira-DeIjwCK3.js +0 -1
  299. package/client/dist/assets/jobs-1GV2-4zd2.js +0 -1
  300. package/client/dist/assets/jobs-26F9gGcE.js +0 -1
  301. package/client/dist/assets/jobs-BgQzB0Sx2.js +0 -1
  302. package/client/dist/assets/jobs-BqWui9_f.js +0 -1
  303. package/client/dist/assets/jobs-CE8qeIYj.js +0 -1
  304. package/client/dist/assets/jobs-DrYmsKy5.js +0 -1
  305. package/client/dist/assets/jobs-DzeuASRf.js +0 -1
  306. package/client/dist/assets/jobs-Wh7dn28q2.js +0 -1
  307. package/client/dist/assets/json.worker-DzV-CpCQ.js +0 -58
  308. package/client/dist/assets/lib-0PFDyMxJ.js +0 -7
  309. package/client/dist/assets/loops-CO6q75W8.js +0 -1
  310. package/client/dist/assets/mcp-HwBbQDk0.js +0 -1
  311. package/client/dist/assets/monaco.contribution-Desq7oq4.js +0 -2
  312. package/client/dist/assets/nav-BTg8s-cD.js +0 -1
  313. package/client/dist/assets/preload-helper-DSXbuxSR.js +0 -1
  314. package/client/dist/assets/settings-cJNynGIZ.js +0 -1
  315. package/client/dist/assets/setup-D2CmgAro.js +0 -1
  316. package/client/dist/assets/specs-PGIOzMtc.js +0 -1
  317. package/client/dist/assets/terminal-CFt56A5f.js +0 -1
  318. package/client/dist/assets/tickets-B7zT4IRf.js +0 -1
  319. package/client/dist/assets/tickets-BHv3ewvq.js +0 -1
  320. package/client/dist/assets/tickets-BoXom7l8.js +0 -1
  321. package/client/dist/assets/tickets-C3b8cHhd2.js +0 -1
  322. package/client/dist/assets/tickets-CFXnJsYF.js +0 -1
  323. package/client/dist/assets/tickets-CWYFvx1W2.js +0 -1
  324. package/client/dist/assets/tickets-DTHo9c3C.js +0 -1
  325. package/client/dist/assets/tickets-SuX_HkK4.js +0 -1
  326. package/client/dist/assets/upload-CmR4h9H1.js +0 -1
  327. package/client/dist/assets/value-JA3bf47v.js +0 -1
  328. /package/client/dist/assets/{abap-D0E4FfVW.js → abap-08VXUWAP.js} +0 -0
  329. /package/client/dist/assets/{activity--iN4Ad2R2.js → activity-B-rp4h_v.js} +0 -0
  330. /package/client/dist/assets/{activity-6hM_ZSg9.js → activity-C6kJpm9q2.js} +0 -0
  331. /package/client/dist/assets/{activity-CIOLxGvh.js → activity-CGx4kb_j.js} +0 -0
  332. /package/client/dist/assets/{activity-BZNfOdiS.js → activity-CnoX2bH7.js} +0 -0
  333. /package/client/dist/assets/{activity-BuVo5sPB.js → activity-DAyekdlt2.js} +0 -0
  334. /package/client/dist/assets/{activity-D2r2h3SE2.js → activity-DCxtpT7j.js} +0 -0
  335. /package/client/dist/assets/{activity-DtxrAUMo2.js → activity-JzwRaWPD.js} +0 -0
  336. /package/client/dist/assets/{addspec-BMtLZLW62.js → addspec-BXjvkh5q.js} +0 -0
  337. /package/client/dist/assets/{addspec-CRuvWn8d.js → addspec-C4bwrmpi.js} +0 -0
  338. /package/client/dist/assets/{addspec-D-JLDsUV.js → addspec-CArHXUDM.js} +0 -0
  339. /package/client/dist/assets/{addspec-mgPOt5q02.js → addspec-DTKJcXiR.js} +0 -0
  340. /package/client/dist/assets/{addspec-DfhDbg5c.js → addspec-DbNBbrha2.js} +0 -0
  341. /package/client/dist/assets/{addspec-CP0HRuxY.js → addspec-htHGZHOe2.js} +0 -0
  342. /package/client/dist/assets/{addspec-bsNDdqRm2.js → addspec-lr_LP6jk.js} +0 -0
  343. /package/client/dist/assets/{agents-78bTZGZh.js → agents-6ZOJXWVk2.js} +0 -0
  344. /package/client/dist/assets/{agents-DDtCJ5M02.js → agents-BZ0XQKjg.js} +0 -0
  345. /package/client/dist/assets/{agents-CWo-KyAB.js → agents-BqWklnHR.js} +0 -0
  346. /package/client/dist/assets/{agents-BUdEi3-N.js → agents-Bz-thHkB2.js} +0 -0
  347. /package/client/dist/assets/{agents-bvyRLczL.js → agents-CJclElx6.js} +0 -0
  348. /package/client/dist/assets/{agents-BpoUaoW92.js → agents-D_l9L0VA.js} +0 -0
  349. /package/client/dist/assets/{agents-BLhWCmDS2.js → agents-u_C0jPB5.js} +0 -0
  350. /package/client/dist/assets/{agentstudio-BMQxc_bz.js → agentstudio-BF19hbXj.js} +0 -0
  351. /package/client/dist/assets/{agentstudio-C_Jq8Tl4.js → agentstudio-BLGvxWBx2.js} +0 -0
  352. /package/client/dist/assets/{agentstudio-Dtq3dGYk.js → agentstudio-D9UKiWTj2.js} +0 -0
  353. /package/client/dist/assets/{agentstudio-CWgXG5W52.js → agentstudio-Drs37nIM.js} +0 -0
  354. /package/client/dist/assets/{agentstudio-sI27TpBu2.js → agentstudio-nEnKkcXM.js} +0 -0
  355. /package/client/dist/assets/{agentstudio-jKMTdT70.js → agentstudio-pyFcSBKy.js} +0 -0
  356. /package/client/dist/assets/{agentstudio-COZriimI2.js → agentstudio-zfTzQh3U.js} +0 -0
  357. /package/client/dist/assets/{aiedit-hknZlly82.js → aiedit-B0KUbpB6.js} +0 -0
  358. /package/client/dist/assets/{aiedit-eMN4eQPF.js → aiedit-BoG24nlg2.js} +0 -0
  359. /package/client/dist/assets/{aiedit-pc5JDUq1.js → aiedit-CjlSRQVP.js} +0 -0
  360. /package/client/dist/assets/{aiedit-B03pD_Na.js → aiedit-DCjA7jLJ.js} +0 -0
  361. /package/client/dist/assets/{aiedit-6CHdb_iv2.js → aiedit-DNXxq_Dr.js} +0 -0
  362. /package/client/dist/assets/{aiedit-igmw-ZoZ2.js → aiedit-DQ-0_pvE.js} +0 -0
  363. /package/client/dist/assets/{aiedit-BtRqRxML.js → aiedit-olnv3QSt2.js} +0 -0
  364. /package/client/dist/assets/{analytics-CCprAqk3.js → analytics-BC7mwR4s.js} +0 -0
  365. /package/client/dist/assets/{analytics-CR1TJqUi2.js → analytics-BiNg8edP.js} +0 -0
  366. /package/client/dist/assets/{analytics-Bj-SmKHa.js → analytics-C0s_T2GN.js} +0 -0
  367. /package/client/dist/assets/{analytics-C5dpiS0o.js → analytics-CdsAPAwZ2.js} +0 -0
  368. /package/client/dist/assets/{analytics-BWMUZ2Pq2.js → analytics-Cr1_q-lC.js} +0 -0
  369. /package/client/dist/assets/{analytics-DfNdXR0y2.js → analytics-IEsKZvpW2.js} +0 -0
  370. /package/client/dist/assets/{analytics-rmHuJv0V.js → analytics-SRPf84OT.js} +0 -0
  371. /package/client/dist/assets/{apex-D_X_8J4d.js → apex-BWPQTe0t.js} +0 -0
  372. /package/client/dist/assets/{atom-one-dark-BMDPFwRZ.js → atom-one-dark-qiGhVQxg.js} +0 -0
  373. /package/client/dist/assets/{attachments-BgBNSqQd.js → attachments-D0wXXt9T2.js} +0 -0
  374. /package/client/dist/assets/{attachments-C9GTTJsx2.js → attachments-DenIjuVI.js} +0 -0
  375. /package/client/dist/assets/{attachments-BjyMmrwE.js → attachments-DhO7LPfy.js} +0 -0
  376. /package/client/dist/assets/{attachments-DPCmkFZu2.js → attachments-DoH5w0x4.js} +0 -0
  377. /package/client/dist/assets/{attachments-DCZOL1RU.js → attachments-Q9yNtiEM.js} +0 -0
  378. /package/client/dist/assets/{attachments-rNn1ekRy2.js → attachments-V_WZAYXx2.js} +0 -0
  379. /package/client/dist/assets/{attachments-DL-bPy9E.js → attachments-eaeScpjM.js} +0 -0
  380. /package/client/dist/assets/{auth-DpZq5p1e.js → auth-D_5bQv5W.js} +0 -0
  381. /package/client/dist/assets/{azcli-PZLnZ3jH.js → azcli-Bc_sGQ0U.js} +0 -0
  382. /package/client/dist/assets/{bat-KlBOxpO4.js → bat-i0X4ZdIN.js} +0 -0
  383. /package/client/dist/assets/{bicep-CgG9oF3V.js → bicep-B5-_aFwp.js} +0 -0
  384. /package/client/dist/assets/{browser-BPkhILSJ.js → browser-C25DkQiO.js} +0 -0
  385. /package/client/dist/assets/{browser-BqLqJVVT.js → browser-CiJ7-L32.js} +0 -0
  386. /package/client/dist/assets/{browser-rWKj-RR_2.js → browser-CvgDySd9.js} +0 -0
  387. /package/client/dist/assets/{browser-Cvg-QHvd2.js → browser-CyR1Kp3m2.js} +0 -0
  388. /package/client/dist/assets/{browser-FRv8P02E.js → browser-DwZljcRh2.js} +0 -0
  389. /package/client/dist/assets/{browser-Cc0iDqUi.js → browser-DxP-alBK.js} +0 -0
  390. /package/client/dist/assets/{browser-BijVYG0Y2.js → browser-rRCwi7Y7.js} +0 -0
  391. /package/client/dist/assets/{cameligo-6R45j6Ph.js → cameligo-DMUM7wLl.js} +0 -0
  392. /package/client/dist/assets/{chat-Bom6bqK3.js → chat-2rIB8HhT.js} +0 -0
  393. /package/client/dist/assets/{chat-vylxqR0P2.js → chat-BR8oe53x2.js} +0 -0
  394. /package/client/dist/assets/{chat-Dnwg-NpQ.js → chat-Bua03tty.js} +0 -0
  395. /package/client/dist/assets/{chat-BgL3zv6x2.js → chat-CZpAQan1.js} +0 -0
  396. /package/client/dist/assets/{chat-DsbwJhus.js → chat-CdY5SGvS.js} +0 -0
  397. /package/client/dist/assets/{chat-Bh3NdgJs.js → chat-DZyu3ImY2.js} +0 -0
  398. /package/client/dist/assets/{chat-DdwZjT8K2.js → chat-D_I7nU5K.js} +0 -0
  399. /package/client/dist/assets/{clojure-CLAAMO7J.js → clojure-Cm7r79vr.js} +0 -0
  400. /package/client/dist/assets/{clsx-CNmW0Nip.js → clsx-CjueKrWZ.js} +0 -0
  401. /package/client/dist/assets/{code-UPVvmAKM2.js → code-76gS0IdL.js} +0 -0
  402. /package/client/dist/assets/{code-BRcDlze52.js → code-C1VwxlML.js} +0 -0
  403. /package/client/dist/assets/{code-CeQ0FZpC.js → code-CnMZyJpr.js} +0 -0
  404. /package/client/dist/assets/{code-DgeHz9CB.js → code-D20ef9Cf2.js} +0 -0
  405. /package/client/dist/assets/{code-DzC1rMk12.js → code-DdOXaWRY.js} +0 -0
  406. /package/client/dist/assets/{code-CwnBPPyA.js → code-Duohuf9l.js} +0 -0
  407. /package/client/dist/assets/{code-BdMy3Ffq.js → code-geHmknB92.js} +0 -0
  408. /package/client/dist/assets/{coffee-AruSygo-.js → coffee-Ba7i2nA0.js} +0 -0
  409. /package/client/dist/assets/{commands-DkU0qliq.js → commands-B9RZO4qS.js} +0 -0
  410. /package/client/dist/assets/{commands-u-4eEHCN.js → commands-BEwbAjtD2.js} +0 -0
  411. /package/client/dist/assets/{commands-JWMfGDmr.js → commands-Bbxu8kLU.js} +0 -0
  412. /package/client/dist/assets/{commands-C4Y3C-PF2.js → commands-CDU3qv68.js} +0 -0
  413. /package/client/dist/assets/{commands-xHDDGfcP2.js → commands-CzezgtQj.js} +0 -0
  414. /package/client/dist/assets/{commands-CSDs9q_2.js → commands-DTQBF_q52.js} +0 -0
  415. /package/client/dist/assets/{commands-RHOchN4n2.js → commands-hOtKuOXc.js} +0 -0
  416. /package/client/dist/assets/{cpp-DEE5r4Pw.js → cpp-C7h46wYY.js} +0 -0
  417. /package/client/dist/assets/{csharp-BVXJUvbK.js → csharp-BKxtCVv1.js} +0 -0
  418. /package/client/dist/assets/{csp-cXOF0JoV.js → csp-bTuwJoIa.js} +0 -0
  419. /package/client/dist/assets/{css-C_zQuAoy.js → css-DIMkf-bt.js} +0 -0
  420. /package/client/dist/assets/{cypher-t95m_Q-O.js → cypher-CVaqCwHa.js} +0 -0
  421. /package/client/dist/assets/{dart-B4r1Tg-e.js → dart-onAF5SnQ.js} +0 -0
  422. /package/client/dist/assets/{dockerfile-6mt7zHQw.js → dockerfile-DZFCIeNp.js} +0 -0
  423. /package/client/dist/assets/{ecl-DEY_Fz_Q.js → ecl-D05T4iGw.js} +0 -0
  424. /package/client/dist/assets/{elixir-R6-q1c5t.js → elixir-6RTg0lbw.js} +0 -0
  425. /package/client/dist/assets/{explore-B7fblXBb2.js → explore-BG99VGpL.js} +0 -0
  426. /package/client/dist/assets/{explore-CNBzrEPS.js → explore-C0TjprJW2.js} +0 -0
  427. /package/client/dist/assets/{explore-C0k8HrkN.js → explore-C2dgEo7q.js} +0 -0
  428. /package/client/dist/assets/{explore-BHtEpoNN2.js → explore-C8NDHbob.js} +0 -0
  429. /package/client/dist/assets/{explore-Dahc1lOx.js → explore-CWWgzQGR.js} +0 -0
  430. /package/client/dist/assets/{explore-D_mWBYCS2.js → explore-CcQXpdtS.js} +0 -0
  431. /package/client/dist/assets/{explore-UvW1jEMa.js → explore-DsXAgnmJ2.js} +0 -0
  432. /package/client/dist/assets/{flow9-CNQvHBOt.js → flow9-C5_-GSwl.js} +0 -0
  433. /package/client/dist/assets/{format-command-CuWuRvka.js → format-command-Dfa1o2d-.js} +0 -0
  434. /package/client/dist/assets/{fsharp-vPZkFXwv.js → fsharp-C8Ef5oNN.js} +0 -0
  435. /package/client/dist/assets/{go-KUzBMYJ_.js → go-C-y9NEjX.js} +0 -0
  436. /package/client/dist/assets/{graphql-C3W0rCsD.js → graphql-fmXr3nnJ.js} +0 -0
  437. /package/client/dist/assets/{hcl-BOOMFSNX.js → hcl-CpzslTdj.js} +0 -0
  438. /package/client/dist/assets/{ini-CFat1WJr.js → ini-sBoK_t0W.js} +0 -0
  439. /package/client/dist/assets/{integrations-TAcYNh0b.js → integrations-CH0zy1Mw2.js} +0 -0
  440. /package/client/dist/assets/{integrations-C1ThkTC52.js → integrations-CIAjhd7L.js} +0 -0
  441. /package/client/dist/assets/{integrations-2YcNyAl3.js → integrations-CZdwOe8A.js} +0 -0
  442. /package/client/dist/assets/{integrations-BXCvXUlG2.js → integrations-Ct7omMld.js} +0 -0
  443. /package/client/dist/assets/{integrations-Crk2rFhL2.js → integrations-D6MSxy3-.js} +0 -0
  444. /package/client/dist/assets/{integrations-Dn4JiGtW.js → integrations-DbZa-Zyc.js} +0 -0
  445. /package/client/dist/assets/{integrations-D9YrJ_Ep.js → integrations-b8pH-vfl2.js} +0 -0
  446. /package/client/dist/assets/{java-Dee-CNAM.js → java-BEtHBSE6.js} +0 -0
  447. /package/client/dist/assets/{jira-D5efgwp_.js → jira-BySfUuQZ.js} +0 -0
  448. /package/client/dist/assets/{jira-bDs7Im2C.js → jira-Byw2C0F8.js} +0 -0
  449. /package/client/dist/assets/{jira-BHAKtc4B.js → jira-CNeYi06w2.js} +0 -0
  450. /package/client/dist/assets/{jira-Be369RIR2.js → jira-D11gPhxz.js} +0 -0
  451. /package/client/dist/assets/{jira-CPpgA-5z2.js → jira-D38Gj7AT.js} +0 -0
  452. /package/client/dist/assets/{jira-uNH1gETL2.js → jira-gAvOyije.js} +0 -0
  453. /package/client/dist/assets/{jira-DS7ownDy.js → jira-pRaL7JQo2.js} +0 -0
  454. /package/client/dist/assets/{julia-CGNlLYuU.js → julia-Bri6UV-V.js} +0 -0
  455. /package/client/dist/assets/{kotlin-CY7lhknJ.js → kotlin-BOotOW0E.js} +0 -0
  456. /package/client/dist/assets/{less-D0KN7hfJ.js → less-B9JPFI3C.js} +0 -0
  457. /package/client/dist/assets/{lexon-CuRJufPs.js → lexon-CfSJPG6W.js} +0 -0
  458. /package/client/dist/assets/{loops-LIgO_-eM.js → loops-CPpgMapU.js} +0 -0
  459. /package/client/dist/assets/{loops-DJNhp17p2.js → loops-CWWbKuVL.js} +0 -0
  460. /package/client/dist/assets/{loops-D6D3XHjj.js → loops-Crc9It8o2.js} +0 -0
  461. /package/client/dist/assets/{loops-CvOWsi5r.js → loops-DH2hZcbK2.js} +0 -0
  462. /package/client/dist/assets/{loops-vhJdbcz92.js → loops-DS5TmvIU2.js} +0 -0
  463. /package/client/dist/assets/{loops-K5_HXfJa.js → loops-P2f0cN8-.js} +0 -0
  464. /package/client/dist/assets/{loops-BvqQ-bHR2.js → loops-fJV-9hH7.js} +0 -0
  465. /package/client/dist/assets/{lua-DHvH62Zf.js → lua-CsQS60Ue.js} +0 -0
  466. /package/client/dist/assets/{m3-tlthQ8Fo.js → m3-D-oSqn_W.js} +0 -0
  467. /package/client/dist/assets/{markdown-Dbx1LgUZ.js → markdown-Cimd5fb3.js} +0 -0
  468. /package/client/dist/assets/{mcp-Dcl-XxkR.js → mcp-Czd_iyp02.js} +0 -0
  469. /package/client/dist/assets/{mcp-XJNRfxVp2.js → mcp-D2pc5GGx.js} +0 -0
  470. /package/client/dist/assets/{mcp-sRmWE8WJ.js → mcp-D4ZjtIx32.js} +0 -0
  471. /package/client/dist/assets/{mcp-DtbxuH3V2.js → mcp-DIfBUv2D2.js} +0 -0
  472. /package/client/dist/assets/{mcp-H-gnQUdN.js → mcp-DsTwmjST.js} +0 -0
  473. /package/client/dist/assets/{mcp-N1nzp8oL.js → mcp-SMkn8boG.js} +0 -0
  474. /package/client/dist/assets/{mcp-CjVKXAhD2.js → mcp-fSfiVYAc.js} +0 -0
  475. /package/client/dist/assets/{mips-D_O3IUz_.js → mips-CIPQ_RoX.js} +0 -0
  476. /package/client/dist/assets/{msdax-C_ZmCnRt.js → msdax-DauUninz.js} +0 -0
  477. /package/client/dist/assets/{mysql-C-dCEYSX.js → mysql-SOo6toE5.js} +0 -0
  478. /package/client/dist/assets/{nav-BElxljIF2.js → nav-BRJBxaGt2.js} +0 -0
  479. /package/client/dist/assets/{nav-COu2E_-L.js → nav-CAGKv3_F.js} +0 -0
  480. /package/client/dist/assets/{nav-BXO7PJJG2.js → nav-CNskRJKQ2.js} +0 -0
  481. /package/client/dist/assets/{nav-CW9pxWfC2.js → nav-DY93irBL.js} +0 -0
  482. /package/client/dist/assets/{nav-CXHUA4uf.js → nav-DhjvYgqt.js} +0 -0
  483. /package/client/dist/assets/{nav-DdHF5VWJ.js → nav-Gb-r4wop.js} +0 -0
  484. /package/client/dist/assets/{nav-C8kOuGh8.js → nav-TvumAFD22.js} +0 -0
  485. /package/client/dist/assets/{objective-c-DZY9Aqxd.js → objective-c-FvmIjYaQ.js} +0 -0
  486. /package/client/dist/assets/{pascal-BVHLXoor.js → pascal-DrH0SRf2.js} +0 -0
  487. /package/client/dist/assets/{pascaligo-XdG_6u3k.js → pascaligo-D-ptJ9y-.js} +0 -0
  488. /package/client/dist/assets/{perl-b9QV0BcD.js → perl-oz_6vUea.js} +0 -0
  489. /package/client/dist/assets/{pgsql-Du5FDOgT.js → pgsql-DTj74zXo.js} +0 -0
  490. /package/client/dist/assets/{php-WJD0z2Z_.js → php-nr791fC2.js} +0 -0
  491. /package/client/dist/assets/{pla-CbciTf2w.js → pla-CopQ2nXW.js} +0 -0
  492. /package/client/dist/assets/{postiats-C1QxmvVq.js → postiats-43DmfD33.js} +0 -0
  493. /package/client/dist/assets/{powerquery-BEYDhPLZ.js → powerquery-D3hlyOfw.js} +0 -0
  494. /package/client/dist/assets/{powershell-CfaYw4XL.js → powershell-DmHpPYUd.js} +0 -0
  495. /package/client/dist/assets/{protobuf-pwZEDOYa.js → protobuf-C531GsRP.js} +0 -0
  496. /package/client/dist/assets/{pug-5ftqSPVe.js → pug-Z5eAx3Zn.js} +0 -0
  497. /package/client/dist/assets/{qsharp-uI6qZMq4.js → qsharp-DkqhCAOL.js} +0 -0
  498. /package/client/dist/assets/{r-BK54P1EG.js → r-BwWrilGY.js} +0 -0
  499. /package/client/dist/assets/{redis-DI0yd7kN.js → redis-ClamHrr6.js} +0 -0
  500. /package/client/dist/assets/{redshift-Dov26eyI.js → redshift-DT7zqm-g.js} +0 -0
  501. /package/client/dist/assets/{restructuredtext-QqJ_1dC-.js → restructuredtext-BYgofb2h.js} +0 -0
  502. /package/client/dist/assets/{ruby-5WjveeVb.js → ruby-DezsRK8O.js} +0 -0
  503. /package/client/dist/assets/{rust-BzVSfqFH.js → rust-DdL9SqIa.js} +0 -0
  504. /package/client/dist/assets/{sb-DMIUfT-3.js → sb-CcwsVR0C.js} +0 -0
  505. /package/client/dist/assets/{scala-CSKA7lqt.js → scala-DHpiXF5c.js} +0 -0
  506. /package/client/dist/assets/{scheme-CojOP2Qc.js → scheme-BeGwcela.js} +0 -0
  507. /package/client/dist/assets/{scss-DoBZGJG6.js → scss-gp-XZpBa.js} +0 -0
  508. /package/client/dist/assets/{settings-1w1O5Og82.js → settings-B9ilOSYO.js} +0 -0
  509. /package/client/dist/assets/{settings-Jc46vUhd.js → settings-BqlWHRHA.js} +0 -0
  510. /package/client/dist/assets/{settings-BSihkr0e.js → settings-D8dlqt4d.js} +0 -0
  511. /package/client/dist/assets/{settings-BiDOB3hV2.js → settings-DAZqjX2a2.js} +0 -0
  512. /package/client/dist/assets/{settings-DNL68KTE.js → settings-DRh39v1S2.js} +0 -0
  513. /package/client/dist/assets/{settings-BoubJhDP2.js → settings-Mj1OJbfd2.js} +0 -0
  514. /package/client/dist/assets/{settings-CB-EZ_W8.js → settings-lRReaSYm.js} +0 -0
  515. /package/client/dist/assets/{setup-D84y3wx-.js → setup-CGZjy2IF2.js} +0 -0
  516. /package/client/dist/assets/{setup-BuQNKztt2.js → setup-CXa0Sx16.js} +0 -0
  517. /package/client/dist/assets/{setup-BwMovM9z2.js → setup-D2ktKKMF2.js} +0 -0
  518. /package/client/dist/assets/{setup-DU0ULX8q2.js → setup-DG2H7ha5.js} +0 -0
  519. /package/client/dist/assets/{setup-BXpw07r-.js → setup-DK5bHDUq2.js} +0 -0
  520. /package/client/dist/assets/{setup-7J8TjG4O.js → setup-DrWlIQTZ.js} +0 -0
  521. /package/client/dist/assets/{setup-BiduSX9d.js → setup-rzHUVgAu.js} +0 -0
  522. /package/client/dist/assets/{shell-YOaE4udl.js → shell-CC2rA5mh.js} +0 -0
  523. /package/client/dist/assets/{solidity-l3jSyVLu.js → solidity-BEEn4gHE.js} +0 -0
  524. /package/client/dist/assets/{sophia-BhSGTaQJ.js → sophia-CRfGWb83.js} +0 -0
  525. /package/client/dist/assets/{sparql-qhk6QD9w.js → sparql-D_Lu-MrJ.js} +0 -0
  526. /package/client/dist/assets/{specs-CWtKcVDC.js → specs-BcyxaCwf2.js} +0 -0
  527. /package/client/dist/assets/{specs-DTplt2XM.js → specs-C1q9geAi.js} +0 -0
  528. /package/client/dist/assets/{specs-PozUNhqb.js → specs-CLZKysq-2.js} +0 -0
  529. /package/client/dist/assets/{specs-C9Sd7BhH2.js → specs-DZMS9o54.js} +0 -0
  530. /package/client/dist/assets/{specs-D2clrt2c2.js → specs-Dm6zUI-R.js} +0 -0
  531. /package/client/dist/assets/{specs-CBDvEYKv.js → specs-V3TvhKLX.js} +0 -0
  532. /package/client/dist/assets/{specs-CIPr9reA2.js → specs-dV3Mq5Mu2.js} +0 -0
  533. /package/client/dist/assets/{sql-DoO3y21-.js → sql-NEE52Syq.js} +0 -0
  534. /package/client/dist/assets/{st-ChZAY8oG.js → st-DbInun42.js} +0 -0
  535. /package/client/dist/assets/{swift-D290Tr7_.js → swift-Bxkupp3x.js} +0 -0
  536. /package/client/dist/assets/{systemverilog-DsxzgJ-o.js → systemverilog-Bz4Y3fRF.js} +0 -0
  537. /package/client/dist/assets/{tcl-BKDUHAAR.js → tcl-DISqw1ZD.js} +0 -0
  538. /package/client/dist/assets/{terminal-De2Brd23.js → terminal-BZiIu3rf.js} +0 -0
  539. /package/client/dist/assets/{terminal-cqfd3EDV.js → terminal-C2NAd7BW2.js} +0 -0
  540. /package/client/dist/assets/{terminal-WSAvHXoU.js → terminal-IahE4LW92.js} +0 -0
  541. /package/client/dist/assets/{terminal-Z7KsDM_n.js → terminal-QpXZU2Bn2.js} +0 -0
  542. /package/client/dist/assets/{terminal-xGbCZJHg.js → terminal-ScjyXNxQ.js} +0 -0
  543. /package/client/dist/assets/{terminal-BRm03iFA2.js → terminal-d7GadXbN.js} +0 -0
  544. /package/client/dist/assets/{terminal-DxQWCqSD2.js → terminal-tITa81iz.js} +0 -0
  545. /package/client/dist/assets/{twig-UpZQaM8G.js → twig-De2hgUGE.js} +0 -0
  546. /package/client/dist/assets/{typespec-DzaKvN8i.js → typespec-B8J7ngcE.js} +0 -0
  547. /package/client/dist/assets/{vb-CZpbb7tk.js → vb-DV3o63ZY.js} +0 -0
  548. /package/client/dist/assets/{wgsl-CNt3OIvl.js → wgsl-DpFanUEy.js} +0 -0
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PrContinuationIsolationError = void 0;
36
37
  exports.launchIsolatedRail = launchIsolatedRail;
37
38
  exports.reconcileRailWorktrees = reconcileRailWorktrees;
38
39
  /**
@@ -41,9 +42,10 @@ exports.reconcileRailWorktrees = reconcileRailWorktrees;
41
42
  * ledger) into a running parallel rail. Gated OFF by default (rail-isolation's
42
43
  * opt-in flag), so this path is inert unless explicitly enabled.
43
44
  *
44
- * Flow: allocate one worktree+branch per ticket → fan out the loop runs IN those
45
- * worktrees (so concurrent AI CLIs never collide) → when ALL runs settle, run the
46
- * sequential validated merge-back on the base repo, then clean up worktrees.
45
+ * Flow: allocate one worktree+branch per independent delivery → fan out the loop
46
+ * runs IN those worktrees (so concurrent AI CLIs never collide) → when ALL runs
47
+ * settle, run the sequential validated merge-back on the base repo, then clean
48
+ * up worktrees. Tickets continuing one PR are grouped into its single checkout.
47
49
  *
48
50
  * The run spawns with cwd = the worktree (and repoDir = the worktree, so
49
51
  * writes/git land there). Because `git worktree add` materializes only TRACKED
@@ -59,6 +61,7 @@ exports.reconcileRailWorktrees = reconcileRailWorktrees;
59
61
  * via the executors' base env (see workspace-resolution.resolveLoopBaseEnv).
60
62
  */
61
63
  const path = __importStar(require("path"));
64
+ const fs = __importStar(require("fs"));
62
65
  const artifact_registry_1 = require("./artifact-registry");
63
66
  const ids_1 = require("./ids");
64
67
  const loop_constants_1 = require("./loop-constants");
@@ -82,7 +85,19 @@ const file_story_1 = require("./file-story");
82
85
  const providers_1 = require("./providers");
83
86
  const pr_publisher_1 = require("./pr-publisher");
84
87
  const active_pr_continuation_1 = require("./active-pr-continuation");
88
+ const pr_lifecycle_1 = require("./pr-lifecycle");
85
89
  const rail_worktree_release_1 = require("./rail-worktree-release");
90
+ const rail_pr_recovery_git_1 = require("./rail-pr-recovery-git");
91
+ /** A PR follow-up may only run on the verified PR branch in a dedicated
92
+ * worktree. Routers must surface this error instead of degrading to shared cwd. */
93
+ class PrContinuationIsolationError extends Error {
94
+ code = 'pr_continuation_isolation_required';
95
+ constructor(message) {
96
+ super(message);
97
+ this.name = 'PrContinuationIsolationError';
98
+ }
99
+ }
100
+ exports.PrContinuationIsolationError = PrContinuationIsolationError;
86
101
  /**
87
102
  * Ticket data for the conventional branch name (pr-naming): title/labels from
88
103
  * the ticket-spec reader, the Jira key resolved with the authoritative
@@ -125,11 +140,140 @@ function commitFailureSummary(result) {
125
140
  const dirty = result.dirty.length > 0 ? `; dirty=${result.dirty.slice(0, 8).join(', ')}` : '';
126
141
  return `${result.error ?? 'worktree still has uncommitted deliverable changes'}${dirty}`;
127
142
  }
143
+ const COMMIT_SHA_RE = /^[0-9a-f]{40,64}$/i;
144
+ async function recoveryCommitObjectExists(git, repoDir, sha) {
145
+ if (!COMMIT_SHA_RE.test(sha))
146
+ return false;
147
+ try {
148
+ return (await git.run(['cat-file', '-e', `${sha}^{commit}`], repoDir)).code === 0;
149
+ }
150
+ catch {
151
+ return false;
152
+ }
153
+ }
154
+ async function isFastForwardRecoveryCommit(git, repoDir, baselineSha, candidateSha) {
155
+ if (!COMMIT_SHA_RE.test(baselineSha) || !COMMIT_SHA_RE.test(candidateSha))
156
+ return false;
157
+ if (baselineSha.toLowerCase() === candidateSha.toLowerCase())
158
+ return true;
159
+ try {
160
+ return (await git.run(['merge-base', '--is-ancestor', baselineSha, candidateSha], repoDir)).code === 0;
161
+ }
162
+ catch {
163
+ return false;
164
+ }
165
+ }
166
+ async function releaseDeliveredRecoveryLineage(db, git, repoDir, delivery, deliveredSha) {
167
+ const currentProtection = await (0, rail_pr_recovery_git_1.inspectRecoveryCommitProtection)(git, repoDir, delivery.id);
168
+ if (currentProtection.kind === 'present' &&
169
+ currentProtection.sha.toLowerCase() === deliveredSha.toLowerCase()) {
170
+ await (0, rail_pr_recovery_git_1.releaseRecoveryCommit)(git, repoDir, delivery.id, deliveredSha);
171
+ }
172
+ const visited = new Set([delivery.id]);
173
+ let predecessorId = delivery.supersedes_delivery_id;
174
+ for (let depth = 0; predecessorId && depth < 32 && !visited.has(predecessorId); depth++) {
175
+ visited.add(predecessorId);
176
+ const predecessor = (0, rail_pr_store_1.getPrDelivery)(db, predecessorId);
177
+ if (!predecessor)
178
+ break;
179
+ if (predecessor.decision !== 'discarded' && predecessor.decision !== 'superseded')
180
+ break;
181
+ const protection = await (0, rail_pr_recovery_git_1.inspectRecoveryCommitProtection)(git, repoDir, predecessor.id);
182
+ if (protection.kind === 'present' && await isFastForwardRecoveryCommit(git, repoDir, protection.sha, deliveredSha)) {
183
+ await (0, rail_pr_recovery_git_1.releaseRecoveryCommit)(git, repoDir, predecessor.id, protection.sha);
184
+ }
185
+ predecessorId = predecessor.supersedes_delivery_id;
186
+ }
187
+ }
188
+ /** Prove that the linked checkout is on the expected PR branch and that its
189
+ * HEAD is exactly the commit named by refs/heads/<branch>. When expectedHeadSha
190
+ * is supplied (allocation and pre-push phases), also freeze the checkout to
191
+ * that immutable object. Post-run verification deliberately omits the old
192
+ * baseline because a successful implementation is expected to create a new
193
+ * commit. The handle's branch string alone is insufficient: createWorktree may
194
+ * reuse a stale mounted path. */
195
+ async function verifyContinuationWorktree(git, repoDir, handle, target, expectedHeadSha) {
196
+ if (handle.branch !== target.branch) {
197
+ throw new PrContinuationIsolationError(`PR branch ${target.branch} resolved to mounted branch ${handle.branch}; free the stale worktree and retry`);
198
+ }
199
+ const checkedOut = await git.run(['rev-parse', '--abbrev-ref', 'HEAD'], handle.worktreePath);
200
+ const actualBranch = checkedOut.code === 0 ? checkedOut.stdout.trim() : '';
201
+ if (actualBranch !== target.branch) {
202
+ throw new PrContinuationIsolationError(`worktree ${handle.worktreePath} is on ${actualBranch || 'an unverifiable ref'}, expected PR branch ${target.branch}; fix the checkout and retry`);
203
+ }
204
+ const [head, branchRef] = await Promise.all([
205
+ git.run(['rev-parse', '--verify', 'HEAD'], handle.worktreePath),
206
+ git.run(['rev-parse', '--verify', `refs/heads/${target.branch}`], repoDir),
207
+ ]);
208
+ const headSha = head.code === 0 ? head.stdout.trim() : '';
209
+ const branchSha = branchRef.code === 0 ? branchRef.stdout.trim() : '';
210
+ if (!COMMIT_SHA_RE.test(headSha) || headSha !== branchSha) {
211
+ throw new PrContinuationIsolationError(`worktree HEAD does not match refs/heads/${target.branch}; reconcile the PR branch and retry`);
212
+ }
213
+ if (expectedHeadSha !== undefined && (!expectedHeadSha || headSha.toLowerCase() !== expectedHeadSha.toLowerCase())) {
214
+ throw new PrContinuationIsolationError(`local PR branch ${target.branch} is not at the verified continuation commit; preserve or reconcile the local commits before retrying`);
215
+ }
216
+ return headSha;
217
+ }
218
+ async function readHeadSha(git, cwd) {
219
+ try {
220
+ const result = await git.run(['rev-parse', '--verify', 'HEAD'], cwd);
221
+ const sha = result.code === 0 ? result.stdout.trim() : '';
222
+ return COMMIT_SHA_RE.test(sha) ? sha : null;
223
+ }
224
+ catch {
225
+ return null;
226
+ }
227
+ }
228
+ async function branchHasDelta(git, cwd, baseRef) {
229
+ try {
230
+ const result = await git.run(['diff', '--quiet', `${baseRef}...HEAD`, '--'], cwd);
231
+ if (result.code === 0)
232
+ return false;
233
+ if (result.code === 1)
234
+ return true;
235
+ return null;
236
+ }
237
+ catch {
238
+ return null;
239
+ }
240
+ }
241
+ function errorDetail(err) {
242
+ return err instanceof Error ? err.message : String(err);
243
+ }
244
+ function aggregateImplementationOutcome(results) {
245
+ const succeeded = results.filter((result) => result.implementationOutcome === 'succeeded').length;
246
+ if (succeeded === results.length && results.length > 0)
247
+ return 'succeeded';
248
+ if (succeeded > 0)
249
+ return 'partially_succeeded';
250
+ return results.length > 0 ? 'failed' : 'unknown';
251
+ }
252
+ function branchRecords(results) {
253
+ return results.flatMap((result) => result.run.ticketIds.map((ticketId) => ({
254
+ ticketId,
255
+ branch: result.run.handle.branch,
256
+ // Legacy consumers interpret this as delivery eligibility, not engine truth.
257
+ succeeded: result.deliveryOutcome === 'ready',
258
+ runId: result.run.runId,
259
+ implementationOutcome: result.implementationOutcome,
260
+ deliveryOutcome: result.deliveryOutcome,
261
+ initialSha: result.initialSha,
262
+ finalSha: result.finalSha,
263
+ ...(result.changed === undefined ? {} : { changed: result.changed }),
264
+ failureCode: result.failureCode ?? null,
265
+ branchOwnership: result.run.branchOwnership,
266
+ worktreePath: result.run.handle.worktreePath,
267
+ overlayExcludes: result.run.overlayExcludes,
268
+ overlayCleanupEvidence: result.run.overlayCleanupEvidence,
269
+ })));
270
+ }
128
271
  /**
129
272
  * Launch the rail's tickets in isolated worktrees + schedule the merge-back.
130
- * Returns the loop run ids. Throws if worktree allocation fails (the caller then
131
- * falls back to the shared-cwd path) — but only after tearing down any partial
132
- * allocation, so a failure never leaves orphaned worktrees.
273
+ * Returns the loop run ids. Throws if worktree allocation fails, but only after
274
+ * tearing down owned partial allocation. A fresh launch may degrade to shared
275
+ * cwd in the router; a PR continuation raises PrContinuationIsolationError and
276
+ * must fail closed because shared cwd cannot prove which branch receives work.
133
277
  */
134
278
  async function launchIsolatedRail(input, io = {}) {
135
279
  const { ctx, railIndex, ticketIds, loopId, loopName, loopGraph, provider, model, effort } = input;
@@ -151,23 +295,21 @@ async function launchIsolatedRail(input, io = {}) {
151
295
  // every await and late-joining clients hydrate mid-build via GET /rails —
152
296
  // and re-broadcast the durable rail.pr_state snapshot on every mutation.
153
297
  let prDeliveryId = null;
154
- const broadcastPrState = () => {
155
- if (!prDeliveryId)
156
- return;
157
- const row = (0, rail_pr_store_1.getPrDelivery)(ctx.db, prDeliveryId);
158
- if (row)
159
- ctx.broadcast((0, rail_pr_store_1.toRailPrStateMessage)(ctx.project.id, (0, rail_pr_store_1.toPrDeliverySnapshot)(row)));
160
- };
298
+ let supersededDelivery = null;
161
299
  // Origin-conversation card sync: when the launch came from an agent-chat
162
300
  // conversation, mirror the row's state into its inline decision card —
163
301
  // 'post' at launch (the user sees "working in an isolated worktree" from
164
302
  // second zero), 'update' on every later transition (settle, alloc-failure).
165
303
  // Null-safe: registry empty in tests / disabled builds; update falls back to
166
304
  // post when no card exists yet.
167
- const syncOriginCard = (verb) => {
168
- if (!prDeliveryId)
305
+ const broadcastDeliveryState = (deliveryId) => {
306
+ const row = (0, rail_pr_store_1.getPrDelivery)(ctx.db, deliveryId);
307
+ if (!row)
169
308
  return;
170
- const row = (0, rail_pr_store_1.getPrDelivery)(ctx.db, prDeliveryId);
309
+ ctx.broadcast((0, rail_pr_store_1.toRailPrStateMessage)(ctx.project.id, (0, rail_pr_store_1.toPrDeliverySnapshot)(row)));
310
+ };
311
+ const syncDeliveryCard = (deliveryId, verb) => {
312
+ const row = (0, rail_pr_store_1.getPrDelivery)(ctx.db, deliveryId);
171
313
  if (!row?.origin_conversation_id)
172
314
  return;
173
315
  const mgr = (0, agent_chat_registry_1.getAgentChatManager)();
@@ -179,11 +321,47 @@ async function launchIsolatedRail(input, io = {}) {
179
321
  else
180
322
  mgr.updatePrDecisionCard(row.origin_conversation_id, envelope);
181
323
  };
324
+ const emitPrDeliveryState = (deliveryId, verb) => {
325
+ broadcastDeliveryState(deliveryId);
326
+ syncDeliveryCard(deliveryId, verb);
327
+ };
328
+ const broadcastPrState = () => {
329
+ if (prDeliveryId)
330
+ broadcastDeliveryState(prDeliveryId);
331
+ };
332
+ const syncOriginCard = (verb) => {
333
+ if (prDeliveryId)
334
+ syncDeliveryCard(prDeliveryId, verb);
335
+ };
336
+ const closeFailedGeneration = (err) => {
337
+ if (!prDeliveryId || (0, rail_pr_store_1.getPrDelivery)(ctx.db, prDeliveryId)?.decision !== 'building')
338
+ return;
339
+ const closed = supersededDelivery
340
+ ? (0, rail_pr_store_1.failPrDeliveryAndRestoreSuperseded)(ctx.db, prDeliveryId, supersededDelivery)
341
+ : (0, rail_pr_store_1.transitionDecision)(ctx.db, prDeliveryId, 'building', 'discarded', {
342
+ implementationOutcome: 'unknown',
343
+ deliveryOutcome: 'blocked',
344
+ statusCode: 'delivery_failed',
345
+ statusDetail: errorDetail(err),
346
+ });
347
+ if (!closed)
348
+ return;
349
+ try {
350
+ emitPrDeliveryState(prDeliveryId, 'update');
351
+ }
352
+ catch { /* durable row is authoritative */ }
353
+ if (supersededDelivery) {
354
+ try {
355
+ emitPrDeliveryState(supersededDelivery.id, 'update');
356
+ }
357
+ catch { /* durable row is authoritative */ }
358
+ }
359
+ };
182
360
  // Units of isolation: `all` → ONE unit covering every ticket (one worktree, one
183
361
  // run — the batch pipeline handles the tickets internally); `per-ticket` → one
184
362
  // unit per ticket. The branch/ledger key is the unit's primary (first) ticket.
185
363
  const scope = input.scope ?? 'per-ticket';
186
- const units = scope === 'all'
364
+ let units = scope === 'all'
187
365
  ? [{ ticketId: ticketIds[0], ticketIds: [...ticketIds] }]
188
366
  : ticketIds.map((id) => ({ ticketId: id, ticketIds: [id] }));
189
367
  // Effective artifact root for the per-worktree overlay: a RELOCATED project's
@@ -231,187 +409,319 @@ async function launchIsolatedRail(input, io = {}) {
231
409
  let integration;
232
410
  let launchContinuation = null;
233
411
  const allocated = [];
234
- await (0, repo_lock_1.withRepoLock)(baseRepo, async () => {
235
- // Bring the repo's remote-tracking refs up to date BEFORE resolving the
236
- // integration branch or allocating any worktree — otherwise `git worktree
237
- // add -b <branch> <path> <bare-name>` resolves against whatever (possibly
238
- // stale) commit the user's LOCAL branch happens to be at. `fetchOrigin` only
239
- // ever touches `refs/remotes/origin/*`; it never mutates the checked-out
240
- // branch/working tree. De-duped per repo for a short TTL so a "Launch all"
241
- // batch (N independent launch requests for the same repo, serialized by
242
- // this same withRepoLock) performs one real fetch, not one per rail. A
243
- // failed fetch (no network / no remote / auth error) never blocks the
244
- // launch — resolveWorktreeBaseRef below degrades to the local ref.
245
- const fetchResult = await (0, integration_branch_1.fetchOrigin)(git, baseRepo);
246
- // Resolve the project's designated integration branch ONCE, and branch every
247
- // ticket's worktree off it (not the ambient HEAD). Empty setting → auto-resolve
248
- // (repo default → HEAD fallback). See server/integration-branch.ts.
249
- integration = await (0, integration_branch_1.resolveIntegrationBranch)(git, {
250
- repoDir: baseRepo,
251
- projectSetting: (0, db_1.getProjectSettings)(ctx.db).integrationBranch,
252
- });
253
- // Prefer the freshly-fetched remote-tracking ref (origin/<branch>) over the
254
- // bare local name for repo-default/project-setting sources — see
255
- // resolveWorktreeBaseRef for the exact fallback policy. `explicit` is left
256
- // completely untouched (rare, launch-time-chosen override).
257
- const worktreeBaseRef = await (0, integration_branch_1.resolveWorktreeBaseRef)(git, {
258
- repoDir: baseRepo, integration, fetchOk: fetchResult.ok,
259
- });
260
- if (worktreeBaseRef.warning) {
261
- console.warn(`[rail-isolated] ${worktreeBaseRef.warning} (repo ${baseRepo})`);
262
- try {
263
- ctx.broadcast({ type: 'rail.fetch_degraded', projectId: ctx.project.id, railIndex, warning: worktreeBaseRef.warning });
264
- }
265
- catch { /* non-fatal, mirrors notifyOverlayDegraded's broadcast guard below */ }
266
- }
267
- // Active-PR continuation: if a ticket is already parked on_review with a
268
- // matching OPEN GitHub PR (or a Jira-linked in_progress ticket explicitly
269
- // matches an open PR, covering unmapped Jira "Review" statuses), run the next
270
- // pass directly on that PR's head branch. Fresh tickets stay on the normal
271
- // integration-base path. Keep this deliberately single-target for now: one
272
- // rail delivery row can represent one PR URL, so multi-PR batches continue to
273
- // use the existing new-work flow instead of silently mixing PRs.
274
- const continuationTargets = await (0, active_pr_continuation_1.resolveActivePrContinuationTargets)({
275
- db: ctx.db,
276
- git,
277
- exec,
278
- repoDir: baseRepo,
279
- ticketIds: units.map((u) => u.ticketId),
280
- integrationBranch: integration.branch,
281
- fetchOk: fetchResult.ok,
282
- getTicketSpec: (ticketId) => {
412
+ try {
413
+ await (0, repo_lock_1.withRepoLock)(baseRepo, async () => {
414
+ // Bring the repo's remote-tracking refs up to date BEFORE resolving the
415
+ // integration branch or allocating any worktree — otherwise `git worktree
416
+ // add -b <branch> <path> <bare-name>` resolves against whatever (possibly
417
+ // stale) commit the user's LOCAL branch happens to be at. `fetchOrigin` only
418
+ // ever touches `refs/remotes/origin/*`; it never mutates the checked-out
419
+ // branch/working tree. De-duped per repo for a short TTL so a "Launch all"
420
+ // batch (N independent launch requests for the same repo, serialized by
421
+ // this same withRepoLock) performs one real fetch, not one per rail. A
422
+ // failed fetch (no network / no remote / auth error) never blocks the
423
+ // launch — resolveWorktreeBaseRef below degrades to the local ref.
424
+ const fetchResult = await (0, integration_branch_1.fetchOrigin)(git, baseRepo);
425
+ // Resolve the project's designated integration branch ONCE, and branch every
426
+ // ticket's worktree off it (not the ambient HEAD). Empty setting → auto-resolve
427
+ // (repo default → HEAD fallback). See server/integration-branch.ts.
428
+ integration = await (0, integration_branch_1.resolveIntegrationBranch)(git, {
429
+ repoDir: baseRepo,
430
+ projectSetting: (0, db_1.getProjectSettings)(ctx.db).integrationBranch,
431
+ });
432
+ // Prefer the freshly-fetched remote-tracking ref (origin/<branch>) over the
433
+ // bare local name for repo-default/project-setting sources — see
434
+ // resolveWorktreeBaseRef for the exact fallback policy. `explicit` is left
435
+ // completely untouched (rare, launch-time-chosen override).
436
+ const worktreeBaseRef = await (0, integration_branch_1.resolveWorktreeBaseRef)(git, {
437
+ repoDir: baseRepo, integration, fetchOk: fetchResult.ok,
438
+ });
439
+ if (worktreeBaseRef.warning) {
440
+ console.warn(`[rail-isolated] ${worktreeBaseRef.warning} (repo ${baseRepo})`);
283
441
  try {
284
- return ctx.getTicketSpec(ticketId);
285
- }
286
- catch {
287
- return undefined;
442
+ ctx.broadcast({ type: 'rail.fetch_degraded', projectId: ctx.project.id, railIndex, warning: worktreeBaseRef.warning });
288
443
  }
289
- },
290
- });
291
- const uniqueContinuationKeys = new Set(units
292
- .map((u) => continuationTargets.get(u.ticketId))
293
- .filter((t) => !!t)
294
- .map((t) => `${t.prUrl ?? ''}\n${t.branch}`));
295
- if (uniqueContinuationKeys.size === 1 && units.every((u) => continuationTargets.has(u.ticketId))) {
296
- launchContinuation = continuationTargets.get(units[0].ticketId) ?? null;
297
- }
298
- if (prMode) {
299
- prDeliveryId = (0, rail_pr_store_1.createPrDelivery)(ctx.db, {
300
- railIndex,
301
- loopId,
302
- railKey: `${railIndex}-${loopId}`,
303
- ticketIds: [...ticketIds],
304
- baseBranch: launchContinuation?.baseBranch ?? integration.branch,
305
- loopName,
306
- originSurface: input.originSurface ?? 'dashboard',
307
- originConversationId: input.originConversationId ?? null,
308
- }).id;
309
- input.onPrDeliveryCreated?.(prDeliveryId);
310
- if (launchContinuation) {
311
- (0, rail_pr_store_1.transitionDecision)(ctx.db, prDeliveryId, 'building', 'building', {
312
- branch: launchContinuation.branch,
313
- prUrl: launchContinuation.prUrl,
314
- prNumber: launchContinuation.prNumber,
315
- prState: 'pr-created',
316
- });
444
+ catch { /* non-fatal, mirrors notifyOverlayDegraded's broadcast guard below */ }
317
445
  }
318
- broadcastPrState();
319
- syncOriginCard('post');
320
- }
321
- // Conventional branch naming (pr-naming): `<type>/<ref>-<kebab-title>`, the
322
- // Jira key prevailing over the local id when linked. Collisions with foreign
323
- // branches suffix `-2`… (bounded); a branch a PRIOR rail run allocated for
324
- // the SAME ticket is ours to resume, so it is NOT treated as a collision.
325
- // The integration branch is never used; exhaustion falls back to the legacy
326
- // `sr/<slug>/ticket-<id>` name. Snapshot under the lock so concurrent
327
- // launches see each other's just-created branches.
328
- const takenBranches = await (0, worktree_manager_1.listLocalBranches)(git, baseRepo);
329
- const unitBranchName = (ticketId) => {
330
- const continuation = launchContinuation && continuationTargets.get(ticketId);
331
- if (continuation) {
332
- takenBranches.add(continuation.branch);
333
- return continuation.branch;
334
- }
335
- const preferred = (0, pr_naming_1.ticketBranchName)(unitNamingInput(ctx, ticketId));
336
- const branch = (0, pr_naming_1.resolveCollisionFreeName)(preferred, {
337
- taken: (name) => takenBranches.has(name) && !(0, rail_worktrees_store_1.railWorktreeBranchExistsForTicket)(ctx.db, ticketId, name),
338
- reserved: [integration.branch],
339
- }) ?? (0, worktree_manager_1.worktreeBranch)(slug, ticketId);
340
- takenBranches.add(branch);
341
- return branch;
342
- };
343
- try {
344
- for (const unit of units) {
345
- const continuationTarget = launchContinuation ? continuationTargets.get(unit.ticketId) ?? null : null;
346
- const handle = await create(git, {
446
+ // Active-PR continuation: if a ticket is already parked on_review with a
447
+ // matching OPEN GitHub PR (or a Jira-linked in_progress ticket explicitly
448
+ // matches an open PR, covering unmapped Jira "Review" statuses), run the next
449
+ // pass directly on that PR's head branch. Fresh tickets stay on the normal
450
+ // integration-base path. Keep this deliberately single-target for now: one
451
+ // rail delivery row can represent one PR URL, so multi-PR batches continue to
452
+ // use the existing new-work flow instead of silently mixing PRs.
453
+ const continuationTargets = prMode
454
+ ? await (0, active_pr_continuation_1.resolveActivePrContinuationTargets)({
455
+ db: ctx.db,
456
+ git,
457
+ exec,
347
458
  repoDir: baseRepo,
348
- worktreesRoot,
349
- slug,
350
- ticketId: unit.ticketId,
351
- baseRef: continuationTarget?.baseRef ?? worktreeBaseRef.baseRef,
352
- branch: unitBranchName(unit.ticketId),
353
- refreshFromBaseRef: Boolean(continuationTarget?.baseRef),
354
- });
355
- // Per-run overlay: merge-link the framework surface the checkout didn't
356
- // bring into the worktree (idempotent; resume-safe via its manifest).
357
- let overlayExcludes = [];
358
- try {
359
- const res = overlay({
360
- worktreePath: handle.worktreePath,
361
- sourceRoot: overlaySourceRoot,
362
- providerDir: overlayProviderDir,
363
- instructionsFilename: overlayInstructions,
459
+ // Continuation authority is scoped to the launch's exact durable ticket
460
+ // set. In scope=all there is only one isolation unit, but its primary
461
+ // ticket must never stand in for the full batch during PR discovery.
462
+ ticketIds: [...ticketIds],
463
+ integrationBranch: integration.branch,
464
+ fetchOk: fetchResult.ok,
465
+ getTicketSpec: (ticketId) => {
466
+ try {
467
+ return ctx.getTicketSpec(ticketId);
468
+ }
469
+ catch {
470
+ return undefined;
471
+ }
472
+ },
473
+ })
474
+ : new Map();
475
+ const uniqueContinuationKeys = new Set(ticketIds
476
+ .map((ticketId) => continuationTargets.get(ticketId))
477
+ .filter((t) => !!t)
478
+ .map((t) => `${t.prUrl ?? ''}\n${t.branch}`));
479
+ if (uniqueContinuationKeys.size === 1 && ticketIds.every((ticketId) => continuationTargets.has(ticketId))) {
480
+ launchContinuation = continuationTargets.get(ticketIds[0]) ?? null;
481
+ }
482
+ if (input.requiredPrContinuation && (!launchContinuation ||
483
+ launchContinuation.branch !== input.requiredPrContinuation.branch ||
484
+ launchContinuation.baseBranch !== input.requiredPrContinuation.baseBranch ||
485
+ launchContinuation.prUrl !== input.requiredPrContinuation.prUrl ||
486
+ launchContinuation.prNumber !== input.requiredPrContinuation.prNumber ||
487
+ launchContinuation.deliverySha !== input.requiredPrContinuation.deliverySha)) {
488
+ const observed = await (0, pr_lifecycle_1.observePrLifecycle)(exec, baseRepo, input.requiredPrContinuation.prUrl, input.requiredPrContinuation.deliverySha);
489
+ const reason = observed.ok
490
+ ? (0, pr_lifecycle_1.matchesRecordedPrIdentity)(observed, input.requiredPrContinuation.branch, input.requiredPrContinuation.baseBranch) && observed.state === 'OPEN' && observed.includesExpectedSha !== true
491
+ ? 'the open PR head no longer exposes the previously verified delivery commit; use Retry push'
492
+ : observed.state === 'CLOSED' || observed.state === 'MERGED'
493
+ ? `the attached PR is ${observed.state.toLowerCase()}; use Verify PR to reconcile it before relaunching`
494
+ : `the PR no longer matches its recorded head/base (${observed.state})`
495
+ : `GitHub lifecycle could not be confirmed (${observed.detail})`;
496
+ throw new PrContinuationIsolationError(`cannot safely continue PR branch ${input.requiredPrContinuation.branch}: ${reason}`);
497
+ }
498
+ // A git branch can only be checked out by one worktree. When several rail
499
+ // tickets all continue the same PR, make them one atomic batch run in one
500
+ // checkout instead of attempting N worktrees for the same branch (the second
501
+ // `git worktree add` necessarily fails). `ticketIds` preserves the full rail
502
+ // scope for {{spec.ids}} / ticket commands and completion bookkeeping.
503
+ if (launchContinuation && units.length > 1) {
504
+ units = [{
505
+ ticketId: units[0].ticketId,
506
+ ticketIds: units.flatMap((unit) => unit.ticketIds),
507
+ }];
508
+ }
509
+ if (prMode) {
510
+ const generation = (0, rail_pr_store_1.createPrDeliveryGeneration)(ctx.db, {
511
+ railIndex,
512
+ loopId,
513
+ railKey: `${railIndex}-${loopId}`,
514
+ ticketIds: [...ticketIds],
515
+ baseBranch: launchContinuation?.baseBranch ?? integration.branch,
516
+ loopName,
517
+ originSurface: input.originSurface ?? 'dashboard',
518
+ originConversationId: input.originConversationId ?? null,
519
+ isContinuation: Boolean(launchContinuation),
520
+ supersedesDeliveryId: launchContinuation?.source === 'rail-pr-delivery'
521
+ ? launchContinuation.deliveryId
522
+ : null,
523
+ }, input.requiredPrContinuation
524
+ ? { id: input.requiredPrContinuation.deliveryId, decision: input.requiredPrContinuation.decision }
525
+ : null);
526
+ prDeliveryId = generation.delivery.id;
527
+ supersededDelivery = generation.superseded;
528
+ input.onPrDeliveryCreated?.(prDeliveryId);
529
+ if (launchContinuation) {
530
+ (0, rail_pr_store_1.transitionDecision)(ctx.db, prDeliveryId, 'building', 'building', {
531
+ branch: launchContinuation.branch,
532
+ prUrl: launchContinuation.prUrl,
533
+ prNumber: launchContinuation.prNumber,
534
+ prState: 'pr-created',
535
+ // Persist the frozen OPEN head at generation creation, not only after
536
+ // settlement. If allocation/process recovery fails, this terminal
537
+ // continuation can still prove and resume the same borrowed PR instead
538
+ // of shadowing its predecessor with an evidence-less row.
539
+ deliverySha: launchContinuation.deliverySha,
540
+ isContinuation: true,
541
+ supersedesDeliveryId: supersededDelivery?.id ?? launchContinuation.deliveryId,
364
542
  });
365
- overlayExcludes = res.createdPaths;
366
- if (res.warnings.length > 0)
367
- notifyOverlayDegraded(unit.ticketId, res.warnings);
368
- }
369
- catch (err) {
370
- // applyWorktreeOverlay never throws; this guards injected test doubles
371
- // and future edits — the spawn proceeds regardless.
372
- notifyOverlayDegraded(unit.ticketId, [err instanceof Error ? err.message : String(err)]);
373
- }
374
- // Code-Explorer provenance: freeze the worktree's pre-run state (HEAD sha
375
- // + the overlay's untracked set) so the settle diff attributes exactly
376
- // the run's own writes. Best-effort — a snapshot failure only loses
377
- // provenance, never the run.
378
- let provenanceSnapshot = null;
379
- if ((0, feature_flags_1.isCodeExplorerEnabled)()) {
543
+ }
544
+ if (supersededDelivery) {
380
545
  try {
381
- provenanceSnapshot = (io.snapshot ?? file_provenance_1.snapshotWorkingTree)(handle.worktreePath);
546
+ emitPrDeliveryState(supersededDelivery.id, 'update');
547
+ }
548
+ catch { /* durable supersession is authoritative */ }
549
+ }
550
+ try {
551
+ broadcastPrState();
552
+ }
553
+ catch { /* hydration can recover */ }
554
+ try {
555
+ syncOriginCard('post');
556
+ }
557
+ catch { /* hydration can recover */ }
558
+ }
559
+ // Conventional branch naming (pr-naming): `<type>/<ref>-<kebab-title>`, the
560
+ // Jira key prevailing over the local id when linked. Collisions with foreign
561
+ // branches suffix `-2`… (bounded); a branch a PRIOR rail run allocated for
562
+ // the SAME ticket is ours to resume, so it is NOT treated as a collision.
563
+ // The integration branch is never used; exhaustion falls back to the legacy
564
+ // `sr/<slug>/ticket-<id>` name. Snapshot under the lock so concurrent
565
+ // launches see each other's just-created branches.
566
+ const takenBranches = await (0, worktree_manager_1.listLocalBranches)(git, baseRepo);
567
+ const preexistingBranches = new Set(takenBranches);
568
+ const unitBranchName = (ticketId) => {
569
+ const continuation = launchContinuation && continuationTargets.get(ticketId);
570
+ if (continuation) {
571
+ takenBranches.add(continuation.branch);
572
+ return { branch: continuation.branch, ownership: 'borrowed-pr' };
573
+ }
574
+ const preferred = (0, pr_naming_1.ticketBranchName)(unitNamingInput(ctx, ticketId));
575
+ const branch = (0, pr_naming_1.resolveCollisionFreeName)(preferred, {
576
+ taken: (name) => takenBranches.has(name) && !(0, rail_worktrees_store_1.railWorktreeBranchExistsForTicket)(ctx.db, ticketId, name),
577
+ reserved: [integration.branch],
578
+ }) ?? (0, worktree_manager_1.worktreeBranch)(slug, ticketId);
579
+ takenBranches.add(branch);
580
+ return { branch, ownership: preexistingBranches.has(branch) ? 'preexisting' : 'created' };
581
+ };
582
+ try {
583
+ for (const unit of units) {
584
+ const continuationTarget = launchContinuation ? continuationTargets.get(unit.ticketId) ?? null : null;
585
+ const branchPlan = unitBranchName(unit.ticketId);
586
+ let handle = null;
587
+ let branchOwnership = branchPlan.ownership;
588
+ let worktreeOwnership = 'created';
589
+ let initialSha = null;
590
+ try {
591
+ handle = await create(git, {
592
+ repoDir: baseRepo,
593
+ worktreesRoot,
594
+ slug,
595
+ ticketId: unit.ticketId,
596
+ baseRef: continuationTarget?.baseRef ?? worktreeBaseRef.baseRef,
597
+ branch: branchPlan.branch,
598
+ refreshFromBaseRef: Boolean(continuationTarget?.baseRef),
599
+ });
600
+ worktreeOwnership = handle.worktreeCreated === false ? 'preexisting' : 'created';
601
+ if (!continuationTarget && (handle.branch !== branchPlan.branch || handle.branchCreated === false)) {
602
+ branchOwnership = 'preexisting';
603
+ }
604
+ if (continuationTarget) {
605
+ initialSha = await verifyContinuationWorktree(git, baseRepo, handle, continuationTarget, continuationTarget.deliverySha);
606
+ }
607
+ else {
608
+ initialSha = await readHeadSha(git, handle.worktreePath);
609
+ }
610
+ // Per-run overlay: merge-link the framework surface the checkout didn't
611
+ // bring into the worktree (idempotent; resume-safe via its manifest).
612
+ let overlayExcludes = [];
613
+ let overlayCleanupEvidence = [];
614
+ try {
615
+ const res = overlay({
616
+ worktreePath: handle.worktreePath,
617
+ sourceRoot: overlaySourceRoot,
618
+ providerDir: overlayProviderDir,
619
+ instructionsFilename: overlayInstructions,
620
+ });
621
+ overlayExcludes = res.createdPaths;
622
+ overlayCleanupEvidence = res.cleanupEvidence ?? [];
623
+ if (res.warnings.length > 0)
624
+ notifyOverlayDegraded(unit.ticketId, res.warnings);
625
+ }
626
+ catch (err) {
627
+ // applyWorktreeOverlay never throws; this guards injected test doubles
628
+ // and future edits — the spawn proceeds regardless.
629
+ notifyOverlayDegraded(unit.ticketId, [err instanceof Error ? err.message : String(err)]);
630
+ }
631
+ // Code-Explorer provenance: freeze the worktree's pre-run state (HEAD sha
632
+ // + the overlay's untracked set) so the settle diff attributes exactly
633
+ // the run's own writes. Best-effort — a snapshot failure only loses
634
+ // provenance, never the run.
635
+ let provenanceSnapshot = null;
636
+ if ((0, feature_flags_1.isCodeExplorerEnabled)()) {
637
+ try {
638
+ provenanceSnapshot = (io.snapshot ?? file_provenance_1.snapshotWorkingTree)(handle.worktreePath);
639
+ }
640
+ catch (err) {
641
+ console.warn(`[rail-isolated] provenance snapshot failed: ${err.message}`);
642
+ }
643
+ }
644
+ const runId = (0, ids_1.newId)();
645
+ const ledgerId = (0, ids_1.newId)();
646
+ (0, rail_worktrees_store_1.createRailWorktree)(ctx.db, {
647
+ id: ledgerId, railIndex, ticketId: unit.ticketId, runId,
648
+ branch: handle.branch, worktreePath: handle.worktreePath,
649
+ });
650
+ allocated.push({
651
+ ticketId: unit.ticketId,
652
+ ticketIds: unit.ticketIds,
653
+ runId,
654
+ ledgerId,
655
+ handle,
656
+ overlayExcludes,
657
+ overlayCleanupEvidence,
658
+ provenanceSnapshot,
659
+ continuationTarget,
660
+ baseRef: continuationTarget?.baseRef ?? worktreeBaseRef.baseRef,
661
+ initialSha,
662
+ branchOwnership,
663
+ worktreeOwnership,
664
+ });
382
665
  }
383
666
  catch (err) {
384
- console.warn(`[rail-isolated] provenance snapshot failed: ${err.message}`);
667
+ // A handle that never reached `allocated` is still our responsibility,
668
+ // but only unmount/delete resources this call actually created.
669
+ if (handle && worktreeOwnership === 'created') {
670
+ await remove(git, {
671
+ repoDir: baseRepo,
672
+ worktreePath: handle.worktreePath,
673
+ branch: handle.branch,
674
+ deleteBranch: branchOwnership === 'created',
675
+ }).catch(() => { });
676
+ }
677
+ throw err;
385
678
  }
386
679
  }
387
- const runId = (0, ids_1.newId)();
388
- const ledgerId = (0, ids_1.newId)();
389
- (0, rail_worktrees_store_1.createRailWorktree)(ctx.db, {
390
- id: ledgerId, railIndex, ticketId: unit.ticketId, runId,
391
- branch: handle.branch, worktreePath: handle.worktreePath,
392
- });
393
- allocated.push({ ticketId: unit.ticketId, ticketIds: unit.ticketIds, runId, ledgerId, handle, overlayExcludes, provenanceSnapshot, continuationTarget });
394
680
  }
395
- }
396
- catch (err) {
397
- for (const a of allocated) {
398
- await remove(git, { repoDir: baseRepo, worktreePath: a.handle.worktreePath, branch: a.handle.branch }).catch(() => { });
399
- }
400
- // Close the just-inserted decision row (kept as 'discarded' for audit, never
401
- // deleted) so a wedged 'building' row can never block relaunching the slot.
402
- // Router-driven PR continuations may intentionally fall back to shared-cwd
403
- // on the same PR branch; in that case the router preserves and updates the
404
- // row as the live iteration card instead of showing a false discard.
405
- if (!input.preservePrDeliveryOnAllocationFailure && prDeliveryId && (0, rail_pr_store_1.transitionDecision)(ctx.db, prDeliveryId, 'building', 'discarded')) {
406
- broadcastPrState();
407
- syncOriginCard('update');
681
+ catch (err) {
682
+ for (const a of allocated) {
683
+ if (a.worktreeOwnership === 'created') {
684
+ await remove(git, {
685
+ repoDir: baseRepo,
686
+ worktreePath: a.handle.worktreePath,
687
+ branch: a.handle.branch,
688
+ deleteBranch: a.branchOwnership === 'created',
689
+ }).catch(() => { });
690
+ }
691
+ (0, rail_worktrees_store_1.updateRailWorktreeState)(ctx.db, a.ledgerId, 'failed');
692
+ }
693
+ // Close the failed generation. A continuation atomically restores the exact
694
+ // predecessor it superseded; a stale concurrent generation wins instead of
695
+ // letting this catch overwrite it.
696
+ closeFailedGeneration(err);
697
+ if (launchContinuation) {
698
+ if (err instanceof PrContinuationIsolationError)
699
+ throw err;
700
+ const detail = err instanceof Error ? err.message : String(err);
701
+ throw new PrContinuationIsolationError(`cannot allocate a verified worktree for PR branch ${launchContinuation.branch}: ${detail}; free the branch checkout and retry`);
702
+ }
703
+ throw err;
408
704
  }
409
- throw err;
705
+ }); // withRepoLock — allocation done; the fan-out below runs in parallel.
706
+ }
707
+ catch (err) {
708
+ closeFailedGeneration(err);
709
+ // Auto-detected GitHub continuations do not have a router-side delivery
710
+ // contract yet. Preserve the fail-closed signal for errors that happen
711
+ // after detection but before the allocation catch (for example DB setup).
712
+ // TypeScript does not observe assignments performed inside withRepoLock's
713
+ // async callback, so retain the explicit runtime type at this boundary.
714
+ const detectedContinuation = launchContinuation;
715
+ if (detectedContinuation && !(err instanceof PrContinuationIsolationError)) {
716
+ const detail = err instanceof Error ? err.message : String(err);
717
+ throw new PrContinuationIsolationError(`cannot prepare a verified worktree for PR branch ${detectedContinuation.branch}: ${detail}`);
410
718
  }
411
- }); // withRepoLock — allocation done; the fan-out below runs in parallel.
412
- // 2. Fan out: one loop run per ticket, spawning IN its worktree. In PR mode a
413
- // COMPLETED run's tickets park at on_review (the user decides done vs
414
- // discard via the PR flow); failure outcomes ignore the field.
719
+ throw err;
720
+ }
721
+ // 2. Fan out: one loop run per independent worktree. Same-PR tickets form one
722
+ // atomic batch run because git cannot mount one branch in multiple linked
723
+ // worktrees. In PR mode a COMPLETED run's tickets park at on_review (the
724
+ // user decides done vs discard via the PR flow); failures ignore the field.
415
725
  const runFinishedOpts = { ticketCompletionStatus: prMode ? 'on_review' : 'done' };
416
726
  // Code-Explorer provenance at settle: diff the worktree against its pre-run
417
727
  // snapshot and record file_provenance/story rows (keyed by runId), exactly
@@ -427,58 +737,210 @@ async function launchIsolatedRail(input, io = {}) {
427
737
  if (provenanceRecorded.has(a.runId))
428
738
  return;
429
739
  provenanceRecorded.add(a.runId);
430
- recordProvenance({
431
- db: ctx.db,
432
- projectId: ctx.project.id,
433
- runId: a.runId,
434
- ticketId: a.ticketId,
435
- repoDir: a.handle.worktreePath,
436
- snapshot: a.provenanceSnapshot,
437
- broadcast: (msg) => ctx.broadcast(msg),
438
- });
740
+ try {
741
+ recordProvenance({
742
+ db: ctx.db,
743
+ projectId: ctx.project.id,
744
+ runId: a.runId,
745
+ ticketId: a.ticketId,
746
+ repoDir: a.handle.worktreePath,
747
+ snapshot: a.provenanceSnapshot,
748
+ broadcast: (msg) => ctx.broadcast(msg),
749
+ });
750
+ }
751
+ catch (err) {
752
+ console.warn(`[rail-isolated] provenance recording failed for ${a.runId}: ${errorDetail(err)}`);
753
+ }
754
+ };
755
+ const markWorktree = (a, state) => {
756
+ try {
757
+ return (0, rail_worktrees_store_1.updateRailWorktreeState)(ctx.db, a.ledgerId, state)
758
+ ? null
759
+ : `worktree ledger ${a.ledgerId} no longer exists`;
760
+ }
761
+ catch (err) {
762
+ return errorDetail(err);
763
+ }
764
+ };
765
+ /** This function resolves for every allocated unit. Engine truth is captured
766
+ * before any delivery effect and is the only value sent to the deferred
767
+ * terminal callback. Delivery failures become blocked evidence instead. */
768
+ const settleAllocatedRun = async (a, enginePromise) => {
769
+ let actualOutcome = 'failed';
770
+ let engineFailure;
771
+ try {
772
+ const result = await enginePromise;
773
+ actualOutcome = result.outcome;
774
+ }
775
+ catch (err) {
776
+ engineFailure = errorDetail(err);
777
+ console.error(`[rail-isolated] loop run ${a.runId} rejected: ${engineFailure}`);
778
+ }
779
+ const implementationOutcome = actualOutcome === 'success' ? 'succeeded' : 'failed';
780
+ recordRunProvenance(a);
781
+ let callbackFailure;
782
+ try {
783
+ ctx.onLoopRunFinished(a.runId, actualOutcome, runFinishedOpts);
784
+ }
785
+ catch (err) {
786
+ callbackFailure = errorDetail(err);
787
+ console.error(`[rail-isolated] terminal callback failed for ${a.runId}: ${callbackFailure}`);
788
+ }
789
+ // The engine may have edited a copied overlay file. Re-authenticate only
790
+ // automatic-cleanup authority. Allocation-time paths remain conservative
791
+ // NEVER-COMMIT exclusions, while a modified copy is preserved in the
792
+ // worktree instead of being silently staged or deleted.
793
+ if (a.overlayCleanupEvidence.length > 0) {
794
+ a.overlayCleanupEvidence = (0, worktree_overlay_1.revalidateOverlayCleanupEvidence)({
795
+ worktreePath: a.handle.worktreePath,
796
+ sourceRoot: overlaySourceRoot,
797
+ providerDir: overlayProviderDir,
798
+ instructionsFilename: overlayInstructions,
799
+ }, a.overlayCleanupEvidence);
800
+ }
801
+ let commit;
802
+ try {
803
+ commit = await (0, worktree_manager_1.commitWorktreeAndVerify)(git, a.handle.worktreePath, worktreeCommitMessage(ctx, a.ticketId, a.runId, implementationOutcome === 'failed'), a.overlayExcludes);
804
+ }
805
+ catch (err) {
806
+ commit = { staged: false, committed: false, clean: false, dirty: [], error: errorDetail(err) };
807
+ }
808
+ if (!commit.clean) {
809
+ const detail = commitFailureSummary(commit);
810
+ console.error(`[rail-isolated] run ${a.runId} left an unsafe worktree: ${detail}`);
811
+ markWorktree(a, 'needs-review');
812
+ return {
813
+ run: a,
814
+ implementationOutcome,
815
+ deliveryOutcome: 'blocked',
816
+ initialSha: a.initialSha,
817
+ finalSha: await readHeadSha(git, a.handle.worktreePath),
818
+ failureCode: 'commit_failed',
819
+ failureDetail: detail,
820
+ safeToRelease: false,
821
+ };
822
+ }
823
+ let finalSha = null;
824
+ let verificationFailure;
825
+ if (a.continuationTarget) {
826
+ try {
827
+ finalSha = await verifyContinuationWorktree(git, baseRepo, a.handle, a.continuationTarget);
828
+ }
829
+ catch (err) {
830
+ verificationFailure = errorDetail(err);
831
+ }
832
+ }
833
+ else {
834
+ finalSha = await readHeadSha(git, a.handle.worktreePath);
835
+ }
836
+ const changed = commit.committed || Boolean(a.initialSha && finalSha && a.initialSha !== finalSha);
837
+ // A successful commit without a provable object is not retryable or safe to
838
+ // detach. A continuation additionally requires HEAD == refs/heads/<branch>.
839
+ if (verificationFailure || !finalSha || (!commit.committed && a.branchOwnership === 'created' && !a.initialSha)) {
840
+ const detail = verificationFailure
841
+ ?? (!finalSha
842
+ ? 'the final HEAD object could not be verified'
843
+ : 'the initial HEAD object was not captured, so a no-change result cannot be proven');
844
+ console.error(`[rail-isolated] run ${a.runId} finished on an unverified ref: ${detail}`);
845
+ markWorktree(a, 'needs-review');
846
+ return {
847
+ run: a,
848
+ implementationOutcome,
849
+ deliveryOutcome: 'blocked',
850
+ initialSha: a.initialSha,
851
+ finalSha,
852
+ changed,
853
+ failureCode: 'branch_verification_failed',
854
+ failureDetail: detail,
855
+ safeToRelease: false,
856
+ };
857
+ }
858
+ // Failed clean units remain `building` until aggregate cleanup removes the
859
+ // mount and atomically terminalizes the ledger as failed. Marking them
860
+ // terminal first would make idempotent cleanup skip a still-mounted path.
861
+ const ledgerFailure = implementationOutcome === 'succeeded'
862
+ ? (markWorktree(a, 'built') ?? undefined)
863
+ : undefined;
864
+ const settlementFailure = callbackFailure ?? ledgerFailure;
865
+ if (settlementFailure) {
866
+ markWorktree(a, 'needs-review');
867
+ return {
868
+ run: a,
869
+ implementationOutcome,
870
+ deliveryOutcome: 'blocked',
871
+ initialSha: a.initialSha,
872
+ finalSha,
873
+ changed,
874
+ failureCode: 'settlement_interrupted',
875
+ failureDetail: settlementFailure,
876
+ safeToRelease: false,
877
+ };
878
+ }
879
+ if (implementationOutcome === 'failed') {
880
+ return {
881
+ run: a,
882
+ implementationOutcome,
883
+ deliveryOutcome: 'not_started',
884
+ initialSha: a.initialSha,
885
+ finalSha,
886
+ changed,
887
+ ...(engineFailure ? { failureDetail: engineFailure } : {}),
888
+ safeToRelease: true,
889
+ };
890
+ }
891
+ // A resumed non-PR branch may already contain reviewable commits from an
892
+ // earlier attempt, even when this iteration itself did not add one.
893
+ let resumedBranchDelta = null;
894
+ if (!changed && a.continuationTarget === null && a.branchOwnership === 'preexisting') {
895
+ resumedBranchDelta = await branchHasDelta(git, a.handle.worktreePath, a.baseRef);
896
+ if (resumedBranchDelta === null) {
897
+ const detail = `cannot prove whether resumed branch ${a.handle.branch} has commits ahead of ${a.baseRef}`;
898
+ markWorktree(a, 'needs-review');
899
+ return {
900
+ run: a,
901
+ implementationOutcome,
902
+ deliveryOutcome: 'blocked',
903
+ initialSha: a.initialSha,
904
+ finalSha,
905
+ changed,
906
+ failureCode: 'branch_verification_failed',
907
+ failureDetail: detail,
908
+ safeToRelease: false,
909
+ };
910
+ }
911
+ }
912
+ const noChanges = !changed && Boolean(a.initialSha && finalSha && a.initialSha === finalSha && (a.continuationTarget !== null ||
913
+ a.branchOwnership === 'created' ||
914
+ resumedBranchDelta === false));
915
+ return {
916
+ run: a,
917
+ implementationOutcome,
918
+ deliveryOutcome: noChanges ? 'no_changes' : 'ready',
919
+ initialSha: a.initialSha,
920
+ finalSha,
921
+ changed,
922
+ safeToRelease: true,
923
+ };
439
924
  };
440
925
  const runPromises = [];
441
926
  for (const a of allocated) {
442
- ctx.railLoopRuns.set(a.runId, { railIndex, ticketIds: a.ticketIds });
927
+ ctx.railLoopRuns.set(a.runId, {
928
+ railIndex,
929
+ ticketIds: a.ticketIds,
930
+ requiresTerminalIntent: true,
931
+ });
443
932
  const spec = ctx.getTicketSpec(a.ticketId);
444
- const p = ctx.loopRunManager
445
- .run({
933
+ const enginePromise = ctx.loopRunManager.run({
446
934
  runId: a.runId, loopId, loopName, graph: loopGraph, projectId: ctx.project.id,
447
935
  cwd: a.handle.worktreePath, repoDir: a.handle.worktreePath,
448
936
  isolation: { branch: a.handle.branch, worktreePath: a.handle.worktreePath },
449
937
  railIndex, ticketId: a.ticketId,
450
938
  spec: spec ? { ...spec, ticketIds: a.ticketIds } : { ticketIds: a.ticketIds },
939
+ ticketCompletionStatus: runFinishedOpts.ticketCompletionStatus,
940
+ deferTerminalOutcome: true,
451
941
  constants, provider, model, effort,
452
- })
453
- .then(async (r) => {
454
- const succeeded = r.outcome === 'success';
455
- recordRunProvenance(a);
456
- // Commit the run's work to its branch so PR creation/merge-back can
457
- // trust the branch as the durable source of truth. A "successful" loop
458
- // with deliverable local changes still dirty is treated as failed
459
- // instead of surfacing a Create PR card that cannot ship the work.
460
- const commit = await (0, worktree_manager_1.commitWorktreeAndVerify)(git, a.handle.worktreePath, worktreeCommitMessage(ctx, a.ticketId, a.runId), a.overlayExcludes);
461
- const deliverable = succeeded && commit.clean;
462
- if (succeeded && !commit.clean) {
463
- console.error(`[rail-isolated] run ${a.runId} finished success but commit verification failed: ${commitFailureSummary(commit)}`);
464
- }
465
- ctx.onLoopRunFinished(r.runId, deliverable ? 'success' : 'failed', runFinishedOpts);
466
- (0, rail_worktrees_store_1.updateRailWorktreeState)(ctx.db, a.ledgerId, deliverable ? 'built' : 'failed');
467
- return { run: a, succeeded: deliverable };
468
- })
469
- .catch(async (err) => {
470
- console.error('[rail-isolated] loop run failed:', err);
471
- ctx.onLoopRunFinished(a.runId, 'failed', runFinishedOpts);
472
- recordRunProvenance(a);
473
- // Commit partial work too → durable in git → resumable on re-launch.
474
- const commit = await (0, worktree_manager_1.commitWorktreeAndVerify)(git, a.handle.worktreePath, worktreeCommitMessage(ctx, a.ticketId, a.runId, true), a.overlayExcludes);
475
- if (!commit.clean) {
476
- console.error(`[rail-isolated] failed run ${a.runId} left uncommitted deliverable changes: ${commitFailureSummary(commit)}`);
477
- }
478
- (0, rail_worktrees_store_1.updateRailWorktreeState)(ctx.db, a.ledgerId, 'failed');
479
- return { run: a, succeeded: false };
480
942
  });
481
- runPromises.push(p);
943
+ runPromises.push(settleAllocatedRun(a, enginePromise));
482
944
  try {
483
945
  ctx.jiraSyncManager.onRailLaunch(a.ticketIds, a.runId);
484
946
  }
@@ -500,11 +962,28 @@ async function launchIsolatedRail(input, io = {}) {
500
962
  // work isolated on its branches and mark the decision row on_review —
501
963
  // the draft PR is only created when the user clicks [Create PR] on the
502
964
  // pr-decision endpoint (ask-first, safe-pr-review-flow).
503
- void Promise.allSettled(runPromises).then(async (settled) => {
504
- const results = settled.flatMap((s) => (s.status === 'fulfilled' ? [s.value] : []));
505
- const branches = results.map((r) => ({
506
- ticketId: r.run.ticketId, branch: r.run.handle.branch, succeeded: r.succeeded,
507
- }));
965
+ const guardedRunPromises = runPromises.map((promise, index) => promise.catch(async (err) => {
966
+ const run = allocated[index];
967
+ const detail = errorDetail(err);
968
+ console.error(`[rail-isolated] unexpected settlement rejection for ${run.runId}: ${detail}`);
969
+ const durable = ctx.db.prepare('SELECT final_outcome FROM loop_runs WHERE id = ?').get(run.runId);
970
+ const implementationOutcome = durable?.final_outcome === 'success' ? 'succeeded' : 'failed';
971
+ markWorktree(run, 'needs-review');
972
+ return {
973
+ run,
974
+ implementationOutcome,
975
+ deliveryOutcome: 'blocked',
976
+ initialSha: run.initialSha,
977
+ finalSha: await readHeadSha(git, run.handle.worktreePath),
978
+ failureCode: 'settlement_interrupted',
979
+ failureDetail: detail,
980
+ safeToRelease: false,
981
+ };
982
+ }));
983
+ void Promise.all(guardedRunPromises).then(async (settledResults) => {
984
+ // Cardinality is invariant: one structured result per allocated unit. The
985
+ // guarded promises above synthesize a blocked record for any rejection.
986
+ let results = settledResults;
508
987
  if (prMode) {
509
988
  // Build-settle: persist the per-unit branch outcomes + this launch's
510
989
  // worktree ledger ids on the row (the deferred create-pr / discard actions
@@ -513,71 +992,334 @@ async function launchIsolatedRail(input, io = {}) {
513
992
  // keep the implementation card visible as a failed job state with run-log
514
993
  // chips instead of silently auto-discarding or leaving it at "building".
515
994
  const worktreeIds = allocated.map((a) => a.ledgerId);
516
- const anySucceeded = results.some((r) => r.succeeded);
517
- const settledContinuation = anySucceeded ? launchContinuation : null;
995
+ let deliverySha = null;
518
996
  let continuationPushFailed = false;
519
997
  let continuationPushFailureReason = null;
520
- if (settledContinuation) {
521
- const pushed = await (0, pr_publisher_1.pushBranch)(exec, {
522
- repoDir: baseRepo,
523
- branch: settledContinuation.branch,
524
- baseBranch: settledContinuation.baseBranch,
525
- });
526
- if (pushed.state === 'local-only') {
998
+ let continuationNeedsNewPr = false;
999
+ let continuationExactPushCompleted = false;
1000
+ let continuationRemoteIsDraft = launchContinuation?.isDraft ?? null;
1001
+ let continuationMergedWithSha = false;
1002
+ let continuationClosedWithSha = false;
1003
+ let continuationLifecycleBlocked = false;
1004
+ const unchangedContinuation = launchContinuation
1005
+ ? results.find((result) => result.run.continuationTarget && result.deliveryOutcome === 'no_changes')
1006
+ : undefined;
1007
+ if (unchangedContinuation?.finalSha)
1008
+ deliverySha = unchangedContinuation.finalSha;
1009
+ // Revalidate immediately before an existing-PR push. Settlement's SHA is
1010
+ // immutable delivery evidence; a moved ref blocks instead of substituting
1011
+ // a newer object behind the user's back.
1012
+ if (launchContinuation) {
1013
+ const candidateIndex = results.findIndex((result) => result.deliveryOutcome === 'ready' && result.run.continuationTarget !== null);
1014
+ if (candidateIndex >= 0) {
1015
+ const candidate = results[candidateIndex];
1016
+ try {
1017
+ const verified = await verifyContinuationWorktree(git, baseRepo, candidate.run.handle, candidate.run.continuationTarget, candidate.finalSha);
1018
+ if (!candidate.finalSha || verified !== candidate.finalSha) {
1019
+ throw new PrContinuationIsolationError('PR branch moved after settlement; refusing to push an unverified object');
1020
+ }
1021
+ deliverySha = candidate.finalSha;
1022
+ }
1023
+ catch (err) {
1024
+ const detail = errorDetail(err);
1025
+ markWorktree(candidate.run, 'needs-review');
1026
+ results = results.map((result, resultIndex) => resultIndex === candidateIndex
1027
+ ? {
1028
+ ...result,
1029
+ deliveryOutcome: 'blocked',
1030
+ failureCode: 'branch_verification_failed',
1031
+ failureDetail: detail,
1032
+ safeToRelease: false,
1033
+ }
1034
+ : result);
1035
+ }
1036
+ }
1037
+ }
1038
+ const implementationOutcome = aggregateImplementationOutcome(results);
1039
+ let ready = results.filter((result) => result.deliveryOutcome === 'ready');
1040
+ let blocked = results.filter((result) => result.deliveryOutcome === 'blocked');
1041
+ let noChanges = results.filter((result) => result.deliveryOutcome === 'no_changes');
1042
+ const continuationHadReadyWork = ready.length > 0;
1043
+ if (launchContinuation && deliverySha && (continuationHadReadyWork || noChanges.length > 0)) {
1044
+ const beforePush = await (0, pr_lifecycle_1.observePrLifecycle)(exec, baseRepo, launchContinuation.prUrl, deliverySha);
1045
+ if (!beforePush.ok) {
527
1046
  continuationPushFailed = true;
528
- continuationPushFailureReason = pushed.reason;
529
- console.warn(`[rail-isolated] existing PR follow-up push failed for ${settledContinuation.branch}: ${continuationPushFailureReason}`);
530
- }
531
- }
532
- const next = anySucceeded
533
- ? (settledContinuation
534
- ? (continuationPushFailed
535
- ? 'pr_failed'
536
- : (settledContinuation.isDraft === false ? 'pr_ready' : 'pr_draft'))
537
- : 'on_review')
538
- : 'implementation_failed';
539
- const patch = settledContinuation
540
- ? {
541
- branches,
542
- worktreeIds,
543
- branch: settledContinuation.branch,
544
- prUrl: settledContinuation.prUrl,
545
- prNumber: settledContinuation.prNumber,
546
- prState: continuationPushFailed ? 'local-only' : 'pr-created',
547
- }
548
- : { branches, worktreeIds };
1047
+ continuationPushFailureReason = `could not confirm the existing PR is open before push: ${beforePush.detail}`;
1048
+ }
1049
+ else if (!(0, pr_lifecycle_1.isExactOpenPr)(beforePush, launchContinuation.branch, launchContinuation.baseBranch)) {
1050
+ const identityMatches = (0, pr_lifecycle_1.matchesRecordedPrIdentity)(beforePush, launchContinuation.branch, launchContinuation.baseBranch);
1051
+ if (beforePush.state === 'MERGED' && identityMatches && beforePush.includesExpectedSha === true) {
1052
+ // Another actor delivered the exact object before our push. Keep the
1053
+ // old PR attached so poll-merge can apply the terminal ticket effect.
1054
+ continuationRemoteIsDraft = false;
1055
+ continuationMergedWithSha = true;
1056
+ }
1057
+ else if (beforePush.state === 'CLOSED' && identityMatches &&
1058
+ beforePush.includesExpectedSha === true && !continuationHadReadyWork) {
1059
+ // An unchanged iteration may safely offer Reopen when the closed PR
1060
+ // already contains its exact head. Changed work is never assumed to
1061
+ // belong to a closed PR before we push it.
1062
+ continuationClosedWithSha = true;
1063
+ }
1064
+ else {
1065
+ continuationNeedsNewPr = true;
1066
+ continuationPushFailureReason = beforePush.includesExpectedSha === null
1067
+ ? `the previous PR is ${beforePush.state.toLowerCase()} and GitHub could not prove it included this implementation; create a new draft PR from the preserved commit`
1068
+ : `the previous PR is ${beforePush.state.toLowerCase()} without this implementation; create a new draft PR from the preserved commit`;
1069
+ }
1070
+ }
1071
+ else if (!continuationHadReadyWork) {
1072
+ // No-change still needs lifecycle truth: the PR may have merged while
1073
+ // the run was active, and an advanced remote head must not be silently
1074
+ // dismissed as though it were the verified unchanged object.
1075
+ if (beforePush.headRefOid?.toLowerCase() !== deliverySha.toLowerCase()) {
1076
+ continuationLifecycleBlocked = true;
1077
+ continuationPushFailureReason = 'the open PR head no longer exposes the exact verified commit from this unchanged iteration';
1078
+ }
1079
+ else {
1080
+ continuationRemoteIsDraft = beforePush.isDraft;
1081
+ }
1082
+ }
1083
+ else {
1084
+ continuationRemoteIsDraft = beforePush.isDraft;
1085
+ const pushRemote = await (0, pr_lifecycle_1.verifyPushRemoteForPr)(exec, baseRepo, launchContinuation.prUrl);
1086
+ if (!pushRemote.ok) {
1087
+ continuationPushFailed = true;
1088
+ continuationPushFailureReason = `refusing to push the preserved implementation: ${pushRemote.detail}`;
1089
+ }
1090
+ else {
1091
+ try {
1092
+ const pushed = await (0, pr_publisher_1.pushBranch)(exec, {
1093
+ repoDir: baseRepo,
1094
+ branch: launchContinuation.branch,
1095
+ baseBranch: launchContinuation.baseBranch,
1096
+ remote: pushRemote.pushTarget,
1097
+ sourceSha: deliverySha,
1098
+ });
1099
+ if (pushed.state === 'local-only') {
1100
+ continuationPushFailed = true;
1101
+ continuationPushFailureReason = pushed.reason;
1102
+ }
1103
+ else {
1104
+ continuationExactPushCompleted = true;
1105
+ const afterPush = await (0, pr_lifecycle_1.observePrLifecycle)(exec, baseRepo, launchContinuation.prUrl, deliverySha);
1106
+ if (!afterPush.ok) {
1107
+ continuationPushFailed = true;
1108
+ continuationPushFailureReason = `exact commit was pushed, but the PR lifecycle could not be confirmed: ${afterPush.detail}`;
1109
+ }
1110
+ else if ((0, pr_lifecycle_1.isExactOpenPr)(afterPush, launchContinuation.branch, launchContinuation.baseBranch)) {
1111
+ if (afterPush.headRefOid?.toLowerCase() === deliverySha.toLowerCase()) {
1112
+ continuationRemoteIsDraft = afterPush.isDraft;
1113
+ }
1114
+ else {
1115
+ // GitHub observation can lag a successful push, or another
1116
+ // writer may have moved the remote head. The retry remains
1117
+ // safe because it always uses the immutable SHA and a normal
1118
+ // non-forced refspec; preserve evidence and keep Retry push.
1119
+ continuationPushFailed = true;
1120
+ continuationPushFailureReason = 'the exact push completed, but the PR does not yet expose the verified commit as its head; Retry push remains safe and uses the preserved SHA';
1121
+ }
1122
+ }
1123
+ else if (afterPush.state === 'MERGED' &&
1124
+ (0, pr_lifecycle_1.matchesRecordedPrIdentity)(afterPush, launchContinuation.branch, launchContinuation.baseBranch) &&
1125
+ afterPush.includesExpectedSha === true) {
1126
+ // The PR won the race but demonstrably included this exact SHA.
1127
+ continuationRemoteIsDraft = false;
1128
+ continuationMergedWithSha = true;
1129
+ }
1130
+ else if (afterPush.state === 'CLOSED' &&
1131
+ (0, pr_lifecycle_1.matchesRecordedPrIdentity)(afterPush, launchContinuation.branch, launchContinuation.baseBranch) &&
1132
+ afterPush.includesExpectedSha === true) {
1133
+ continuationClosedWithSha = true;
1134
+ }
1135
+ else if (afterPush.state === 'MERGED' || afterPush.state === 'CLOSED') {
1136
+ continuationNeedsNewPr = true;
1137
+ continuationPushFailureReason = afterPush.includesExpectedSha === null
1138
+ ? `the previous PR became ${afterPush.state.toLowerCase()} and GitHub could not prove it included this implementation; create a new draft PR from the preserved commit`
1139
+ : `the previous PR became ${afterPush.state.toLowerCase()} without this implementation; create a new draft PR from the preserved commit`;
1140
+ }
1141
+ else {
1142
+ continuationNeedsNewPr = true;
1143
+ continuationPushFailureReason = 'the existing PR no longer matches its recorded head/base after the exact push; create a new draft PR from the preserved commit';
1144
+ }
1145
+ }
1146
+ }
1147
+ catch (err) {
1148
+ continuationPushFailed = true;
1149
+ continuationPushFailureReason = errorDetail(err);
1150
+ }
1151
+ }
1152
+ }
1153
+ if (continuationPushFailed) {
1154
+ console.warn(`[rail-isolated] existing PR follow-up push failed for ${launchContinuation.branch}: ${continuationPushFailureReason}`);
1155
+ }
1156
+ }
1157
+ if (continuationNeedsNewPr && noChanges.length > 0) {
1158
+ // The iteration itself made no new commit, but the old PR did not deliver
1159
+ // its verified head. That head is therefore real work for a fresh PR.
1160
+ const noChangeRuns = new Set(noChanges.map((result) => result.run.runId));
1161
+ results = results.map((result) => noChangeRuns.has(result.run.runId)
1162
+ ? { ...result, deliveryOutcome: 'ready' }
1163
+ : result);
1164
+ }
1165
+ if (continuationLifecycleBlocked) {
1166
+ const affectedRuns = new Set(results
1167
+ .filter((result) => result.run.continuationTarget !== null &&
1168
+ (result.deliveryOutcome === 'ready' || result.deliveryOutcome === 'no_changes'))
1169
+ .map((result) => result.run.runId));
1170
+ for (const result of results) {
1171
+ if (affectedRuns.has(result.run.runId))
1172
+ markWorktree(result.run, 'needs-review');
1173
+ }
1174
+ results = results.map((result) => affectedRuns.has(result.run.runId)
1175
+ ? {
1176
+ ...result,
1177
+ deliveryOutcome: 'blocked',
1178
+ failureCode: 'branch_verification_failed',
1179
+ failureDetail: continuationPushFailureReason ?? 'PR lifecycle verification failed',
1180
+ safeToRelease: false,
1181
+ }
1182
+ : result);
1183
+ }
1184
+ ready = results.filter((result) => result.deliveryOutcome === 'ready');
1185
+ blocked = results.filter((result) => result.deliveryOutcome === 'blocked');
1186
+ noChanges = results.filter((result) => result.deliveryOutcome === 'no_changes');
1187
+ let deliveryOutcome;
1188
+ if (implementationOutcome === 'failed') {
1189
+ deliveryOutcome = blocked.length > 0 ? 'blocked' : 'not_started';
1190
+ }
1191
+ else if (continuationLifecycleBlocked) {
1192
+ deliveryOutcome = 'blocked';
1193
+ }
1194
+ else if (continuationMergedWithSha || continuationClosedWithSha) {
1195
+ deliveryOutcome = 'delivered';
1196
+ }
1197
+ else if (launchContinuation && ready.length > 0) {
1198
+ deliveryOutcome = continuationNeedsNewPr
1199
+ ? (implementationOutcome === 'partially_succeeded' ? 'partial' : 'ready')
1200
+ : continuationPushFailed ? 'retryable_failure' : 'delivered';
1201
+ }
1202
+ else if (ready.length > 0) {
1203
+ deliveryOutcome = implementationOutcome === 'partially_succeeded' || blocked.length > 0 ? 'partial' : 'ready';
1204
+ }
1205
+ else if (noChanges.length === results.length && implementationOutcome === 'succeeded') {
1206
+ deliveryOutcome = 'no_changes';
1207
+ }
1208
+ else if (implementationOutcome === 'partially_succeeded') {
1209
+ deliveryOutcome = 'partial';
1210
+ }
1211
+ else {
1212
+ deliveryOutcome = 'blocked';
1213
+ }
1214
+ const next = implementationOutcome === 'failed'
1215
+ ? 'implementation_failed'
1216
+ : continuationLifecycleBlocked
1217
+ ? 'pr_failed'
1218
+ : continuationMergedWithSha
1219
+ ? 'pr_ready'
1220
+ : continuationClosedWithSha
1221
+ ? 'pr_closed'
1222
+ : ready.length > 0
1223
+ ? (launchContinuation
1224
+ ? (continuationNeedsNewPr
1225
+ ? 'on_review'
1226
+ : continuationPushFailed
1227
+ ? 'pr_failed'
1228
+ : (continuationRemoteIsDraft === false ? 'pr_ready' : 'pr_draft'))
1229
+ : 'on_review')
1230
+ : deliveryOutcome === 'no_changes'
1231
+ ? 'no_changes'
1232
+ : 'pr_failed';
1233
+ const firstFailure = results.find((result) => result.failureCode);
1234
+ const statusCode = implementationOutcome === 'failed'
1235
+ ? 'implementation_failed'
1236
+ : continuationLifecycleBlocked
1237
+ ? 'branch_verification_failed'
1238
+ : continuationMergedWithSha
1239
+ ? 'pr_ready'
1240
+ : continuationClosedWithSha
1241
+ ? 'pr_closed'
1242
+ : continuationNeedsNewPr
1243
+ ? 'ready_for_review'
1244
+ : continuationPushFailed
1245
+ ? 'push_failed'
1246
+ : deliveryOutcome === 'no_changes'
1247
+ ? 'no_changes'
1248
+ : implementationOutcome === 'partially_succeeded'
1249
+ ? 'partial_success'
1250
+ : firstFailure?.failureCode
1251
+ ?? (launchContinuation && ready.length > 0 ? 'existing_pr_updated' : 'ready_for_review');
1252
+ const statusDetail = continuationPushFailureReason ?? firstFailure?.failureDetail ?? null;
1253
+ // Only proven-clean, durably referenced results are automatically
1254
+ // detached. Fresh reviewable worktrees stay mounted for the later PR
1255
+ // decision; dirty/unknown/ref-mismatched work is never passed to --force.
1256
+ const failedSafeIds = results
1257
+ .filter((result) => result.implementationOutcome === 'failed' && result.safeToRelease)
1258
+ .map((result) => result.run.ledgerId);
1259
+ const completedSafeIds = results
1260
+ .filter((result) => result.safeToRelease && (result.deliveryOutcome === 'no_changes' ||
1261
+ (launchContinuation !== null && result.deliveryOutcome === 'ready')))
1262
+ .map((result) => result.run.ledgerId);
1263
+ const settledBranchRecords = branchRecords(results);
1264
+ const expectedHeadByBranch = (0, rail_worktree_release_1.durableBranchHeads)(settledBranchRecords);
1265
+ const overlayEvidenceByBranch = (0, rail_worktree_release_1.durableOverlayCleanupEvidence)(settledBranchRecords);
1266
+ const patch = {
1267
+ branches: settledBranchRecords,
1268
+ worktreeIds,
1269
+ implementationOutcome,
1270
+ deliveryOutcome,
1271
+ statusCode,
1272
+ statusDetail,
1273
+ deliverySha,
1274
+ ...(launchContinuation ? {
1275
+ branch: launchContinuation.branch,
1276
+ prUrl: continuationNeedsNewPr ? null : launchContinuation.prUrl,
1277
+ prNumber: continuationNeedsNewPr ? null : launchContinuation.prNumber,
1278
+ // A stale CLOSED/MERGED PR is detached from this generation. The exact
1279
+ // object remains locally or on its recorded branch, ready for Create PR.
1280
+ prState: continuationNeedsNewPr
1281
+ ? (continuationExactPushCompleted ? 'pushed' : 'local-only')
1282
+ : 'pr-created',
1283
+ // Once detached, this generation owns the lifecycle of the new PR,
1284
+ // while per-unit branchOwnership still protects the borrowed branch.
1285
+ isContinuation: !continuationNeedsNewPr,
1286
+ } : {}),
1287
+ };
549
1288
  if (prDeliveryId && (0, rail_pr_store_1.transitionDecision)(ctx.db, prDeliveryId, 'building', next, patch)) {
550
- if (settledContinuation && !continuationPushFailed) {
551
- await (0, rail_worktree_release_1.releaseRailWorktrees)({ db: ctx.db, git, repoDir: baseRepo, worktreeIds });
1289
+ // Persist truthful settlement before cleanup. A crash can leave a
1290
+ // safely-recoverable mount, but can no longer lose no-change/partial/
1291
+ // exact-SHA evidence and regress the card back to `building`.
1292
+ const settledPrDeliveryId = prDeliveryId;
1293
+ const onSafetyArchive = (archive) => {
1294
+ if (!(0, rail_pr_store_1.appendPrDeliverySafetyArchive)(ctx.db, settledPrDeliveryId, archive)) {
1295
+ throw new Error(`delivery ${settledPrDeliveryId} disappeared while recording safety archive ${archive}`);
1296
+ }
1297
+ };
1298
+ const cleanupWarnings = [
1299
+ ...await (0, rail_worktree_release_1.releaseRailWorktrees)({
1300
+ db: ctx.db, git, repoDir: baseRepo, worktreeIds: failedSafeIds,
1301
+ state: 'failed', remove, expectedHeadByBranch, overlayEvidenceByBranch, onSafetyArchive,
1302
+ }),
1303
+ ...await (0, rail_worktree_release_1.releaseRailWorktrees)({
1304
+ db: ctx.db, git, repoDir: baseRepo, worktreeIds: completedSafeIds,
1305
+ remove, expectedHeadByBranch, overlayEvidenceByBranch, onSafetyArchive,
1306
+ }),
1307
+ ];
1308
+ if (cleanupWarnings.length > 0) {
1309
+ (0, rail_pr_store_1.transitionDecision)(ctx.db, prDeliveryId, next, next, { cleanupWarnings });
552
1310
  }
553
1311
  broadcastPrState();
554
1312
  // Completion driver: refresh the origin conversation's card in place —
555
1313
  // on_review asks the question, discarded informs the outcome.
556
1314
  syncOriginCard('update');
557
1315
  }
558
- if (!anySucceeded) {
559
- // FAILED-IMPLEMENTATION CLEANUP (0 succeeded): unmount every worktree NOW rather
560
- // than leaving it for a restart's reconcile sweep — a still-mounted
561
- // worktree POISONS the next run of the same ticket: the worktree path is
562
- // keyed by ticketId, so the next allocation silently reuses this
563
- // checkout and its stale branch. BRANCHES are kept: per-run settle
564
- // committed partial work to them and a re-launch resumes a kept branch
565
- // by name (createWorktree's resume path); user-discard remains the one
566
- // explicit branch-deleting action. Ledger rows are already terminal
567
- // ('failed') from per-run settle — ensured defensively here.
568
- for (const a of allocated) {
569
- await remove(git, {
570
- repoDir: baseRepo, worktreePath: a.handle.worktreePath, branch: a.handle.branch, deleteBranch: false,
571
- }).catch((err) => {
572
- console.warn(`[rail-isolated] failed to remove failed worktree ${a.handle.worktreePath}: ${err.message}`);
573
- });
574
- const wt = (0, rail_worktrees_store_1.getRailWorktree)(ctx.db, a.ledgerId);
575
- if (wt && !(0, rail_worktrees_store_1.isTerminalMergeState)(wt.merge_state))
576
- (0, rail_worktrees_store_1.updateRailWorktreeState)(ctx.db, a.ledgerId, 'failed');
577
- }
578
- }
579
1316
  return; // legacy merge-back never runs in PR mode
580
1317
  }
1318
+ const branches = results.flatMap((result) => result.run.ticketIds.map((ticketId) => ({
1319
+ ticketId,
1320
+ branch: result.run.handle.branch,
1321
+ succeeded: result.deliveryOutcome === 'ready',
1322
+ })));
581
1323
  try {
582
1324
  const outcomes = await (0, rail_merge_orchestrator_1.runMergeBack)({
583
1325
  git, executor: (0, loop_executors_1.createLoopExecutors)(), baseDir: baseRepo,
@@ -605,33 +1347,674 @@ async function launchIsolatedRail(input, io = {}) {
605
1347
  const r = results.find((x) => x.run.ticketId === o.ticketId);
606
1348
  if (!r)
607
1349
  continue;
1350
+ if (!r.safeToRelease) {
1351
+ (0, rail_worktrees_store_1.updateRailWorktreeState)(ctx.db, r.run.ledgerId, 'needs-review');
1352
+ continue;
1353
+ }
608
1354
  // Merged → drop the branch; needs-review / skipped → keep the branch for the human.
609
1355
  await remove(git, {
610
1356
  repoDir: baseRepo, worktreePath: r.run.handle.worktreePath, branch: o.branch,
611
- deleteBranch: o.state === 'merged',
1357
+ // Legacy merge-back is still an automatic cleanup path. A non-force
1358
+ // unmount fails closed if anything changed after merge-back, and the
1359
+ // branch is retained because this path has no durable immutable tip
1360
+ // evidence with which to authorize a destructive `branch -D`.
1361
+ deleteBranch: false,
1362
+ force: false,
1363
+ }).then(() => {
1364
+ if (r.implementationOutcome === 'failed')
1365
+ (0, rail_worktrees_store_1.updateRailWorktreeState)(ctx.db, r.run.ledgerId, 'failed');
612
1366
  }).catch(() => { });
613
1367
  }
614
1368
  }
615
1369
  catch (err) {
616
1370
  console.error('[rail-isolated] merge-back failed:', err);
617
1371
  }
1372
+ }).catch(async (err) => {
1373
+ const detail = errorDetail(err);
1374
+ console.error(`[rail-isolated] aggregate settlement failed: ${detail}`);
1375
+ if (!prMode || !prDeliveryId || (0, rail_pr_store_1.getPrDelivery)(ctx.db, prDeliveryId)?.decision !== 'building')
1376
+ return;
1377
+ const retained = await Promise.all(guardedRunPromises);
1378
+ for (const result of retained)
1379
+ markWorktree(result.run, 'needs-review');
1380
+ if ((0, rail_pr_store_1.transitionDecision)(ctx.db, prDeliveryId, 'building', 'pr_failed', {
1381
+ branches: branchRecords(retained.map((result) => ({
1382
+ ...result,
1383
+ deliveryOutcome: 'blocked',
1384
+ failureCode: result.failureCode ?? 'settlement_interrupted',
1385
+ failureDetail: result.failureDetail ?? detail,
1386
+ safeToRelease: false,
1387
+ }))),
1388
+ worktreeIds: allocated.map((run) => run.ledgerId),
1389
+ implementationOutcome: aggregateImplementationOutcome(retained),
1390
+ deliveryOutcome: 'blocked',
1391
+ statusCode: 'settlement_interrupted',
1392
+ statusDetail: detail,
1393
+ })) {
1394
+ broadcastPrState();
1395
+ syncOriginCard('update');
1396
+ }
618
1397
  });
619
1398
  return allocated.map((a) => a.runId);
620
1399
  }
1400
+ function recoveryOverlayExcludes(repoDir, worktreePath) {
1401
+ try {
1402
+ const parsed = JSON.parse(fs.readFileSync(path.join(worktreePath, worktree_overlay_1.OVERLAY_MANIFEST), 'utf8'));
1403
+ const values = Array.isArray(parsed) ? parsed : parsed?.paths;
1404
+ if (!Array.isArray(values))
1405
+ return [];
1406
+ return values.filter((value) => {
1407
+ if (typeof value !== 'string' || value.length === 0 || path.isAbsolute(value))
1408
+ return false;
1409
+ if (value.includes('\\') || value.split('/').includes('..') || /[\0:*?\[\]]/.test(value))
1410
+ return false;
1411
+ if (value === worktree_overlay_1.OVERLAY_MANIFEST)
1412
+ return true;
1413
+ if (!/^\.(?:claude|codex|gemini)\/[^/]+(?:\/[^/]+)*$/.test(value))
1414
+ return false;
1415
+ try {
1416
+ // Manifest contents are worktree-writable and therefore not authority.
1417
+ // Ignore only links that still prove they are overlay scaffolding;
1418
+ // copied/modified files remain recoverable dirty data.
1419
+ const destination = path.join(worktreePath, value);
1420
+ if (!fs.lstatSync(destination).isSymbolicLink())
1421
+ return false;
1422
+ const linkTarget = fs.readlinkSync(destination);
1423
+ return path.resolve(path.dirname(destination), linkTarget) === path.resolve(repoDir, value);
1424
+ }
1425
+ catch {
1426
+ return false;
1427
+ }
1428
+ });
1429
+ }
1430
+ catch {
1431
+ return [];
1432
+ }
1433
+ }
1434
+ async function authenticatedRecoveryDisplayPath(git, repoDir, ledgerPath, expectedBranch, registeredPaths) {
1435
+ if (!path.isAbsolute(ledgerPath))
1436
+ return null;
1437
+ let realPath;
1438
+ let repoRealPath;
1439
+ try {
1440
+ const stat = fs.lstatSync(ledgerPath);
1441
+ if (stat.isSymbolicLink() || !stat.isDirectory())
1442
+ return null;
1443
+ realPath = fs.realpathSync(ledgerPath);
1444
+ repoRealPath = fs.realpathSync(repoDir);
1445
+ }
1446
+ catch {
1447
+ return null;
1448
+ }
1449
+ if (realPath === repoRealPath)
1450
+ return null;
1451
+ const registeredMatches = registeredPaths.filter((registeredPath) => {
1452
+ try {
1453
+ return fs.realpathSync(registeredPath) === realPath;
1454
+ }
1455
+ catch {
1456
+ return false;
1457
+ }
1458
+ });
1459
+ if (registeredMatches.length !== 1)
1460
+ return null;
1461
+ try {
1462
+ const [branch, head] = await Promise.all([
1463
+ git.run(['rev-parse', '--abbrev-ref', 'HEAD'], realPath),
1464
+ git.run(['rev-parse', '--verify', 'HEAD'], realPath),
1465
+ ]);
1466
+ if (branch.code !== 0 || branch.stdout.trim() !== expectedBranch)
1467
+ return null;
1468
+ if (head.code !== 0 || !COMMIT_SHA_RE.test(head.stdout.trim()))
1469
+ return null;
1470
+ return realPath;
1471
+ }
1472
+ catch {
1473
+ return null;
1474
+ }
1475
+ }
1476
+ async function inspectRecoveryWorktree(git, repoDir, row) {
1477
+ const overlayExcludes = recoveryOverlayExcludes(repoDir, row.worktree_path);
1478
+ try {
1479
+ const [status, actualBranch, head, branchRef] = await Promise.all([
1480
+ git.run([
1481
+ 'status', '--porcelain', '--untracked-files=all', '--ignored=matching', '--', '.',
1482
+ ...worktree_manager_1.PR_NEVER_STAGE_PATHS.map((entry) => `:(exclude)${entry}`),
1483
+ ...overlayExcludes.map((entry) => `:(top,exclude,literal)${entry}`),
1484
+ ], row.worktree_path),
1485
+ git.run(['rev-parse', '--abbrev-ref', 'HEAD'], row.worktree_path),
1486
+ git.run(['rev-parse', '--verify', 'HEAD'], row.worktree_path),
1487
+ git.run(['rev-parse', '--verify', `refs/heads/${row.branch}`], repoDir),
1488
+ ]);
1489
+ const sha = head.code === 0 && COMMIT_SHA_RE.test(head.stdout.trim()) ? head.stdout.trim() : null;
1490
+ const refSha = branchRef.code === 0 ? branchRef.stdout.trim() : '';
1491
+ if (status.code !== 0)
1492
+ return { safe: false, sha, detail: 'git status could not verify worktree cleanliness' };
1493
+ if (status.stdout.trim().length > 0)
1494
+ return { safe: false, sha, detail: 'the interrupted worktree contains uncommitted changes' };
1495
+ if (actualBranch.code !== 0 || actualBranch.stdout.trim() !== row.branch) {
1496
+ return { safe: false, sha, detail: `worktree is not on expected branch ${row.branch}` };
1497
+ }
1498
+ if (!sha || refSha !== sha) {
1499
+ return { safe: false, sha, detail: `HEAD does not match refs/heads/${row.branch}` };
1500
+ }
1501
+ return { safe: true, sha };
1502
+ }
1503
+ catch (err) {
1504
+ return { safe: false, sha: null, detail: errorDetail(err) };
1505
+ }
1506
+ }
621
1507
  /**
622
- * Startup reconciliation: a crash mid-fan-out leaves worktrees on disk and ledger
623
- * rows stuck in a non-terminal state. Remove each orphan's worktree (best-effort,
624
- * keeping its branch for inspection) and mark the row `failed`. No-op (no git
625
- * calls) when there are no stuck rows — so it is free for projects that never used
626
- * isolation. Returns how many rows were reconciled.
1508
+ * Startup reconciliation is serialized with launch allocation. It proves each
1509
+ * orphan clean and durably referenced before removal; successful, dirty,
1510
+ * unknown, or ref-mismatched work remains mounted as needs-review. Building
1511
+ * delivery generations are then recovered from the durable engine outcomes.
627
1512
  */
628
1513
  async function reconcileRailWorktrees(db, repoDir, io = {}) {
629
1514
  const git = io.git ?? worktree_manager_1.defaultGitRunner;
1515
+ const exec = io.exec ?? pr_publisher_1.defaultExec;
630
1516
  const remove = io.remove ?? worktree_manager_1.removeWorktree;
631
- const stuck = (0, rail_worktrees_store_1.listNonTerminalRailWorktrees)(db);
632
- for (const row of stuck) {
633
- await remove(git, { repoDir, worktreePath: row.worktree_path, branch: row.branch, deleteBranch: false }).catch(() => { });
634
- (0, rail_worktrees_store_1.updateRailWorktreeState)(db, row.id, 'failed');
635
- }
636
- return stuck.length;
1517
+ return (0, repo_lock_1.withRepoLock)(repoDir, async () => {
1518
+ const stuck = (0, rail_worktrees_store_1.listNonTerminalRailWorktrees)(db);
1519
+ const autoReleaseWorktreeIds = new Set();
1520
+ const settledDeliveries = db.prepare(`
1521
+ SELECT decision, delivery_outcome, worktree_ids FROM rail_pr_deliveries
1522
+ WHERE decision <> 'building'
1523
+ AND delivery_outcome IN ('delivered', 'no_changes', 'retryable_failure')
1524
+ `).all();
1525
+ for (const delivery of settledDeliveries) {
1526
+ try {
1527
+ const ids = JSON.parse(delivery.worktree_ids);
1528
+ if (Array.isArray(ids)) {
1529
+ for (const id of ids)
1530
+ if (typeof id === 'string')
1531
+ autoReleaseWorktreeIds.add(id);
1532
+ }
1533
+ }
1534
+ catch { /* malformed history never authorizes cleanup */ }
1535
+ }
1536
+ const verifiedShaByRun = new Map();
1537
+ for (const row of stuck) {
1538
+ const run = row.run_id
1539
+ ? db.prepare('SELECT status, final_outcome FROM loop_runs WHERE id = ?').get(row.run_id)
1540
+ : undefined;
1541
+ const inspection = await inspectRecoveryWorktree(git, repoDir, row);
1542
+ if (run?.status === 'completed' && run.final_outcome === 'success') {
1543
+ if (inspection.safe && inspection.sha) {
1544
+ if (row.run_id)
1545
+ verifiedShaByRun.set(row.run_id, inspection.sha);
1546
+ if (autoReleaseWorktreeIds.has(row.id)) {
1547
+ try {
1548
+ await remove(git, {
1549
+ repoDir,
1550
+ worktreePath: row.worktree_path,
1551
+ branch: row.branch,
1552
+ deleteBranch: false,
1553
+ force: false,
1554
+ });
1555
+ (0, rail_worktrees_store_1.updateRailWorktreeState)(db, row.id, 'released');
1556
+ }
1557
+ catch {
1558
+ (0, rail_worktrees_store_1.updateRailWorktreeState)(db, row.id, 'built');
1559
+ }
1560
+ }
1561
+ else {
1562
+ (0, rail_worktrees_store_1.updateRailWorktreeState)(db, row.id, 'built');
1563
+ }
1564
+ }
1565
+ else {
1566
+ (0, rail_worktrees_store_1.updateRailWorktreeState)(db, row.id, 'needs-review');
1567
+ }
1568
+ continue;
1569
+ }
1570
+ if (run?.status === 'completed' && inspection.safe) {
1571
+ try {
1572
+ await remove(git, {
1573
+ repoDir,
1574
+ worktreePath: row.worktree_path,
1575
+ branch: row.branch,
1576
+ deleteBranch: false,
1577
+ force: false,
1578
+ });
1579
+ (0, rail_worktrees_store_1.updateRailWorktreeState)(db, row.id, 'failed');
1580
+ }
1581
+ catch {
1582
+ // A failed non-force removal leaves mount state uncertain; retain the ledger as
1583
+ // needs-review instead of claiming cleanup succeeded.
1584
+ (0, rail_worktrees_store_1.updateRailWorktreeState)(db, row.id, 'needs-review');
1585
+ }
1586
+ continue;
1587
+ }
1588
+ // Missing/incomplete run evidence, dirty state, status failure, and ref
1589
+ // mismatch are all recoverable uncertainty. Never force-remove them.
1590
+ (0, rail_worktrees_store_1.updateRailWorktreeState)(db, row.id, 'needs-review');
1591
+ }
1592
+ const recovered = (0, rail_pr_store_1.reconcileFailedBuildingPrDeliveries)(db, { startup: true });
1593
+ // Migration 49 repairs legacy false implementation failures before Git is
1594
+ // available. Revisit those already-pr_failed rows here: if their own
1595
+ // durable run/worktree ledger proves one exact clean object, the card can
1596
+ // safely offer Retry push instead of forcing destructive cleanup.
1597
+ const migratedInterrupted = db.prepare(`
1598
+ SELECT id FROM rail_pr_deliveries
1599
+ WHERE decision = 'pr_failed'
1600
+ AND implementation_outcome IN ('succeeded', 'partially_succeeded')
1601
+ AND delivery_outcome = 'blocked'
1602
+ AND status_code IN ('settlement_interrupted', 'recovery_unavailable')
1603
+ AND pr_url IS NOT NULL
1604
+ AND branch IS NOT NULL
1605
+ `).all()
1606
+ .map(({ id }) => (0, rail_pr_store_1.getPrDelivery)(db, id))
1607
+ .filter((delivery) => delivery !== undefined);
1608
+ // Audit rows healed by an earlier release too. Migration 49 left its causal
1609
+ // marker in the unit records even after Retry push changed the top-level
1610
+ // status, which lets a newer build replace a mistakenly frozen old branch
1611
+ // tip with the unique run-owned settlement commit.
1612
+ const previouslyRecovered = db.prepare(`
1613
+ SELECT id FROM rail_pr_deliveries
1614
+ WHERE decision IN ('pr_draft', 'pr_ready')
1615
+ AND implementation_outcome IN ('succeeded', 'partially_succeeded')
1616
+ AND delivery_outcome = 'delivered'
1617
+ AND delivery_sha IS NOT NULL
1618
+ AND pr_url IS NOT NULL
1619
+ AND branch IS NOT NULL
1620
+ `).all()
1621
+ .map(({ id }) => (0, rail_pr_store_1.getPrDelivery)(db, id))
1622
+ .filter((delivery) => {
1623
+ if (!delivery)
1624
+ return false;
1625
+ try {
1626
+ const units = JSON.parse(delivery.branches);
1627
+ return Array.isArray(units) && units.some((unit) => unit?.failureCode === 'settlement_interrupted');
1628
+ }
1629
+ catch {
1630
+ return false;
1631
+ }
1632
+ });
1633
+ const recoveryCandidates = [...new Map([...recovered, ...migratedInterrupted, ...previouslyRecovered].map((delivery) => [delivery.id, delivery])).values()];
1634
+ // `git fsck` can be materially more expensive than the ordinary refs/log
1635
+ // lookup. Defer it until a causally eligible legacy run actually needs
1636
+ // inspection, then share the exact result across every candidate in this
1637
+ // serialized reconciliation pass.
1638
+ let unreachableRecoveryScan = null;
1639
+ const getUnreachableRecoveryScan = () => {
1640
+ if (!unreachableRecoveryScan) {
1641
+ unreachableRecoveryScan = (0, rail_pr_recovery_git_1.scanUnreachableRecoveryCommits)(git, repoDir);
1642
+ }
1643
+ return unreachableRecoveryScan;
1644
+ };
1645
+ let recoveryProtectionScan = null;
1646
+ const getRecoveryProtectionScan = () => {
1647
+ if (!recoveryProtectionScan) {
1648
+ recoveryProtectionScan = (0, rail_pr_recovery_git_1.listRecoveryCommitProtections)(git, repoDir);
1649
+ }
1650
+ return recoveryProtectionScan;
1651
+ };
1652
+ for (const delivery of recoveryCandidates) {
1653
+ if (!['succeeded', 'partially_succeeded'].includes(delivery.implementation_outcome))
1654
+ continue;
1655
+ // Exact-SHA promotion here is specifically an existing-PR retry. A fresh
1656
+ // recovered generation remains on_review/ready and follows normal draft
1657
+ // creation; it must not be relabelled as a failed push merely because its
1658
+ // clean worktree also yielded a verifiable SHA.
1659
+ if (!delivery.pr_url || !delivery.branch)
1660
+ continue;
1661
+ let runIds = [];
1662
+ try {
1663
+ const parsed = JSON.parse(delivery.run_ids);
1664
+ if (Array.isArray(parsed))
1665
+ runIds = parsed.filter((value) => typeof value === 'string');
1666
+ }
1667
+ catch { /* malformed legacy row remains blocked without a retry SHA */ }
1668
+ let recoveryAwareBranches;
1669
+ const liveRecoveryPaths = [];
1670
+ try {
1671
+ const units = JSON.parse(delivery.branches);
1672
+ const worktreeIds = JSON.parse(delivery.worktree_ids);
1673
+ const ledgers = Array.isArray(worktreeIds)
1674
+ ? worktreeIds
1675
+ .filter((value) => typeof value === 'string')
1676
+ .map((worktreeId) => (0, rail_worktrees_store_1.getRailWorktree)(db, worktreeId))
1677
+ .filter((worktree) => Boolean(worktree))
1678
+ : [];
1679
+ if (Array.isArray(units)) {
1680
+ const registeredPaths = await (0, worktree_manager_1.listWorktrees)(git, repoDir);
1681
+ recoveryAwareBranches = [];
1682
+ for (const unit of units) {
1683
+ const ledger = ledgers.find((candidate) => (candidate.branch === unit.branch &&
1684
+ (unit.runId ? candidate.run_id === unit.runId : true) &&
1685
+ fs.existsSync(candidate.worktree_path)));
1686
+ if (!ledger) {
1687
+ // A path is device-local, ephemeral evidence. Never keep an old
1688
+ // pointer merely because a previous reconciliation projected it:
1689
+ // another computer or later cleanup may no longer have those
1690
+ // bytes, and the UI must not advertise a historical path as live.
1691
+ const { worktreePath: _stalePath, ...withoutStalePath } = unit;
1692
+ recoveryAwareBranches.push(withoutStalePath);
1693
+ continue;
1694
+ }
1695
+ const authenticatedPath = await authenticatedRecoveryDisplayPath(git, repoDir, ledger.worktree_path, unit.branch, registeredPaths);
1696
+ if (!authenticatedPath) {
1697
+ const { worktreePath: _stalePath, ...withoutUnauthenticatedPath } = unit;
1698
+ recoveryAwareBranches.push(withoutUnauthenticatedPath);
1699
+ continue;
1700
+ }
1701
+ liveRecoveryPaths.push(authenticatedPath);
1702
+ recoveryAwareBranches.push({ ...unit, worktreePath: authenticatedPath });
1703
+ }
1704
+ }
1705
+ }
1706
+ catch { /* malformed legacy unit/path evidence stays undisclosed */ }
1707
+ const recoveryLocation = [...new Set(liveRecoveryPaths)][0] ?? null;
1708
+ const retainBlockedWithDetail = (detail, deliverySha = null,
1709
+ // A currently authenticated worktree is actionable local evidence;
1710
+ // without one, repeated Commit promises are misleading. Recheck stays
1711
+ // legal in recovery_unavailable and can still promote a later restored
1712
+ // worktree or newly discovered orphan object.
1713
+ statusCode = recoveryLocation
1714
+ ? 'settlement_interrupted'
1715
+ : 'recovery_unavailable') => {
1716
+ const next = delivery.decision === 'pr_draft' || delivery.decision === 'pr_ready'
1717
+ ? 'pr_failed'
1718
+ : delivery.decision;
1719
+ const projectedBranchesUnchanged = recoveryAwareBranches === undefined ||
1720
+ delivery.branches === JSON.stringify(recoveryAwareBranches);
1721
+ if (delivery.decision === next && delivery.status_detail === detail &&
1722
+ delivery.delivery_sha === deliverySha && delivery.is_continuation === 1 &&
1723
+ delivery.status_code === statusCode && projectedBranchesUnchanged)
1724
+ return;
1725
+ (0, rail_pr_store_1.transitionDecision)(db, delivery.id, delivery.decision, next, {
1726
+ deliveryOutcome: 'blocked',
1727
+ statusCode,
1728
+ statusDetail: detail,
1729
+ deliverySha,
1730
+ isContinuation: true,
1731
+ branches: recoveryAwareBranches,
1732
+ });
1733
+ };
1734
+ const retainRetryableWithDetail = (detail, deliverySha) => {
1735
+ (0, rail_pr_store_1.transitionDecision)(db, delivery.id, delivery.decision, 'pr_failed', {
1736
+ deliveryOutcome: 'retryable_failure',
1737
+ statusCode: 'settlement_interrupted',
1738
+ statusDetail: detail,
1739
+ deliverySha,
1740
+ isContinuation: true,
1741
+ branches: recoveryAwareBranches,
1742
+ });
1743
+ };
1744
+ if (runIds.length === 0) {
1745
+ retainBlockedWithDetail(recoveryLocation
1746
+ ? `Exact commit recovery is unavailable because this legacy delivery has no durable run identifiers; inspect the preserved worktree at ${recoveryLocation}.`
1747
+ : 'Exact commit recovery is unavailable because this legacy delivery has no durable run identifiers; no remaining local evidence was deleted.');
1748
+ continue;
1749
+ }
1750
+ const protectionScan = await getRecoveryProtectionScan();
1751
+ const protectedCandidateSha = protectionScan.ok
1752
+ ? protectionScan.protections.get((0, rail_pr_recovery_git_1.recoveryRefForDelivery)(delivery.id)) ?? null
1753
+ : null;
1754
+ const protectedRunSha = protectedCandidateSha && runIds.length === 1 &&
1755
+ await (0, rail_pr_recovery_git_1.commitCarriesRunMarker)(git, repoDir, protectedCandidateSha, runIds[0])
1756
+ ? protectedCandidateSha
1757
+ : null;
1758
+ const ambiguousRunCommits = new Set();
1759
+ const unsafeRunEvidence = new Set();
1760
+ if (protectedCandidateSha && !protectedRunSha)
1761
+ unsafeRunEvidence.add('protected-ref');
1762
+ let unreachableDiscoveryFailed = false;
1763
+ for (const runId of runIds) {
1764
+ let inspectedSha = verifiedShaByRun.get(runId) ?? null;
1765
+ verifiedShaByRun.delete(runId);
1766
+ const ledger = db.prepare(`
1767
+ SELECT branch, worktree_path, merge_state FROM rail_worktrees
1768
+ WHERE run_id = ? AND branch = ?
1769
+ ORDER BY created_at DESC, rowid DESC LIMIT 1
1770
+ `).get(runId, delivery.branch);
1771
+ if (ledger?.merge_state === 'needs-review') {
1772
+ unsafeRunEvidence.add(runId);
1773
+ }
1774
+ else if (ledger && fs.existsSync(ledger.worktree_path)) {
1775
+ const inspection = await inspectRecoveryWorktree(git, repoDir, ledger);
1776
+ if (!inspection.safe || !inspection.sha) {
1777
+ unsafeRunEvidence.add(runId);
1778
+ }
1779
+ else {
1780
+ inspectedSha = inspection.sha;
1781
+ }
1782
+ }
1783
+ else if (ledger && !['released', 'failed'].includes(ledger.merge_state) && !inspectedSha) {
1784
+ unsafeRunEvidence.add(runId);
1785
+ }
1786
+ if (protectedRunSha && runId === runIds[0]) {
1787
+ if (inspectedSha && inspectedSha.toLowerCase() !== protectedRunSha.toLowerCase()) {
1788
+ unsafeRunEvidence.add(runId);
1789
+ }
1790
+ verifiedShaByRun.set(runId, protectedRunSha);
1791
+ continue;
1792
+ }
1793
+ const discovery = await (0, rail_pr_recovery_git_1.discoverRunMarkedCommit)(git, repoDir, runId, await getUnreachableRecoveryScan());
1794
+ if (discovery.kind === 'unique') {
1795
+ if (inspectedSha && inspectedSha.toLowerCase() !== discovery.sha.toLowerCase()) {
1796
+ unsafeRunEvidence.add(runId);
1797
+ }
1798
+ verifiedShaByRun.set(runId, discovery.sha);
1799
+ continue;
1800
+ }
1801
+ if (discovery.kind === 'ambiguous') {
1802
+ ambiguousRunCommits.add(runId);
1803
+ continue;
1804
+ }
1805
+ if (discovery.kind === 'scan_failed') {
1806
+ unreachableDiscoveryFailed = true;
1807
+ continue;
1808
+ }
1809
+ const terminal = ledger && ['released', 'failed'].includes(ledger.merge_state) ? ledger : undefined;
1810
+ if (!terminal && !inspectedSha)
1811
+ continue;
1812
+ try {
1813
+ let sha = inspectedSha ?? '';
1814
+ if (!sha && terminal && fs.existsSync(terminal.worktree_path)) {
1815
+ const inspection = await inspectRecoveryWorktree(git, repoDir, terminal);
1816
+ if (inspection.safe && inspection.sha)
1817
+ sha = inspection.sha;
1818
+ }
1819
+ else if (!sha && terminal) {
1820
+ const ref = await git.run(['rev-parse', '--verify', `refs/heads/${terminal.branch}`], repoDir);
1821
+ sha = ref.code === 0 ? ref.stdout.trim() : '';
1822
+ }
1823
+ if (COMMIT_SHA_RE.test(sha) && await (0, rail_pr_recovery_git_1.commitCarriesRunMarker)(git, repoDir, sha, runId)) {
1824
+ verifiedShaByRun.set(runId, sha);
1825
+ }
1826
+ }
1827
+ catch { /* exact retry remains blocked */ }
1828
+ }
1829
+ const shas = [...new Set(runIds.map((runId) => verifiedShaByRun.get(runId)).filter((sha) => !!sha))];
1830
+ const hasOneCompleteCandidate = !unreachableDiscoveryFailed &&
1831
+ ambiguousRunCommits.size === 0 && shas.length === 1 &&
1832
+ runIds.every((runId) => verifiedShaByRun.has(runId));
1833
+ let protectedUnsafeCandidate = null;
1834
+ if (unsafeRunEvidence.size > 0 && hasOneCompleteCandidate) {
1835
+ const protection = await (0, rail_pr_recovery_git_1.protectRecoveryCommit)(git, repoDir, delivery.id, shas[0]);
1836
+ if (!protection.ok) {
1837
+ retainBlockedWithDetail(`${protection.detail}; the unsafe worktree and every user-visible ref were left unchanged.`);
1838
+ continue;
1839
+ }
1840
+ protectedUnsafeCandidate = shas[0];
1841
+ }
1842
+ if (unsafeRunEvidence.size > 0 || unreachableDiscoveryFailed ||
1843
+ ambiguousRunCommits.size > 0 || shas.length !== 1 ||
1844
+ runIds.some((runId) => !verifiedShaByRun.has(runId))) {
1845
+ retainBlockedWithDetail(unsafeRunEvidence.size > 0
1846
+ ? recoveryLocation
1847
+ ? `Exact commit recovery found dirty, needs-review, missing, or mismatched worktree evidence; inspect the preserved worktree at ${recoveryLocation}.`
1848
+ : 'Exact commit recovery found dirty, needs-review, missing, or mismatched worktree evidence; no remaining local evidence was deleted.'
1849
+ : ambiguousRunCommits.size > 0 || shas.length > 1
1850
+ ? recoveryLocation
1851
+ ? `Exact commit recovery found multiple different commits in this legacy delivery; inspect the preserved worktree at ${recoveryLocation}.`
1852
+ : 'Exact commit recovery found multiple different commits; no ambiguous object was selected or deleted.'
1853
+ : recoveryLocation
1854
+ ? `Exact commit recovery could not prove a run-owned commit; inspect the preserved worktree at ${recoveryLocation}.`
1855
+ : 'Exact commit recovery could not prove a run-owned commit from this delivery’s refs, reflogs, or unreachable objects; no remaining local evidence was deleted.', protectedUnsafeCandidate, recoveryLocation ? 'settlement_interrupted' : 'recovery_unavailable');
1856
+ continue;
1857
+ }
1858
+ const candidateSha = shas[0];
1859
+ const protectedCommit = await (0, rail_pr_recovery_git_1.protectRecoveryCommit)(git, repoDir, delivery.id, candidateSha);
1860
+ if (!protectedCommit.ok) {
1861
+ retainBlockedWithDetail(`${protectedCommit.detail}; no local evidence was changed or removed.`);
1862
+ continue;
1863
+ }
1864
+ let causallyRecoveredBranches;
1865
+ try {
1866
+ const units = recoveryAwareBranches ?? JSON.parse(delivery.branches);
1867
+ if (Array.isArray(units)) {
1868
+ causallyRecoveredBranches = units.map((unit) => {
1869
+ const sha = unit.runId ? verifiedShaByRun.get(unit.runId) : undefined;
1870
+ if (unit.failureCode !== 'settlement_interrupted' || !sha)
1871
+ return unit;
1872
+ const { failureCode: _legacyFailure, ...rest } = unit;
1873
+ return {
1874
+ ...rest,
1875
+ succeeded: true,
1876
+ deliveryOutcome: 'ready',
1877
+ finalSha: sha,
1878
+ changed: true,
1879
+ };
1880
+ });
1881
+ }
1882
+ }
1883
+ catch { /* malformed unit evidence stays untouched and conservative */ }
1884
+ const detachFromStalePr = (detail) => {
1885
+ const partial = delivery.implementation_outcome === 'partially_succeeded';
1886
+ (0, rail_pr_store_1.transitionDecision)(db, delivery.id, delivery.decision, 'on_review', {
1887
+ prUrl: null,
1888
+ prNumber: null,
1889
+ prState: 'local-only',
1890
+ deliveryOutcome: partial ? 'partial' : 'ready',
1891
+ statusCode: partial ? 'partial_success' : 'ready_for_review',
1892
+ statusDetail: `${detail}; create a new draft PR from the preserved run-owned commit`,
1893
+ deliverySha: candidateSha,
1894
+ branches: causallyRecoveredBranches,
1895
+ isContinuation: false,
1896
+ });
1897
+ };
1898
+ const observed = await (0, pr_lifecycle_1.observePrLifecycle)(exec, repoDir, delivery.pr_url, candidateSha);
1899
+ if (!observed.ok) {
1900
+ retainRetryableWithDetail(`Recovered the exact run-owned commit, but the recorded PR could not be observed: ${observed.detail}; Retry push will revalidate before any push.`, candidateSha);
1901
+ continue;
1902
+ }
1903
+ const identityMatches = (0, pr_lifecycle_1.matchesRecordedPrIdentity)(observed, delivery.branch, delivery.base_branch);
1904
+ if (observed.state === 'MERGED') {
1905
+ if (identityMatches && observed.includesExpectedSha === true) {
1906
+ const transitioned = (0, rail_pr_store_1.transitionDecision)(db, delivery.id, delivery.decision, 'pr_ready', {
1907
+ deliveryOutcome: 'delivered', statusCode: 'pr_ready', statusDetail: null,
1908
+ deliverySha: candidateSha, branches: causallyRecoveredBranches, isContinuation: true,
1909
+ });
1910
+ if (transitioned)
1911
+ await releaseDeliveredRecoveryLineage(db, git, repoDir, delivery, candidateSha);
1912
+ }
1913
+ else {
1914
+ detachFromStalePr(observed.includesExpectedSha === true
1915
+ ? 'the previous PR was merged after its recorded head/base identity changed'
1916
+ : 'the previous PR was merged without the recovered implementation commit');
1917
+ }
1918
+ continue;
1919
+ }
1920
+ if (observed.state === 'CLOSED') {
1921
+ if (identityMatches && observed.includesExpectedSha === true) {
1922
+ const transitioned = (0, rail_pr_store_1.transitionDecision)(db, delivery.id, delivery.decision, 'pr_closed', {
1923
+ deliveryOutcome: 'delivered', statusCode: 'pr_closed', statusDetail: null,
1924
+ deliverySha: candidateSha, branches: causallyRecoveredBranches, isContinuation: true,
1925
+ });
1926
+ if (transitioned)
1927
+ await releaseDeliveredRecoveryLineage(db, git, repoDir, delivery, candidateSha);
1928
+ }
1929
+ else {
1930
+ detachFromStalePr(observed.includesExpectedSha === true
1931
+ ? 'the previous PR was closed after its recorded head/base identity changed'
1932
+ : 'the previous PR was closed without the recovered implementation commit');
1933
+ }
1934
+ continue;
1935
+ }
1936
+ if (!(0, pr_lifecycle_1.isExactOpenPr)(observed, delivery.branch, delivery.base_branch)) {
1937
+ detachFromStalePr('the previous open PR no longer matches its recorded head/base identity');
1938
+ continue;
1939
+ }
1940
+ if (observed.includesExpectedSha === true) {
1941
+ const next = observed.isDraft ? 'pr_draft' : 'pr_ready';
1942
+ const transitioned = (0, rail_pr_store_1.transitionDecision)(db, delivery.id, delivery.decision, next, {
1943
+ deliveryOutcome: 'delivered',
1944
+ statusCode: next === 'pr_ready' ? 'pr_ready' : 'existing_pr_updated',
1945
+ statusDetail: null,
1946
+ deliverySha: candidateSha,
1947
+ branches: causallyRecoveredBranches,
1948
+ isContinuation: true,
1949
+ });
1950
+ if (transitioned)
1951
+ await releaseDeliveredRecoveryLineage(db, git, repoDir, delivery, candidateSha);
1952
+ continue;
1953
+ }
1954
+ if (observed.headRefOid && !await recoveryCommitObjectExists(git, repoDir, observed.headRefOid)) {
1955
+ try {
1956
+ await git.run(['fetch', 'origin', `refs/heads/${delivery.branch}`], repoDir);
1957
+ }
1958
+ catch { /* the immutable object check below remains authoritative */ }
1959
+ }
1960
+ if (!observed.headRefOid ||
1961
+ !await recoveryCommitObjectExists(git, repoDir, observed.headRefOid) ||
1962
+ !await isFastForwardRecoveryCommit(git, repoDir, observed.headRefOid, candidateSha)) {
1963
+ retainBlockedWithDetail(observed.headRefOid && await recoveryCommitObjectExists(git, repoDir, observed.headRefOid)
1964
+ ? 'Recovered the exact run-owned commit, but it is not a fast-forward of the live PR head. The commit remains protected and nothing was pushed or removed.'
1965
+ : 'Recovered the exact run-owned commit, but the live PR head object could not be verified locally. The commit remains protected and nothing was pushed or removed.', candidateSha, 'recovery_unavailable');
1966
+ continue;
1967
+ }
1968
+ (0, rail_pr_store_1.transitionDecision)(db, delivery.id, delivery.decision, 'pr_failed', {
1969
+ deliveryOutcome: 'retryable_failure',
1970
+ statusCode: 'settlement_interrupted',
1971
+ statusDetail: 'Recovered a clean exact continuation commit; delivery can be retried safely.',
1972
+ deliverySha: candidateSha,
1973
+ branches: causallyRecoveredBranches,
1974
+ isContinuation: true,
1975
+ });
1976
+ }
1977
+ // Internal refs are deliberately released only after the durable row proves
1978
+ // exact delivery/no-change. If a previous process crashed after that row
1979
+ // transition but before `update-ref -d`, retry the idempotent exact cleanup
1980
+ // now, including discarded/superseded predecessor refs whose objects are
1981
+ // ancestors of the delivered successor.
1982
+ const settledRecoveryRefs = db.prepare(`
1983
+ SELECT id FROM rail_pr_deliveries
1984
+ WHERE delivery_sha IS NOT NULL
1985
+ AND (delivery_outcome IN ('delivered','no_changes') OR decision IN ('merged','completed'))
1986
+ `).all()
1987
+ .map(({ id }) => (0, rail_pr_store_1.getPrDelivery)(db, id))
1988
+ .filter((row) => Boolean(row?.delivery_sha));
1989
+ if (settledRecoveryRefs.length > 0) {
1990
+ const protectionScan = await getRecoveryProtectionScan();
1991
+ if (protectionScan.ok && protectionScan.protections.size > 0) {
1992
+ for (const settled of settledRecoveryRefs) {
1993
+ const visited = new Set();
1994
+ let lineage = settled;
1995
+ let hasProtectedLineage = false;
1996
+ for (let depth = 0; lineage && depth < 32 && !visited.has(lineage.id); depth++) {
1997
+ visited.add(lineage.id);
1998
+ if (protectionScan.protections.has((0, rail_pr_recovery_git_1.recoveryRefForDelivery)(lineage.id))) {
1999
+ hasProtectedLineage = true;
2000
+ break;
2001
+ }
2002
+ lineage = lineage.supersedes_delivery_id
2003
+ ? (0, rail_pr_store_1.getPrDelivery)(db, lineage.supersedes_delivery_id)
2004
+ : undefined;
2005
+ }
2006
+ if (hasProtectedLineage) {
2007
+ await releaseDeliveredRecoveryLineage(db, git, repoDir, settled, settled.delivery_sha);
2008
+ }
2009
+ }
2010
+ }
2011
+ }
2012
+ for (const delivery of recoveryCandidates) {
2013
+ try {
2014
+ io.onDeliveryRecovered?.(delivery.id);
2015
+ }
2016
+ catch { /* durable state wins over advisory surfaces */ }
2017
+ }
2018
+ return stuck.length;
2019
+ });
637
2020
  }