specrails-desktop 2.29.0 → 2.29.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (545) hide show
  1. package/client/dist/assets/ActivityFeedPage-ClpD5CFj.js +1 -0
  2. package/client/dist/assets/AgentBrowserCapture-CSPs9Ofc.js +1 -0
  3. package/client/dist/assets/AgentModeCodePane-DwcyklA5.js +2 -0
  4. package/client/dist/assets/AgentModeJobsPane-C69bqlMa.js +1 -0
  5. package/client/dist/assets/AgentsPage-pRiRr9-W.js +86 -0
  6. package/client/dist/assets/AnalyticsPage-C14exKsC.js +1 -0
  7. package/client/dist/assets/BarChart-nBOMNfiy.js +33 -0
  8. package/client/dist/assets/CodePage-CK76JKm9.js +2 -0
  9. package/client/dist/assets/DesktopAnalyticsPage-Cnv0f3XQ.js +1 -0
  10. package/client/dist/assets/DocsDialog-CHUm_Ym0.js +11 -0
  11. package/client/dist/assets/DocsPage-COI-oDok.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-CypM7R1F.js +19 -0
  15. package/client/dist/assets/JobDetailModal-B_W0H9dw.js +1 -0
  16. package/client/dist/assets/JobDetailPage-CeeXOoKS.js +1 -0
  17. package/client/dist/assets/JobsPage-CwhJG9jw.js +1 -0
  18. package/client/dist/assets/LoopBuilderPage-C51t1WOM.js +7 -0
  19. package/client/dist/assets/LoopPreviewModal-Y2HgtIJu.js +1 -0
  20. package/client/dist/assets/LoopsPage-Bvn1Kw5V.js +1 -0
  21. package/client/dist/assets/MinimizedChatsContext-DiFvZDPW.js +1 -0
  22. package/client/dist/assets/PluginsPage-BGPRJpdf.js +2 -0
  23. package/client/dist/assets/ProjectSettingsDialog-D_GTyGLc.js +1 -0
  24. package/client/dist/assets/TemplatePreviewModal-szwBhd7h.js +1 -0
  25. package/client/dist/assets/TicketDetailModal-9UEyK974.js +14 -0
  26. package/client/dist/assets/Trans-B_vKZ-Hb.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-4rmXoo_p.js +1 -0
  33. package/client/dist/assets/agent-BxZuNGih.js +1 -0
  34. package/client/dist/assets/agent-C3uXOYv_.js +1 -0
  35. package/client/dist/assets/agent-CA8Y4wbw.js +1 -0
  36. package/client/dist/assets/agent-CASKrhG12.js +1 -0
  37. package/client/dist/assets/agent-Cv-jud5n2.js +1 -0
  38. package/client/dist/assets/agent-DitUxYh2.js +1 -0
  39. package/client/dist/assets/agent-Doow4X7o.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-CD1QHriA2.js +1 -0
  58. package/client/dist/assets/common-CwwPaRxx.js +1 -0
  59. package/client/dist/assets/common-DVQuhs-m.js +1 -0
  60. package/client/dist/assets/common-Dg2jaX7d.js +1 -0
  61. package/client/dist/assets/common-DuBwj6tK.js +1 -0
  62. package/client/dist/assets/common-MRj9MHcc.js +1 -0
  63. package/client/dist/assets/common-O4ZpnMLp.js +1 -0
  64. package/client/dist/assets/common-fyeF0v7C2.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-B_NkSHYm.js +1 -0
  69. package/client/dist/assets/dashboard-ClBb_3pq.js +1 -0
  70. package/client/dist/assets/dashboard-CpCmJvMB.js +1 -0
  71. package/client/dist/assets/dashboard-Czohu35O2.js +1 -0
  72. package/client/dist/assets/dashboard-DcjbD0UK.js +1 -0
  73. package/client/dist/assets/dashboard-Dvx5F3we.js +1 -0
  74. package/client/dist/assets/dashboard-R1bOPjBh2.js +1 -0
  75. package/client/dist/assets/dashboard-owcDucDi.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-UjXbEWhN.js → dist-js-CcBCbb32.js} +1 -1
  80. package/client/dist/assets/{dist-js-BTAcxhPb.js → dist-js-VwztRkGj.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-ByHIauTQ.js +1 -0
  89. package/client/dist/assets/{freemarker2-J5iIIIK-.js → freemarker2-CELlTDJ2.js} +1 -1
  90. package/client/dist/assets/getTimezoneOffsetInMilliseconds-BxHAksaU.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-CgaRxpII.js +2 -0
  97. package/client/dist/assets/index-BflYQpWm.css +2 -0
  98. package/client/dist/assets/index-C1_-BTXy.js +72 -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/{settings-Bp24JHlv2.js → settings-B9ilOSYO.js} +1 -1
  137. package/client/dist/assets/{settings-Bx2ObkTR2.js → settings-BqlWHRHA.js} +1 -1
  138. package/client/dist/assets/{settings-C_T0-j9d.js → settings-D8dlqt4d.js} +1 -1
  139. package/client/dist/assets/{settings-DhaFKpcw2.js → settings-DAZqjX2a2.js} +1 -1
  140. package/client/dist/assets/{settings-B_v3zgJf.js → settings-DRh39v1S2.js} +1 -1
  141. package/client/dist/assets/{settings-BemR_e3F.js → settings-Mj1OJbfd2.js} +1 -1
  142. package/client/dist/assets/{settings-BvIwQ3A1.js → settings-lRReaSYm.js} +1 -1
  143. package/client/dist/assets/setup-CmK87LuS.js +1 -0
  144. package/client/dist/assets/sparkles-DrY8hb0X.js +1 -0
  145. package/client/dist/assets/specs-DII0C8gw.js +1 -0
  146. package/client/dist/assets/spending-C4p06nen.js +1 -0
  147. package/client/dist/assets/terminal-BFRZnMPP.js +1 -0
  148. package/client/dist/assets/tickets-B8BuAaj5.js +1 -0
  149. package/client/dist/assets/tickets-B_yh4qQk.js +1 -0
  150. package/client/dist/assets/tickets-Bs37scwY2.js +1 -0
  151. package/client/dist/assets/tickets-CFk2n2Or.js +1 -0
  152. package/client/dist/assets/tickets-DQp7JS2R2.js +1 -0
  153. package/client/dist/assets/tickets-D_yDquEw.js +1 -0
  154. package/client/dist/assets/tickets-DpmdE8tv2.js +1 -0
  155. package/client/dist/assets/tickets-pAUJkDTu.js +1 -0
  156. package/client/dist/assets/trash-2-BOU3WdZZ.js +1 -0
  157. package/client/dist/assets/triangle-alert-H9T4jAmh.js +1 -0
  158. package/client/dist/assets/{ts.worker-METxwbDZ.js → ts.worker-BtjBFNHX.js} +48 -33
  159. package/client/dist/assets/{tsMode-BmT8WogI.js → tsMode-DEjSThU9.js} +1 -1
  160. package/client/dist/assets/{typescript-CQrQPsoW.js → typescript-CRRnD2Ts.js} +1 -1
  161. package/client/dist/assets/upload-DYMjAQ6M.js +1 -0
  162. package/client/dist/assets/useBrowserCaptureSession-2OqX0vap.js +1 -0
  163. package/client/dist/assets/useDesktop-X8YB9xzv.js +1 -0
  164. package/client/dist/assets/useSharedWebSocket-DaPh7QBL.js +1 -0
  165. package/client/dist/assets/utils-BC4DHlz2.js +1 -0
  166. package/client/dist/assets/value-s-gzBeyF.js +1 -0
  167. package/client/dist/assets/{workers-U0UXyLUf.js → workers-uOo7WLaa.js} +1 -1
  168. package/client/dist/assets/x-CBMI9ejn.js +1 -0
  169. package/client/dist/assets/{xml-WRxJs8_I.js → xml-EDREH2X7.js} +1 -1
  170. package/client/dist/assets/{yaml-CjpSTjJU.js → yaml-BjaDVX1E.js} +1 -1
  171. package/client/dist/assets/zap-CHo3aCD1.js +1 -0
  172. package/client/dist/index.html +73 -32
  173. package/docs/guide/en/integrations/5-mcp-server.md +2 -2
  174. package/docs/internals/safe-pr-review-flow.md +190 -132
  175. package/docs/mcp.md +15 -5
  176. package/package.json +11 -7
  177. package/server/dist/active-pr-continuation.js +16 -1
  178. package/server/dist/agent-chat-manager.js +360 -181
  179. package/server/dist/agent-cwd-manager.js +52 -8
  180. package/server/dist/agent-generator.js +90 -48
  181. package/server/dist/agent-mcp-config.js +69 -36
  182. package/server/dist/agent-operator-prompt.js +3 -2
  183. package/server/dist/agent-refine-manager.js +47 -6
  184. package/server/dist/agent-tier.js +13 -11
  185. package/server/dist/artifact-registry.js +33 -2
  186. package/server/dist/auth.js +5 -4
  187. package/server/dist/browser-capture-manager.js +92 -29
  188. package/server/dist/chat-manager.js +302 -46
  189. package/server/dist/contract-refine-runner.js +19 -6
  190. package/server/dist/db.js +665 -8
  191. package/server/dist/desktop-router.js +1 -2
  192. package/server/dist/headroom-manager.js +1310 -121
  193. package/server/dist/headroom-routing.js +120 -2
  194. package/server/dist/index.js +48 -13
  195. package/server/dist/interactive-job-session.js +237 -82
  196. package/server/dist/jira/jira-adf.js +10 -0
  197. package/server/dist/jira/jira-db.js +15 -3
  198. package/server/dist/jira/jira-sync-manager.js +182 -13
  199. package/server/dist/job-listing.js +140 -0
  200. package/server/dist/job-spawn-idempotency.js +67 -0
  201. package/server/dist/loop-run-manager.js +508 -98
  202. package/server/dist/loop-runs-store.js +266 -13
  203. package/server/dist/mcp/agent-capability.js +71 -0
  204. package/server/dist/mcp/guide.js +5 -2
  205. package/server/dist/mcp/mcp-server.js +5 -10
  206. package/server/dist/mcp/mcp-token.js +5 -5
  207. package/server/dist/mcp/tools/jobs.js +20 -13
  208. package/server/dist/mcp/tools/types.js +20 -42
  209. package/server/dist/mobile/mobile-router.js +1 -1
  210. package/server/dist/pr-lifecycle.js +77 -0
  211. package/server/dist/pr-publisher.js +94 -27
  212. package/server/dist/process-admission.js +82 -0
  213. package/server/dist/project-registry.js +572 -174
  214. package/server/dist/project-router-git.js +13 -2
  215. package/server/dist/project-router-jobs.js +111 -55
  216. package/server/dist/project-router-tickets.js +32 -2
  217. package/server/dist/providers/claude-adapter.js +21 -13
  218. package/server/dist/queue-manager.js +2332 -623
  219. package/server/dist/rail-isolated-launch.js +1141 -280
  220. package/server/dist/rail-isolation.js +4 -2
  221. package/server/dist/rail-pr-decision.js +756 -270
  222. package/server/dist/rail-pr-delivery.js +2 -1
  223. package/server/dist/rail-pr-store.js +317 -18
  224. package/server/dist/rail-pr-ticket-effects.js +328 -0
  225. package/server/dist/rail-worktree-release.js +28 -6
  226. package/server/dist/rails-router.js +149 -112
  227. package/server/dist/rails-store.js +106 -3
  228. package/server/dist/ticket-store.js +23 -1
  229. package/server/dist/transient-children.js +13 -0
  230. package/server/dist/util/cli-prompt.js +42 -8
  231. package/server/dist/util/secure-token-file.js +84 -0
  232. package/server/dist/worktree-manager.js +70 -21
  233. package/client/dist/assets/ActivityFeedPage-Be-6L0Ld.js +0 -1
  234. package/client/dist/assets/AgentBrowserCapture-CpZ-dd8O.js +0 -1
  235. package/client/dist/assets/AgentIntegrationsModal-DO1pN_Ez.js +0 -1
  236. package/client/dist/assets/AgentModeCodePane-DLltTOqp.js +0 -2
  237. package/client/dist/assets/AgentModeJobsPane-Dp5YvmW0.js +0 -1
  238. package/client/dist/assets/AgentsPage-ehHuVUds.js +0 -86
  239. package/client/dist/assets/AnalyticsPage-C4Ik95FX.js +0 -1
  240. package/client/dist/assets/BarChart-DVZgrhJd.js +0 -33
  241. package/client/dist/assets/CodePage-D4OCMfxL.js +0 -2
  242. package/client/dist/assets/DesktopAnalyticsPage-2Xf-52x2.js +0 -1
  243. package/client/dist/assets/DocsDialog-CHihLjWd.js +0 -11
  244. package/client/dist/assets/DocsPage-1vEk7XbH.js +0 -11
  245. package/client/dist/assets/ExportDropdown-DLHFQxVn.js +0 -1
  246. package/client/dist/assets/IntegrationsPage-CCb2YuyJ.js +0 -1
  247. package/client/dist/assets/InteractiveJobComposer-CxJJEnjw.js +0 -19
  248. package/client/dist/assets/JobDetailModal-CJI7qRkm.js +0 -1
  249. package/client/dist/assets/JobDetailPage-DwA9bbwx.js +0 -1
  250. package/client/dist/assets/JobsPage-BDIyFfd5.js +0 -1
  251. package/client/dist/assets/LoopBuilderPage-CpBaW59t.js +0 -7
  252. package/client/dist/assets/LoopPreviewModal-CwpKcv0Y.js +0 -1
  253. package/client/dist/assets/LoopsPage-BO0Pa8oz.js +0 -1
  254. package/client/dist/assets/PluginsPage-BBGTBpGh.js +0 -2
  255. package/client/dist/assets/ProjectSettingsDialog-M9zGQo0l.js +0 -1
  256. package/client/dist/assets/TemplatePreviewModal-eBI0zL5P.js +0 -1
  257. package/client/dist/assets/activity-Dbl71S-E.js +0 -1
  258. package/client/dist/assets/addon-image-CwoZj17F.js +0 -1
  259. package/client/dist/assets/addon-ligatures-CWDrED80.js +0 -2
  260. package/client/dist/assets/addspec-DrXqP8ip.js +0 -1
  261. package/client/dist/assets/agent-BEXYxDbs.js +0 -1
  262. package/client/dist/assets/agent-BoXSzaBk2.js +0 -1
  263. package/client/dist/assets/agent-C2-rROL52.js +0 -1
  264. package/client/dist/assets/agent-CeRTdGT12.js +0 -1
  265. package/client/dist/assets/agent-DZ8zTrQT.js +0 -1
  266. package/client/dist/assets/agent-DqyzT33X.js +0 -1
  267. package/client/dist/assets/agent-fQI_6tsA.js +0 -1
  268. package/client/dist/assets/agent-tJdAobaM.js +0 -1
  269. package/client/dist/assets/agents-CGDn_3kB.js +0 -1
  270. package/client/dist/assets/agentstudio-Dxe_94PK.js +0 -1
  271. package/client/dist/assets/aiedit-DrfHQnlz.js +0 -1
  272. package/client/dist/assets/analytics-CxDYcBl9.js +0 -1
  273. package/client/dist/assets/attachments-DvxQbBTC.js +0 -1
  274. package/client/dist/assets/brain-DecvjjPU.js +0 -1
  275. package/client/dist/assets/browser-DM8G151q.js +0 -1
  276. package/client/dist/assets/chat-0dg5hOo_.js +0 -1
  277. package/client/dist/assets/chunk-BNv3lrIs.js +0 -1
  278. package/client/dist/assets/code-B62fClk-.js +0 -1
  279. package/client/dist/assets/commands-DCJ-R9E4.js +0 -1
  280. package/client/dist/assets/common-BfQD173j.js +0 -1
  281. package/client/dist/assets/common-CF8iGXrG.js +0 -1
  282. package/client/dist/assets/common-Cfmwcrl52.js +0 -1
  283. package/client/dist/assets/common-D2cimsul.js +0 -1
  284. package/client/dist/assets/common-DH7fjyL1.js +0 -1
  285. package/client/dist/assets/common-DXrmj6WD.js +0 -1
  286. package/client/dist/assets/common-DZbXlmq72.js +0 -1
  287. package/client/dist/assets/common-DtXmzX2o2.js +0 -1
  288. package/client/dist/assets/dashboard-5C2FOZQX.js +0 -1
  289. package/client/dist/assets/dashboard-B6iQ-IL_.js +0 -1
  290. package/client/dist/assets/dashboard-BFfnCtq52.js +0 -1
  291. package/client/dist/assets/dashboard-BIC8KtRf2.js +0 -1
  292. package/client/dist/assets/dashboard-CEgg9NAr.js +0 -1
  293. package/client/dist/assets/dashboard-CiOyDh9L.js +0 -1
  294. package/client/dist/assets/dashboard-D6x5OnNj2.js +0 -1
  295. package/client/dist/assets/dashboard-DBuLfWo9.js +0 -1
  296. package/client/dist/assets/editor.api2-C3B0tp2x.js +0 -872
  297. package/client/dist/assets/editor.worker-Bd9IXS8d.js +0 -26
  298. package/client/dist/assets/explore-_XyU5e7R.js +0 -1
  299. package/client/dist/assets/index-BVbgKXUu.css +0 -2
  300. package/client/dist/assets/index-OBDI7bHm.js +0 -158
  301. package/client/dist/assets/integrations-By80EBkf.js +0 -1
  302. package/client/dist/assets/jira-DeIjwCK3.js +0 -1
  303. package/client/dist/assets/jobs-BFIdeDTw.js +0 -1
  304. package/client/dist/assets/jobs-BJ-Gcao22.js +0 -1
  305. package/client/dist/assets/jobs-DAGiPVIS2.js +0 -1
  306. package/client/dist/assets/jobs-DAU8C_tZ.js +0 -1
  307. package/client/dist/assets/jobs-Da0GaaSc.js +0 -1
  308. package/client/dist/assets/jobs-DrYmsKy5.js +0 -1
  309. package/client/dist/assets/jobs-P3unxtxo2.js +0 -1
  310. package/client/dist/assets/jobs-p360zdf1.js +0 -1
  311. package/client/dist/assets/json.worker-DzV-CpCQ.js +0 -58
  312. package/client/dist/assets/lib-C5ZxrPSR.js +0 -7
  313. package/client/dist/assets/loops-CO6q75W8.js +0 -1
  314. package/client/dist/assets/mcp-HwBbQDk0.js +0 -1
  315. package/client/dist/assets/monaco.contribution-Desq7oq4.js +0 -2
  316. package/client/dist/assets/nav-BTg8s-cD.js +0 -1
  317. package/client/dist/assets/preload-helper-DSXbuxSR.js +0 -1
  318. package/client/dist/assets/settings-BYCMtxOz.js +0 -1
  319. package/client/dist/assets/setup-D2CmgAro.js +0 -1
  320. package/client/dist/assets/specs-PGIOzMtc.js +0 -1
  321. package/client/dist/assets/terminal-CFt56A5f.js +0 -1
  322. package/client/dist/assets/tickets-BHv3ewvq.js +0 -1
  323. package/client/dist/assets/tickets-BIxU86j12.js +0 -1
  324. package/client/dist/assets/tickets-C2BhwzwR.js +0 -1
  325. package/client/dist/assets/tickets-D1IeX2EW.js +0 -1
  326. package/client/dist/assets/tickets-DZut8Ryt.js +0 -1
  327. package/client/dist/assets/tickets-LfK6LzKl2.js +0 -1
  328. package/client/dist/assets/tickets-dQfsc7Ph.js +0 -1
  329. package/client/dist/assets/tickets-sVun0I6v.js +0 -1
  330. package/client/dist/assets/upload-lgjKJgrl.js +0 -1
  331. package/client/dist/assets/value-Bc0_jlJA.js +0 -1
  332. /package/client/dist/assets/{abap-D0E4FfVW.js → abap-08VXUWAP.js} +0 -0
  333. /package/client/dist/assets/{activity-BbWiDfn-2.js → activity-B-rp4h_v.js} +0 -0
  334. /package/client/dist/assets/{activity-DvECTYGt.js → activity-C6kJpm9q2.js} +0 -0
  335. /package/client/dist/assets/{activity-D3Ic-HTS.js → activity-CGx4kb_j.js} +0 -0
  336. /package/client/dist/assets/{activity-BZZlEG3a.js → activity-CnoX2bH7.js} +0 -0
  337. /package/client/dist/assets/{activity-DtzRy92x.js → activity-DAyekdlt2.js} +0 -0
  338. /package/client/dist/assets/{activity-FleOlK0l2.js → activity-DCxtpT7j.js} +0 -0
  339. /package/client/dist/assets/{activity-DFbvNqhE2.js → activity-JzwRaWPD.js} +0 -0
  340. /package/client/dist/assets/{addspec-CFxGvmJR2.js → addspec-BXjvkh5q.js} +0 -0
  341. /package/client/dist/assets/{addspec-NBbcTbLc.js → addspec-C4bwrmpi.js} +0 -0
  342. /package/client/dist/assets/{addspec-BKrH2Ucz.js → addspec-CArHXUDM.js} +0 -0
  343. /package/client/dist/assets/{addspec-CR8MB5Fr2.js → addspec-DTKJcXiR.js} +0 -0
  344. /package/client/dist/assets/{addspec-B_LQMHW3.js → addspec-DbNBbrha2.js} +0 -0
  345. /package/client/dist/assets/{addspec-DaA-CbTt.js → addspec-htHGZHOe2.js} +0 -0
  346. /package/client/dist/assets/{addspec-Bw_YQwo62.js → addspec-lr_LP6jk.js} +0 -0
  347. /package/client/dist/assets/{agents-BHgn--3j.js → agents-6ZOJXWVk2.js} +0 -0
  348. /package/client/dist/assets/{agents-VsyFhGOS2.js → agents-BZ0XQKjg.js} +0 -0
  349. /package/client/dist/assets/{agents-HFCqQH0Z.js → agents-BqWklnHR.js} +0 -0
  350. /package/client/dist/assets/{agents-B_qGaB0F.js → agents-Bz-thHkB2.js} +0 -0
  351. /package/client/dist/assets/{agents-CP8_ezYG.js → agents-CJclElx6.js} +0 -0
  352. /package/client/dist/assets/{agents-B9pFSJQI2.js → agents-D_l9L0VA.js} +0 -0
  353. /package/client/dist/assets/{agents-CV-_xBvX2.js → agents-u_C0jPB5.js} +0 -0
  354. /package/client/dist/assets/{agentstudio-BEp7Sb46.js → agentstudio-BF19hbXj.js} +0 -0
  355. /package/client/dist/assets/{agentstudio-Br3WD7Xs.js → agentstudio-BLGvxWBx2.js} +0 -0
  356. /package/client/dist/assets/{agentstudio-BOjG6tgJ.js → agentstudio-D9UKiWTj2.js} +0 -0
  357. /package/client/dist/assets/{agentstudio-D2A5peWV2.js → agentstudio-Drs37nIM.js} +0 -0
  358. /package/client/dist/assets/{agentstudio-CuTt3ZGe2.js → agentstudio-nEnKkcXM.js} +0 -0
  359. /package/client/dist/assets/{agentstudio-RsxnZtUA.js → agentstudio-pyFcSBKy.js} +0 -0
  360. /package/client/dist/assets/{agentstudio-RfZavULt2.js → agentstudio-zfTzQh3U.js} +0 -0
  361. /package/client/dist/assets/{aiedit-DnBzjlVz2.js → aiedit-B0KUbpB6.js} +0 -0
  362. /package/client/dist/assets/{aiedit-D0Syl-mC.js → aiedit-BoG24nlg2.js} +0 -0
  363. /package/client/dist/assets/{aiedit-CcugGPbS.js → aiedit-CjlSRQVP.js} +0 -0
  364. /package/client/dist/assets/{aiedit-DrIjSojo.js → aiedit-DCjA7jLJ.js} +0 -0
  365. /package/client/dist/assets/{aiedit-CV520D4W2.js → aiedit-DNXxq_Dr.js} +0 -0
  366. /package/client/dist/assets/{aiedit-26p5A_HY2.js → aiedit-DQ-0_pvE.js} +0 -0
  367. /package/client/dist/assets/{aiedit-a-Z-g5iC.js → aiedit-olnv3QSt2.js} +0 -0
  368. /package/client/dist/assets/{analytics-DEe5BMz8.js → analytics-BC7mwR4s.js} +0 -0
  369. /package/client/dist/assets/{analytics-4HvXb0Vn2.js → analytics-BiNg8edP.js} +0 -0
  370. /package/client/dist/assets/{analytics-CpSTNfui.js → analytics-C0s_T2GN.js} +0 -0
  371. /package/client/dist/assets/{analytics-4-eskbtX.js → analytics-CdsAPAwZ2.js} +0 -0
  372. /package/client/dist/assets/{analytics-Bpe41fnX2.js → analytics-Cr1_q-lC.js} +0 -0
  373. /package/client/dist/assets/{analytics-DcOITHla2.js → analytics-IEsKZvpW2.js} +0 -0
  374. /package/client/dist/assets/{analytics-Bd9rHGjV.js → analytics-SRPf84OT.js} +0 -0
  375. /package/client/dist/assets/{apex-D_X_8J4d.js → apex-BWPQTe0t.js} +0 -0
  376. /package/client/dist/assets/{atom-one-dark-BMDPFwRZ.js → atom-one-dark-qiGhVQxg.js} +0 -0
  377. /package/client/dist/assets/{attachments-CgOQ9K-6.js → attachments-D0wXXt9T2.js} +0 -0
  378. /package/client/dist/assets/{attachments-DlEQu56g2.js → attachments-DenIjuVI.js} +0 -0
  379. /package/client/dist/assets/{attachments-CA2f3qL3.js → attachments-DhO7LPfy.js} +0 -0
  380. /package/client/dist/assets/{attachments-D2jwJXQ42.js → attachments-DoH5w0x4.js} +0 -0
  381. /package/client/dist/assets/{attachments-CHDbhKeH.js → attachments-Q9yNtiEM.js} +0 -0
  382. /package/client/dist/assets/{attachments-CKAY5aYG2.js → attachments-V_WZAYXx2.js} +0 -0
  383. /package/client/dist/assets/{attachments-jVdKzAXn.js → attachments-eaeScpjM.js} +0 -0
  384. /package/client/dist/assets/{auth-DpZq5p1e.js → auth-D_5bQv5W.js} +0 -0
  385. /package/client/dist/assets/{azcli-PZLnZ3jH.js → azcli-Bc_sGQ0U.js} +0 -0
  386. /package/client/dist/assets/{bat-KlBOxpO4.js → bat-i0X4ZdIN.js} +0 -0
  387. /package/client/dist/assets/{bicep-CgG9oF3V.js → bicep-B5-_aFwp.js} +0 -0
  388. /package/client/dist/assets/{browser-Cl-FwPXs.js → browser-C25DkQiO.js} +0 -0
  389. /package/client/dist/assets/{browser-JsiohNmW.js → browser-CiJ7-L32.js} +0 -0
  390. /package/client/dist/assets/{browser-BbJ_q_OT2.js → browser-CvgDySd9.js} +0 -0
  391. /package/client/dist/assets/{browser-BQFDxrRq.js → browser-CyR1Kp3m2.js} +0 -0
  392. /package/client/dist/assets/{browser-Do2jf8_N.js → browser-DwZljcRh2.js} +0 -0
  393. /package/client/dist/assets/{browser-C4VJ5CW12.js → browser-DxP-alBK.js} +0 -0
  394. /package/client/dist/assets/{browser-kA31eYhu2.js → browser-rRCwi7Y7.js} +0 -0
  395. /package/client/dist/assets/{cameligo-6R45j6Ph.js → cameligo-DMUM7wLl.js} +0 -0
  396. /package/client/dist/assets/{chat-BalviUAD.js → chat-2rIB8HhT.js} +0 -0
  397. /package/client/dist/assets/{chat-Ds8GvCnv.js → chat-BR8oe53x2.js} +0 -0
  398. /package/client/dist/assets/{chat-D5xEtsf3.js → chat-Bua03tty.js} +0 -0
  399. /package/client/dist/assets/{chat-SaG-ng1x2.js → chat-CZpAQan1.js} +0 -0
  400. /package/client/dist/assets/{chat-CJddjN3D2.js → chat-CdY5SGvS.js} +0 -0
  401. /package/client/dist/assets/{chat-n43nZGvn.js → chat-DZyu3ImY2.js} +0 -0
  402. /package/client/dist/assets/{chat-BMWD_Rx12.js → chat-D_I7nU5K.js} +0 -0
  403. /package/client/dist/assets/{clojure-CLAAMO7J.js → clojure-Cm7r79vr.js} +0 -0
  404. /package/client/dist/assets/{clsx-CNmW0Nip.js → clsx-CjueKrWZ.js} +0 -0
  405. /package/client/dist/assets/{code-C9U6M5-e2.js → code-76gS0IdL.js} +0 -0
  406. /package/client/dist/assets/{code-BotxM43N2.js → code-C1VwxlML.js} +0 -0
  407. /package/client/dist/assets/{code-HDK4P6Rx.js → code-CnMZyJpr.js} +0 -0
  408. /package/client/dist/assets/{code-DUe1Bouz.js → code-D20ef9Cf2.js} +0 -0
  409. /package/client/dist/assets/{code-Bit_z2Xd2.js → code-DdOXaWRY.js} +0 -0
  410. /package/client/dist/assets/{code-F_KjccGf.js → code-Duohuf9l.js} +0 -0
  411. /package/client/dist/assets/{code-CIPmDEd0.js → code-geHmknB92.js} +0 -0
  412. /package/client/dist/assets/{coffee-AruSygo-.js → coffee-Ba7i2nA0.js} +0 -0
  413. /package/client/dist/assets/{commands-BFQZc9AG.js → commands-B9RZO4qS.js} +0 -0
  414. /package/client/dist/assets/{commands-BMkC5-ki.js → commands-BEwbAjtD2.js} +0 -0
  415. /package/client/dist/assets/{commands-KBbhsnw8.js → commands-Bbxu8kLU.js} +0 -0
  416. /package/client/dist/assets/{commands-CzFSoIiq2.js → commands-CDU3qv68.js} +0 -0
  417. /package/client/dist/assets/{commands-CWX9WmDX2.js → commands-CzezgtQj.js} +0 -0
  418. /package/client/dist/assets/{commands-BllkJauO.js → commands-DTQBF_q52.js} +0 -0
  419. /package/client/dist/assets/{commands-cKTaLwwV2.js → commands-hOtKuOXc.js} +0 -0
  420. /package/client/dist/assets/{cpp-DEE5r4Pw.js → cpp-C7h46wYY.js} +0 -0
  421. /package/client/dist/assets/{csharp-BVXJUvbK.js → csharp-BKxtCVv1.js} +0 -0
  422. /package/client/dist/assets/{csp-cXOF0JoV.js → csp-bTuwJoIa.js} +0 -0
  423. /package/client/dist/assets/{css-C_zQuAoy.js → css-DIMkf-bt.js} +0 -0
  424. /package/client/dist/assets/{cypher-t95m_Q-O.js → cypher-CVaqCwHa.js} +0 -0
  425. /package/client/dist/assets/{dart-B4r1Tg-e.js → dart-onAF5SnQ.js} +0 -0
  426. /package/client/dist/assets/{dockerfile-6mt7zHQw.js → dockerfile-DZFCIeNp.js} +0 -0
  427. /package/client/dist/assets/{ecl-DEY_Fz_Q.js → ecl-D05T4iGw.js} +0 -0
  428. /package/client/dist/assets/{elixir-R6-q1c5t.js → elixir-6RTg0lbw.js} +0 -0
  429. /package/client/dist/assets/{explore-CAc8Qzu32.js → explore-BG99VGpL.js} +0 -0
  430. /package/client/dist/assets/{explore-BO_OinBD.js → explore-C0TjprJW2.js} +0 -0
  431. /package/client/dist/assets/{explore-Bhy7WWXv.js → explore-C2dgEo7q.js} +0 -0
  432. /package/client/dist/assets/{explore-ELbrSZyQ2.js → explore-C8NDHbob.js} +0 -0
  433. /package/client/dist/assets/{explore-DW_YTkQh.js → explore-CWWgzQGR.js} +0 -0
  434. /package/client/dist/assets/{explore-DCZL9gRN2.js → explore-CcQXpdtS.js} +0 -0
  435. /package/client/dist/assets/{explore-BEpIs343.js → explore-DsXAgnmJ2.js} +0 -0
  436. /package/client/dist/assets/{flow9-CNQvHBOt.js → flow9-C5_-GSwl.js} +0 -0
  437. /package/client/dist/assets/{format-command-Db1viey1.js → format-command-Dfa1o2d-.js} +0 -0
  438. /package/client/dist/assets/{fsharp-vPZkFXwv.js → fsharp-C8Ef5oNN.js} +0 -0
  439. /package/client/dist/assets/{go-KUzBMYJ_.js → go-C-y9NEjX.js} +0 -0
  440. /package/client/dist/assets/{graphql-C3W0rCsD.js → graphql-fmXr3nnJ.js} +0 -0
  441. /package/client/dist/assets/{hcl-BOOMFSNX.js → hcl-CpzslTdj.js} +0 -0
  442. /package/client/dist/assets/{ini-CFat1WJr.js → ini-sBoK_t0W.js} +0 -0
  443. /package/client/dist/assets/{integrations-CG2dp3Kd.js → integrations-CH0zy1Mw2.js} +0 -0
  444. /package/client/dist/assets/{integrations-CUNe3t9l2.js → integrations-CIAjhd7L.js} +0 -0
  445. /package/client/dist/assets/{integrations-CWjh5VRk.js → integrations-CZdwOe8A.js} +0 -0
  446. /package/client/dist/assets/{integrations-RJfzXkPx2.js → integrations-Ct7omMld.js} +0 -0
  447. /package/client/dist/assets/{integrations-CAjq5tdW2.js → integrations-D6MSxy3-.js} +0 -0
  448. /package/client/dist/assets/{integrations-JOHSCvB3.js → integrations-DbZa-Zyc.js} +0 -0
  449. /package/client/dist/assets/{integrations-CP_aiV_8.js → integrations-b8pH-vfl2.js} +0 -0
  450. /package/client/dist/assets/{java-Dee-CNAM.js → java-BEtHBSE6.js} +0 -0
  451. /package/client/dist/assets/{jira-CQ7mrZde.js → jira-BySfUuQZ.js} +0 -0
  452. /package/client/dist/assets/{jira-DIRkZk9U.js → jira-Byw2C0F8.js} +0 -0
  453. /package/client/dist/assets/{jira-C5UjxHE4.js → jira-CNeYi06w2.js} +0 -0
  454. /package/client/dist/assets/{jira-ZdKb-50F2.js → jira-D11gPhxz.js} +0 -0
  455. /package/client/dist/assets/{jira-DTYu3hJi2.js → jira-D38Gj7AT.js} +0 -0
  456. /package/client/dist/assets/{jira-B5wszJ1-2.js → jira-gAvOyije.js} +0 -0
  457. /package/client/dist/assets/{jira-DtZpokjJ.js → jira-pRaL7JQo2.js} +0 -0
  458. /package/client/dist/assets/{julia-CGNlLYuU.js → julia-Bri6UV-V.js} +0 -0
  459. /package/client/dist/assets/{kotlin-CY7lhknJ.js → kotlin-BOotOW0E.js} +0 -0
  460. /package/client/dist/assets/{less-D0KN7hfJ.js → less-B9JPFI3C.js} +0 -0
  461. /package/client/dist/assets/{lexon-CuRJufPs.js → lexon-CfSJPG6W.js} +0 -0
  462. /package/client/dist/assets/{loops-DKzYFcq6.js → loops-CPpgMapU.js} +0 -0
  463. /package/client/dist/assets/{loops-Du-bNVpc2.js → loops-CWWbKuVL.js} +0 -0
  464. /package/client/dist/assets/{loops-DDs4KPu_.js → loops-Crc9It8o2.js} +0 -0
  465. /package/client/dist/assets/{loops-Bi4ywYGn.js → loops-DH2hZcbK2.js} +0 -0
  466. /package/client/dist/assets/{loops-DvGbSfxw2.js → loops-DS5TmvIU2.js} +0 -0
  467. /package/client/dist/assets/{loops-D3wdUkZV.js → loops-P2f0cN8-.js} +0 -0
  468. /package/client/dist/assets/{loops-BjK8lmoZ2.js → loops-fJV-9hH7.js} +0 -0
  469. /package/client/dist/assets/{lua-DHvH62Zf.js → lua-CsQS60Ue.js} +0 -0
  470. /package/client/dist/assets/{m3-tlthQ8Fo.js → m3-D-oSqn_W.js} +0 -0
  471. /package/client/dist/assets/{markdown-Dbx1LgUZ.js → markdown-Cimd5fb3.js} +0 -0
  472. /package/client/dist/assets/{mcp-DfmzoT5t.js → mcp-Czd_iyp02.js} +0 -0
  473. /package/client/dist/assets/{mcp-BG-OCJca2.js → mcp-D2pc5GGx.js} +0 -0
  474. /package/client/dist/assets/{mcp-CrWdq5db.js → mcp-D4ZjtIx32.js} +0 -0
  475. /package/client/dist/assets/{mcp-CbJ04oFS2.js → mcp-DIfBUv2D2.js} +0 -0
  476. /package/client/dist/assets/{mcp-Dd844F5_.js → mcp-DsTwmjST.js} +0 -0
  477. /package/client/dist/assets/{mcp-zSJ2TsJj.js → mcp-SMkn8boG.js} +0 -0
  478. /package/client/dist/assets/{mcp-DyULurYS2.js → mcp-fSfiVYAc.js} +0 -0
  479. /package/client/dist/assets/{mips-D_O3IUz_.js → mips-CIPQ_RoX.js} +0 -0
  480. /package/client/dist/assets/{msdax-C_ZmCnRt.js → msdax-DauUninz.js} +0 -0
  481. /package/client/dist/assets/{mysql-C-dCEYSX.js → mysql-SOo6toE5.js} +0 -0
  482. /package/client/dist/assets/{nav-DUuw9VIU2.js → nav-BRJBxaGt2.js} +0 -0
  483. /package/client/dist/assets/{nav-D0Erv9ZJ2.js → nav-CAGKv3_F.js} +0 -0
  484. /package/client/dist/assets/{nav-BY_4EWRv.js → nav-CNskRJKQ2.js} +0 -0
  485. /package/client/dist/assets/{nav-CVXtv0wX2.js → nav-DY93irBL.js} +0 -0
  486. /package/client/dist/assets/{nav-B2_ToNqS.js → nav-DhjvYgqt.js} +0 -0
  487. /package/client/dist/assets/{nav-S4HM94vW.js → nav-Gb-r4wop.js} +0 -0
  488. /package/client/dist/assets/{nav-BAMkVEId.js → nav-TvumAFD22.js} +0 -0
  489. /package/client/dist/assets/{objective-c-DZY9Aqxd.js → objective-c-FvmIjYaQ.js} +0 -0
  490. /package/client/dist/assets/{pascal-BVHLXoor.js → pascal-DrH0SRf2.js} +0 -0
  491. /package/client/dist/assets/{pascaligo-XdG_6u3k.js → pascaligo-D-ptJ9y-.js} +0 -0
  492. /package/client/dist/assets/{perl-b9QV0BcD.js → perl-oz_6vUea.js} +0 -0
  493. /package/client/dist/assets/{pgsql-Du5FDOgT.js → pgsql-DTj74zXo.js} +0 -0
  494. /package/client/dist/assets/{php-WJD0z2Z_.js → php-nr791fC2.js} +0 -0
  495. /package/client/dist/assets/{pla-CbciTf2w.js → pla-CopQ2nXW.js} +0 -0
  496. /package/client/dist/assets/{postiats-C1QxmvVq.js → postiats-43DmfD33.js} +0 -0
  497. /package/client/dist/assets/{powerquery-BEYDhPLZ.js → powerquery-D3hlyOfw.js} +0 -0
  498. /package/client/dist/assets/{powershell-CfaYw4XL.js → powershell-DmHpPYUd.js} +0 -0
  499. /package/client/dist/assets/{protobuf-pwZEDOYa.js → protobuf-C531GsRP.js} +0 -0
  500. /package/client/dist/assets/{pug-5ftqSPVe.js → pug-Z5eAx3Zn.js} +0 -0
  501. /package/client/dist/assets/{qsharp-uI6qZMq4.js → qsharp-DkqhCAOL.js} +0 -0
  502. /package/client/dist/assets/{r-BK54P1EG.js → r-BwWrilGY.js} +0 -0
  503. /package/client/dist/assets/{redis-DI0yd7kN.js → redis-ClamHrr6.js} +0 -0
  504. /package/client/dist/assets/{redshift-Dov26eyI.js → redshift-DT7zqm-g.js} +0 -0
  505. /package/client/dist/assets/{restructuredtext-QqJ_1dC-.js → restructuredtext-BYgofb2h.js} +0 -0
  506. /package/client/dist/assets/{ruby-5WjveeVb.js → ruby-DezsRK8O.js} +0 -0
  507. /package/client/dist/assets/{rust-BzVSfqFH.js → rust-DdL9SqIa.js} +0 -0
  508. /package/client/dist/assets/{sb-DMIUfT-3.js → sb-CcwsVR0C.js} +0 -0
  509. /package/client/dist/assets/{scala-CSKA7lqt.js → scala-DHpiXF5c.js} +0 -0
  510. /package/client/dist/assets/{scheme-CojOP2Qc.js → scheme-BeGwcela.js} +0 -0
  511. /package/client/dist/assets/{scss-DoBZGJG6.js → scss-gp-XZpBa.js} +0 -0
  512. /package/client/dist/assets/{setup-JeBwoXoM.js → setup-CGZjy2IF2.js} +0 -0
  513. /package/client/dist/assets/{setup-1vN48B-m2.js → setup-CXa0Sx16.js} +0 -0
  514. /package/client/dist/assets/{setup-BmJ0Kx16.js → setup-D2ktKKMF2.js} +0 -0
  515. /package/client/dist/assets/{setup-T_ApeQDV2.js → setup-DG2H7ha5.js} +0 -0
  516. /package/client/dist/assets/{setup-DQWsu2TR.js → setup-DK5bHDUq2.js} +0 -0
  517. /package/client/dist/assets/{setup-q0wRHUp1.js → setup-DrWlIQTZ.js} +0 -0
  518. /package/client/dist/assets/{setup-mXnzww4f.js → setup-rzHUVgAu.js} +0 -0
  519. /package/client/dist/assets/{shell-YOaE4udl.js → shell-CC2rA5mh.js} +0 -0
  520. /package/client/dist/assets/{solidity-l3jSyVLu.js → solidity-BEEn4gHE.js} +0 -0
  521. /package/client/dist/assets/{sophia-BhSGTaQJ.js → sophia-CRfGWb83.js} +0 -0
  522. /package/client/dist/assets/{sparql-qhk6QD9w.js → sparql-D_Lu-MrJ.js} +0 -0
  523. /package/client/dist/assets/{specs-BGRKhM8t.js → specs-BcyxaCwf2.js} +0 -0
  524. /package/client/dist/assets/{specs-GkiL-snY.js → specs-C1q9geAi.js} +0 -0
  525. /package/client/dist/assets/{specs-DbtIzV8a.js → specs-CLZKysq-2.js} +0 -0
  526. /package/client/dist/assets/{specs-Ce5y2iFc2.js → specs-DZMS9o54.js} +0 -0
  527. /package/client/dist/assets/{specs-CT8exGOy2.js → specs-Dm6zUI-R.js} +0 -0
  528. /package/client/dist/assets/{specs-C6J2zrLM.js → specs-V3TvhKLX.js} +0 -0
  529. /package/client/dist/assets/{specs-DZDoZsDq.js → specs-dV3Mq5Mu2.js} +0 -0
  530. /package/client/dist/assets/{sql-DoO3y21-.js → sql-NEE52Syq.js} +0 -0
  531. /package/client/dist/assets/{st-ChZAY8oG.js → st-DbInun42.js} +0 -0
  532. /package/client/dist/assets/{swift-D290Tr7_.js → swift-Bxkupp3x.js} +0 -0
  533. /package/client/dist/assets/{systemverilog-DsxzgJ-o.js → systemverilog-Bz4Y3fRF.js} +0 -0
  534. /package/client/dist/assets/{tcl-BKDUHAAR.js → tcl-DISqw1ZD.js} +0 -0
  535. /package/client/dist/assets/{terminal-BTpE7iki.js → terminal-BZiIu3rf.js} +0 -0
  536. /package/client/dist/assets/{terminal-DEyt0prF.js → terminal-C2NAd7BW2.js} +0 -0
  537. /package/client/dist/assets/{terminal-a6Uq8B0p.js → terminal-IahE4LW92.js} +0 -0
  538. /package/client/dist/assets/{terminal-DDk_bTw0.js → terminal-QpXZU2Bn2.js} +0 -0
  539. /package/client/dist/assets/{terminal-BNSg2lR7.js → terminal-ScjyXNxQ.js} +0 -0
  540. /package/client/dist/assets/{terminal-Cvabyzm42.js → terminal-d7GadXbN.js} +0 -0
  541. /package/client/dist/assets/{terminal-D0z12_sJ2.js → terminal-tITa81iz.js} +0 -0
  542. /package/client/dist/assets/{twig-UpZQaM8G.js → twig-De2hgUGE.js} +0 -0
  543. /package/client/dist/assets/{typespec-DzaKvN8i.js → typespec-B8J7ngcE.js} +0 -0
  544. /package/client/dist/assets/{vb-CZpbb7tk.js → vb-DV3o63ZY.js} +0 -0
  545. /package/client/dist/assets/{wgsl-CNt3OIvl.js → wgsl-DpFanUEy.js} +0 -0
@@ -38,7 +38,7 @@ exports.isPrDecisionAction = isPrDecisionAction;
38
38
  exports.executePrDecision = executePrDecision;
39
39
  /**
40
40
  * Execute a user's ask-first PR decision (safe-pr-review-flow) against its
41
- * authoritative `rail_pr_deliveries` row: the FOUR actions both decision
41
+ * authoritative `rail_pr_deliveries` row: the actions both decision
42
42
  * surfaces (the dashboard rail row and the launching agent-chat card) POST to
43
43
  * `/rails/pr-decision`.
44
44
  *
@@ -57,8 +57,8 @@ exports.executePrDecision = executePrDecision;
57
57
  * conflict). Without it, a repo with no GitHub remote wedges
58
58
  * forever in retry(local-only)/discard — no way to ACCEPT work.
59
59
  *
60
- * Every mutation rides the compare-and-set `transitionDecision` (a raced
61
- * concurrent decision loses with a 409) and re-broadcasts the durable
60
+ * Every mutation rides a lease-owned compare-and-set (a raced concurrent
61
+ * decision loses with a 409 before effects) and re-broadcasts the durable
62
62
  * `rail.pr_state` snapshot so both surfaces converge; agent-chat-originated
63
63
  * rows also update their inline decision card in place. Deps are injected
64
64
  * (db/git/exec/broadcast/jira/agent-chat) so the whole matrix is unit-testable
@@ -78,10 +78,18 @@ const rail_worktrees_store_1 = require("./rail-worktrees-store");
78
78
  const ticket_store_1 = require("./ticket-store");
79
79
  const workspace_resolution_1 = require("./workspace-resolution");
80
80
  const agent_chat_registry_1 = require("./agent-chat-registry");
81
- exports.PR_DECISION_ACTIONS = ['create-pr', 'publish', 'discard', 'poll-merge', 'merge-local'];
81
+ const ids_1 = require("./ids");
82
+ const repo_lock_1 = require("./repo-lock");
83
+ const rail_pr_ticket_effects_1 = require("./rail-pr-ticket-effects");
84
+ const pr_lifecycle_1 = require("./pr-lifecycle");
85
+ exports.PR_DECISION_ACTIONS = ['create-pr', 'publish', 'discard', 'dismiss', 'poll-merge', 'reopen', 'merge-local', 'acknowledge-no-changes'];
82
86
  function isPrDecisionAction(v) {
83
87
  return typeof v === 'string' && exports.PR_DECISION_ACTIONS.includes(v);
84
88
  }
89
+ // Action handlers keep using one finalization helper, but the operation lease
90
+ // must be absent from every emitted snapshot. Calls made while a lease is held
91
+ // are deferred until executePrDecision's finally releases that exact token.
92
+ const deferredFinalizations = new Set();
85
93
  function staleDecision(current) {
86
94
  return { status: 409, body: { error: 'stale_decision', current } };
87
95
  }
@@ -101,21 +109,31 @@ function ghFailed(r) {
101
109
  function actionAllowed(action, row) {
102
110
  switch (action) {
103
111
  case 'create-pr':
104
- return row.decision === 'on_review' || row.decision === 'pr_failed' ||
112
+ return row.decision === 'on_review' ||
113
+ (row.decision === 'pr_failed' && (row.delivery_outcome === 'retryable_failure' || row.delivery_outcome === 'unknown')) ||
105
114
  (row.decision === 'pr_draft' && row.pr_url === null);
106
115
  case 'publish':
107
116
  return row.decision === 'pr_draft' && row.pr_url !== null;
108
117
  case 'discard':
109
118
  return row.decision === 'on_review' || row.decision === 'pr_draft' ||
110
- row.decision === 'pr_ready' || row.decision === 'implementation_failed' ||
119
+ row.decision === 'pr_ready' || row.decision === 'pr_closed' || row.decision === 'no_changes' ||
120
+ row.decision === 'implementation_failed' ||
111
121
  row.decision === 'pr_failed';
122
+ case 'dismiss':
123
+ return row.is_continuation === 1 && row.decision !== 'building' &&
124
+ row.decision !== 'merged' && row.decision !== 'discarded' && row.decision !== 'superseded' &&
125
+ row.delivery_outcome !== 'blocked';
112
126
  case 'poll-merge':
113
- return (row.decision === 'pr_draft' || row.decision === 'pr_ready') && row.pr_url !== null;
127
+ return (row.decision === 'pr_draft' || row.decision === 'pr_ready' || row.decision === 'pr_closed') && row.pr_url !== null;
128
+ case 'reopen':
129
+ return row.decision === 'pr_closed' && row.pr_url !== null;
114
130
  case 'merge-local':
115
131
  // Remote-less acceptance ONLY: once a real PR exists (pr_url), GitHub is
116
132
  // the merge authority — merging under an open PR would leave it dangling.
117
133
  return row.pr_url === null &&
118
134
  (row.decision === 'on_review' || row.decision === 'pr_failed' || row.decision === 'pr_draft');
135
+ case 'acknowledge-no-changes':
136
+ return row.decision === 'no_changes' && row.is_continuation !== 1;
119
137
  }
120
138
  }
121
139
  async function executePrDecision(deps, input) {
@@ -128,12 +146,43 @@ async function executePrDecision(deps, input) {
128
146
  return staleDecision(row.decision);
129
147
  if (!actionAllowed(input.action, row))
130
148
  return illegalAction(row.decision);
131
- switch (input.action) {
132
- case 'create-pr': return runCreatePr(deps, row);
133
- case 'publish': return runPublish(deps, row);
134
- case 'discard': return runDiscard(deps, row);
135
- case 'poll-merge': return runPollMerge(deps, row);
136
- case 'merge-local': return runMergeLocal(deps, row);
149
+ // Claim before any git/GitHub/cleanup/ticket effect. The visible decision is
150
+ // unchanged while the winner works, but a second surface cannot start.
151
+ const operationToken = (0, ids_1.newId)();
152
+ if (!(0, rail_pr_store_1.claimPrDeliveryOperation)(deps.db, row.id, row.decision, input.action, operationToken)) {
153
+ const current = (0, rail_pr_store_1.getPrDelivery)(deps.db, row.id);
154
+ if (!current || current.decision !== input.expectedDecision) {
155
+ return staleDecision(current?.decision ?? row.decision);
156
+ }
157
+ return {
158
+ status: 409,
159
+ body: { error: 'operation_in_progress', current: current.decision, operation: current.operation },
160
+ };
161
+ }
162
+ const claimedRow = (0, rail_pr_store_1.getPrDelivery)(deps.db, row.id);
163
+ if (!claimedRow || claimedRow.operation_token !== operationToken) {
164
+ return { status: 409, body: { error: 'operation_in_progress', current: claimedRow?.decision ?? row.decision } };
165
+ }
166
+ try {
167
+ switch (input.action) {
168
+ case 'create-pr': return await runCreatePr(deps, claimedRow);
169
+ case 'publish': return await runPublish(deps, claimedRow);
170
+ case 'discard': return await runDiscard(deps, claimedRow);
171
+ case 'dismiss': return await runDismiss(deps, claimedRow);
172
+ case 'poll-merge': return await runPollMerge(deps, claimedRow);
173
+ case 'reopen': return await runReopen(deps, claimedRow);
174
+ case 'merge-local': return await runMergeLocal(deps, claimedRow);
175
+ case 'acknowledge-no-changes': return await runAcknowledgeNoChanges(deps, claimedRow);
176
+ }
177
+ return illegalAction(row.decision);
178
+ }
179
+ finally {
180
+ try {
181
+ (0, rail_pr_store_1.releasePrDeliveryOperation)(deps.db, row.id, operationToken);
182
+ }
183
+ catch { /* durable state remains authoritative */ }
184
+ if (deferredFinalizations.delete(row.id))
185
+ finalizeTransition(deps, row.id);
137
186
  }
