chainlesschain 0.163.7 → 0.164.0
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/package.json +5 -1
- package/scripts/macos-mcp-launcher-build.mjs +317 -0
- package/scripts/macos-mcp-launcher-postinstall.sh +54 -0
- package/scripts/macos-mcp-launcher-preinstall.sh +61 -0
- package/src/assets/web-panel/assets/{AIOps-axqQfwv8.js → AIOps-D2MEn7fI.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DV-Ro0By.js → ActionButton-lCW_4Gf0.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Bt72WEVu.js → Analytics-Bs7i-nsS.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BtepG55E.js → AppLayout-CK7B6S_j.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-B7KaJ7S_.js → Artifacts-C8ILjpyo.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-FNFaUg7A.js → Audit-Bb9cNaWc.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DFb6J2wQ.js → BackgroundAgents-sC9ooXIf.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CC7YBhyj.js → Backup-D-DrsplT.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-RkgTNVtj.js → BaseInput-uCNZpwSK.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-hHEmGZy-.js → Chat-CsDK1uE9.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CAP9YhHv.js → ChatBubbleRenderer-OqSDt50w.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-z5wXOQP1.js → Checkbox-JhTXM1-K.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DVaDJnxy.js → Codegen-BbzZLnyD.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CJmaRDR5.js → Col-D4VW4m4j.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DF29_2bI.js → Community-BXrYawxV.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DtkQkBzn.js → Compact-DCihGy1u.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-xoHDxp7I.js → Compliance-DlyZ-lk8.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BUm-R7ht.js → Cowork-DvGZrhbO.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-Cgl14ZkB.js → Cron-DUncZi8h.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BNcbmoqh.js → Crosschain-C_hLuUcm.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Bwja0Zo9.js → DID-b6YSUqxr.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Bk5XjEdJ.js → Dashboard-uvM15Cfb.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-Ci36Oitq.js → Dropdown-D1-Nd4qY.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer--U04ZN4q.js → EmailListRenderer-8ApvL6Zc.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-B2gNPR2s.js → FamilyGuardDashboard-LYJt8811.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-C4KghYn7.js → Federation-BbI3HGpv.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-QL7x_Q_0.js → FormItemContext-DSoEKuFu.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-F3Ryltwm.js → GenericCardRenderer-B0S9OWd5.js} +1 -1
- package/src/assets/web-panel/assets/{Git-2HassK5k.js → Git-DIs9Zhf3.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DoBIp7oe.js → Governance-jzZXrxcl.js} +1 -1
- package/src/assets/web-panel/assets/{Inference--W01QhZ-.js → Inference-JUPjf2ZI.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DmvwqGpT.js → KnowledgeGraph-DxnfJvlP.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BBJ4yAel.js → Logs-iXiz8BEE.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-BLCZMzhJ.js → MarkdownRenderer-GmB6DOr6.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-D5lNngSI.js → Marketplace-Y3Xch312.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CkEVOMKH.js → McpTools-D7kGGbO7.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-B1Ow1xVE.js → Memory-Do_hVxzh.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-SgYbUfb6.js → MobileBridge-CtrR0Tas.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-BL0Czcoj.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-DcTXSSma.js → Mtc-qo5QCTGd.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Dt3WG4oj.js → MtcAudit-cq5mnjYj.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-B9B38UZx.js → Multisig-Bq0MAQHw.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-B0S4GGvG.js → NLProgramming-DdWE6Vi2.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DZ5U58Ng.js → Notes-BxMBFcIp.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-PjL5ZW34.js → NotificationSettings-RTrAcbUe.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DrAX2ESt.js → OrderTableRenderer-DJbpGqzT.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-Cv1GYs-c.js → Organization-SQzIPdqC.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Bbkp_PL3.js → Overflow-02yECh22.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BD8QUgoZ.js → P2P-BNg4pNCM.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BO-2PeaB.js → PdhVaultBrowser-BxPJZsD3.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-D_OPVa02.js → Permissions-CLnDnPAz.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-mh-pEmaB.js → PersonalDataHub-C5m0AjGf.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-6JC2EDdb.js → Pipeline-VwOf5Dhv.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CUshzUAG.js → Privacy-CC0W2hZd.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Cs2Aalri.js → ProjectInit-Dz893Bob.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-1ak49tcj.js → ProjectSettings-CxQkvl0k.js} +2 -2
- package/src/assets/web-panel/assets/Projects-DDAjkUkT.js +1 -0
- package/src/assets/web-panel/assets/{Providers-CXXxvafH.js → Providers-DeB9IkxK.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-PTcDsF58.js → QrScannerModal-OCeoaswn.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CDwucrJw.js → QuickAsk-DenotaAj.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CJlLVRXR.js → Recommend-Cies_jjI.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DlN-ETsO.js → RemoteSession-Dxzl5n2T.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-DI23LR6m.js → Reputation-CW0mvxgZ.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Cl7UQMrM.js → Row-SEd32qZU.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CexHNfh8.js → RssFeed-BSwnJ3Lm.js} +3 -3
- package/src/assets/web-panel/assets/{Search-BneBWvRN.js → Search-xe2m8bkC.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DX7Phgrp.js → Security-Mq2F9Nro.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BWXO8Mte.js → Services-BKGw2V3P.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DCyOjKPJ.js → Skeleton-DW95-Rxf.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CXqERsTB.js → Skills-CHgChCv_.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-bhbrbz8E.js → Sla-tKh6k3R7.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-3IPU0HBZ.js → SpeechSettings-D6UL3dA5.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DBNTQLbM.js → SyncSettings-D0Fw8fVL.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-quP0bkN-.js → Tasks-20x1GB3D.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BouWmgho.js → Templates-DJ-lts-a.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-qv0D9s7z.js → Tenant-BFdGpbs7.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BxvbE8BB.js → Terminal-Xpx61vHL.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D-IeLnJ2.js → TimelineRenderer-C6UIvyZQ.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CFMKKyVG.js → Tokens-Dellq08f.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-L8as3UuJ.js → Trigger-VKw8d0J-.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-MxmA4VZG.js → Trust-DNftdKT5.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-C0VvJYRi.js → UkeySign-DX9VAKJI.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-Bf5QZj8N.js → VideoEditing-B2h3jO8q.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BIJ9riRz.js → Wallet-CjGkof7f.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-B8p9Mhkw.js → WebAuthn-2Zfcu55P.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-BVsuaPen.js → WorkflowEditor-B9eZ8UxZ.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BfkjoKpV.js → chat-CQNfD-YO.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DeAlsWK8.js → colors-D4usyFT2.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Cs97bz5j.js → compact-item-s4TpjvjX.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BqbG60LH.js → createContext-BRCSvYxj.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DKJjSWRn.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-i_RY0b1X.js → hasIn-AdkUbkV-.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOVkf1K3.js → index--00U6vXZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-B-3tEpTc.js → index-B2V-eKfK.js} +1 -1
- package/src/assets/web-panel/assets/{index-D2B4VPyP.js → index-B4JJCfQ2.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkBi5TmC.js → index-BIoG5yYf.js} +1 -1
- package/src/assets/web-panel/assets/{index-79V_zv1s.js → index-BMcimU-2.js} +1 -1
- package/src/assets/web-panel/assets/{index-U4f5izHz.js → index-BPhkFZ1I.js} +1 -1
- package/src/assets/web-panel/assets/{index-DiSkx4UB.js → index-BQ3DXBtD.js} +3 -3
- package/src/assets/web-panel/assets/{index-dIHr4B_u.js → index-Bad4VNvz.js} +1 -1
- package/src/assets/web-panel/assets/{index-CIG8fQ2_.js → index-BgiXR_-_.js} +1 -1
- package/src/assets/web-panel/assets/{index-DqBgBpgp.js → index-BtobShro.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZxO_-E8a.js → index-Bw2MEnUk.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUUrv141.js → index-C1mJtE6q.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bm7lLR4c.js → index-C484EJiS.js} +1 -1
- package/src/assets/web-panel/assets/{index-COdGA-8n.js → index-C5KAQFPQ.js} +1 -1
- package/src/assets/web-panel/assets/index-C61cnl3K.js +1 -0
- package/src/assets/web-panel/assets/{index-CED2PL_y.js → index-C7Y8Aluc.js} +1 -1
- package/src/assets/web-panel/assets/{index-YXgLng0K.js → index-CCsM_Ybx.js} +1 -1
- package/src/assets/web-panel/assets/{index-0-agYlWL.js → index-CJCFe2Qf.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5UkyA3c.js → index-CJVEOa4i.js} +1 -1
- package/src/assets/web-panel/assets/{index-B1CbOH1i.js → index-CJdQrZth.js} +1 -1
- package/src/assets/web-panel/assets/{index-s1opAL-M.js → index-CKC1t3Ic.js} +1 -1
- package/src/assets/web-panel/assets/{index-yObj1O4n.js → index-CQhAVU5t.js} +1 -1
- package/src/assets/web-panel/assets/{index-CgsqdnRV.js → index-CYdoQsa1.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dk7-v4hP.js → index-Cduu2Hhr.js} +1 -1
- package/src/assets/web-panel/assets/{index-B-JVTrpr.js → index-CkwiSLhU.js} +1 -1
- package/src/assets/web-panel/assets/{index-CeWoNHau.js → index-Cvyt-zhm.js} +1 -1
- package/src/assets/web-panel/assets/{index-CE2VtTlp.js → index-D2GjI2oe.js} +1 -1
- package/src/assets/web-panel/assets/index-D4XFHxBr.js +1 -0
- package/src/assets/web-panel/assets/{index-DmbfhJUP.js → index-DARsAyUp.js} +1 -1
- package/src/assets/web-panel/assets/{index-BESLFt4q.js → index-DVKfQ0Ux.js} +1 -1
- package/src/assets/web-panel/assets/{index-PT_5sBMA.js → index-DZT36ZYq.js} +1 -1
- package/src/assets/web-panel/assets/{index-gyFpvL7l.js → index-D_XBHopw.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYUVuwKt.js → index-DdCB70SS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcUXAB2s.js → index-DzRjHxEv.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dk2qouAz.js → index-MMN9V6JF.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9evtvg2.js → index-RfMr79MW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuLQobzW.js → index-_IRVsV9X.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cf3WCLmy.js → index-nXyk8GZ9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CG3PRyAL.js → index-s_buFMEG.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-C9h9yKL3.js → initDefaultProps-Blmpk7HL.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BvmLsPdu.js → motion-DPBJMv1n.js} +1 -1
- package/src/assets/web-panel/assets/{move-CoL4ekU1.js → move-ButdzCM7.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-C4pMy6xV.js → mtc-parser-cyRC_QCe.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DPuJfNxx.js → omit-1epSV7hz.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-D0KEeaK2.js → pickAttrs-DuPY8xJs.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BpkyINxt.js → placementArrow-6XZqarhe.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DQI_wOq8.js → responsiveObserve-B40pXBWM.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DYfdDiRg.js → slide-jbZ9Wu4n.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-B5dtvb0D.js → statusUtils-BKQQcVy4.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CF310mSj.js → styleChecker-NiKfWjUT.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-D1Qp4BLp.js → useFlexGapSupport-bQUCvPeb.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-C7_iyvJK.js → useFs-CUGji7ph.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DZUt_fIm.js → usePersonalDataHub-4veyN2N2.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-D3dsi0lZ.js → vnode-1-nLug-Z.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-WdbOlvdQ.js → zoom-dhCeRDMo.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +6 -6
- package/src/commands/automation.js +103 -1
- package/src/commands/cowork.js +174 -19
- package/src/commands/permissions.js +240 -16
- package/src/commands/plugin.js +1293 -56
- package/src/commands/session-location.js +276 -0
- package/src/commands/session.js +2 -0
- package/src/commands/team-merge-review.js +937 -0
- package/src/commands/team.js +2 -0
- package/src/data/changelog.json +27 -1
- package/src/gateways/ws/action-protocol.js +35 -10
- package/src/gateways/ws/remote-session-protocol.js +24 -1
- package/src/gateways/ws/ws-agent-handler.js +12 -0
- package/src/gateways/ws/ws-server.js +48 -12
- package/src/harness/jsonl-session-store.js +341 -24
- package/src/harness/mcp-client.js +8 -1
- package/src/harness/remote-session-audit.js +1 -0
- package/src/harness/remote-session-crypto.js +210 -2
- package/src/harness/remote-session-registry.js +159 -6
- package/src/harness/session-list-index.js +233 -14
- package/src/lib/agent-authority.js +6 -0
- package/src/lib/agent-schedule-store.js +139 -28
- package/src/lib/agent-team/team-merge-review-store.js +765 -0
- package/src/lib/agent-team/team-merge-review-transaction.js +1223 -0
- package/src/lib/agent-team/team-merge-review.js +1847 -0
- package/src/lib/automation-center-incidents.js +314 -0
- package/src/lib/automation-center-runtime.js +448 -0
- package/src/lib/automation-center.js +182 -38
- package/src/lib/automation-engine.js +41 -0
- package/src/lib/automation-execution-authority.js +241 -12
- package/src/lib/automation-execution-incident.js +737 -0
- package/src/lib/background-agent-keeper-client.js +248 -0
- package/src/lib/background-agent-keeper-protocol.js +147 -0
- package/src/lib/background-agent-supervisor.js +520 -40
- package/src/lib/background-interaction-resolver.js +12 -0
- package/src/lib/background-turn-bootstrap-protocol.js +126 -0
- package/src/lib/cowork-cron.js +85 -14
- package/src/lib/cowork-workflow.js +336 -16
- package/src/lib/dynamic-workflow-facade.js +648 -0
- package/src/lib/execution-location-contract.js +551 -0
- package/src/lib/execution-location-runtime.js +130 -0
- package/src/lib/ide-context.js +94 -6
- package/src/lib/mcp-stdio-package-materialization.js +166 -14
- package/src/lib/packer/pkg-config-generator.js +2 -0
- package/src/lib/permission-authority.js +77 -0
- package/src/lib/permission-side-effect-center.js +495 -0
- package/src/lib/plugin-runtime/install.js +514 -13
- package/src/lib/plugin-runtime/marketplace-artifact-readback.js +900 -0
- package/src/lib/plugin-runtime/marketplace-catalog.js +1181 -0
- package/src/lib/plugin-runtime/marketplace-impact.js +408 -0
- package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1567 -0
- package/src/lib/plugin-security.js +60 -3
- package/src/lib/process-execution-broker/index.js +1095 -57
- package/src/lib/process-execution-broker/linux-bwrap-descriptor-launch.js +375 -0
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +90 -16
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +159 -18
- package/src/lib/process-execution-broker/macos-mcp-launcher-contract.js +278 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher-protocol.json +23 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher.c +1497 -0
- package/src/lib/process-execution-broker/mcp-fd-entry-bootstrap.js +37 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +2330 -279
- package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
- package/src/lib/process-execution-broker/windows-sandbox.cs +50 -4
- package/src/lib/public-ide-capability-manifest.js +3 -0
- package/src/lib/remote-approval-bridge.js +123 -17
- package/src/lib/remote-membership-authority.js +632 -0
- package/src/lib/remote-membership-coordinator.js +2409 -0
- package/src/lib/remote-membership-host-store.js +1750 -0
- package/src/lib/scheduler-kernel/agenda-adapter.js +17 -7
- package/src/lib/scheduler-kernel/automation-adapter.js +105 -11
- package/src/lib/scheduler-kernel/automation-event-adapter.js +87 -11
- package/src/lib/scheduler-kernel/contract.js +91 -0
- package/src/lib/scheduler-kernel/cowork-cron-adapter.js +12 -1
- package/src/lib/scheduler-kernel/loop-adapter.js +7 -1
- package/src/lib/scheduler-kernel/routine-adapter.js +7 -1
- package/src/lib/scheduler-kernel/runtime-control-capabilities.js +31 -0
- package/src/lib/scheduler-kernel/runtime.js +176 -3
- package/src/lib/scheduler-kernel/store.js +1253 -45
- package/src/lib/scoped-permission-store.js +369 -0
- package/src/lib/session-anti-rollback-anchor.js +262 -5
- package/src/lib/sub-agent-context.js +4 -0
- package/src/lib/workflow-definition-contract.js +149 -0
- package/src/repl/agent-repl.js +41 -7
- package/src/repl/chat-repl.js +2 -0
- package/src/repl/clipboard-image.js +735 -16
- package/src/repl/ide-status.js +1 -1
- package/src/repl/prompt-interactions.js +59 -6
- package/src/runtime/agent-core.js +93 -3
- package/src/runtime/headless-runner.js +30 -25
- package/src/runtime/headless-stream.js +32 -25
- package/src/runtime/mcp-config.js +81 -10
- package/src/workers/background-agent-keeper.js +498 -0
- package/src/workers/background-agent-worker.js +358 -13
- package/src/workers/background-turn-bootstrap.js +143 -0
- package/src/assets/web-panel/assets/MobileProjects-BWdOvQAj.js +0 -1
- package/src/assets/web-panel/assets/Projects-BV4oApsm.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DPty0mGN.js +0 -1
- package/src/assets/web-panel/assets/index-Bt7g0Vq_.js +0 -1
- package/src/assets/web-panel/assets/index-ChVT0p9u.js +0 -1
|
@@ -0,0 +1,1223 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import {
|
|
6
|
+
canonicalMergeReviewJson,
|
|
7
|
+
computeMergeReviewHunkDigest,
|
|
8
|
+
TEAM_MERGE_REVIEW_LIMITS,
|
|
9
|
+
} from "./team-merge-review.js";
|
|
10
|
+
import executionBroker from "../process-execution-broker/index.js";
|
|
11
|
+
import { redactSecrets } from "../secret-scan.js";
|
|
12
|
+
|
|
13
|
+
const OID_RE = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i;
|
|
14
|
+
const DIGEST_RE = /^sha256:[a-f0-9]{64}$/;
|
|
15
|
+
const MAX_BRANCH_CHARS = 255;
|
|
16
|
+
const MAX_PATH_CHARS = 4096;
|
|
17
|
+
const MAX_PATCH_BYTES = 8 * 1024 * 1024;
|
|
18
|
+
const MAX_TOTAL_PATCH_BYTES = 64 * 1024 * 1024;
|
|
19
|
+
const MAX_FILES = 4096;
|
|
20
|
+
const MAX_HUNKS = 16_384;
|
|
21
|
+
const MAX_CANDIDATES = 256;
|
|
22
|
+
const NULL_DEVICE = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
23
|
+
const ALLOWED_GIT_ENVIRONMENT = new Set([
|
|
24
|
+
"GIT_AUTHOR_DATE",
|
|
25
|
+
"GIT_AUTHOR_EMAIL",
|
|
26
|
+
"GIT_AUTHOR_NAME",
|
|
27
|
+
"GIT_COMMITTER_DATE",
|
|
28
|
+
"GIT_COMMITTER_EMAIL",
|
|
29
|
+
"GIT_COMMITTER_NAME",
|
|
30
|
+
"GIT_INDEX_FILE",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const STATUS_NAMES = Object.freeze({
|
|
34
|
+
A: "added",
|
|
35
|
+
C: "copied",
|
|
36
|
+
D: "deleted",
|
|
37
|
+
M: "modified",
|
|
38
|
+
R: "renamed",
|
|
39
|
+
T: "type_changed",
|
|
40
|
+
U: "unmerged",
|
|
41
|
+
X: "unmerged",
|
|
42
|
+
B: "unmerged",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const _deps = {
|
|
46
|
+
execFileSync: executionBroker.execFileSync.bind(executionBroker),
|
|
47
|
+
gitExecutable: null,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
let cachedGitExecutable = null;
|
|
51
|
+
|
|
52
|
+
function isPathWithin(child, root) {
|
|
53
|
+
const relative = path.relative(root, child);
|
|
54
|
+
return (
|
|
55
|
+
relative === "" ||
|
|
56
|
+
(!path.isAbsolute(relative) &&
|
|
57
|
+
relative !== ".." &&
|
|
58
|
+
!relative.startsWith(`..${path.sep}`))
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function validateGitExecutable(value, cwd) {
|
|
63
|
+
if (typeof value !== "string" || !path.isAbsolute(value)) return null;
|
|
64
|
+
let executable;
|
|
65
|
+
try {
|
|
66
|
+
executable = fs.realpathSync.native(value);
|
|
67
|
+
const entry = fs.statSync(executable);
|
|
68
|
+
if (!entry.isFile()) return null;
|
|
69
|
+
if (process.platform !== "win32" && (Number(entry.mode) & 0o111) === 0) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (isPathWithin(executable, path.resolve(cwd))) return null;
|
|
76
|
+
return executable;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveTrustedGitExecutable(cwd) {
|
|
80
|
+
const injected = validateGitExecutable(_deps.gitExecutable, cwd);
|
|
81
|
+
if (_deps.gitExecutable != null) {
|
|
82
|
+
if (!injected) throw new Error("configured Git executable is not trusted");
|
|
83
|
+
return injected;
|
|
84
|
+
}
|
|
85
|
+
const cached = validateGitExecutable(cachedGitExecutable, cwd);
|
|
86
|
+
if (cached) return cached;
|
|
87
|
+
const configured = validateGitExecutable(
|
|
88
|
+
process.env.CHAINLESSCHAIN_GIT_BIN,
|
|
89
|
+
cwd,
|
|
90
|
+
);
|
|
91
|
+
if (process.env.CHAINLESSCHAIN_GIT_BIN != null) {
|
|
92
|
+
if (!configured) {
|
|
93
|
+
throw new Error("CHAINLESSCHAIN_GIT_BIN must be a trusted absolute file");
|
|
94
|
+
}
|
|
95
|
+
cachedGitExecutable = configured;
|
|
96
|
+
return configured;
|
|
97
|
+
}
|
|
98
|
+
const executableName = process.platform === "win32" ? "git.exe" : "git";
|
|
99
|
+
for (const entry of String(process.env.PATH || "").split(path.delimiter)) {
|
|
100
|
+
if (!entry || !path.isAbsolute(entry)) continue;
|
|
101
|
+
const candidate = validateGitExecutable(
|
|
102
|
+
path.join(entry, executableName),
|
|
103
|
+
cwd,
|
|
104
|
+
);
|
|
105
|
+
if (candidate) {
|
|
106
|
+
cachedGitExecutable = candidate;
|
|
107
|
+
return candidate;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
throw new Error(
|
|
111
|
+
"trusted Git executable was not found on an absolute PATH entry",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function safeError(value, fallback = "git operation failed") {
|
|
116
|
+
const text = redactSecrets(String(value || "")).trim();
|
|
117
|
+
return (text || fallback).slice(0, 4096);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function hardenedGitEnvironment(overrides = {}) {
|
|
121
|
+
const environment = {};
|
|
122
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
123
|
+
if (!key.toUpperCase().startsWith("GIT_")) environment[key] = value;
|
|
124
|
+
}
|
|
125
|
+
Object.assign(environment, {
|
|
126
|
+
GIT_ATTR_NOSYSTEM: "1",
|
|
127
|
+
GIT_CONFIG_COUNT: "0",
|
|
128
|
+
GIT_CONFIG_GLOBAL: NULL_DEVICE,
|
|
129
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
130
|
+
GIT_EXTERNAL_DIFF: "",
|
|
131
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
132
|
+
GIT_PAGER: "",
|
|
133
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
134
|
+
LC_ALL: "C",
|
|
135
|
+
});
|
|
136
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
137
|
+
const normalized = key.toUpperCase();
|
|
138
|
+
if (
|
|
139
|
+
normalized.startsWith("GIT_") &&
|
|
140
|
+
!ALLOWED_GIT_ENVIRONMENT.has(normalized)
|
|
141
|
+
) {
|
|
142
|
+
throw new Error(`unsupported Git environment override: ${key}`);
|
|
143
|
+
}
|
|
144
|
+
environment[key] = value;
|
|
145
|
+
}
|
|
146
|
+
return environment;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function git(cwd, args, options = {}) {
|
|
150
|
+
const executable = resolveTrustedGitExecutable(cwd);
|
|
151
|
+
const hardenedArgs = [
|
|
152
|
+
"--no-pager",
|
|
153
|
+
"-c",
|
|
154
|
+
`core.hooksPath=${NULL_DEVICE}`,
|
|
155
|
+
"-c",
|
|
156
|
+
"commit.gpgSign=false",
|
|
157
|
+
"-c",
|
|
158
|
+
"core.fsmonitor=false",
|
|
159
|
+
"-c",
|
|
160
|
+
"core.untrackedCache=false",
|
|
161
|
+
"-c",
|
|
162
|
+
"diff.external=",
|
|
163
|
+
...args,
|
|
164
|
+
];
|
|
165
|
+
const environment = hardenedGitEnvironment(options.env);
|
|
166
|
+
try {
|
|
167
|
+
return _deps.execFileSync(executable, hardenedArgs, {
|
|
168
|
+
cwd,
|
|
169
|
+
encoding: "utf8",
|
|
170
|
+
stdio: [options.input == null ? "ignore" : "pipe", "pipe", "pipe"],
|
|
171
|
+
maxBuffer: MAX_PATCH_BYTES + 1024 * 1024,
|
|
172
|
+
...(options.input == null ? {} : { input: options.input }),
|
|
173
|
+
env: environment,
|
|
174
|
+
origin: options.origin || "team-merge-review:git",
|
|
175
|
+
policy: "allow",
|
|
176
|
+
scope: "team-merge-review",
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (options.allowFailure) {
|
|
180
|
+
return {
|
|
181
|
+
failed: true,
|
|
182
|
+
status: Number.isInteger(error?.status) ? error.status : 1,
|
|
183
|
+
stdout: String(error?.stdout || ""),
|
|
184
|
+
stderr: safeError(error?.stderr || error?.message),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const failure = new Error(safeError(error?.stderr || error?.message));
|
|
188
|
+
failure.code = "TEAM_MERGE_REVIEW_GIT_FAILED";
|
|
189
|
+
failure.cause = error;
|
|
190
|
+
throw failure;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function digest(value) {
|
|
195
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function assertOid(value, label) {
|
|
199
|
+
const normalized = String(value || "")
|
|
200
|
+
.trim()
|
|
201
|
+
.toLowerCase();
|
|
202
|
+
if (!OID_RE.test(normalized)) {
|
|
203
|
+
throw new TypeError(`${label} must be an exact Git commit/blob OID`);
|
|
204
|
+
}
|
|
205
|
+
return normalized;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function assertDigest(value, label) {
|
|
209
|
+
const normalized = String(value || "")
|
|
210
|
+
.trim()
|
|
211
|
+
.toLowerCase();
|
|
212
|
+
if (!DIGEST_RE.test(normalized)) {
|
|
213
|
+
throw new TypeError(`${label} must be a sha256 digest`);
|
|
214
|
+
}
|
|
215
|
+
return normalized;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function assertBranch(value, label = "branch") {
|
|
219
|
+
const branch = String(value || "").trim();
|
|
220
|
+
if (
|
|
221
|
+
!branch ||
|
|
222
|
+
branch.length > MAX_BRANCH_CHARS ||
|
|
223
|
+
branch.startsWith("-") ||
|
|
224
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*$/u.test(
|
|
225
|
+
branch,
|
|
226
|
+
) ||
|
|
227
|
+
branch.includes("..") ||
|
|
228
|
+
branch.endsWith(".lock") ||
|
|
229
|
+
branch.endsWith(".") ||
|
|
230
|
+
/[\0\r\n]/.test(branch)
|
|
231
|
+
) {
|
|
232
|
+
throw new TypeError(`${label} is unsafe`);
|
|
233
|
+
}
|
|
234
|
+
return branch;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function assertRepoPath(value) {
|
|
238
|
+
const filePath = String(value || "");
|
|
239
|
+
const segments = filePath.replaceAll("\\", "/").split("/");
|
|
240
|
+
if (
|
|
241
|
+
!filePath ||
|
|
242
|
+
filePath.length > MAX_PATH_CHARS ||
|
|
243
|
+
path.isAbsolute(filePath) ||
|
|
244
|
+
segments.includes("..") ||
|
|
245
|
+
/[\0\r\n]/.test(filePath)
|
|
246
|
+
) {
|
|
247
|
+
throw new TypeError("merge-review file path is unsafe");
|
|
248
|
+
}
|
|
249
|
+
return filePath.replaceAll("\\", "/");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseCheckAttrZ(value) {
|
|
253
|
+
const fields = String(value || "").split("\0");
|
|
254
|
+
if (fields.at(-1) === "") fields.pop();
|
|
255
|
+
if (fields.length % 3 !== 0) {
|
|
256
|
+
throw new Error("Git returned malformed attribute evidence");
|
|
257
|
+
}
|
|
258
|
+
const rows = [];
|
|
259
|
+
for (let index = 0; index < fields.length; index += 3) {
|
|
260
|
+
rows.push({
|
|
261
|
+
path: assertRepoPath(fields[index]),
|
|
262
|
+
attribute: fields[index + 1],
|
|
263
|
+
value: fields[index + 2],
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return rows;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function assertNoActiveWorktreeFilters(repoDir) {
|
|
270
|
+
const tracked = String(git(repoDir, ["ls-files", "-z"]));
|
|
271
|
+
if (!tracked) return;
|
|
272
|
+
const rows = parseCheckAttrZ(
|
|
273
|
+
git(repoDir, ["check-attr", "-z", "--stdin", "filter"], {
|
|
274
|
+
input: tracked,
|
|
275
|
+
origin: "team-merge-review:filter-audit",
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
const active = rows.find(
|
|
279
|
+
(row) => !["unspecified", "unset"].includes(row.value),
|
|
280
|
+
);
|
|
281
|
+
if (active) {
|
|
282
|
+
const error = new Error(
|
|
283
|
+
`merge-review refuses active Git filter '${active.value}' on ${active.path}`,
|
|
284
|
+
);
|
|
285
|
+
error.code = "TEAM_MERGE_REVIEW_GIT_FILTER_UNSAFE";
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function assertSafeSelectionAttributes(repoDir, filePaths) {
|
|
291
|
+
const paths = [...new Set((filePaths || []).map(assertRepoPath))];
|
|
292
|
+
if (paths.length === 0) return;
|
|
293
|
+
if (
|
|
294
|
+
paths.some((filePath) => path.posix.basename(filePath) === ".gitattributes")
|
|
295
|
+
) {
|
|
296
|
+
const error = new Error(
|
|
297
|
+
"merge-review requires .gitattributes changes to use a separate trusted workflow",
|
|
298
|
+
);
|
|
299
|
+
error.code = "TEAM_MERGE_REVIEW_GIT_ATTRIBUTES_UNSAFE";
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
const rows = parseCheckAttrZ(
|
|
303
|
+
git(
|
|
304
|
+
repoDir,
|
|
305
|
+
["check-attr", "-z", "filter", "diff", "merge", "--", ...paths],
|
|
306
|
+
{ origin: "team-merge-review:attribute-audit" },
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
const active = rows.find((row) => {
|
|
310
|
+
if (row.attribute === "filter") {
|
|
311
|
+
return !["unspecified", "unset"].includes(row.value);
|
|
312
|
+
}
|
|
313
|
+
return !["unspecified", "unset", "set", "text", "binary"].includes(
|
|
314
|
+
row.value,
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
if (active) {
|
|
318
|
+
const error = new Error(
|
|
319
|
+
`merge-review refuses executable Git ${active.attribute} driver '${active.value}' on ${active.path}`,
|
|
320
|
+
);
|
|
321
|
+
error.code = "TEAM_MERGE_REVIEW_GIT_DRIVER_UNSAFE";
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function worktreeIsDirty(repoDir) {
|
|
327
|
+
assertNoActiveWorktreeFilters(repoDir);
|
|
328
|
+
return (
|
|
329
|
+
String(
|
|
330
|
+
git(repoDir, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]),
|
|
331
|
+
).length > 0
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function resolveRepoRoot(repoDir) {
|
|
336
|
+
const requested = fs.realpathSync.native(path.resolve(repoDir));
|
|
337
|
+
const root = fs.realpathSync.native(
|
|
338
|
+
path.resolve(String(git(repoDir, ["rev-parse", "--show-toplevel"])).trim()),
|
|
339
|
+
);
|
|
340
|
+
if (!isPathWithin(requested, root)) {
|
|
341
|
+
throw new Error("Git repository root does not contain the requested path");
|
|
342
|
+
}
|
|
343
|
+
return root;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function resolveMergeReviewRepositoryRoot(repoDir = process.cwd()) {
|
|
347
|
+
return resolveRepoRoot(repoDir);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function resolveCurrentBranch(repoDir) {
|
|
351
|
+
const branch = String(
|
|
352
|
+
git(repoDir, ["symbolic-ref", "--quiet", "--short", "HEAD"]),
|
|
353
|
+
).trim();
|
|
354
|
+
return assertBranch(branch, "current branch");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function resolveBranchOid(repoDir, branch) {
|
|
358
|
+
const safeBranch = assertBranch(branch);
|
|
359
|
+
const oid = String(
|
|
360
|
+
git(repoDir, [
|
|
361
|
+
"show-ref",
|
|
362
|
+
"--verify",
|
|
363
|
+
"--hash",
|
|
364
|
+
`refs/heads/${safeBranch}`,
|
|
365
|
+
]),
|
|
366
|
+
).trim();
|
|
367
|
+
return assertOid(oid, `${safeBranch} OID`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function resolveBlobOid(repoDir, commitOid, filePath) {
|
|
371
|
+
const result = git(
|
|
372
|
+
repoDir,
|
|
373
|
+
["rev-parse", "--verify", `${commitOid}:${filePath}`],
|
|
374
|
+
{ allowFailure: true },
|
|
375
|
+
);
|
|
376
|
+
if (typeof result === "object" && result?.failed) return null;
|
|
377
|
+
return assertOid(String(result).trim(), `${filePath} blob OID`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function parseNameStatusZ(value) {
|
|
381
|
+
const fields = String(value || "").split("\0");
|
|
382
|
+
if (fields.at(-1) === "") fields.pop();
|
|
383
|
+
const records = [];
|
|
384
|
+
for (let index = 0; index < fields.length;) {
|
|
385
|
+
const statusCode = fields[index++];
|
|
386
|
+
if (!/^[ACDMRTUXB][0-9]{0,3}$/.test(statusCode || "")) {
|
|
387
|
+
throw new Error("Git returned malformed name-status data");
|
|
388
|
+
}
|
|
389
|
+
const kind = statusCode[0];
|
|
390
|
+
const firstPath = assertRepoPath(fields[index++]);
|
|
391
|
+
let oldPath = firstPath;
|
|
392
|
+
let newPath = firstPath;
|
|
393
|
+
if (kind === "R" || kind === "C") {
|
|
394
|
+
newPath = assertRepoPath(fields[index++]);
|
|
395
|
+
} else if (kind === "D") {
|
|
396
|
+
newPath = null;
|
|
397
|
+
} else if (kind === "A") {
|
|
398
|
+
oldPath = null;
|
|
399
|
+
}
|
|
400
|
+
records.push({
|
|
401
|
+
status: STATUS_NAMES[kind],
|
|
402
|
+
statusCode,
|
|
403
|
+
oldPath,
|
|
404
|
+
newPath,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
if (records.length > MAX_FILES) {
|
|
408
|
+
throw new Error(`merge review exceeds ${MAX_FILES} changed files`);
|
|
409
|
+
}
|
|
410
|
+
return records;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function diffArgs(baseOid, candidateOid, record) {
|
|
414
|
+
const paths = [record.oldPath, record.newPath].filter(Boolean);
|
|
415
|
+
return [
|
|
416
|
+
"diff",
|
|
417
|
+
"--binary",
|
|
418
|
+
"--full-index",
|
|
419
|
+
"--no-ext-diff",
|
|
420
|
+
"--no-textconv",
|
|
421
|
+
"--no-color",
|
|
422
|
+
baseOid,
|
|
423
|
+
candidateOid,
|
|
424
|
+
"--",
|
|
425
|
+
...new Set(paths),
|
|
426
|
+
];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function splitPatchHunks(patchText) {
|
|
430
|
+
const patch = String(patchText || "");
|
|
431
|
+
if (Buffer.byteLength(patch, "utf8") > MAX_PATCH_BYTES) {
|
|
432
|
+
throw new Error(`merge-review patch exceeds ${MAX_PATCH_BYTES} bytes`);
|
|
433
|
+
}
|
|
434
|
+
const matcher = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[^\n]*(?:\n|$)/gm;
|
|
435
|
+
const matches = [...patch.matchAll(matcher)];
|
|
436
|
+
if (matches.length === 0) return { preamble: patch, hunks: [] };
|
|
437
|
+
const preamble = patch.slice(0, matches[0].index);
|
|
438
|
+
const hunks = matches.map((match, index) => {
|
|
439
|
+
const start = match.index;
|
|
440
|
+
const end = matches[index + 1]?.index ?? patch.length;
|
|
441
|
+
const body = patch.slice(start, end);
|
|
442
|
+
return {
|
|
443
|
+
header: match[0].trimEnd(),
|
|
444
|
+
oldStart: Number(match[1]),
|
|
445
|
+
oldLines: match[2] == null ? 1 : Number(match[2]),
|
|
446
|
+
newStart: Number(match[3]),
|
|
447
|
+
newLines: match[4] == null ? 1 : Number(match[4]),
|
|
448
|
+
body,
|
|
449
|
+
bodyDigest: digest(body),
|
|
450
|
+
};
|
|
451
|
+
});
|
|
452
|
+
return { preamble, hunks };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function supportsHunkSelection(record, preamble) {
|
|
456
|
+
return (
|
|
457
|
+
record.status === "modified" &&
|
|
458
|
+
record.oldPath === record.newPath &&
|
|
459
|
+
!/^(?:old mode|new mode|new file mode|deleted file mode|similarity index|dissimilarity index|rename from|rename to|copy from|copy to)\b/mu.test(
|
|
460
|
+
preamble,
|
|
461
|
+
)
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function inspectCandidateFiles(repoDir, baseOid, candidate, budget) {
|
|
466
|
+
const statusText = git(repoDir, [
|
|
467
|
+
"diff",
|
|
468
|
+
"--name-status",
|
|
469
|
+
"-z",
|
|
470
|
+
"--find-renames",
|
|
471
|
+
"--no-ext-diff",
|
|
472
|
+
"--no-textconv",
|
|
473
|
+
baseOid,
|
|
474
|
+
candidate.oid,
|
|
475
|
+
"--",
|
|
476
|
+
]);
|
|
477
|
+
const records = parseNameStatusZ(statusText);
|
|
478
|
+
budget.files += records.length;
|
|
479
|
+
if (budget.files > MAX_FILES) {
|
|
480
|
+
throw new Error(`merge review exceeds ${MAX_FILES} changed files`);
|
|
481
|
+
}
|
|
482
|
+
return records.map((record) => {
|
|
483
|
+
const filePath = record.newPath || record.oldPath;
|
|
484
|
+
const patchText = String(
|
|
485
|
+
git(repoDir, diffArgs(baseOid, candidate.oid, record)),
|
|
486
|
+
);
|
|
487
|
+
const { preamble, hunks } = splitPatchHunks(patchText);
|
|
488
|
+
budget.patchBytes += Buffer.byteLength(patchText, "utf8");
|
|
489
|
+
if (budget.patchBytes > MAX_TOTAL_PATCH_BYTES) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`merge review exceeds ${MAX_TOTAL_PATCH_BYTES} total patch bytes`,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
const selectableHunks = supportsHunkSelection(record, preamble)
|
|
495
|
+
? hunks
|
|
496
|
+
: [];
|
|
497
|
+
budget.hunks += selectableHunks.length;
|
|
498
|
+
if (budget.hunks > MAX_HUNKS) {
|
|
499
|
+
throw new Error(`merge review exceeds ${MAX_HUNKS} diff hunks`);
|
|
500
|
+
}
|
|
501
|
+
const baseBlobOid = record.oldPath
|
|
502
|
+
? resolveBlobOid(repoDir, baseOid, record.oldPath)
|
|
503
|
+
: null;
|
|
504
|
+
const candidateBlobOid = record.newPath
|
|
505
|
+
? resolveBlobOid(repoDir, candidate.oid, record.newPath)
|
|
506
|
+
: null;
|
|
507
|
+
const binary =
|
|
508
|
+
hunks.length === 0 &&
|
|
509
|
+
(/^GIT binary patch$/m.test(patchText) ||
|
|
510
|
+
/^Binary files .* differ$/m.test(patchText));
|
|
511
|
+
return {
|
|
512
|
+
candidateKey: candidate.key,
|
|
513
|
+
path: filePath,
|
|
514
|
+
oldPath: record.oldPath,
|
|
515
|
+
newPath: record.newPath,
|
|
516
|
+
status: record.status,
|
|
517
|
+
statusCode: record.statusCode,
|
|
518
|
+
binary,
|
|
519
|
+
baseBlobOid,
|
|
520
|
+
candidateBlobOid,
|
|
521
|
+
patchDigest: digest(patchText),
|
|
522
|
+
selected: false,
|
|
523
|
+
hunks: selectableHunks.map((hunk) => ({
|
|
524
|
+
oldStart: hunk.oldStart,
|
|
525
|
+
oldLines: hunk.oldLines,
|
|
526
|
+
newStart: hunk.newStart,
|
|
527
|
+
newLines: hunk.newLines,
|
|
528
|
+
patch: hunk.body,
|
|
529
|
+
})),
|
|
530
|
+
_patch: patchText,
|
|
531
|
+
_preamble: preamble,
|
|
532
|
+
_hunkBodies: null,
|
|
533
|
+
};
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function inspectMergeReviewRepository({
|
|
538
|
+
repoDir = process.cwd(),
|
|
539
|
+
baseBranch = null,
|
|
540
|
+
branches,
|
|
541
|
+
keys = null,
|
|
542
|
+
} = {}) {
|
|
543
|
+
const root = resolveRepoRoot(repoDir);
|
|
544
|
+
const base = assertBranch(
|
|
545
|
+
baseBranch || resolveCurrentBranch(root),
|
|
546
|
+
"base branch",
|
|
547
|
+
);
|
|
548
|
+
const baseOid = resolveBranchOid(root, base);
|
|
549
|
+
const requested = Array.isArray(branches) ? branches : [];
|
|
550
|
+
if (requested.length === 0 || requested.length > MAX_CANDIDATES) {
|
|
551
|
+
throw new TypeError("at least one candidate branch is required");
|
|
552
|
+
}
|
|
553
|
+
const seen = new Set();
|
|
554
|
+
const seenKeys = new Set();
|
|
555
|
+
const candidates = requested.map((value, index) => {
|
|
556
|
+
const branch = assertBranch(value, `candidate branch ${index + 1}`);
|
|
557
|
+
if (branch === base || seen.has(branch)) {
|
|
558
|
+
throw new TypeError(
|
|
559
|
+
`candidate branch is duplicate or equals base: ${branch}`,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
seen.add(branch);
|
|
563
|
+
const key = String(
|
|
564
|
+
keys?.[index] ||
|
|
565
|
+
`candidate:${digest(branch).slice("sha256:".length, 23)}`,
|
|
566
|
+
).slice(0, 256);
|
|
567
|
+
if (
|
|
568
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(key) ||
|
|
569
|
+
seenKeys.has(key)
|
|
570
|
+
) {
|
|
571
|
+
throw new TypeError(`candidate key is unsafe or duplicate: ${key}`);
|
|
572
|
+
}
|
|
573
|
+
seenKeys.add(key);
|
|
574
|
+
return {
|
|
575
|
+
key,
|
|
576
|
+
branch,
|
|
577
|
+
oid: resolveBranchOid(root, branch),
|
|
578
|
+
};
|
|
579
|
+
});
|
|
580
|
+
const budget = { files: 0, hunks: 0, patchBytes: 0 };
|
|
581
|
+
const files = candidates.flatMap((candidate) =>
|
|
582
|
+
inspectCandidateFiles(root, baseOid, candidate, budget),
|
|
583
|
+
);
|
|
584
|
+
if (files.length === 0) {
|
|
585
|
+
throw new Error(
|
|
586
|
+
"candidate branches have no changes relative to the exact base",
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
repoRoot: root,
|
|
591
|
+
base: { branch: base, oid: baseOid },
|
|
592
|
+
candidates,
|
|
593
|
+
files,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function publicFile(file) {
|
|
598
|
+
const record = { ...file };
|
|
599
|
+
delete record._patch;
|
|
600
|
+
delete record._preamble;
|
|
601
|
+
delete record._hunkBodies;
|
|
602
|
+
return {
|
|
603
|
+
...record,
|
|
604
|
+
hunks: record.hunks.map(({ patch, ...hunk }) => ({
|
|
605
|
+
...hunk,
|
|
606
|
+
patchDigest: digest(patch),
|
|
607
|
+
})),
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export function publicInspection(inspection) {
|
|
612
|
+
return {
|
|
613
|
+
repoRoot: inspection.repoRoot,
|
|
614
|
+
base: { ...inspection.base },
|
|
615
|
+
candidates: inspection.candidates.map((candidate) => ({ ...candidate })),
|
|
616
|
+
files: inspection.files.map(publicFile),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** Convert the private Git inspection into the pure core's build input. */
|
|
621
|
+
export function mergeReviewInputFromInspection(inspection) {
|
|
622
|
+
if (!inspection?.base || !Array.isArray(inspection.candidates)) {
|
|
623
|
+
throw new TypeError("a merge-review inspection is required");
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
base: {
|
|
627
|
+
branch: inspection.base.branch,
|
|
628
|
+
commitOid: inspection.base.oid,
|
|
629
|
+
},
|
|
630
|
+
candidates: inspection.candidates.map((candidate) => ({
|
|
631
|
+
key: candidate.key,
|
|
632
|
+
branch: candidate.branch,
|
|
633
|
+
commitOid: candidate.oid,
|
|
634
|
+
})),
|
|
635
|
+
files: inspection.files.map((file) => ({
|
|
636
|
+
candidateKey: file.candidateKey,
|
|
637
|
+
path: file.path,
|
|
638
|
+
oldPath: file.oldPath,
|
|
639
|
+
status: file.status,
|
|
640
|
+
oldBlobOid: file.baseBlobOid,
|
|
641
|
+
newBlobOid: file.candidateBlobOid,
|
|
642
|
+
binary: file.binary,
|
|
643
|
+
patchDigest: file.patchDigest,
|
|
644
|
+
hunks: file.hunks.map((hunk) => ({
|
|
645
|
+
oldStart: hunk.oldStart,
|
|
646
|
+
oldLines: hunk.oldLines,
|
|
647
|
+
newStart: hunk.newStart,
|
|
648
|
+
newLines: hunk.newLines,
|
|
649
|
+
patch: hunk.patch,
|
|
650
|
+
})),
|
|
651
|
+
})),
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Bind core-issued stable IDs back to the private patches used by the staging
|
|
657
|
+
* transaction. This is deliberately exact: any plan drift fails before Git is
|
|
658
|
+
* mutated.
|
|
659
|
+
*/
|
|
660
|
+
export function bindMergeReviewInspection(inspection, review) {
|
|
661
|
+
const expected = mergeReviewInputFromInspection(inspection);
|
|
662
|
+
if (
|
|
663
|
+
canonicalMergeReviewJson(expected.base) !==
|
|
664
|
+
canonicalMergeReviewJson(review?.base) ||
|
|
665
|
+
canonicalMergeReviewJson(expected.candidates) !==
|
|
666
|
+
canonicalMergeReviewJson(review?.candidates)
|
|
667
|
+
) {
|
|
668
|
+
throw new Error("merge-review Git inspection no longer binds the review");
|
|
669
|
+
}
|
|
670
|
+
const reviewFiles = new Map(
|
|
671
|
+
review.files.map((file) => [`${file.candidateKey}\0${file.path}`, file]),
|
|
672
|
+
);
|
|
673
|
+
const files = inspection.files.map((file) => {
|
|
674
|
+
const reviewed = reviewFiles.get(`${file.candidateKey}\0${file.path}`);
|
|
675
|
+
if (!reviewed) {
|
|
676
|
+
throw new Error(`merge-review file evidence drifted: ${file.path}`);
|
|
677
|
+
}
|
|
678
|
+
const privateHunks = [...file.hunks].sort(
|
|
679
|
+
(left, right) =>
|
|
680
|
+
left.oldStart - right.oldStart || left.newStart - right.newStart,
|
|
681
|
+
);
|
|
682
|
+
if (privateHunks.length !== reviewed.hunks.length) {
|
|
683
|
+
throw new Error(`merge-review hunk evidence drifted: ${file.path}`);
|
|
684
|
+
}
|
|
685
|
+
const _hunkBodies = {};
|
|
686
|
+
const hunks = reviewed.hunks.map((hunk, index) => {
|
|
687
|
+
const source = privateHunks[index];
|
|
688
|
+
if (
|
|
689
|
+
source.oldStart !== hunk.oldStart ||
|
|
690
|
+
source.oldLines !== hunk.oldLines ||
|
|
691
|
+
source.newStart !== hunk.newStart ||
|
|
692
|
+
source.newLines !== hunk.newLines ||
|
|
693
|
+
computeMergeReviewHunkDigest(source.patch) !== hunk.digest
|
|
694
|
+
) {
|
|
695
|
+
throw new Error(`merge-review hunk ranges drifted: ${file.path}`);
|
|
696
|
+
}
|
|
697
|
+
_hunkBodies[hunk.id] = source.patch;
|
|
698
|
+
return { ...hunk };
|
|
699
|
+
});
|
|
700
|
+
return {
|
|
701
|
+
...file,
|
|
702
|
+
id: reviewed.id,
|
|
703
|
+
hunks,
|
|
704
|
+
_hunkBodies,
|
|
705
|
+
};
|
|
706
|
+
});
|
|
707
|
+
if (files.length !== review.files.length) {
|
|
708
|
+
throw new Error("merge-review file set drifted");
|
|
709
|
+
}
|
|
710
|
+
return { ...inspection, files };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function validateSelections(inspection, { fileIds = [], hunkIds = [] } = {}) {
|
|
714
|
+
const selectedFileIds = [...new Set(fileIds.map(String))];
|
|
715
|
+
const selectedHunkIds = [...new Set(hunkIds.map(String))];
|
|
716
|
+
if (
|
|
717
|
+
selectedFileIds.length !== fileIds.length ||
|
|
718
|
+
selectedHunkIds.length !== hunkIds.length
|
|
719
|
+
) {
|
|
720
|
+
throw new TypeError("merge-review selections must not contain duplicates");
|
|
721
|
+
}
|
|
722
|
+
if (
|
|
723
|
+
selectedFileIds.length + selectedHunkIds.length >
|
|
724
|
+
TEAM_MERGE_REVIEW_LIMITS.selectionIds
|
|
725
|
+
) {
|
|
726
|
+
throw new RangeError(
|
|
727
|
+
`merge-review selection exceeds ${TEAM_MERGE_REVIEW_LIMITS.selectionIds} IDs`,
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
const filesById = new Map(inspection.files.map((file) => [file.id, file]));
|
|
731
|
+
const hunksById = new Map();
|
|
732
|
+
for (const file of inspection.files) {
|
|
733
|
+
for (const hunk of file.hunks) hunksById.set(hunk.id, { file, hunk });
|
|
734
|
+
}
|
|
735
|
+
const selected = new Map();
|
|
736
|
+
for (const fileId of selectedFileIds) {
|
|
737
|
+
const file = filesById.get(fileId);
|
|
738
|
+
if (!file) throw new TypeError(`unknown merge-review file id: ${fileId}`);
|
|
739
|
+
selected.set(file.id, { file, wholeFile: true, hunkIds: [] });
|
|
740
|
+
}
|
|
741
|
+
for (const hunkId of selectedHunkIds) {
|
|
742
|
+
const binding = hunksById.get(hunkId);
|
|
743
|
+
if (!binding)
|
|
744
|
+
throw new TypeError(`unknown merge-review hunk id: ${hunkId}`);
|
|
745
|
+
const current = selected.get(binding.file.id);
|
|
746
|
+
if (current?.wholeFile) {
|
|
747
|
+
throw new TypeError(
|
|
748
|
+
`cannot select both a whole file and its hunk: ${binding.file.path}`,
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
const entry = current || {
|
|
752
|
+
file: binding.file,
|
|
753
|
+
wholeFile: false,
|
|
754
|
+
hunkIds: [],
|
|
755
|
+
};
|
|
756
|
+
entry.hunkIds.push(hunkId);
|
|
757
|
+
selected.set(binding.file.id, entry);
|
|
758
|
+
}
|
|
759
|
+
if (selected.size === 0) {
|
|
760
|
+
throw new TypeError("at least one file or hunk selection is required");
|
|
761
|
+
}
|
|
762
|
+
for (const entry of selected.values()) {
|
|
763
|
+
if (
|
|
764
|
+
!entry.wholeFile &&
|
|
765
|
+
(entry.file.binary || entry.file.hunks.length === 0)
|
|
766
|
+
) {
|
|
767
|
+
throw new TypeError(
|
|
768
|
+
`file requires whole-file selection: ${entry.file.path}`,
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return { selected, fileIds: selectedFileIds, hunkIds: selectedHunkIds };
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function patchForSelection(entry) {
|
|
776
|
+
if (entry.wholeFile) return entry.file._patch;
|
|
777
|
+
const bodies = entry.hunkIds.map((id) => entry.file._hunkBodies[id]);
|
|
778
|
+
if (bodies.some((body) => typeof body !== "string")) {
|
|
779
|
+
throw new Error(
|
|
780
|
+
`selected hunk evidence is unavailable: ${entry.file.path}`,
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
return `${entry.file._preamble}${bodies.join("")}`;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function collectConflictPaths(repoDir, env) {
|
|
787
|
+
const result = git(
|
|
788
|
+
repoDir,
|
|
789
|
+
[
|
|
790
|
+
"diff",
|
|
791
|
+
"--cached",
|
|
792
|
+
"--name-only",
|
|
793
|
+
"--diff-filter=U",
|
|
794
|
+
"--no-ext-diff",
|
|
795
|
+
"--no-textconv",
|
|
796
|
+
"-z",
|
|
797
|
+
],
|
|
798
|
+
{ allowFailure: true, env },
|
|
799
|
+
);
|
|
800
|
+
const output = typeof result === "string" ? result : result.stdout || "";
|
|
801
|
+
return output.split("\0").filter(Boolean).map(assertRepoPath);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function removeTemporaryIndex(indexPath) {
|
|
805
|
+
if (!indexPath) return;
|
|
806
|
+
for (const target of [indexPath, `${indexPath}.lock`]) {
|
|
807
|
+
try {
|
|
808
|
+
fs.rmSync(target, { force: true });
|
|
809
|
+
} catch {
|
|
810
|
+
// A retained result ref preserves recovery authority even if a temporary
|
|
811
|
+
// index cleanup is delayed by a host filesystem lock.
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function assertBaseReady(repoRoot, expected) {
|
|
817
|
+
const currentBranch = resolveCurrentBranch(repoRoot);
|
|
818
|
+
const currentOid = resolveBranchOid(repoRoot, expected.branch);
|
|
819
|
+
if (currentBranch !== expected.branch || currentOid !== expected.oid) {
|
|
820
|
+
const error = new Error("base branch or OID changed after merge review");
|
|
821
|
+
error.code = "TEAM_MERGE_REVIEW_BASE_STALE";
|
|
822
|
+
throw error;
|
|
823
|
+
}
|
|
824
|
+
if (worktreeIsDirty(repoRoot)) {
|
|
825
|
+
const error = new Error(
|
|
826
|
+
"base worktree must be clean before reviewed publish",
|
|
827
|
+
);
|
|
828
|
+
error.code = "TEAM_MERGE_REVIEW_BASE_DIRTY";
|
|
829
|
+
throw error;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export function readMergeReviewBaseState({
|
|
834
|
+
repoDir = process.cwd(),
|
|
835
|
+
branch,
|
|
836
|
+
} = {}) {
|
|
837
|
+
const root = resolveRepoRoot(repoDir);
|
|
838
|
+
const safeBranch = assertBranch(branch, "base branch");
|
|
839
|
+
const currentBranch = resolveCurrentBranch(root);
|
|
840
|
+
const oid = resolveBranchOid(root, safeBranch);
|
|
841
|
+
const dirty = worktreeIsDirty(root);
|
|
842
|
+
return { repoRoot: root, branch: safeBranch, currentBranch, oid, dirty };
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
export function prepareMergeReviewTransaction({
|
|
846
|
+
inspection,
|
|
847
|
+
fileIds = [],
|
|
848
|
+
hunkIds = [],
|
|
849
|
+
reviewId,
|
|
850
|
+
stateDir,
|
|
851
|
+
actor = "cli-operator",
|
|
852
|
+
reason = "reviewed multi-agent merge",
|
|
853
|
+
decidedAt = null,
|
|
854
|
+
} = {}) {
|
|
855
|
+
if (!inspection?.repoRoot || !inspection?.base || !inspection?.candidates) {
|
|
856
|
+
throw new TypeError("a verified merge-review inspection is required");
|
|
857
|
+
}
|
|
858
|
+
const selections = validateSelections(inspection, { fileIds, hunkIds });
|
|
859
|
+
const root = resolveRepoRoot(inspection.repoRoot);
|
|
860
|
+
if (root !== path.resolve(inspection.repoRoot)) {
|
|
861
|
+
throw new Error("merge-review repository root changed");
|
|
862
|
+
}
|
|
863
|
+
if (resolveBranchOid(root, inspection.base.branch) !== inspection.base.oid) {
|
|
864
|
+
throw new Error("base OID changed before merge-review preparation");
|
|
865
|
+
}
|
|
866
|
+
for (const candidate of inspection.candidates) {
|
|
867
|
+
if (resolveBranchOid(root, candidate.branch) !== candidate.oid) {
|
|
868
|
+
throw new Error(`candidate branch changed: ${candidate.branch}`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
assertSafeSelectionAttributes(
|
|
872
|
+
root,
|
|
873
|
+
[...selections.selected.values()].map((entry) => entry.file.path),
|
|
874
|
+
);
|
|
875
|
+
const safeReviewId = String(reviewId || "");
|
|
876
|
+
if (!/^tmr_[a-f0-9]{32}$/.test(safeReviewId)) {
|
|
877
|
+
throw new TypeError("reviewId is invalid");
|
|
878
|
+
}
|
|
879
|
+
const stagingRoot = path.resolve(stateDir || os.tmpdir(), "staging");
|
|
880
|
+
fs.mkdirSync(stagingRoot, { recursive: true, mode: 0o700 });
|
|
881
|
+
const indexPath = path.join(
|
|
882
|
+
stagingRoot,
|
|
883
|
+
`${safeReviewId}-${process.pid}-${randomUUID()}.index`,
|
|
884
|
+
);
|
|
885
|
+
const indexEnv = { GIT_INDEX_FILE: indexPath };
|
|
886
|
+
git(root, ["read-tree", inspection.base.oid], {
|
|
887
|
+
env: indexEnv,
|
|
888
|
+
origin: "team-merge-review:prepare-index",
|
|
889
|
+
});
|
|
890
|
+
try {
|
|
891
|
+
const conflicts = [];
|
|
892
|
+
for (const candidate of inspection.candidates) {
|
|
893
|
+
const entries = [...selections.selected.values()].filter(
|
|
894
|
+
(entry) => entry.file.candidateKey === candidate.key,
|
|
895
|
+
);
|
|
896
|
+
for (const entry of entries) {
|
|
897
|
+
const patchText = patchForSelection(entry);
|
|
898
|
+
const result = git(
|
|
899
|
+
root,
|
|
900
|
+
["apply", "--cached", "--3way", "--whitespace=nowarn", "-"],
|
|
901
|
+
{
|
|
902
|
+
input: patchText,
|
|
903
|
+
env: indexEnv,
|
|
904
|
+
allowFailure: true,
|
|
905
|
+
origin: "team-merge-review:apply-selection",
|
|
906
|
+
},
|
|
907
|
+
);
|
|
908
|
+
if (typeof result === "object" && result.failed) {
|
|
909
|
+
const paths = collectConflictPaths(root, indexEnv);
|
|
910
|
+
conflicts.push({
|
|
911
|
+
candidateKey: candidate.key,
|
|
912
|
+
path: entry.file.path,
|
|
913
|
+
type: paths.length > 0 ? "content_conflict" : "patch_rejected",
|
|
914
|
+
explanation: safeError(
|
|
915
|
+
result.stderr,
|
|
916
|
+
"selected change could not be applied",
|
|
917
|
+
),
|
|
918
|
+
suggestion:
|
|
919
|
+
"Refresh the review against the latest exact branch OIDs and inspect overlapping hunks.",
|
|
920
|
+
hunkIds: [...entry.hunkIds],
|
|
921
|
+
relatedPaths: paths,
|
|
922
|
+
});
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (conflicts.length > 0) break;
|
|
927
|
+
}
|
|
928
|
+
if (conflicts.length > 0) {
|
|
929
|
+
return {
|
|
930
|
+
success: false,
|
|
931
|
+
state: "conflicted",
|
|
932
|
+
conflicts,
|
|
933
|
+
selection: {
|
|
934
|
+
fileIds: selections.fileIds,
|
|
935
|
+
hunkIds: selections.hunkIds,
|
|
936
|
+
},
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
const treeStatus = git(
|
|
940
|
+
root,
|
|
941
|
+
[
|
|
942
|
+
"diff",
|
|
943
|
+
"--cached",
|
|
944
|
+
"--name-only",
|
|
945
|
+
"--no-ext-diff",
|
|
946
|
+
"--no-textconv",
|
|
947
|
+
"-z",
|
|
948
|
+
inspection.base.oid,
|
|
949
|
+
"--",
|
|
950
|
+
],
|
|
951
|
+
{ env: indexEnv },
|
|
952
|
+
);
|
|
953
|
+
if (!String(treeStatus).replaceAll("\0", "")) {
|
|
954
|
+
throw new Error("reviewed selection produced no staged changes");
|
|
955
|
+
}
|
|
956
|
+
const identity = {
|
|
957
|
+
GIT_AUTHOR_NAME: "ChainlessChain Merge Review",
|
|
958
|
+
GIT_AUTHOR_EMAIL: "merge-review@chainlesschain.local",
|
|
959
|
+
GIT_COMMITTER_NAME: "ChainlessChain Merge Review",
|
|
960
|
+
GIT_COMMITTER_EMAIL: "merge-review@chainlesschain.local",
|
|
961
|
+
...(decidedAt
|
|
962
|
+
? {
|
|
963
|
+
GIT_AUTHOR_DATE: new Date(decidedAt).toISOString(),
|
|
964
|
+
GIT_COMMITTER_DATE: new Date(decidedAt).toISOString(),
|
|
965
|
+
}
|
|
966
|
+
: {}),
|
|
967
|
+
};
|
|
968
|
+
const treeOid = assertOid(
|
|
969
|
+
String(
|
|
970
|
+
git(root, ["write-tree"], { env: { ...identity, ...indexEnv } }),
|
|
971
|
+
).trim(),
|
|
972
|
+
"reviewed result tree OID",
|
|
973
|
+
);
|
|
974
|
+
const resultOid = assertOid(
|
|
975
|
+
String(
|
|
976
|
+
git(root, ["commit-tree", treeOid, "-p", inspection.base.oid], {
|
|
977
|
+
env: identity,
|
|
978
|
+
input:
|
|
979
|
+
`Merge reviewed agent work (${safeReviewId})\n\n` +
|
|
980
|
+
`Actor: ${String(actor).slice(0, 256)}\n` +
|
|
981
|
+
`Reason: ${String(reason).slice(0, 1024)}\n`,
|
|
982
|
+
origin: "team-merge-review:commit-tree",
|
|
983
|
+
}),
|
|
984
|
+
).trim(),
|
|
985
|
+
"reviewed result OID",
|
|
986
|
+
);
|
|
987
|
+
const parentOid = assertOid(
|
|
988
|
+
String(git(root, ["rev-parse", `${resultOid}^`])).trim(),
|
|
989
|
+
"reviewed result parent OID",
|
|
990
|
+
);
|
|
991
|
+
if (parentOid !== inspection.base.oid) {
|
|
992
|
+
throw new Error(
|
|
993
|
+
"reviewed result is not a direct child of the exact base",
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
const resultRef = `refs/chainlesschain/merge-review/${safeReviewId}/result`;
|
|
997
|
+
const existingRef = git(root, ["rev-parse", "--verify", resultRef], {
|
|
998
|
+
allowFailure: true,
|
|
999
|
+
origin: "team-merge-review:retain-result-check",
|
|
1000
|
+
});
|
|
1001
|
+
if (typeof existingRef === "string") {
|
|
1002
|
+
if (assertOid(existingRef.trim(), "retained result OID") !== resultOid) {
|
|
1003
|
+
throw new Error(
|
|
1004
|
+
"merge-review retained result ref changed unexpectedly",
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
} else {
|
|
1008
|
+
git(
|
|
1009
|
+
root,
|
|
1010
|
+
["update-ref", resultRef, resultOid, "0".repeat(resultOid.length)],
|
|
1011
|
+
{
|
|
1012
|
+
origin: "team-merge-review:retain-result",
|
|
1013
|
+
},
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
success: true,
|
|
1018
|
+
state: "prepared",
|
|
1019
|
+
resultOid,
|
|
1020
|
+
resultRef,
|
|
1021
|
+
selection: {
|
|
1022
|
+
fileIds: selections.fileIds,
|
|
1023
|
+
hunkIds: selections.hunkIds,
|
|
1024
|
+
},
|
|
1025
|
+
};
|
|
1026
|
+
} finally {
|
|
1027
|
+
removeTemporaryIndex(indexPath);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
export function publishPreparedMergeReview({ inspection, prepared } = {}) {
|
|
1032
|
+
if (!prepared?.success || prepared.state !== "prepared") {
|
|
1033
|
+
throw new TypeError("a prepared merge-review result is required");
|
|
1034
|
+
}
|
|
1035
|
+
const root = resolveRepoRoot(inspection.repoRoot);
|
|
1036
|
+
assertBaseReady(root, inspection.base);
|
|
1037
|
+
const resultOid = assertOid(prepared.resultOid, "prepared result OID");
|
|
1038
|
+
const parentOid = assertOid(
|
|
1039
|
+
String(git(root, ["rev-parse", `${resultOid}^`])).trim(),
|
|
1040
|
+
"prepared result parent OID",
|
|
1041
|
+
);
|
|
1042
|
+
if (parentOid !== inspection.base.oid) {
|
|
1043
|
+
throw new Error(
|
|
1044
|
+
"prepared result no longer descends directly from the base",
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
const mergeResult = git(root, ["merge", "--ff-only", resultOid], {
|
|
1048
|
+
allowFailure: true,
|
|
1049
|
+
origin: "team-merge-review:publish",
|
|
1050
|
+
});
|
|
1051
|
+
const finalState = readMergeReviewBaseState({
|
|
1052
|
+
repoDir: root,
|
|
1053
|
+
branch: inspection.base.branch,
|
|
1054
|
+
});
|
|
1055
|
+
if (
|
|
1056
|
+
finalState.currentBranch === inspection.base.branch &&
|
|
1057
|
+
finalState.oid === resultOid &&
|
|
1058
|
+
finalState.dirty === false
|
|
1059
|
+
) {
|
|
1060
|
+
return {
|
|
1061
|
+
success: true,
|
|
1062
|
+
state: "published",
|
|
1063
|
+
baseOid: inspection.base.oid,
|
|
1064
|
+
resultOid,
|
|
1065
|
+
ambiguous: typeof mergeResult === "object" && mergeResult.failed,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
const error = new Error(
|
|
1069
|
+
typeof mergeResult === "object"
|
|
1070
|
+
? safeError(mergeResult.stderr, "reviewed publish failed")
|
|
1071
|
+
: "reviewed publish did not advance the exact base",
|
|
1072
|
+
);
|
|
1073
|
+
error.code =
|
|
1074
|
+
finalState.oid === inspection.base.oid
|
|
1075
|
+
? "TEAM_MERGE_REVIEW_PUBLISH_FAILED"
|
|
1076
|
+
: "TEAM_MERGE_REVIEW_PUBLISH_AMBIGUOUS";
|
|
1077
|
+
error.details = {
|
|
1078
|
+
expectedBaseOid: inspection.base.oid,
|
|
1079
|
+
currentOid: finalState.oid,
|
|
1080
|
+
resultOid,
|
|
1081
|
+
};
|
|
1082
|
+
throw error;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
export function rollbackPublishedMergeReview({
|
|
1086
|
+
repoDir,
|
|
1087
|
+
base,
|
|
1088
|
+
resultOid,
|
|
1089
|
+
reviewId,
|
|
1090
|
+
decidedAt,
|
|
1091
|
+
actor = "cli-operator",
|
|
1092
|
+
reason = "controlled merge-review rollback",
|
|
1093
|
+
evidenceDigest,
|
|
1094
|
+
expectedEvidenceDigest,
|
|
1095
|
+
} = {}) {
|
|
1096
|
+
assertDigest(evidenceDigest, "rollback evidence digest");
|
|
1097
|
+
if (
|
|
1098
|
+
evidenceDigest !==
|
|
1099
|
+
assertDigest(expectedEvidenceDigest, "expected evidence digest")
|
|
1100
|
+
) {
|
|
1101
|
+
throw new Error("rollback evidence digest is stale");
|
|
1102
|
+
}
|
|
1103
|
+
const root = resolveRepoRoot(repoDir);
|
|
1104
|
+
const expectedBase = {
|
|
1105
|
+
branch: assertBranch(base?.branch, "rollback base branch"),
|
|
1106
|
+
oid: assertOid(base?.oid, "rollback base OID"),
|
|
1107
|
+
};
|
|
1108
|
+
const publishedOid = assertOid(resultOid, "published result OID");
|
|
1109
|
+
const safeReviewId = String(reviewId || "");
|
|
1110
|
+
if (!/^tmr_[a-f0-9]{32}$/u.test(safeReviewId)) {
|
|
1111
|
+
throw new TypeError("reviewId is invalid");
|
|
1112
|
+
}
|
|
1113
|
+
const rollbackAt = new Date(decidedAt);
|
|
1114
|
+
if (!Number.isFinite(rollbackAt.getTime())) {
|
|
1115
|
+
throw new TypeError("rollback decidedAt is invalid");
|
|
1116
|
+
}
|
|
1117
|
+
const resultParent = assertOid(
|
|
1118
|
+
String(git(root, ["rev-parse", `${publishedOid}^`])).trim(),
|
|
1119
|
+
"published result parent OID",
|
|
1120
|
+
);
|
|
1121
|
+
if (resultParent !== expectedBase.oid) {
|
|
1122
|
+
const error = new Error(
|
|
1123
|
+
"controlled rollback refused because the published result is not bound to the exact base",
|
|
1124
|
+
);
|
|
1125
|
+
error.code = "TEAM_MERGE_REVIEW_ROLLBACK_STALE";
|
|
1126
|
+
throw error;
|
|
1127
|
+
}
|
|
1128
|
+
const baseTree = assertOid(
|
|
1129
|
+
String(git(root, ["rev-parse", `${expectedBase.oid}^{tree}`])).trim(),
|
|
1130
|
+
"rollback base tree OID",
|
|
1131
|
+
);
|
|
1132
|
+
const identity = {
|
|
1133
|
+
GIT_AUTHOR_NAME: "ChainlessChain Merge Review",
|
|
1134
|
+
GIT_AUTHOR_EMAIL: "merge-review@chainlesschain.local",
|
|
1135
|
+
GIT_COMMITTER_NAME: "ChainlessChain Merge Review",
|
|
1136
|
+
GIT_COMMITTER_EMAIL: "merge-review@chainlesschain.local",
|
|
1137
|
+
GIT_AUTHOR_DATE: rollbackAt.toISOString(),
|
|
1138
|
+
GIT_COMMITTER_DATE: rollbackAt.toISOString(),
|
|
1139
|
+
};
|
|
1140
|
+
const rollbackOid = assertOid(
|
|
1141
|
+
String(
|
|
1142
|
+
git(root, ["commit-tree", baseTree, "-p", publishedOid], {
|
|
1143
|
+
env: identity,
|
|
1144
|
+
input:
|
|
1145
|
+
`Rollback reviewed agent work (${safeReviewId})\n\n` +
|
|
1146
|
+
`Actor: ${String(actor).slice(0, 256)}\n` +
|
|
1147
|
+
`Reason: ${String(reason).slice(0, 1024)}\n`,
|
|
1148
|
+
origin: "team-merge-review:rollback-commit-tree",
|
|
1149
|
+
}),
|
|
1150
|
+
).trim(),
|
|
1151
|
+
"rollback result OID",
|
|
1152
|
+
);
|
|
1153
|
+
const rollbackRef = `refs/chainlesschain/merge-review/${safeReviewId}/rollback`;
|
|
1154
|
+
const existingRef = git(root, ["rev-parse", "--verify", rollbackRef], {
|
|
1155
|
+
allowFailure: true,
|
|
1156
|
+
origin: "team-merge-review:retain-rollback-check",
|
|
1157
|
+
});
|
|
1158
|
+
if (typeof existingRef === "string") {
|
|
1159
|
+
if (
|
|
1160
|
+
assertOid(existingRef.trim(), "retained rollback OID") !== rollbackOid
|
|
1161
|
+
) {
|
|
1162
|
+
throw new Error(
|
|
1163
|
+
"merge-review retained rollback ref changed unexpectedly",
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
} else {
|
|
1167
|
+
git(
|
|
1168
|
+
root,
|
|
1169
|
+
["update-ref", rollbackRef, rollbackOid, "0".repeat(rollbackOid.length)],
|
|
1170
|
+
{ origin: "team-merge-review:retain-rollback" },
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
const before = readMergeReviewBaseState({
|
|
1174
|
+
repoDir: root,
|
|
1175
|
+
branch: expectedBase.branch,
|
|
1176
|
+
});
|
|
1177
|
+
if (
|
|
1178
|
+
before.currentBranch !== expectedBase.branch ||
|
|
1179
|
+
![publishedOid, rollbackOid].includes(before.oid)
|
|
1180
|
+
) {
|
|
1181
|
+
const error = new Error(
|
|
1182
|
+
"controlled rollback refused because the base branch advanced or changed",
|
|
1183
|
+
);
|
|
1184
|
+
error.code = "TEAM_MERGE_REVIEW_ROLLBACK_STALE";
|
|
1185
|
+
throw error;
|
|
1186
|
+
}
|
|
1187
|
+
if (before.dirty) {
|
|
1188
|
+
const error = new Error(
|
|
1189
|
+
"controlled rollback requires a clean base worktree",
|
|
1190
|
+
);
|
|
1191
|
+
error.code = "TEAM_MERGE_REVIEW_ROLLBACK_DIRTY";
|
|
1192
|
+
throw error;
|
|
1193
|
+
}
|
|
1194
|
+
const rollbackResult = git(root, ["merge", "--ff-only", rollbackOid], {
|
|
1195
|
+
allowFailure: true,
|
|
1196
|
+
origin: "team-merge-review:rollback",
|
|
1197
|
+
});
|
|
1198
|
+
const finalState = readMergeReviewBaseState({
|
|
1199
|
+
repoDir: root,
|
|
1200
|
+
branch: expectedBase.branch,
|
|
1201
|
+
});
|
|
1202
|
+
if (
|
|
1203
|
+
finalState.currentBranch !== expectedBase.branch ||
|
|
1204
|
+
finalState.oid !== rollbackOid ||
|
|
1205
|
+
finalState.dirty
|
|
1206
|
+
) {
|
|
1207
|
+
const error = new Error(
|
|
1208
|
+
typeof rollbackResult === "object"
|
|
1209
|
+
? safeError(rollbackResult.stderr, "controlled rollback failed")
|
|
1210
|
+
: "controlled rollback did not publish the exact clean rollback commit",
|
|
1211
|
+
);
|
|
1212
|
+
error.code = "TEAM_MERGE_REVIEW_ROLLBACK_FAILED";
|
|
1213
|
+
throw error;
|
|
1214
|
+
}
|
|
1215
|
+
return {
|
|
1216
|
+
success: true,
|
|
1217
|
+
state: "rolled_back",
|
|
1218
|
+
baseOid: expectedBase.oid,
|
|
1219
|
+
resultOid: publishedOid,
|
|
1220
|
+
rollbackOid,
|
|
1221
|
+
rollbackRef,
|
|
1222
|
+
};
|
|
1223
|
+
}
|