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