138
187
  }
139
188
  /**
@@ -141,11 +190,30 @@ async function executePrDecision(deps, input) {
141
190
  * between the pre-check and here — surface the same 409 with the fresh state.
142
191
  */
143
192
  function casTransition(deps, row, next, patch = {}) {
144
- if ((0, rail_pr_store_1.transitionDecision)(deps.db, row.id, row.decision, next, patch))
193
+ if (row.operation_token && (0, rail_pr_store_1.transitionClaimedDecision)(deps.db, row.id, row.decision, next, row.operation_token, patch))
194
+ return null;
195
+ const current = (0, rail_pr_store_1.getPrDelivery)(deps.db, row.id);
196
+ return staleDecision(current?.decision ?? row.decision);
197
+ }
198
+ function casTransitionWithTicketEffect(deps, row, next, patch, effect) {
199
+ if (row.operation_token &&
200
+ (0, rail_pr_ticket_effects_1.transitionClaimedDecisionWithTicketEffect)(deps.db, row.id, row.decision, next, row.operation_token, patch, effect))
145
201
  return null;
146
202
  const current = (0, rail_pr_store_1.getPrDelivery)(deps.db, row.id);
147
203
  return staleDecision(current?.decision ?? row.decision);
148
204
  }
205
+ function applyTerminalTicketEffect(deps, row, terminalDecision, cleanupWarnings) {
206
+ const result = (0, rail_pr_ticket_effects_1.applyRailPrTicketEffect)(deps, row.id);
207
+ if (result.ok)
208
+ return;
209
+ cleanupWarnings.push(`ticket status update pending: ${result.error ?? 'unknown ticket-store error'}`);
210
+ if (!row.operation_token)
211
+ return;
212
+ (0, rail_pr_store_1.transitionClaimedDecision)(deps.db, row.id, terminalDecision, terminalDecision, row.operation_token, {
213
+ statusCode: 'cleanup_incomplete',
214
+ cleanupWarnings,
215
+ });
216
+ }
149
217
  /**
150
218
  * Post-transition fan-out: re-broadcast the durable rail.pr_state snapshot
151
219
  * (both surfaces converge on it) and, for agent-chat-originated launches,
@@ -156,6 +224,10 @@ function finalizeTransition(deps, id) {
156
224
  if (!row)
157
225
  return undefined;
158
226
  const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
227
+ if (row.operation_token) {
228
+ deferredFinalizations.add(id);
229
+ return snap;
230
+ }
159
231
  deps.broadcast((0, rail_pr_store_1.toRailPrStateMessage)(deps.project.id, snap));
160
232
  if (row.origin_conversation_id) {
161
233
  const agent = (deps.agentChat ?? agent_chat_registry_1.getAgentChatManager)();
@@ -174,45 +246,189 @@ function batchWorktreeRoot(slug) {
174
246
  // Mirrors rail-isolated-launch's worktreesRoot — derivable, never stored.
175
247
  return path.join((0, artifact_registry_1.resolveHome)(), '.specrails', 'projects', slug, 'worktrees');
176
248
  }
177
- async function resolveExistingPrDecisionAfterPush(deps, row) {
178
- if (!row.pr_url)
179
- return 'pr_ready';
180
- const r = await deps.exec.run('gh', ['pr', 'view', row.pr_url, '--json', 'isDraft,state'], deps.project.path);
181
- if (r.code !== 0)
182
- return 'pr_ready';
183
- try {
184
- const parsed = JSON.parse(r.stdout);
185
- if (parsed.isDraft === true)
186
- return 'pr_draft';
187
- }
188
- catch {
189
- /* fall through to the conservative published/open state */
249
+ /** A legacy record's `succeeded` bit meant delivery eligibility. New records
250
+ * state that eligibility directly; blocked/no-change units are never swept as
251
+ * a side effect of delivering another unit in the same batch. */
252
+ function unitWasDelivered(unit) {
253
+ return unit.deliveryOutcome === undefined ? unit.succeeded : unit.deliveryOutcome === 'ready';
254
+ }
255
+ /** Branch deletion is ownership-based, never name-derived. Legacy rows without
256
+ * ownership evidence degrade to preservation. A multi-unit assembled head is
257
+ * owned when it is distinct from every recorded unit branch. */
258
+ function ownedDeliveryBranches(row, snap, cleanupWarnings) {
259
+ const unknownOwnership = row.is_continuation !== 1
260
+ ? snap.branches.filter((unit) => unitWasDelivered(unit) && unit.branchOwnership === undefined)
261
+ : [];
262
+ if (cleanupWarnings && unknownOwnership.length > 0) {
263
+ cleanupWarnings.push(`branch cleanup preserved ${unknownOwnership.length} legacy ${unknownOwnership.length === 1 ? 'branch' : 'branches'} because ownership was not recorded`);
190
264
  }
191
- return 'pr_ready';
265
+ const owned = new Set(snap.branches
266
+ .filter((unit) => unit.branchOwnership === 'created' && unitWasDelivered(unit))
267
+ .map((unit) => unit.branch));
268
+ if (row.is_continuation !== 1 && snap.branches.length > 1 && row.branch &&
269
+ !snap.branches.some((unit) => unit.branch === row.branch))
270
+ owned.add(row.branch);
271
+ return owned;
272
+ }
273
+ function releasableWorktreeIds(deps, snap) {
274
+ const preservedBranches = new Set(snap.branches.filter((unit) => !unitWasDelivered(unit)).map((unit) => unit.branch));
275
+ return snap.worktreeIds.filter((id) => {
276
+ const wt = (0, rail_worktrees_store_1.getRailWorktree)(deps.db, id);
277
+ return !wt || !preservedBranches.has(wt.branch);
278
+ });
279
+ }
280
+ async function observePrLifecycle(deps, prUrl) {
281
+ const observed = await (0, pr_lifecycle_1.observePrLifecycle)(deps.exec, deps.project.path, prUrl);
282
+ return observed.ok ? { state: observed.state, isDraft: observed.isDraft } : null;
283
+ }
284
+ function mergedEvidenceDetail(observation) {
285
+ return observation.includesExpectedSha === null
286
+ ? 'GitHub did not return enough commit evidence to prove the previous PR included the verified implementation commit'
287
+ : 'the previous PR merged without the verified implementation commit';
288
+ }
289
+ async function rerouteFromStalePr(deps, row, detail, exactWasPushed) {
290
+ const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
291
+ const cleanupWarnings = await (0, rail_worktree_release_1.releaseRailWorktrees)({
292
+ db: deps.db, git: deps.git, repoDir: deps.project.path,
293
+ worktreeIds: releasableWorktreeIds(deps, snap),
294
+ });
295
+ const partial = row.implementation_outcome === 'partially_succeeded';
296
+ const conflict = casTransition(deps, row, 'on_review', {
297
+ prUrl: null,
298
+ prNumber: null,
299
+ prState: exactWasPushed ? 'pushed' : 'local-only',
300
+ deliveryOutcome: partial ? 'partial' : 'ready',
301
+ statusCode: cleanupWarnings.length > 0 ? 'cleanup_incomplete' : partial ? 'partial_success' : 'ready_for_review',
302
+ statusDetail: `${detail}; create a new draft PR from the preserved exact commit`,
303
+ cleanupWarnings,
304
+ // The replacement PR belongs to this generation. Per-unit branch ownership
305
+ // remains unchanged so a borrowed/pre-existing ref is still never deleted.
306
+ isContinuation: false,
307
+ });
308
+ if (conflict)
309
+ return conflict;
310
+ const after = finalizeTransition(deps, row.id);
311
+ return {
312
+ status: 200,
313
+ body: {
314
+ ok: true,
315
+ decision: 'on_review',
316
+ prUrl: null,
317
+ prState: after?.prState ?? (exactWasPushed ? 'pushed' : 'local-only'),
318
+ rerouted: true,
319
+ detail,
320
+ },
321
+ };
322
+ }
323
+ async function settleObservedExistingPr(deps, row, observation) {
324
+ const next = observation.state === 'MERGED' || observation.isDraft === false ? 'pr_ready' : 'pr_draft';
325
+ const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
326
+ const cleanupWarnings = await (0, rail_worktree_release_1.releaseRailWorktrees)({
327
+ db: deps.db, git: deps.git, repoDir: deps.project.path,
328
+ worktreeIds: releasableWorktreeIds(deps, snap),
329
+ });
330
+ const conflict = casTransition(deps, row, next, {
331
+ prState: 'pr-created',
332
+ deliveryOutcome: 'delivered',
333
+ statusCode: cleanupWarnings.length > 0 ? 'cleanup_incomplete' : next === 'pr_ready' ? 'pr_ready' : 'existing_pr_updated',
334
+ cleanupWarnings,
335
+ });
336
+ if (conflict)
337
+ return conflict;
338
+ const after = finalizeTransition(deps, row.id);
339
+ return { status: 200, body: { ok: true, decision: next, prUrl: after?.prUrl ?? row.pr_url, prState: 'pr-created' } };
340
+ }
341
+ function persistRetryablePrObservationFailure(deps, row, detail) {
342
+ const conflict = casTransition(deps, row, 'pr_failed', {
343
+ deliveryOutcome: 'retryable_failure',
344
+ statusCode: 'push_failed',
345
+ statusDetail: detail,
346
+ });
347
+ if (conflict)
348
+ return conflict;
349
+ finalizeTransition(deps, row.id);
350
+ return { status: 200, body: { ok: true, decision: 'pr_failed', prUrl: row.pr_url, detail } };
351
+ }
352
+ function persistMovedPrHead(deps, row, detail) {
353
+ const conflict = casTransition(deps, row, 'pr_failed', {
354
+ deliveryOutcome: 'blocked',
355
+ statusCode: 'branch_verification_failed',
356
+ statusDetail: detail,
357
+ });
358
+ if (conflict)
359
+ return conflict;
360
+ finalizeTransition(deps, row.id);
361
+ return { status: 200, body: { ok: true, decision: 'pr_failed', prUrl: row.pr_url, detail } };
192
362
  }
193
363
  async function retryExistingPrFollowupPush(deps, row) {
194
364
  if (!row.branch || !row.pr_url)
195
365
  return { status: 502, body: { error: 'push_failed', detail: 'missing PR branch/url' } };
366
+ if (!row.delivery_sha) {
367
+ const detail = 'missing verified delivery SHA; refusing to resolve a mutable branch for retry';
368
+ const conflict = casTransition(deps, row, 'pr_failed', {
369
+ deliveryOutcome: 'blocked',
370
+ statusCode: 'branch_verification_failed',
371
+ statusDetail: detail,
372
+ });
373
+ if (conflict)
374
+ return conflict;
375
+ finalizeTransition(deps, row.id);
376
+ return { status: 409, body: { error: 'missing_verified_sha', decision: 'pr_failed', prUrl: row.pr_url, detail } };
377
+ }
378
+ const beforePush = await (0, pr_lifecycle_1.observePrLifecycle)(deps.exec, deps.project.path, row.pr_url, row.delivery_sha);
379
+ if (!beforePush.ok) {
380
+ return persistRetryablePrObservationFailure(deps, row, `could not confirm the existing PR is open before retry: ${beforePush.detail}`);
381
+ }
382
+ if (!(0, pr_lifecycle_1.isExactOpenPr)(beforePush, row.branch, row.base_branch)) {
383
+ if (beforePush.state === 'MERGED' && beforePush.includesExpectedSha === true) {
384
+ return settleObservedExistingPr(deps, row, beforePush);
385
+ }
386
+ if (beforePush.state === 'MERGED') {
387
+ return rerouteFromStalePr(deps, row, mergedEvidenceDetail(beforePush), false);
388
+ }
389
+ if (beforePush.state === 'CLOSED') {
390
+ return rerouteFromStalePr(deps, row, 'the previous PR closed before this implementation was delivered', false);
391
+ }
392
+ return persistMovedPrHead(deps, row, 'the existing PR no longer matches its recorded head/base; the verified commit was not pushed');
393
+ }
196
394
  const pushed = await (0, pr_publisher_1.pushBranch)(deps.exec, {
197
395
  repoDir: deps.project.path,
198
396
  branch: row.branch,
199
397
  baseBranch: row.base_branch,
398
+ sourceSha: row.delivery_sha,
200
399
  });
201
400
  if (pushed.state === 'local-only') {
202
- const conflict = casTransition(deps, row, 'pr_failed', { prState: 'local-only' });
401
+ const conflict = casTransition(deps, row, 'pr_failed', {
402
+ prState: 'local-only',
403
+ deliveryOutcome: 'retryable_failure',
404
+ statusCode: 'push_failed',
405
+ statusDetail: pushed.reason,
406
+ });
203
407
  if (conflict)
204
408
  return conflict;
205
409
  finalizeTransition(deps, row.id);
206
410
  return { status: 200, body: { ok: true, decision: 'pr_failed', prUrl: row.pr_url, detail: pushed.reason } };
207
411
  }
208
- const next = await resolveExistingPrDecisionAfterPush(deps, row);
209
- const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
210
- const conflict = casTransition(deps, row, next, { prState: 'pr-created' });
211
- if (conflict)
212
- return conflict;
213
- await (0, rail_worktree_release_1.releaseRailWorktrees)({ db: deps.db, git: deps.git, repoDir: deps.project.path, worktreeIds: snap.worktreeIds });
214
- const after = finalizeTransition(deps, row.id);
215
- return { status: 200, body: { ok: true, decision: next, prUrl: after?.prUrl ?? row.pr_url, prState: 'pr-created' } };
412
+ const afterPush = await (0, pr_lifecycle_1.observePrLifecycle)(deps.exec, deps.project.path, row.pr_url, row.delivery_sha);
413
+ if (!afterPush.ok) {
414
+ return persistRetryablePrObservationFailure(deps, row, `exact commit was pushed, but the PR lifecycle could not be confirmed: ${afterPush.detail}`);
415
+ }
416
+ if ((0, pr_lifecycle_1.isExactOpenPr)(afterPush, row.branch, row.base_branch)) {
417
+ if (afterPush.headRefOid?.toLowerCase() !== row.delivery_sha.toLowerCase()) {
418
+ return persistMovedPrHead(deps, row, 'the PR head moved after the exact push; the verified commit is preserved but no longer proven as the remote head');
419
+ }
420
+ return settleObservedExistingPr(deps, row, afterPush);
421
+ }
422
+ if (afterPush.state === 'MERGED' && afterPush.includesExpectedSha === true) {
423
+ return settleObservedExistingPr(deps, row, afterPush);
424
+ }
425
+ if (afterPush.state === 'MERGED') {
426
+ return rerouteFromStalePr(deps, row, mergedEvidenceDetail(afterPush), true);
427
+ }
428
+ if (afterPush.state === 'CLOSED') {
429
+ return rerouteFromStalePr(deps, row, 'the previous PR closed before this implementation was delivered', true);
430
+ }
431
+ return persistMovedPrHead(deps, row, 'the existing PR no longer matches its recorded head/base after the exact push');
216
432
  }
217
433
  /**
218
434
  * Load the PR's ticket data (title/labels/description) from the ticket store,
@@ -254,63 +470,69 @@ async function branchExists(deps, branch) {
254
470
  const r = await deps.git.run(['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`], deps.project.path);
255
471
  return r.code === 0;
256
472
  }
257
- /**
258
- * Recovery for a recorded unit branch that no longer exists (the wedged
259
- * local-only retry: the settled record drifted from the branch that really
260
- * carries the commits, or the branch was deleted externally). Scan the ticket's
261
- * rail_worktrees ledger (newest first) for ANY other branch a rail allocated
262
- * for the same ticket that still exists AND has commits ahead of the base —
263
- * that branch is the real carrier of the work. Null when nothing recoverable.
264
- */
265
- async function findRecoverableBranch(deps, unit, baseBranch) {
266
- let candidates = [];
473
+ const COMMIT_SHA_RE = /^[0-9a-f]{40,64}$/i;
474
+ async function commitObjectExists(deps, sha) {
475
+ if (!COMMIT_SHA_RE.test(sha))
476
+ return false;
267
477
  try {
268
- candidates = (0, rail_worktrees_store_1.listRailWorktreesForTicket)(deps.db, unit.ticketId);
478
+ return (await deps.git.run(['cat-file', '-e', `${sha}^{commit}`], deps.project.path)).code === 0;
269
479
  }
270
480
  catch {
271
- return null;
481
+ return false;
272
482
  }
273
- for (const wt of candidates) {
274
- if (!wt.branch || wt.branch === unit.branch || wt.branch === baseBranch)
275
- continue;
276
- if (!(await branchExists(deps, wt.branch)))
277
- continue;
278
- const ahead = await deps.git.run(['rev-list', '--count', `${baseBranch}..${wt.branch}`], deps.project.path);
279
- const n = ahead.code === 0 ? parseInt(ahead.stdout.trim(), 10) : NaN;
280
- if (Number.isFinite(n) && n > 0)
281
- return wt.branch;
483
+ }
484
+ /** Resolve a legacy mutable ref once into an immutable object. New settlement
485
+ * rows already carry finalSha and never take this compatibility path. */
486
+ async function captureBranchSha(deps, branch) {
487
+ try {
488
+ const result = await deps.git.run(['rev-parse', '--verify', `refs/heads/${branch}`], deps.project.path);
489
+ const sha = result.code === 0 ? result.stdout.trim() : '';
490
+ return COMMIT_SHA_RE.test(sha) ? sha : null;
491
+ }
492
+ catch {
493
+ return null;
282
494
  }
283
- return null;
284
495
  }
285
496
  async function runCreatePr(deps, row) {
286
497
  if (row.decision === 'pr_failed' && row.pr_url && row.branch) {
287
498
  return retryExistingPrFollowupPush(deps, row);
288
499
  }
289
500
  const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
290
- // Pre-flight: every succeeded unit branch must actually exist before we push
291
- // it — a missing ref can only degrade to local-only again, so a retry on a
292
- // wedged row would loop forever. When a recorded branch is gone, recover the
293
- // REAL branch from the ticket's worktree ledger (see findRecoverableBranch)
294
- // and persist the substitution on the row; unrecoverable → honest pr_failed.
501
+ // Resolve every deliverable unit to an immutable object. New rows carry the
502
+ // verified final SHA from settlement; legacy rows may capture their recorded
503
+ // ref once. Never substitute another historical branch merely because it
504
+ // belongs to the same ticket.
295
505
  let effectiveBranches = snap.branches;
296
- let branchesRecovered = false;
297
- for (const unit of snap.branches) {
506
+ let branchEvidenceCaptured = false;
507
+ for (let index = 0; index < snap.branches.length; index++) {
508
+ const unit = snap.branches[index];
298
509
  if (!unit.succeeded)
299
510
  continue;
300
- if (await branchExists(deps, unit.branch))
301
- continue;
302
- const real = await findRecoverableBranch(deps, unit, row.base_branch);
303
- if (!real) {
304
- const detail = `branch '${unit.branch}' (ticket #${unit.ticketId}) no longer exists locally and no other rail branch for the ticket holds commits — nothing to deliver`;
305
- const conflict = casTransition(deps, row, 'pr_failed');
511
+ let sourceSha = unit.finalSha && COMMIT_SHA_RE.test(unit.finalSha) ? unit.finalSha : null;
512
+ if (sourceSha) {
513
+ if (!(await commitObjectExists(deps, sourceSha)))
514
+ sourceSha = null;
515
+ }
516
+ else {
517
+ sourceSha = await captureBranchSha(deps, unit.branch);
518
+ if (sourceSha && !(await commitObjectExists(deps, sourceSha)))
519
+ sourceSha = null;
520
+ }
521
+ if (!sourceSha) {
522
+ const detail = `the verified commit for branch '${unit.branch}' (ticket #${unit.ticketId}) is unavailable; refusing to deliver a mutable or unrelated ref`;
523
+ const conflict = casTransition(deps, row, 'pr_failed', {
524
+ deliveryOutcome: 'blocked', statusCode: 'branch_verification_failed', statusDetail: detail,
525
+ });
306
526
  if (conflict)
307
527
  return conflict;
308
528
  finalizeTransition(deps, row.id);
309
529
  return { status: 200, body: { ok: true, decision: 'pr_failed', detail } };
310
530
  }
311
- console.warn(`[rail-pr-decision] recorded branch '${unit.branch}' missing — recovered '${real}' from the ticket ${unit.ticketId} worktree ledger`);
312
- effectiveBranches = effectiveBranches.map((b) => (b === unit ? { ...b, branch: real } : b));
313
- branchesRecovered = true;
531
+ if (unit.finalSha !== sourceSha)
532
+ branchEvidenceCaptured = true;
533
+ effectiveBranches = effectiveBranches.map((branch, branchIndex) => branchIndex === index
534
+ ? { ...branch, finalSha: sourceSha }
535
+ : branch);
314
536
  }
315
537
  // Succeeded COVERED tickets for the PR title/body: a single unit covers every
316
538
  // launch ticket (scope='all'), per-ticket units cover exactly their own.
@@ -322,16 +544,7 @@ async function runCreatePr(deps, row) {
322
544
  // The batch name is derived ONLY for multi-unit deliveries — the single-ticket
323
545
  // path delivers straight from its unit branch and must never even compute one.
324
546
  const batchPreferred = succeededUnits.length > 1 ? (0, pr_naming_1.batchBranchNameFor)(ticketData) : null;
325
- // Retry hazard: deliverRailAsPr assembles with `worktree add -b` (no -B) and a
326
- // prior degraded delivery KEEPS the batch branch — so a retry would die on
327
- // "branch already exists". Defensively drop the stale batch branch first
328
- // (batch deliveries only). HARD GUARDS: NEVER a branch present in the row's
329
- // unit records (deleting one would destroy the commits about to be delivered)
330
- // and NEVER the integration branch; a missing branch is tolerated.
331
547
  const unitBranches = new Set(effectiveBranches.map((b) => b.branch));
332
- if (batchPreferred && batchPreferred !== row.base_branch && !unitBranches.has(batchPreferred)) {
333
- await deps.git.run(['branch', '-D', batchPreferred], deps.project.path).catch(() => { });
334
- }
335
548
  // Per-ticket covering branch for the body's honest Tests digest: per-ticket
336
549
  // units map 1:1; a single 'all'-scope unit covers every launch ticket.
337
550
  const branchFor = (ticketId) => {
@@ -358,38 +571,75 @@ async function runCreatePr(deps, row) {
358
571
  });
359
572
  let next;
360
573
  // Underlying git/gh failure detail for a degraded or failed delivery —
361
- // relayed on the response body so the decision surfaces can say WHY instead
362
- // of a bare "retry". Never persisted (the durable columns stay state-only).
574
+ // relayed on the response and persisted as bounded secondary diagnostics.
363
575
  let detail = null;
364
576
  const patch = {};
365
- // Persist any pre-flight substitution so the row records the branch that
366
- // actually carries the commits — the next retry/discard works from reality.
367
- if (branchesRecovered)
577
+ if (branchEvidenceCaptured)
368
578
  patch.branches = effectiveBranches;
369
579
  try {
370
- const result = await (0, rail_pr_delivery_1.deliverRailAsPr)(deps.git, deps.exec, {
371
- baseRepo: deps.project.path,
372
- integrationBranch: row.base_branch,
373
- railKey: row.rail_key,
374
- batchBranch: batchPreferred ?? undefined,
375
- batchWorktreeRoot: batchWorktreeRoot(deps.project.slug),
376
- branches: effectiveBranches,
377
- title,
378
- body,
379
- });
580
+ // A degraded multi-unit attempt already owns an assembled batch branch.
581
+ // Retry that exact head so exact head/base PR discovery remains idempotent;
582
+ // never `branch -D` a merely name-matching ref, which could destroy a user's
583
+ // unrelated or subsequently edited local branch.
584
+ let degradedBatchSha = null;
585
+ if (batchPreferred && row.branch && row.pr_url === null &&
586
+ (row.pr_state === 'pushed' || row.pr_state === 'local-only') &&
587
+ row.branch !== row.base_branch && !unitBranches.has(row.branch)) {
588
+ if (row.delivery_sha && await commitObjectExists(deps, row.delivery_sha)) {
589
+ degradedBatchSha = row.delivery_sha;
590
+ }
591
+ else {
592
+ degradedBatchSha = await captureBranchSha(deps, row.branch);
593
+ }
594
+ }
595
+ const result = degradedBatchSha
596
+ ? {
597
+ state: 'delivered',
598
+ branch: row.branch,
599
+ pr: await (0, pr_publisher_1.publishDraftPr)(deps.exec, {
600
+ repoDir: deps.project.path,
601
+ branch: row.branch,
602
+ baseBranch: row.base_branch,
603
+ title,
604
+ body,
605
+ sourceSha: degradedBatchSha,
606
+ }),
607
+ ticketIds: succeededTicketIds,
608
+ }
609
+ : await (0, rail_pr_delivery_1.deliverRailAsPr)(deps.git, deps.exec, {
610
+ baseRepo: deps.project.path,
611
+ integrationBranch: row.base_branch,
612
+ railKey: row.rail_key,
613
+ batchBranch: batchPreferred ?? undefined,
614
+ batchWorktreeRoot: batchWorktreeRoot(deps.project.slug),
615
+ branches: effectiveBranches.map((unit) => ({
616
+ ...unit,
617
+ sourceSha: unit.finalSha ?? undefined,
618
+ })),
619
+ title,
620
+ body,
621
+ });
380
622
  if (result.state === 'delivered') {
381
- next = 'pr_draft';
623
+ next = result.pr.state === 'pr-created' && result.pr.isDraft === false ? 'pr_ready' : 'pr_draft';
382
624
  patch.branch = result.branch;
383
625
  patch.prState = result.pr.state;
626
+ const resultSha = degradedBatchSha
627
+ ?? (succeededUnits.length === 1 ? succeededUnits[0].finalSha ?? null : await captureBranchSha(deps, result.branch));
628
+ if (resultSha && await commitObjectExists(deps, resultSha))
629
+ patch.deliverySha = resultSha;
384
630
  if (result.pr.state === 'pr-created' && result.pr.prUrl) {
385
631
  patch.prUrl = result.pr.prUrl;
386
632
  patch.prNumber = parsePrNumber(result.pr.prUrl);
633
+ patch.deliveryOutcome = 'delivered';
634
+ patch.statusCode = next === 'pr_ready' ? 'pr_ready' : 'pr_draft_ready';
387
635
  }
388
636
  else {
389
637
  // Degraded (pushed / local-only): no PR exists — only retry or discard.
390
638
  patch.prUrl = null;
391
639
  patch.prNumber = null;
392
640
  detail = result.pr.reason ?? null;
641
+ patch.deliveryOutcome = 'retryable_failure';
642
+ patch.statusCode = result.pr.state === 'local-only' ? 'push_failed' : 'delivery_failed';
393
643
  }
394
644
  }
395
645
  else {
@@ -397,6 +647,11 @@ async function runCreatePr(deps, row) {
397
647
  // succeeded unit, but a wedged row must not 500) → retryable failure.
398
648
  next = 'pr_failed';
399
649
  detail = result.reason;
650
+ const retryable = !result.reason.startsWith('merge-conflict:') &&
651
+ !result.reason.startsWith('batch-branch-collision:') &&
652
+ result.reason !== 'missing-batch-branch' && result.state === 'assembly-failed';
653
+ patch.deliveryOutcome = retryable ? 'retryable_failure' : 'blocked';
654
+ patch.statusCode = 'delivery_failed';
400
655
  }
401
656
  }
402
657
  catch (err) {
@@ -405,13 +660,19 @@ async function runCreatePr(deps, row) {
405
660
  console.error('[rail-pr-decision] create-pr delivery failed:', err);
406
661
  next = 'pr_failed';
407
662
  detail = err instanceof Error ? err.message : String(err);
663
+ patch.deliveryOutcome = 'blocked';
664
+ patch.statusCode = 'delivery_failed';
665
+ }
666
+ if (detail)
667
+ patch.statusDetail = detail;
668
+ if ((next === 'pr_draft' || next === 'pr_ready') && patch.prUrl) {
669
+ patch.cleanupWarnings = await (0, rail_worktree_release_1.releaseRailWorktrees)({
670
+ db: deps.db, git: deps.git, repoDir: deps.project.path, worktreeIds: releasableWorktreeIds(deps, snap),
671
+ });
408
672
  }
409
673
  const conflict = casTransition(deps, row, next, patch);
410
674
  if (conflict)
411
675
  return conflict;
412
- if (next === 'pr_draft' && patch.prUrl) {
413
- await (0, rail_worktree_release_1.releaseRailWorktrees)({ db: deps.db, git: deps.git, repoDir: deps.project.path, worktreeIds: snap.worktreeIds });
414
- }
415
676
  const after = finalizeTransition(deps, row.id);
416
677
  // Tickets stay on_review — a draft PR is still awaiting the engineer's merge.
417
678
  return {
@@ -426,158 +687,310 @@ async function runCreatePr(deps, row) {
426
687
  };
427
688
  }
428
689
  async function runPublish(deps, row) {
429
- const r = await deps.exec.run('gh', ['pr', 'ready', row.pr_url], deps.project.path);
430
- if (r.code !== 0)
431
- return ghFailed(r); // no transition — the draft is still a draft
690
+ let r;
691
+ try {
692
+ r = await deps.exec.run('gh', ['pr', 'ready', row.pr_url], deps.project.path);
693
+ }
694
+ catch (err) {
695
+ r = { code: 1, stdout: '', stderr: err instanceof Error ? err.message : String(err) };
696
+ }
697
+ if (r.code !== 0) {
698
+ // Crash/timeout ambiguity: `gh` may have made the PR ready before the local
699
+ // command reported failure. Adopt exact remote truth instead of retrying
700
+ // forever on "already ready".
701
+ const lifecycle = await observePrLifecycle(deps, row.pr_url);
702
+ if (lifecycle?.state === 'CLOSED') {
703
+ const conflict = casTransition(deps, row, 'pr_closed', {
704
+ deliveryOutcome: 'delivered', statusCode: 'pr_closed',
705
+ });
706
+ if (conflict)
707
+ return conflict;
708
+ finalizeTransition(deps, row.id);
709
+ return { status: 200, body: { ok: true, decision: 'pr_closed', closed: true, prUrl: row.pr_url } };
710
+ }
711
+ if (lifecycle?.state !== 'OPEN' || lifecycle.isDraft)
712
+ return ghFailed(r);
713
+ }
432
714
  const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
433
- const conflict = casTransition(deps, row, 'pr_ready');
715
+ const cleanupWarnings = await (0, rail_worktree_release_1.releaseRailWorktrees)({
716
+ db: deps.db, git: deps.git, repoDir: deps.project.path, worktreeIds: releasableWorktreeIds(deps, snap),
717
+ });
718
+ const conflict = casTransition(deps, row, 'pr_ready', {
719
+ deliveryOutcome: 'delivered', statusCode: 'pr_ready', cleanupWarnings,
720
+ });
434
721
  if (conflict)
435
722
  return conflict;
436
- await (0, rail_worktree_release_1.releaseRailWorktrees)({ db: deps.db, git: deps.git, repoDir: deps.project.path, worktreeIds: snap.worktreeIds });
437
723
  finalizeTransition(deps, row.id);
438
724
  return { status: 200, body: { ok: true, decision: 'pr_ready', prUrl: row.pr_url } };
439
725
  }
440
- async function runDiscard(deps, row) {
726
+ async function runDiscard(deps, row, opts = { removeNeedsReview: true }) {
441
727
  const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
442
728
  const implementationFailed = row.decision === 'implementation_failed';
443
- // 1. Close the PR (drops its head branch) when one exists. Best-effort — a
444
- // gh failure must never leave the discard half-done.
445
- //
446
- // Implementation-failed rows are different: the implement run produced no
447
- // deliverable update. If the row belongs to an existing-PR continuation it may
448
- // still carry that PR's url/branch from launch time; clearing the failed card
449
- // must not close or delete the user's existing review branch.
450
- if (row.pr_url && !implementationFailed) {
729
+ const preserveBorrowedReview = row.is_continuation === 1;
730
+ const cleanupWarnings = [];
731
+ // A continuation borrows an existing PR/head. Discarding its local iteration
732
+ // may remove Specrails' worktree, but never closes or deletes borrowed review
733
+ // state. Fresh deliveries retain the original destructive semantics.
734
+ if (row.pr_url && !implementationFailed && !preserveBorrowedReview) {
451
735
  try {
452
736
  const r = await deps.exec.run('gh', ['pr', 'close', row.pr_url, '--delete-branch'], deps.project.path);
453
737
  if (r.code !== 0) {
454
- console.warn(`[rail-pr-decision] gh pr close failed (continuing discard): ${(r.stderr.trim() || r.stdout.trim()).split('\n')[0]}`);
738
+ const detail = (r.stderr.trim() || r.stdout.trim()).split('\n')[0] || `exit ${r.code}`;
739
+ cleanupWarnings.push(`PR close ${row.pr_url}: ${detail}`);
740
+ console.warn(`[rail-pr-decision] gh pr close failed (continuing discard): ${detail}`);
455
741
  }
456
742
  }
457
743
  catch (err) {
744
+ cleanupWarnings.push(`PR close ${row.pr_url}: ${err instanceof Error ? err.message : String(err)}`);
458
745
  console.warn('[rail-pr-decision] gh pr close threw (continuing discard):', err);
459
746
  }
460
747
  }
461
748
  // 2. Remove the launch's worktrees (still on disk unless a restart already
462
749
  // swept them) and close their ledger rows — the reconcile-style removal,
463
750
  // branch deletion deferred to step 3 (a checked-out branch can't be -D'd).
464
- const branchSet = new Set(implementationFailed ? [] : snap.branches.map((b) => b.branch));
751
+ const deleteOwnedBranches = !implementationFailed && !preserveBorrowedReview;
752
+ const branchSet = deleteOwnedBranches ? ownedDeliveryBranches(row, snap, cleanupWarnings) : new Set();
465
753
  for (const wtId of snap.worktreeIds) {
466
754
  const wt = (0, rail_worktrees_store_1.getRailWorktree)(deps.db, wtId);
467
755
  if (!wt)
468
756
  continue;
469
- if (!implementationFailed)
470
- branchSet.add(wt.branch);
471
- await (0, worktree_manager_1.removeWorktree)(deps.git, {
472
- repoDir: deps.project.path, worktreePath: wt.worktree_path, branch: wt.branch, deleteBranch: false,
473
- }).catch(() => { });
757
+ if (wt.merge_state === 'needs-review' && opts.removeNeedsReview !== true)
758
+ continue;
759
+ if (wt.merge_state !== 'needs-review' && (0, rail_worktrees_store_1.isTerminalMergeState)(wt.merge_state))
760
+ continue;
761
+ try {
762
+ await (0, worktree_manager_1.removeWorktree)(deps.git, {
763
+ repoDir: deps.project.path, worktreePath: wt.worktree_path, branch: wt.branch, deleteBranch: false,
764
+ });
765
+ }
766
+ catch (err) {
767
+ const detail = err instanceof Error ? err.message : String(err);
768
+ cleanupWarnings.push(`worktree ${wt.worktree_path}: ${detail}`);
769
+ console.warn(`[rail-pr-decision] discard could not remove worktree ${wt.worktree_path}: ${detail}`);
770
+ continue;
771
+ }
474
772
  if (!(0, rail_worktrees_store_1.isTerminalMergeState)(wt.merge_state))
475
773
  (0, rail_worktrees_store_1.updateRailWorktreeState)(deps.db, wt.id, 'failed');
476
774
  }
477
- // 3. Delete every branch the delivery references: per-unit source branches,
478
- // the assembled head and the (possibly stale) batch branch — recomputed
479
- // best-effort from the same ticket data the create-pr path names it from
480
- // (the delivered head is already covered by row.branch). Missing branches
481
- // are tolerated; the integration branch is NEVER deleted.
482
- if (!implementationFailed) {
483
- if (row.branch)
484
- branchSet.add(row.branch);
485
- const succeededUnits = snap.branches.filter((b) => b.succeeded);
486
- if (succeededUnits.length > 1) {
775
+ // 3. Delete only branches this delivery durably records as created: owned
776
+ // per-unit sources and, for a multi-unit fresh delivery, its exact
777
+ // assembled head. A preferred name is never recomputed here because it
778
+ // may belong to the user. Missing branches are tolerated; the integration
779
+ // branch is NEVER deleted.
780
+ if (deleteOwnedBranches) {
781
+ for (const branch of branchSet) {
782
+ if (!branch || branch === row.base_branch)
783
+ continue;
487
784
  try {
488
- branchSet.add((0, pr_naming_1.batchBranchNameFor)(loadPrTicketData(deps, succeededUnits.map((b) => b.ticketId))));
785
+ const deleted = await deps.git.run(['branch', '-D', branch], deps.project.path);
786
+ if (deleted.code !== 0) {
787
+ cleanupWarnings.push(`branch ${branch}: ${(deleted.stderr.trim() || deleted.stdout.trim()).split('\n')[0] || `exit ${deleted.code}`}`);
788
+ }
489
789
  }
490
- catch {
491
- /* best-effort defense-in-depth only */
790
+ catch (err) {
791
+ cleanupWarnings.push(`branch ${branch}: ${err instanceof Error ? err.message : String(err)}`);
492
792
  }
493
793
  }
494
- for (const branch of branchSet) {
495
- if (!branch || branch === row.base_branch)
794
+ }
795
+ const terminalPatch = {
796
+ deliveryOutcome: 'not_started',
797
+ statusCode: cleanupWarnings.length > 0 ? 'cleanup_incomplete' : 'discarded',
798
+ cleanupWarnings,
799
+ };
800
+ const conflict = preserveBorrowedReview
801
+ ? casTransition(deps, row, 'discarded', terminalPatch)
802
+ : casTransitionWithTicketEffect(deps, row, 'discarded', terminalPatch, {
803
+ deliveryId: row.id,
804
+ ticketIds: snap.ticketIds,
805
+ targetStatus: 'todo',
806
+ // The dashboard labels discard-from-no_changes as Refine. Jira must
807
+ // mirror that truthful backlog return and never apply discardStatus.
808
+ jiraAction: row.decision === 'no_changes' ? 'refine' : 'discard',
809
+ prUrl: null,
810
+ });
811
+ if (conflict)
812
+ return conflict;
813
+ if (!preserveBorrowedReview)
814
+ applyTerminalTicketEffect(deps, row, 'discarded', cleanupWarnings);
815
+ finalizeTransition(deps, row.id);
816
+ return {
817
+ status: 200,
818
+ body: {
819
+ ok: true,
820
+ decision: 'discarded',
821
+ ...(cleanupWarnings.length > 0 ? { cleanupWarnings } : {}),
822
+ ...(preserveBorrowedReview ? { preservedBorrowedReview: true } : {}),
823
+ },
824
+ };
825
+ }
826
+ async function runDismiss(deps, row) {
827
+ // Legality restricts dismiss to a clean/retryable continuation. Reuse the
828
+ // ownership-safe discard path; its external PR, head and tickets are borrowed.
829
+ return runDiscard(deps, row, { removeNeedsReview: false });
830
+ }
831
+ async function runAcknowledgeNoChanges(deps, row) {
832
+ const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
833
+ const cleanupWarnings = await (0, rail_worktree_release_1.releaseRailWorktrees)({
834
+ db: deps.db,
835
+ git: deps.git,
836
+ repoDir: deps.project.path,
837
+ worktreeIds: snap.worktreeIds,
838
+ });
839
+ // A fresh no-change branch is disposable only while Git still proves that it
840
+ // has no commits ahead of the integration base. If the user added work after
841
+ // settlement, preserve it and disclose the incomplete cleanup.
842
+ for (const branch of ownedDeliveryBranches(row, snap, cleanupWarnings)) {
843
+ if (!branch || branch === row.base_branch)
844
+ continue;
845
+ try {
846
+ const ahead = await deps.git.run(['rev-list', '--count', `${row.base_branch}..${branch}`], deps.project.path);
847
+ if (ahead.code !== 0 || Number.parseInt(ahead.stdout.trim(), 10) !== 0) {
848
+ cleanupWarnings.push(`branch ${branch}: retained because no-change cleanup could not prove it has zero commits ahead of ${row.base_branch}`);
496
849
  continue;
497
- await deps.git.run(['branch', '-D', branch], deps.project.path).catch(() => { });
850
+ }
851
+ const deleted = await deps.git.run(['branch', '-d', branch], deps.project.path);
852
+ if (deleted.code !== 0) {
853
+ cleanupWarnings.push(`branch ${branch}: ${(deleted.stderr.trim() || deleted.stdout.trim()).split('\n')[0] || `exit ${deleted.code}`}`);
854
+ }
855
+ }
856
+ catch (err) {
857
+ cleanupWarnings.push(`branch ${branch}: ${err instanceof Error ? err.message : String(err)}`);
498
858
  }
499
859
  }
500
- const conflict = casTransition(deps, row, 'discarded');
860
+ const conflict = casTransitionWithTicketEffect(deps, row, 'completed', {
861
+ deliveryOutcome: 'no_changes',
862
+ statusCode: cleanupWarnings.length > 0 ? 'cleanup_incomplete' : 'no_changes',
863
+ cleanupWarnings,
864
+ }, {
865
+ deliveryId: row.id,
866
+ ticketIds: snap.ticketIds,
867
+ targetStatus: 'done',
868
+ jiraAction: 'completed',
869
+ prUrl: null,
870
+ });
501
871
  if (conflict)
502
872
  return conflict;
873
+ applyTerminalTicketEffect(deps, row, 'completed', cleanupWarnings);
503
874
  finalizeTransition(deps, row.id);
504
- // 4. Revert the parked tickets to the backlog + mirror to Jira (outbox-only;
505
- // the local write above is ours).
506
- const changed = applyTicketTransition(deps, snap.ticketIds, 'todo');
507
- try {
508
- deps.jiraSyncManager?.onRailDiscard(changed, row.id);
509
- }
510
- catch (err) {
511
- console.error('[rail-pr-decision] jira onRailDiscard failed:', err);
512
- }
513
- return { status: 200, body: { ok: true, decision: 'discarded' } };
875
+ return {
876
+ status: 200,
877
+ body: {
878
+ ok: true,
879
+ decision: 'completed',
880
+ noChanges: true,
881
+ ...(cleanupWarnings.length > 0 ? { cleanupWarnings } : {}),
882
+ },
883
+ };
514
884
  }
515
885
  async function runPollMerge(deps, row) {
516
- const r = await deps.exec.run('gh', ['pr', 'view', row.pr_url, '--json', 'state,mergedAt'], deps.project.path);
517
- if (r.code !== 0)
518
- return ghFailed(r); // no transition
519
- let state;
520
- try {
521
- state = JSON.parse(r.stdout).state;
886
+ const observed = await (0, pr_lifecycle_1.observePrLifecycle)(deps.exec, deps.project.path, row.pr_url, row.delivery_sha);
887
+ if (!observed.ok)
888
+ return { status: 502, body: { error: 'gh_failed', detail: observed.detail } };
889
+ const { state, isDraft } = observed;
890
+ if (state === 'CLOSED') {
891
+ const conflict = casTransition(deps, row, 'pr_closed', {
892
+ deliveryOutcome: 'delivered', statusCode: 'pr_closed',
893
+ });
894
+ if (conflict)
895
+ return conflict;
896
+ finalizeTransition(deps, row.id);
897
+ return { status: 200, body: { ok: true, decision: 'pr_closed', merged: false, closed: true, prUrl: row.pr_url } };
522
898
  }
523
- catch {
524
- return { status: 502, body: { error: 'gh_failed', detail: 'unparseable gh pr view output' } };
899
+ if (state === 'OPEN') {
900
+ // A successful reopen can be observed even if the explicit action lost its
901
+ // follow-up `gh view` response. Polling heals pr_closed from remote truth.
902
+ if (row.decision === 'pr_closed') {
903
+ const next = isDraft ? 'pr_draft' : 'pr_ready';
904
+ const conflict = casTransition(deps, row, next, {
905
+ deliveryOutcome: 'delivered', statusCode: isDraft ? 'pr_draft_ready' : 'pr_ready',
906
+ });
907
+ if (conflict)
908
+ return conflict;
909
+ finalizeTransition(deps, row.id);
910
+ return { status: 200, body: { ok: true, decision: next, merged: false, reopened: true, prUrl: row.pr_url } };
911
+ }
912
+ return { status: 200, body: { ok: true, decision: row.decision, merged: false } };
525
913
  }
526
914
  if (state !== 'MERGED') {
527
- // Not merged (OPEN/CLOSED/unknown) → observation only, nothing changes.
528
- return { status: 200, body: { ok: true, decision: row.decision, merged: false } };
915
+ return { status: 502, body: { error: 'gh_failed', detail: `unexpected PR state: ${state}` } };
529
916
  }
530
- const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
531
- const branchSet = new Set(snap.branches.map((b) => b.branch));
532
- if (row.branch)
533
- branchSet.add(row.branch);
534
- const succeededUnits = snap.branches.filter((b) => b.succeeded);
535
- if (succeededUnits.length > 1) {
536
- try {
537
- branchSet.add((0, pr_naming_1.batchBranchNameFor)(loadPrTicketData(deps, succeededUnits.map((b) => b.ticketId))));
538
- }
539
- catch {
540
- /* best-effort cleanup only */
541
- }
917
+ // A PR can merge just before a continuation push; GitHub may then accept a
918
+ // later update to the head branch even though that object was never part of
919
+ // the merge. Never move tickets to Done from state=MERGED alone.
920
+ if (!row.delivery_sha) {
921
+ return persistMovedPrHead(deps, row, 'the merged PR has no persisted verified delivery SHA; refusing to infer delivery from a mutable branch');
542
922
  }
543
- await (0, rail_worktree_release_1.releaseRailWorktrees)({ db: deps.db, git: deps.git, repoDir: deps.project.path, worktreeIds: snap.worktreeIds, state: 'merged' });
923
+ if (observed.includesExpectedSha !== true) {
924
+ return rerouteFromStalePr(deps, row, mergedEvidenceDetail(observed), true);
925
+ }
926
+ const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
927
+ const cleanupWarnings = await (0, rail_worktree_release_1.releaseRailWorktrees)({
928
+ db: deps.db, git: deps.git, repoDir: deps.project.path,
929
+ worktreeIds: releasableWorktreeIds(deps, snap), state: 'merged',
930
+ });
931
+ const branchSet = ownedDeliveryBranches(row, snap, cleanupWarnings);
544
932
  for (const branch of branchSet) {
545
933
  if (!branch || branch === row.base_branch)
546
934
  continue;
547
- await deps.git.run(['branch', '-D', branch], deps.project.path).catch(() => { });
935
+ try {
936
+ const deleted = await deps.git.run(['branch', '-D', branch], deps.project.path);
937
+ if (deleted.code !== 0) {
938
+ cleanupWarnings.push(`branch ${branch}: ${(deleted.stderr.trim() || deleted.stdout.trim()).split('\n')[0] || `exit ${deleted.code}`}`);
939
+ }
940
+ }
941
+ catch (err) {
942
+ cleanupWarnings.push(`branch ${branch}: ${err instanceof Error ? err.message : String(err)}`);
943
+ }
548
944
  }
549
- const conflict = casTransition(deps, row, 'merged');
945
+ const conflict = casTransitionWithTicketEffect(deps, row, 'merged', {
946
+ deliveryOutcome: 'delivered',
947
+ statusCode: cleanupWarnings.length > 0 ? 'cleanup_incomplete' : 'merged',
948
+ cleanupWarnings,
949
+ }, {
950
+ deliveryId: row.id,
951
+ ticketIds: snap.ticketIds,
952
+ targetStatus: 'done',
953
+ jiraAction: 'merged',
954
+ prUrl: row.pr_url,
955
+ });
550
956
  if (conflict)
551
957
  return conflict;
958
+ applyTerminalTicketEffect(deps, row, 'merged', cleanupWarnings);
552
959
  finalizeTransition(deps, row.id);
553
- const changed = applyTicketTransition(deps, snap.ticketIds, 'done');
960
+ return { status: 200, body: { ok: true, decision: 'merged', merged: true, prUrl: row.pr_url } };
961
+ }
962
+ async function runReopen(deps, row) {
963
+ let reopened;
554
964
  try {
555
- deps.jiraSyncManager?.onRailMerged(changed, row.id, row.pr_url);
965
+ reopened = await deps.exec.run('gh', ['pr', 'reopen', row.pr_url], deps.project.path);
556
966
  }
557
967
  catch (err) {
558
- console.error('[rail-pr-decision] jira onRailMerged failed:', err);
968
+ reopened = { code: 1, stdout: '', stderr: err instanceof Error ? err.message : String(err) };
559
969
  }
560
- return { status: 200, body: { ok: true, decision: 'merged', merged: true, prUrl: row.pr_url } };
970
+ const lifecycle = await observePrLifecycle(deps, row.pr_url);
971
+ if (lifecycle?.state !== 'OPEN') {
972
+ if (reopened.code !== 0)
973
+ return ghFailed(reopened);
974
+ return { status: 502, body: { error: 'gh_failed', detail: `PR did not reopen (state ${lifecycle?.state ?? 'unknown'})` } };
975
+ }
976
+ const next = lifecycle.isDraft ? 'pr_draft' : 'pr_ready';
977
+ const conflict = casTransition(deps, row, next, {
978
+ deliveryOutcome: 'delivered', statusCode: lifecycle.isDraft ? 'pr_draft_ready' : 'pr_ready',
979
+ });
980
+ if (conflict)
981
+ return conflict;
982
+ finalizeTransition(deps, row.id);
983
+ return { status: 200, body: { ok: true, decision: next, reopened: true, prUrl: row.pr_url } };
561
984
  }
562
- /**
563
- * Remote-less acceptance: merge the delivery's branches into the integration
564
- * branch DIRECTLY in the user's checkout. This is the one decision that touches
565
- * the user's working tree, so it is triple-guarded and transactional-ish:
566
- *
567
- * - The checkout must have the integration branch checked out (a branch can
568
- * only be checked out in one worktree — merging via a temp worktree is
569
- * impossible while the user holds it) → 409 `merge_local_blocked` otherwise.
570
- * - The working tree must be CLEAN → 409 `merge_local_blocked` otherwise.
571
- * - Merge = the assembled head when one exists (row.branch), else each
572
- * succeeded unit branch sequentially, `--no-ff --no-edit`. Any conflict →
573
- * `merge --abort` + 502 `merge_failed`, NO transition (retry or discard).
574
- *
575
- * Success mirrors poll-merge's MERGED arm: decision → merged (prUrl stays
576
- * null), worktrees/branches swept, tickets → done, Jira onRailMerged(null).
577
- */
578
985
  async function runMergeLocal(deps, row) {
986
+ return (0, repo_lock_1.withRepoLock)(deps.project.path, () => runMergeLocalLocked(deps, row));
987
+ }
988
+ async function runMergeLocalLocked(deps, row) {
989
+ deps.assertAdmission?.();
579
990
  const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
580
- // Guards on the user's checkout.
991
+ const cleanupWarnings = [];
992
+ // Snapshot the checkout the user authorized. The same three invariants are
993
+ // revalidated immediately before the one final fast-forward.
581
994
  const head = await deps.git.run(['rev-parse', '--abbrev-ref', 'HEAD'], deps.project.path);
582
995
  const currentBranch = head.code === 0 ? head.stdout.trim() : '';
583
996
  if (currentBranch !== row.base_branch) {
@@ -590,72 +1003,186 @@ async function runMergeLocal(deps, row) {
590
1003
  if (status.code !== 0 || status.stdout.trim() !== '') {
591
1004
  return { status: 409, body: { error: 'merge_local_blocked', reason: 'dirty', base: row.base_branch } };
592
1005
  }
593
- // What to merge: the assembled head carries every unit already; without one
594
- // (straight from on_review) merge each succeeded unit branch in order.
1006
+ const startingHead = await deps.git.run(['rev-parse', '--verify', 'HEAD'], deps.project.path);
1007
+ const startingSha = startingHead.code === 0 ? startingHead.stdout.trim() : '';
1008
+ if (!/^[0-9a-f]{40,64}$/i.test(startingSha)) {
1009
+ return { status: 409, body: { error: 'merge_local_blocked', reason: 'unresolved_head', base: row.base_branch } };
1010
+ }
1011
+ // What to merge: immutable object ids only. The assembled head carries every
1012
+ // unit already; without one, merge each settled unit's exact final SHA.
595
1013
  let toMerge;
596
- if (row.branch && (await branchExists(deps, row.branch))) {
597
- toMerge = [row.branch];
1014
+ let deliveryHeadSha = null;
1015
+ if (row.delivery_sha && await commitObjectExists(deps, row.delivery_sha)) {
1016
+ deliveryHeadSha = row.delivery_sha;
1017
+ }
1018
+ else if (row.branch) {
1019
+ deliveryHeadSha = await captureBranchSha(deps, row.branch);
1020
+ }
1021
+ if (deliveryHeadSha) {
1022
+ toMerge = [deliveryHeadSha];
598
1023
  }
599
1024
  else {
600
1025
  toMerge = [];
601
1026
  for (const unit of snap.branches.filter((b) => b.succeeded)) {
602
- let branch = unit.branch;
603
- if (!(await branchExists(deps, branch)))
604
- branch = await findRecoverableBranch(deps, unit, row.base_branch);
605
- if (!branch) {
1027
+ let sourceSha = unit.finalSha && COMMIT_SHA_RE.test(unit.finalSha) ? unit.finalSha : null;
1028
+ if (sourceSha) {
1029
+ if (!(await commitObjectExists(deps, sourceSha)))
1030
+ sourceSha = null;
1031
+ }
1032
+ else {
1033
+ sourceSha = await captureBranchSha(deps, unit.branch);
1034
+ }
1035
+ if (!sourceSha) {
606
1036
  return {
607
1037
  status: 502,
608
- body: { error: 'merge_failed', detail: `branch '${unit.branch}' (ticket #${unit.ticketId}) no longer exists locally — nothing to merge` },
1038
+ body: { error: 'merge_failed', detail: `verified commit for '${unit.branch}' (ticket #${unit.ticketId}) is unavailable` },
609
1039
  };
610
1040
  }
611
- toMerge.push(branch);
1041
+ toMerge.push(sourceSha);
612
1042
  }
613
1043
  }
614
1044
  if (toMerge.length === 0) {
615
1045
  return { status: 502, body: { error: 'merge_failed', detail: 'no succeeded branch to merge' } };
616
1046
  }
1047
+ // Assemble every merge away from the user's checkout. A conflict can only
1048
+ // dirty this disposable detached worktree, never the integration branch.
1049
+ const safeId = row.id.replace(/[^A-Za-z0-9-]/g, '').slice(0, 64) || 'delivery';
1050
+ const assemblyPath = path.join(deps.assemblyRoot ?? batchWorktreeRoot(deps.project.slug), `local-merge-${safeId}-${(0, ids_1.newId)()}`);
1051
+ const added = await deps.git.run(['worktree', 'add', '--detach', assemblyPath, startingSha], deps.project.path);
1052
+ if (added.code !== 0) {
1053
+ const detail = (added.stderr.trim() || added.stdout.trim()).split('\n')[0] || `exit ${added.code}`;
1054
+ return { status: 502, body: { error: 'merge_failed', detail: `creating isolated assembly: ${detail}` } };
1055
+ }
1056
+ const removeAssembly = async () => {
1057
+ try {
1058
+ const removed = await deps.git.run(['worktree', 'remove', '--force', assemblyPath], deps.project.path);
1059
+ if (removed.code !== 0) {
1060
+ cleanupWarnings.push(`assembly worktree ${assemblyPath}: ${(removed.stderr.trim() || removed.stdout.trim()).split('\n')[0] || `exit ${removed.code}`}`);
1061
+ }
1062
+ }
1063
+ catch (err) {
1064
+ cleanupWarnings.push(`assembly worktree ${assemblyPath}: ${err instanceof Error ? err.message : String(err)}`);
1065
+ }
1066
+ };
1067
+ const persistAssemblyFailure = (detail) => {
1068
+ const conflict = casTransition(deps, row, row.decision, {
1069
+ deliveryOutcome: 'blocked', statusCode: 'delivery_failed', statusDetail: detail, cleanupWarnings,
1070
+ });
1071
+ if (conflict)
1072
+ return conflict;
1073
+ finalizeTransition(deps, row.id);
1074
+ return null;
1075
+ };
617
1076
  for (const branch of toMerge) {
618
- const r = await deps.git.run(['merge', '--no-ff', '--no-edit', branch], deps.project.path);
1077
+ const r = await deps.git.run(['merge', '--no-ff', '--no-edit', branch], assemblyPath);
619
1078
  if (r.code !== 0) {
620
- await deps.git.run(['merge', '--abort'], deps.project.path).catch(() => { });
1079
+ try {
1080
+ const aborted = await deps.git.run(['merge', '--abort'], assemblyPath);
1081
+ if (aborted.code !== 0)
1082
+ cleanupWarnings.push(`assembly abort ${branch}: exit ${aborted.code}`);
1083
+ }
1084
+ catch (err) {
1085
+ cleanupWarnings.push(`assembly abort ${branch}: ${err instanceof Error ? err.message : String(err)}`);
1086
+ }
1087
+ await removeAssembly();
621
1088
  const detail = (r.stderr.trim() || r.stdout.trim()).split('\n')[0] || `exit ${r.code}`;
1089
+ const conflict = persistAssemblyFailure(`merging '${branch}': ${detail}`);
1090
+ if (conflict)
1091
+ return conflict;
622
1092
  return { status: 502, body: { error: 'merge_failed', detail: `merging '${branch}': ${detail}` } };
623
1093
  }
624
1094
  }
625
- // Post-merge sweep (same shape as discard steps 2-3): the work now lives on
1095
+ const assembled = await deps.git.run(['rev-parse', '--verify', 'HEAD'], assemblyPath);
1096
+ const assembledSha = assembled.code === 0 ? assembled.stdout.trim() : '';
1097
+ if (!/^[0-9a-f]{40,64}$/i.test(assembledSha)) {
1098
+ await removeAssembly();
1099
+ const detail = 'isolated assembly produced no verifiable HEAD';
1100
+ const conflict = persistAssemblyFailure(detail);
1101
+ if (conflict)
1102
+ return conflict;
1103
+ return { status: 502, body: { error: 'merge_failed', detail } };
1104
+ }
1105
+ const [finalBranch, finalStatus, finalHead] = await Promise.all([
1106
+ deps.git.run(['rev-parse', '--abbrev-ref', 'HEAD'], deps.project.path),
1107
+ deps.git.run(['status', '--porcelain'], deps.project.path),
1108
+ deps.git.run(['rev-parse', '--verify', 'HEAD'], deps.project.path),
1109
+ ]);
1110
+ const changedReason = finalBranch.code !== 0 || finalBranch.stdout.trim() !== row.base_branch
1111
+ ? 'wrong_branch'
1112
+ : finalStatus.code !== 0 || finalStatus.stdout.trim() !== ''
1113
+ ? 'dirty'
1114
+ : finalHead.code !== 0 || finalHead.stdout.trim() !== startingSha
1115
+ ? 'head_changed'
1116
+ : null;
1117
+ if (changedReason) {
1118
+ await removeAssembly();
1119
+ if (cleanupWarnings.length > 0) {
1120
+ const conflict = casTransition(deps, row, row.decision, { cleanupWarnings });
1121
+ if (conflict)
1122
+ return conflict;
1123
+ finalizeTransition(deps, row.id);
1124
+ }
1125
+ return { status: 409, body: { error: 'merge_local_blocked', reason: changedReason, base: row.base_branch } };
1126
+ }
1127
+ const advanced = await deps.git.run(['merge', '--ff-only', assembledSha], deps.project.path);
1128
+ if (advanced.code !== 0) {
1129
+ await removeAssembly();
1130
+ const detail = (advanced.stderr.trim() || advanced.stdout.trim()).split('\n')[0] || `exit ${advanced.code}`;
1131
+ const conflict = persistAssemblyFailure(`advancing '${row.base_branch}': ${detail}`);
1132
+ if (conflict)
1133
+ return conflict;
1134
+ return { status: 502, body: { error: 'merge_failed', detail: `advancing '${row.base_branch}': ${detail}` } };
1135
+ }
1136
+ await removeAssembly();
1137
+ // Post-advance sweep: the work now lives on
626
1138
  // the integration branch, so the launch's worktrees + branches are spent.
627
- const branchSet = new Set(toMerge);
628
- for (const b of snap.branches)
629
- branchSet.add(b.branch);
630
- if (row.branch)
631
- branchSet.add(row.branch);
632
- for (const wtId of snap.worktreeIds) {
1139
+ const branchSet = ownedDeliveryBranches(row, snap, cleanupWarnings);
1140
+ for (const wtId of releasableWorktreeIds(deps, snap)) {
633
1141
  const wt = (0, rail_worktrees_store_1.getRailWorktree)(deps.db, wtId);
634
1142
  if (!wt)
635
1143
  continue;
636
- branchSet.add(wt.branch);
637
- await (0, worktree_manager_1.removeWorktree)(deps.git, {
638
- repoDir: deps.project.path, worktreePath: wt.worktree_path, branch: wt.branch, deleteBranch: false,
639
- }).catch(() => { });
1144
+ try {
1145
+ await (0, worktree_manager_1.removeWorktree)(deps.git, {
1146
+ repoDir: deps.project.path, worktreePath: wt.worktree_path, branch: wt.branch, deleteBranch: false,
1147
+ });
1148
+ }
1149
+ catch (err) {
1150
+ const detail = err instanceof Error ? err.message : String(err);
1151
+ cleanupWarnings.push(`worktree ${wt.worktree_path}: ${detail}`);
1152
+ console.warn(`[rail-pr-decision] merge-local could not remove worktree ${wt.worktree_path}: ${detail}`);
1153
+ continue;
1154
+ }
640
1155
  if (!(0, rail_worktrees_store_1.isTerminalMergeState)(wt.merge_state))
641
1156
  (0, rail_worktrees_store_1.updateRailWorktreeState)(deps.db, wt.id, 'merged');
642
1157
  }
643
1158
  for (const branch of branchSet) {
644
1159
  if (!branch || branch === row.base_branch)
645
1160
  continue;
646
- await deps.git.run(['branch', '-D', branch], deps.project.path).catch(() => { });
1161
+ try {
1162
+ const deleted = await deps.git.run(['branch', '-D', branch], deps.project.path);
1163
+ if (deleted.code !== 0) {
1164
+ cleanupWarnings.push(`branch ${branch}: ${(deleted.stderr.trim() || deleted.stdout.trim()).split('\n')[0] || `exit ${deleted.code}`}`);
1165
+ }
1166
+ }
1167
+ catch (err) {
1168
+ cleanupWarnings.push(`branch ${branch}: ${err instanceof Error ? err.message : String(err)}`);
1169
+ }
647
1170
  }
648
- const conflict = casTransition(deps, row, 'merged');
1171
+ const conflict = casTransitionWithTicketEffect(deps, row, 'merged', {
1172
+ deliveryOutcome: 'delivered',
1173
+ statusCode: cleanupWarnings.length > 0 ? 'cleanup_incomplete' : 'merged',
1174
+ cleanupWarnings,
1175
+ }, {
1176
+ deliveryId: row.id,
1177
+ ticketIds: snap.ticketIds,
1178
+ targetStatus: 'done',
1179
+ jiraAction: 'merged',
1180
+ prUrl: null,
1181
+ });
649
1182
  if (conflict)
650
1183
  return conflict;
1184
+ applyTerminalTicketEffect(deps, row, 'merged', cleanupWarnings);
651
1185
  finalizeTransition(deps, row.id);
652
- const changed = applyTicketTransition(deps, snap.ticketIds, 'done');
653
- try {
654
- deps.jiraSyncManager?.onRailMerged(changed, row.id, null);
655
- }
656
- catch (err) {
657
- console.error('[rail-pr-decision] jira onRailMerged (local) failed:', err);
658
- }
659
1186
  return { status: 200, body: { ok: true, decision: 'merged', merged: true, local: true } };
660
1187
  }
661
1188
  function resolveTicketFile(deps) {
@@ -664,44 +1191,3 @@ function resolveTicketFile(deps) {
664
1191
  const exec = (0, workspace_resolution_1.resolveProjectExecution)({ slug: deps.project.slug, path: deps.project.path });
665
1192
  return exec.relocated ? exec.ticketsPath : (0, ticket_store_1.resolveTicketStoragePath)(deps.project.path);
666
1193
  }
667
- /**
668
- * Move the launch's on_review tickets to `todo` (discard) or `done` (merged),
669
- * broadcasting ticket_updated per changed spec — mirroring onLoopRunFinished's
670
- * revert style in project-registry. ONLY tickets still parked at on_review move
671
- * (a manually re-triaged spec is respected). Best-effort: a ticket-store
672
- * failure is logged, never fatal (the decision row already transitioned).
673
- */
674
- function applyTicketTransition(deps, ticketIds, to) {
675
- const changed = [];
676
- if (ticketIds.length === 0)
677
- return changed;
678
- try {
679
- const ticketFile = resolveTicketFile(deps);
680
- const now = new Date().toISOString();
681
- const store = (0, ticket_store_1.mutateStore)(ticketFile, (s) => {
682
- for (const tid of ticketIds) {
683
- const ticket = s.tickets[String(tid)];
684
- if (!ticket || ticket.status !== 'on_review')
685
- continue;
686
- ticket.status = to;
687
- ticket.updated_at = now;
688
- changed.push(tid);
689
- }
690
- });
691
- for (const tid of changed) {
692
- const ticket = store.tickets[String(tid)];
693
- if (!ticket)
694
- continue;
695
- deps.broadcast({
696
- type: 'ticket_updated',
697
- ticket: ticket,
698
- projectId: deps.project.id,
699
- timestamp: ticket.updated_at,
700
- });
701
- }
702
- }
703
- catch (err) {
704
- console.error('[rail-pr-decision] failed to apply ticket transition:', err);
705
- }
706
- return changed;
707
- }