chainlesschain 0.162.188 → 0.162.193
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/README.md +61 -11
- package/bin/chainlesschain.js +7 -52
- package/completions/_cc +209 -0
- package/completions/cc.bash +18 -0
- package/completions/cc.fish +13 -0
- package/completions/cc.ps1 +226 -0
- package/package.json +10 -2
- package/src/assets/web-panel/assets/{AIOps-Brr1LVcE.js → AIOps-DBLkaTje.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DQU74DKG.js → ActionButton-BAy-Qebo.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CYp9178y.js → Analytics-Dfb1Mlau.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DW4SCYsW.js → AppLayout-BbFkN8m0.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CYlD_ed1.js → Artifacts-kM4-cEEH.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CWl1rZOi.js → Audit-BSQfYWf3.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BOR-LPsa.js → BackgroundAgents-CWDbI_fG.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DLpG7j1X.js → Backup-BxLPbq_Q.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-B35K9wdr.js → BaseInput-DuIbdrp6.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CwUJb9gB.js → Chat-NEmjGahU.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-1v6aMS1m.js → ChatBubbleRenderer-DfJEo-5_.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-pi3-tPNW.js → Checkbox-CaoeG3Ns.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-FyNF35eL.js → Codegen-BFf_g3xY.js} +1 -1
- package/src/assets/web-panel/assets/{Col-tGt_JDEr.js → Col-CNOd8mdQ.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CC7LonQX.js → Community-BhpDqz-i.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BGXrHHjE.js → Compact-CQ42T7Z6.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DSW0TBZ8.js → Compliance-DI5PcLMN.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Bhk7rbvE.js → Cowork-wYYZe0wR.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DxyiQ2NZ.js → Cron-Dlq03DD_.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-B2ZkxHok.js → Crosschain-BzqS8PP3.js} +1 -1
- package/src/assets/web-panel/assets/{DID-yOJV99eJ.js → DID-It55Yem7.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CKsttz7D.js → Dashboard-9HkH6oxn.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CVFldXSL.js → Dropdown-00e2YWRB.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Bufv638J.js → EmailListRenderer-By-ugRDL.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-4ncjoIjL.js → FamilyGuardDashboard-C396yu5m.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DsknEDoX.js → Federation-BME5sbZY.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Cqe5jWXA.js → FormItemContext-B4TEZCr-.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-NAJaFKxc.js → GenericCardRenderer-_9CX9ieY.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CszKQpnl.js → Git-BrS6WVSr.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BXGodXi6.js → Governance-BMdCWnKF.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CkoYuG2c.js → Inference-Bcp3vzcA.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DFlEr_T0.js → KnowledgeGraph-DlLuqGY5.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CVaENddA.js → Logs-D-W_6x09.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-DElGPxCZ.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-CDlLT0fw.js → Marketplace-Dzn2guvI.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DdwOyszN.js → McpTools-uPict-KL.js} +6 -6
- package/src/assets/web-panel/assets/{Memory-DKQnkKuG.js → Memory-BCw43kOb.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DGseqlzJ.js → MobileBridge-DmV8Wv7F.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-COBbEOlj.js → MobileProjects-C-HaCmSM.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-O61Vwyli.js → Mtc-C6jxJ34Y.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-Dw2SyasS.js → MtcAudit-Za8ExwhQ.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-hSu_l8kf.js → Multisig-KKDbuEgb.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CX1Epurr.js → NLProgramming-Dj97zbCk.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-R9xX8o8_.js → Notes-DBXvMraU.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-jtSNLCuK.js → NotificationSettings-DOxITLYl.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-7DVjZwMM.js +1 -0
- package/src/assets/web-panel/assets/{Organization-Pu2WO-U3.js → Organization-D1i73mjb.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BjnqUyFp.js → Overflow-kW6Q3rvg.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-jjkvz44c.js → P2P-C-v_8p2v.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-tUbbY64W.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-D4yDpivd.js → Permissions-DkHxEZ58.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-0qsL6iH1.js → PersonalDataHub-DWuMPGrK.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-Cx7Ybrk2.js → Pipeline-30IYXWWm.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-PbfR_M_V.js → Privacy-DZNQ4RrH.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CyV6Gsub.js → ProjectInit-De2MCElf.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-C7KZ2Wdk.js → ProjectSettings-5uHk9wsC.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-DQDjO-yK.js → Projects-XjDfrFLb.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DyFVBBMX.js → Providers-Bgwj6gcE.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CzB1BC9_.js → QrScannerModal-gKql9umv.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-Bk636mC1.js → QuickAsk-BWvmwkh7.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DoWxHIT3.js → Recommend-Uok4hZPd.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-oOVIv9wq.js → RemoteSession-DLJzTk9a.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-BucoReGG.js → Reputation-ZVwIPMSO.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DzmOdJU7.js → Row-CN04HxWR.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-C1tK7KGU.js → RssFeed-BdSrEG4g.js} +3 -3
- package/src/assets/web-panel/assets/{Search-CSnInOqk.js → Search-5PwVd0IR.js} +1 -1
- package/src/assets/web-panel/assets/{Security-dlejr15l.js → Security-DdBo4zjN.js} +3 -3
- package/src/assets/web-panel/assets/{Services-BioPZuNh.js → Services-D3letU7M.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DWMibwpC.js → Skeleton--Jb83f1v.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BAAnx1B2.js → Skills-Bg-CulHX.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-jWNp4hNO.js → Sla-7QOa7S-X.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-y5kqKha-.js → SpeechSettings-DQwppjRw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-B9GNsc3e.js → SyncSettings-dNPQGL-s.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BDBYKRen.js → Tasks-Ggs-Z48q.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DRiE5eaN.js → Templates-78TYHgRO.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BGlAvExF.js → Tenant-xHwwv9vO.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-B8uzrbNY.js → Terminal-BJXSJfm4.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-Df48iveY.js → TimelineRenderer-fLbjRhJw.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CcmHndN6.js → Tokens-oMSBSBmB.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DPkWxmQ2.js → Trigger-CfnY_wX_.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DTb5a5AV.js → Trust-AdyTgYKO.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CBmg5vch.js → UkeySign-C5K_81dS.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B0mZrz4O.js → VideoEditing-vcGVDqki.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Bxs7_mpd.js → Wallet-hhOc3hlY.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-C8x3cMxU.js → WebAuthn-CVloC11s.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-DPrNznrP.js → WorkflowEditor-CYeE32R5.js} +1 -1
- package/src/assets/web-panel/assets/{chat-D3bYWu7S.js → chat-CNROR5bN.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DY-PwMu4.js → colors-Cjx-cnwU.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-LDk10Kw6.js → compact-item-DOIfelvD.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-C-2MZnFq.js → createContext-DfA7UZ8u.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-ChcCBGs7.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BQIBjcnB.js → hasIn-BMSCzcfM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQVb2dWp.js → index-5-HXgOX5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BdXnYMCZ.js → index-AWg-0M1S.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cx6D_dx2.js → index-B4iYzpkL.js} +1 -1
- package/src/assets/web-panel/assets/{index-_HRO_cIu.js → index-B5Udeb_6.js} +1 -1
- package/src/assets/web-panel/assets/{index-DlRYqt3S.js → index-B7gf77CJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BrnRO9yE.js → index-BEMwBBHk.js} +1 -1
- package/src/assets/web-panel/assets/{index-VCh5zNJo.js → index-BM1y_9hM.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRI3ngNg.js → index-BUonmKSE.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3GNvExG.js → index-BbQ2eQ8H.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSvo_bp0.js → index-BemJ2u_E.js} +1 -1
- package/src/assets/web-panel/assets/{index-L6qixyLc.js → index-BolaOQxx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BI4KgcYC.js → index-BxOk9vkG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CY4KnaO9.js → index-By61ojUx.js} +1 -1
- package/src/assets/web-panel/assets/{index-L3smcoQX.js → index-C2K9SNVX.js} +1 -1
- package/src/assets/web-panel/assets/{index-BX2b9Orc.js → index-C3L5gdiD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cz4b6Z_7.js → index-C7xn0Nub.js} +1 -1
- package/src/assets/web-panel/assets/{index-COG61sRv.js → index-CAnvhwg6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7FC8cP2.js → index-CEKMcOyU.js} +1 -1
- package/src/assets/web-panel/assets/{index-BN-xdSWZ.js → index-CNC5onm4.js} +1 -1
- package/src/assets/web-panel/assets/{index-DY2FIrmz.js → index-CVL27kXC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiTvBAr-.js → index-CVm1_i3t.js} +1 -1
- package/src/assets/web-panel/assets/{index-DhRt9cYJ.js → index-CdYBye8B.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5pgtZnY.js → index-ClNMrqEY.js} +1 -1
- package/src/assets/web-panel/assets/{index-7xKu7Fns.js → index-Cs6l7uRu.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4K1weKc.js → index-CshQbilz.js} +1 -1
- package/src/assets/web-panel/assets/{index-k7C_oDCO.js → index-Cyweh0Hy.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3Oq-DLL.js → index-DBpWXFed.js} +3 -3
- package/src/assets/web-panel/assets/{index-DYyLJPlF.js → index-DHtpHZAQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dh-tkpKQ.js → index-DLmn0BoK.js} +1 -1
- package/src/assets/web-panel/assets/{index-8JC9BJa1.js → index-DPAWBrMf.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dd2Km97U.js → index-DjEsxO5h.js} +1 -1
- package/src/assets/web-panel/assets/{index-KFZO_lcx.js → index-FBc_CJBd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTIZpPtG.js → index-Icdoqmzt.js} +1 -1
- package/src/assets/web-panel/assets/index-Q2IajL54.js +1 -0
- package/src/assets/web-panel/assets/index-bRkHgOMc.js +1 -0
- package/src/assets/web-panel/assets/{index-lmm9p-jm.js → index-dCd8cSUq.js} +1 -1
- package/src/assets/web-panel/assets/{index-HasPt4-h.js → index-m2vG9gG_.js} +1 -1
- package/src/assets/web-panel/assets/{index-CaslYch6.js → index-mTpHoEzr.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbR6ROOa.js → index-ti_lnTWl.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-zQPSHf2C.js → initDefaultProps-Bu8k7H1Y.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CKCStGVk.js → motion-s3iVK41k.js} +1 -1
- package/src/assets/web-panel/assets/{move-BufVDGYF.js → move-B6B-E5Jz.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DcaFxy5D.js → mtc-parser-BiYgTidA.js} +1 -1
- package/src/assets/web-panel/assets/{omit-EScpkGdo.js → omit-pU1RdUDA.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-72hG_qfT.js → pickAttrs-Cdoq_SeM.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CMFWiH20.js → placementArrow-DIuvAn2K.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DKQHS41p.js → responsiveObserve-8w16YR2D.js} +1 -1
- package/src/assets/web-panel/assets/{slide-QVAFHTIQ.js → slide-VZ01cZe3.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DIz38e2G.js → statusUtils-lh8YMN8Z.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-zK5cyLby.js → styleChecker-CmDyreec.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DhZngk62.js → useFlexGapSupport-hiJWMU5y.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BvV2cvkg.js → useFs-Thnu-Iaj.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CRmxb9l3.js → usePersonalDataHub-LBdOxjwY.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CgQIbobP.js → vnode-Cg6lFY_X.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-D4XrrO0R.js → zoom-DVfsR1YO.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/cli-help.js +181 -0
- package/src/command-help-index.json +181 -0
- package/src/command-manifest.json +1107 -175
- package/src/command-surface-policy.js +197 -0
- package/src/commands/agent.js +98 -33
- package/src/commands/artifacts.js +400 -2
- package/src/commands/background-session.js +41 -3
- package/src/commands/checkpoint-managed.js +1059 -0
- package/src/commands/checkpoint-restore-recovery.js +1476 -0
- package/src/commands/checkpoint.js +950 -22
- package/src/commands/compact.js +101 -25
- package/src/commands/config.js +506 -20
- package/src/commands/doctor.js +4 -1
- package/src/commands/hub.js +1 -1
- package/src/commands/mcp.js +688 -24
- package/src/commands/pack.js +16 -2
- package/src/commands/plugin.js +20 -12
- package/src/commands/session-mcp-recovery.js +218 -0
- package/src/commands/session.js +470 -110
- package/src/commands/setup.js +36 -10
- package/src/commands/skill.js +181 -110
- package/src/commands/status.js +21 -5
- package/src/commands/team-distributed.js +3324 -0
- package/src/commands/team.js +1130 -74
- package/src/commands/update.js +68 -6
- package/src/constants.js +11 -0
- package/src/data/changelog.json +26 -0
- package/src/gateways/ws/action-protocol.js +10 -0
- package/src/gateways/ws/session-protocol.js +555 -27
- package/src/gateways/ws/ws-agent-handler.js +541 -14
- package/src/gateways/ws/ws-session-gateway.js +129 -22
- package/src/gateways/ws/ws-session-state.js +431 -0
- package/src/harness/background-task-manager.js +233 -36
- package/src/harness/jsonl-session-store.js +2551 -167
- package/src/harness/mcp-client.js +802 -61
- package/src/harness/prompt-compressor.js +227 -27
- package/src/harness/session-index.js +89 -38
- package/src/harness/session-list-index.js +254 -0
- package/src/harness/structured-handoff.js +410 -0
- package/src/lazy-dispatch.js +747 -84
- package/src/lib/__tests__/logger.test.js +50 -2
- package/src/lib/advisor-runtime.js +1047 -0
- package/src/lib/agent-core.js +2 -0
- package/src/lib/agent-sandbox.js +120 -26
- package/src/lib/agent-session-export.js +20 -0
- package/src/lib/agent-team/task-lease.js +271 -3
- package/src/lib/agent-team/team-adjudication.js +1501 -0
- package/src/lib/agent-team/team-budget.js +4 -1
- package/src/lib/agent-team/team-control-store.js +582 -0
- package/src/lib/agent-team/team-distributed-queue.js +5246 -0
- package/src/lib/agent-team/team-process-checkpoint.js +211 -0
- package/src/lib/agent-team/team-runner.js +543 -24
- package/src/lib/agent-team/team-worktree.js +1403 -95
- package/src/lib/artifact-store.js +131 -8
- package/src/lib/background-agent-supervisor.js +185 -13
- package/src/lib/background-interaction-journal.js +37 -9
- package/src/lib/background-launch-profile.js +1058 -0
- package/src/lib/checkpoint-restore-already-completed-controller.js +902 -0
- package/src/lib/checkpoint-restore-orchestrator.js +987 -0
- package/src/lib/checkpoint-restore-partial-rollback-controller.js +1953 -0
- package/src/lib/checkpoint-restore-recovery-controller.js +689 -0
- package/src/lib/checkpoint-restore-recovery.js +884 -0
- package/src/lib/checkpoint-restore-retention.js +126 -0
- package/src/lib/checkpoint-restore-saga.js +5079 -0
- package/src/lib/checkpoint-restore-session-recovery.js +1178 -0
- package/src/lib/checkpoint-restore-workspace-target-verifier.js +270 -0
- package/src/lib/checkpoint-store.js +1688 -97
- package/src/lib/checkpoint-timeline-authority.js +522 -0
- package/src/lib/checkpoint-timeline.js +642 -0
- package/src/lib/config-keys.js +18 -119
- package/src/lib/config-manager.js +446 -27
- package/src/lib/config-redaction.js +57 -0
- package/src/lib/config-schema.js +853 -0
- package/src/lib/cowork-mcp-tools.js +5 -0
- package/src/lib/cowork-task-runner.js +458 -63
- package/src/lib/delivery-coordinator.js +1234 -0
- package/src/lib/delivery-evidence.js +450 -0
- package/src/lib/doctor-checkup.js +199 -14
- package/src/lib/executable-path.js +34 -0
- package/src/lib/fatal-handler.js +16 -0
- package/src/lib/file-checkpoint.js +4909 -186
- package/src/lib/file-lines.js +125 -0
- package/src/lib/headless-config-command.js +16 -5
- package/src/lib/hook-runner.cjs +139 -38
- package/src/lib/hooks-v2-workspace-context.js +10 -0
- package/src/lib/impacted-gate-selector.js +424 -0
- package/src/lib/interaction-adapter.js +62 -0
- package/src/lib/jsonl-session-store.js +1 -35
- package/src/lib/logger.js +66 -14
- package/src/lib/managed-checkpoint-render.js +60 -0
- package/src/lib/managed-tool-checkpoint.js +194 -0
- package/src/lib/mcp-call-ledger-store.js +1069 -0
- package/src/lib/mcp-call-ledger.js +747 -0
- package/src/lib/mcp-client.js +5 -0
- package/src/lib/mcp-conflict-scheduler.js +468 -0
- package/src/lib/mcp-effect-contract.js +102 -0
- package/src/lib/mcp-headers-helper-trust.js +145 -0
- package/src/lib/mcp-headers-helper.js +616 -0
- package/src/lib/mcp-host-recovery-runtime.js +131 -0
- package/src/lib/mcp-ledger-recovery-admission.js +1065 -0
- package/src/lib/mcp-recovery-adjudication.js +443 -0
- package/src/lib/output-context.js +86 -0
- package/src/lib/packer/file-identity.js +170 -0
- package/src/lib/packer/index.js +3 -0
- package/src/lib/packer/native-update-state.js +277 -0
- package/src/lib/packer/pack-update-applier.js +1983 -136
- package/src/lib/packer/pack-update-checker.js +34 -0
- package/src/lib/packer/pack-update-downloader.js +875 -86
- package/src/lib/packer/pack-update-signature.js +86 -0
- package/src/lib/packer/pkg-config-generator.js +6 -0
- package/src/lib/paths.js +65 -10
- package/src/lib/plan-mode.js +831 -14
- package/src/lib/plan-persistence.js +540 -0
- package/src/lib/plugin-runtime/hooks.js +227 -69
- package/src/lib/plugin-runtime/plugin-options.js +189 -44
- package/src/lib/pr-automation-policy.js +146 -8
- package/src/lib/process-execution-broker/credential-agent.js +7 -0
- package/src/lib/process-execution-broker/index.js +491 -13
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +28 -1
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +28 -4
- package/src/lib/process-execution-broker/platform-sandbox.js +92 -70
- 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 +284 -19
- package/src/lib/process-execution-broker/workspace-transaction.js +4459 -0
- package/src/lib/project-instructions.js +165 -29
- package/src/lib/project-mcp-trust.js +67 -0
- package/src/lib/public-ide-capability-manifest.js +272 -0
- package/src/lib/public-ide-capability-validation.js +351 -0
- package/src/lib/secret-store.js +267 -43
- package/src/lib/secure-file-identity.js +433 -0
- package/src/lib/secure-fs.js +1089 -0
- package/src/lib/security-audit.js +58 -0
- package/src/lib/service-manager.js +34 -13
- package/src/lib/session-budget-runtime.js +1452 -0
- package/src/lib/session-host-snapshot.js +388 -0
- package/src/lib/session-message-provenance.js +429 -0
- package/src/lib/session-projection.js +804 -0
- package/src/lib/session-resource-budget.js +1495 -0
- package/src/lib/settings-hooks.cjs +138 -22
- package/src/lib/settings-loader.cjs +25 -2
- package/src/lib/side-effect-ledger-store.js +45 -23
- package/src/lib/skill-execution-identity.js +217 -0
- package/src/lib/skill-loader.js +299 -18
- package/src/lib/sub-agent-context.js +236 -28
- package/src/lib/sub-agent-registry.js +5 -0
- package/src/lib/subagent-contract.js +94 -0
- package/src/lib/todo-manager.js +185 -18
- package/src/lib/todo-persistence.js +417 -0
- package/src/lib/turn-binding-store.js +41 -23
- package/src/lib/turn-binding.js +160 -2
- package/src/lib/with-file-lock.js +165 -24
- package/src/lib/ws-chat-handler.js +21 -0
- package/src/program-base.js +18 -0
- package/src/repl/advisor-command.js +103 -0
- package/src/repl/agent-repl.js +2175 -146
- package/src/repl/btw-command.js +93 -10
- package/src/repl/clipboard-image.js +119 -0
- package/src/repl/config-summary.js +64 -108
- package/src/repl/prompt-editor.js +233 -0
- package/src/repl/prompt-interactions.js +344 -0
- package/src/repl/prompt-stash.js +354 -0
- package/src/repl/prompt-suggestions.js +312 -0
- package/src/repl/repl-keybindings.js +227 -0
- package/src/repl/session-recap.js +217 -0
- package/src/repl/slash-command-registry.js +22 -0
- package/src/repl/terminal-layout.js +88 -0
- package/src/runtime/agent-core.js +1630 -447
- package/src/runtime/coding-agent-contract-shared.cjs +13 -1
- package/src/runtime/coding-agent-managed-tool-policy.cjs +7 -2
- package/src/runtime/diagnostics.js +32 -13
- package/src/runtime/headless-runner.js +310 -118
- package/src/runtime/headless-stream.js +356 -95
- package/src/runtime/mcp-config.js +148 -14
- package/src/runtime/policies/agent-policy.js +14 -0
- package/src/runtime/status-diagnostics-lite.js +137 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-7iWGwZ-r.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BU2Yqq5H.js +0 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-69ca_WeR.js +0 -7
- package/src/assets/web-panel/assets/devWarning-B4dXIJqD.js +0 -1
- package/src/assets/web-panel/assets/index-C-C7DaG6.js +0 -1
- package/src/assets/web-panel/assets/index-DkfWzWtd.js +0 -1
|
@@ -18,15 +18,94 @@
|
|
|
18
18
|
* is. When it is not, callers should fall back / report unavailable.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { realpathSync, rmSync } from "node:fs";
|
|
22
23
|
import path from "node:path";
|
|
23
24
|
import executionBroker from "./process-execution-broker/index.js";
|
|
25
|
+
import { credentialAgent } from "./process-execution-broker/credential-agent.js";
|
|
26
|
+
import { computeCheckpointRestoreDigest } from "./checkpoint-restore-orchestrator.js";
|
|
27
|
+
import {
|
|
28
|
+
checkpointRestoreRetentionUnverifiedError,
|
|
29
|
+
withCheckpointRestoreRetention,
|
|
30
|
+
} from "./checkpoint-restore-retention.js";
|
|
24
31
|
|
|
25
32
|
export const _deps = {
|
|
26
33
|
spawnSync: (...args) => executionBroker.spawnSync(...args),
|
|
27
34
|
};
|
|
28
35
|
|
|
29
36
|
const REF_NS = "refs/cc-checkpoints";
|
|
37
|
+
// Publishing a checkpoint is an optimistic ref transaction. A busy session can
|
|
38
|
+
// move between the id/tip scan and the transaction, so retry from a fresh tip
|
|
39
|
+
// and rebuild the shadow commit. Keep the bound finite: a permanently locked
|
|
40
|
+
// or externally churned namespace must fail instead of spinning forever.
|
|
41
|
+
const MAX_REF_PUBLISH_ATTEMPTS = 16;
|
|
42
|
+
const MAX_CHECKPOINT_TIP_REWIND_COMMITS = 10_001;
|
|
43
|
+
const WORKSPACE_BINDING_SCHEMA = "cc-checkpoint-workspace-binding/v1";
|
|
44
|
+
const WORKSPACE_BINDING_KEYS = Object.freeze([
|
|
45
|
+
"engine",
|
|
46
|
+
"prestateIdentity",
|
|
47
|
+
"schema",
|
|
48
|
+
"scopeIdentity",
|
|
49
|
+
"targetPoststateIdentity",
|
|
50
|
+
"version",
|
|
51
|
+
"workspaceRoot",
|
|
52
|
+
"writePlanIdentity",
|
|
53
|
+
]);
|
|
54
|
+
export const CHECKPOINT_ROLLBACK_PLAN_SCHEMA =
|
|
55
|
+
"chainlesschain.checkpoint-rollback-plan";
|
|
56
|
+
export const CHECKPOINT_ROLLBACK_PLAN_VERSION = 1;
|
|
57
|
+
export const CHECKPOINT_ROLLBACK_RESULT_SCHEMA =
|
|
58
|
+
"chainlesschain.checkpoint-rollback-result";
|
|
59
|
+
export const CHECKPOINT_ROLLBACK_RESULT_VERSION = 1;
|
|
60
|
+
export const CHECKPOINT_ROLLBACK_ERROR_CODES = Object.freeze({
|
|
61
|
+
INVALID_ARGUMENT: "CHECKPOINT_ROLLBACK_INVALID_ARGUMENT",
|
|
62
|
+
AUTHORITY_STALE: "CHECKPOINT_ROLLBACK_AUTHORITY_STALE",
|
|
63
|
+
UNSAFE_WORKSPACE: "CHECKPOINT_ROLLBACK_UNSAFE_WORKSPACE",
|
|
64
|
+
PLAN_STALE: "CHECKPOINT_ROLLBACK_PLAN_STALE",
|
|
65
|
+
INVALID_RESULT: "CHECKPOINT_ROLLBACK_INVALID_RESULT",
|
|
66
|
+
INCOMPLETE: "CHECKPOINT_ROLLBACK_INCOMPLETE",
|
|
67
|
+
});
|
|
68
|
+
const CHECKPOINT_ROLLBACK_PLAN_KEYS = Object.freeze([
|
|
69
|
+
"checkpointNamespace",
|
|
70
|
+
"currentRollbackPaths",
|
|
71
|
+
"engine",
|
|
72
|
+
"expectedRollbackStateDigest",
|
|
73
|
+
"expectedWorkspaceBinding",
|
|
74
|
+
"originalBindingVerification",
|
|
75
|
+
"originalCheckpoint",
|
|
76
|
+
"originalMutationPaths",
|
|
77
|
+
"originalMutationTargetCount",
|
|
78
|
+
"originalPlanAuthority",
|
|
79
|
+
"originalWorkspaceBinding",
|
|
80
|
+
"rollbackPlanIdentity",
|
|
81
|
+
"rollbackPrestateDigest",
|
|
82
|
+
"safetyCheckpoint",
|
|
83
|
+
"schema",
|
|
84
|
+
"targetCount",
|
|
85
|
+
"version",
|
|
86
|
+
"workspaceRoot",
|
|
87
|
+
]);
|
|
88
|
+
const ORIGINAL_CHECKPOINT_AUTHORITY_KEYS = Object.freeze([
|
|
89
|
+
"id",
|
|
90
|
+
"identity",
|
|
91
|
+
"treeIdentity",
|
|
92
|
+
]);
|
|
93
|
+
const SAFETY_CHECKPOINT_AUTHORITY_KEYS = Object.freeze([
|
|
94
|
+
"id",
|
|
95
|
+
"identity",
|
|
96
|
+
"planIdentity",
|
|
97
|
+
"treeIdentity",
|
|
98
|
+
]);
|
|
99
|
+
const ORIGINAL_PLAN_AUTHORITY_KEYS = Object.freeze([
|
|
100
|
+
"bindingReconstructable",
|
|
101
|
+
"mutationSetIdentity",
|
|
102
|
+
"safetyPlanIdentity",
|
|
103
|
+
"sourceCheckpointId",
|
|
104
|
+
"sourceCheckpointIdentity",
|
|
105
|
+
]);
|
|
106
|
+
const GIT_CHECKPOINT_IDENTITY_PATTERN = /^git:(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
107
|
+
const GIT_TREE_IDENTITY_PATTERN = /^git-tree:(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
108
|
+
const SHA256_IDENTITY_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
30
109
|
// Deterministic identity for shadow commits so `git commit-tree` never trips on
|
|
31
110
|
// a missing user.name / user.email config.
|
|
32
111
|
const CHECKPOINT_IDENTITY = Object.freeze({
|
|
@@ -35,6 +114,64 @@ const CHECKPOINT_IDENTITY = Object.freeze({
|
|
|
35
114
|
GIT_COMMITTER_NAME: "cc-checkpoint",
|
|
36
115
|
GIT_COMMITTER_EMAIL: "checkpoint@chainlesschain.local",
|
|
37
116
|
});
|
|
117
|
+
const INTERNAL_GIT_ENV_KEYS = new Set([
|
|
118
|
+
"GIT_INDEX_FILE",
|
|
119
|
+
"GIT_AUTHOR_NAME",
|
|
120
|
+
"GIT_AUTHOR_EMAIL",
|
|
121
|
+
"GIT_COMMITTER_NAME",
|
|
122
|
+
"GIT_COMMITTER_EMAIL",
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
function checkpointRetentionOptions(opts = {}) {
|
|
126
|
+
const redirectsAuthority =
|
|
127
|
+
opts.retentionStateDir != null ||
|
|
128
|
+
opts.retentionWorkspaceLockOptions != null ||
|
|
129
|
+
opts.retentionSagaStoreOptionsForTests != null;
|
|
130
|
+
if (
|
|
131
|
+
redirectsAuthority &&
|
|
132
|
+
(process.env.NODE_ENV !== "test" || opts.retentionAllowTestRuntime !== true)
|
|
133
|
+
) {
|
|
134
|
+
throw checkpointRestoreRetentionUnverifiedError(
|
|
135
|
+
null,
|
|
136
|
+
"Git checkpoint retention authority redirection is test-only",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
...(redirectsAuthority
|
|
141
|
+
? { allowTestRuntime: opts.retentionAllowTestRuntime === true }
|
|
142
|
+
: {}),
|
|
143
|
+
...(opts.retentionStateDir != null
|
|
144
|
+
? { stateDir: opts.retentionStateDir }
|
|
145
|
+
: {}),
|
|
146
|
+
...(opts.retentionWorkspaceLockOptions != null
|
|
147
|
+
? { workspaceLockOptions: opts.retentionWorkspaceLockOptions }
|
|
148
|
+
: {}),
|
|
149
|
+
...(opts.retentionSagaStoreOptionsForTests != null
|
|
150
|
+
? {
|
|
151
|
+
sagaStoreOptionsForTests: opts.retentionSagaStoreOptionsForTests,
|
|
152
|
+
}
|
|
153
|
+
: {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function checkpointGitEnvironment(overrides = null) {
|
|
158
|
+
const environment = {};
|
|
159
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
160
|
+
if (
|
|
161
|
+
!credentialAgent.isSensitiveKey(key) &&
|
|
162
|
+
!credentialAgent.isReservedAgentKey(key)
|
|
163
|
+
) {
|
|
164
|
+
environment[key] = value;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const [key, value] of Object.entries(overrides || {})) {
|
|
168
|
+
if (!INTERNAL_GIT_ENV_KEYS.has(key)) {
|
|
169
|
+
throw new Error(`Unsupported checkpoint git environment key: ${key}`);
|
|
170
|
+
}
|
|
171
|
+
environment[key] = value;
|
|
172
|
+
}
|
|
173
|
+
return environment;
|
|
174
|
+
}
|
|
38
175
|
|
|
39
176
|
/**
|
|
40
177
|
* Run git with an argv array (no shell → no quoting hazards). UTF-8 in/out.
|
|
@@ -44,7 +181,7 @@ const CHECKPOINT_IDENTITY = Object.freeze({
|
|
|
44
181
|
* @returns {string} trimmed stdout
|
|
45
182
|
* @throws {Error} with git's stderr when the command fails
|
|
46
183
|
*/
|
|
47
|
-
function git(args, { cwd, env, input } = {}) {
|
|
184
|
+
function git(args, { cwd, env, input, rawOutput = false } = {}) {
|
|
48
185
|
const res = _deps.spawnSync("git", args, {
|
|
49
186
|
origin: "checkpoint:git",
|
|
50
187
|
scope: "checkpoint",
|
|
@@ -55,14 +192,18 @@ function git(args, { cwd, env, input } = {}) {
|
|
|
55
192
|
encoding: "utf-8",
|
|
56
193
|
windowsHide: true,
|
|
57
194
|
maxBuffer: 128 * 1024 * 1024,
|
|
58
|
-
|
|
195
|
+
// Local plumbing never needs provider/API credentials. Passing the whole
|
|
196
|
+
// parent environment would mint short-lived credential references on every
|
|
197
|
+
// git subprocess and can exhaust the Broker store during a long restore.
|
|
198
|
+
env: checkpointGitEnvironment(env),
|
|
59
199
|
});
|
|
60
200
|
if (res.error) throw res.error;
|
|
61
201
|
if (res.status !== 0) {
|
|
62
202
|
const msg = (res.stderr || res.stdout || "").toString().trim();
|
|
63
203
|
throw new Error(msg || `git ${args.join(" ")} failed (exit ${res.status})`);
|
|
64
204
|
}
|
|
65
|
-
|
|
205
|
+
const stdout = (res.stdout || "").toString();
|
|
206
|
+
return rawOutput ? stdout : stdout.trim();
|
|
66
207
|
}
|
|
67
208
|
|
|
68
209
|
/** Best-effort: is this a usable git work tree (and is git on PATH)? */
|
|
@@ -84,6 +225,269 @@ function gitDir(root) {
|
|
|
84
225
|
return path.resolve(root, git(["rev-parse", "--git-dir"], { cwd: root }));
|
|
85
226
|
}
|
|
86
227
|
|
|
228
|
+
function canonicalPath(target) {
|
|
229
|
+
return path.normalize(realpathSync.native(path.resolve(target)));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function pathIdentity(target) {
|
|
233
|
+
const canonical = canonicalPath(target);
|
|
234
|
+
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function sha256Identity(value) {
|
|
238
|
+
return `sha256:${createHash("sha256").update(value, "utf8").digest("hex")}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function isPlainObject(value) {
|
|
242
|
+
return (
|
|
243
|
+
value !== null &&
|
|
244
|
+
typeof value === "object" &&
|
|
245
|
+
!Array.isArray(value) &&
|
|
246
|
+
Object.getPrototypeOf(value) === Object.prototype
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function hasExactKeys(value, expectedKeys) {
|
|
251
|
+
if (!isPlainObject(value)) return false;
|
|
252
|
+
const actual = Object.keys(value).sort();
|
|
253
|
+
const expected = [...expectedKeys].sort();
|
|
254
|
+
return (
|
|
255
|
+
actual.length === expected.length &&
|
|
256
|
+
actual.every((key, index) => key === expected[index])
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function checkpointRollbackError(code, message, details = {}, cause = null) {
|
|
261
|
+
const error = new Error(message, cause ? { cause } : undefined);
|
|
262
|
+
error.code = code;
|
|
263
|
+
Object.assign(error, details);
|
|
264
|
+
return error;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function checkpointRollbackDigest(domain, engine, binding, stateIdentity) {
|
|
268
|
+
return computeCheckpointRestoreDigest(domain, {
|
|
269
|
+
engine,
|
|
270
|
+
scopeIdentity: binding.scopeIdentity,
|
|
271
|
+
stateIdentity,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function freezeWorkspaceBinding(binding) {
|
|
276
|
+
return Object.freeze({ ...binding });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function freezeCheckpointAuthority(authority) {
|
|
280
|
+
return Object.freeze({ ...authority });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function commonGitDir(root) {
|
|
284
|
+
return path.resolve(
|
|
285
|
+
root,
|
|
286
|
+
git(["rev-parse", "--git-common-dir"], { cwd: root }),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function buildWorkspaceBinding(
|
|
291
|
+
root,
|
|
292
|
+
dir,
|
|
293
|
+
{ targetCommit, targetTree, currentTree },
|
|
294
|
+
) {
|
|
295
|
+
const workspaceRoot = canonicalPath(root);
|
|
296
|
+
const scopeIdentity = sha256Identity(
|
|
297
|
+
JSON.stringify({
|
|
298
|
+
schema: "cc-checkpoint-git-scope/v1",
|
|
299
|
+
workspaceRoot: pathIdentity(workspaceRoot),
|
|
300
|
+
gitDir: pathIdentity(dir),
|
|
301
|
+
gitCommonDir: pathIdentity(commonGitDir(root)),
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
const writePlanIdentity = sha256Identity(
|
|
305
|
+
JSON.stringify({
|
|
306
|
+
schema: "cc-checkpoint-git-write-plan/v1",
|
|
307
|
+
checkpointCommit: targetCommit,
|
|
308
|
+
targetTree,
|
|
309
|
+
currentTree,
|
|
310
|
+
scopeIdentity,
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
313
|
+
return {
|
|
314
|
+
schema: WORKSPACE_BINDING_SCHEMA,
|
|
315
|
+
version: 1,
|
|
316
|
+
engine: "git",
|
|
317
|
+
workspaceRoot,
|
|
318
|
+
scopeIdentity,
|
|
319
|
+
prestateIdentity: `git-tree:${currentTree}`,
|
|
320
|
+
writePlanIdentity,
|
|
321
|
+
targetPoststateIdentity: `git-tree:${targetTree}`,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function validWorkspaceBinding(binding) {
|
|
326
|
+
if (!binding || typeof binding !== "object" || Array.isArray(binding)) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
const keys = Object.keys(binding).sort();
|
|
330
|
+
if (
|
|
331
|
+
keys.length !== WORKSPACE_BINDING_KEYS.length ||
|
|
332
|
+
keys.some((key, index) => key !== WORKSPACE_BINDING_KEYS[index])
|
|
333
|
+
) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
const gitTreeIdentity = /^git-tree:(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
337
|
+
return (
|
|
338
|
+
binding.schema === WORKSPACE_BINDING_SCHEMA &&
|
|
339
|
+
binding.version === 1 &&
|
|
340
|
+
binding.engine === "git" &&
|
|
341
|
+
typeof binding.workspaceRoot === "string" &&
|
|
342
|
+
path.isAbsolute(binding.workspaceRoot) &&
|
|
343
|
+
/^sha256:[a-f0-9]{64}$/.test(binding.scopeIdentity) &&
|
|
344
|
+
gitTreeIdentity.test(binding.prestateIdentity) &&
|
|
345
|
+
/^sha256:[a-f0-9]{64}$/.test(binding.writePlanIdentity) &&
|
|
346
|
+
gitTreeIdentity.test(binding.targetPoststateIdentity)
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function workspaceBindingStaleError(
|
|
351
|
+
idOrRef,
|
|
352
|
+
expectedWorkspaceBinding,
|
|
353
|
+
actualWorkspaceBinding,
|
|
354
|
+
reason,
|
|
355
|
+
) {
|
|
356
|
+
const error = new Error(
|
|
357
|
+
`Checkpoint workspace changed before restore: ${idOrRef}`,
|
|
358
|
+
);
|
|
359
|
+
error.code = "CHECKPOINT_WORKSPACE_STALE";
|
|
360
|
+
error.checkpointId = idOrRef;
|
|
361
|
+
error.reason = reason;
|
|
362
|
+
error.expectedWorkspaceBinding = expectedWorkspaceBinding;
|
|
363
|
+
error.actualWorkspaceBinding = actualWorkspaceBinding;
|
|
364
|
+
return error;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function assertWorkspaceBinding(
|
|
368
|
+
idOrRef,
|
|
369
|
+
expectedWorkspaceBinding,
|
|
370
|
+
actualWorkspaceBinding,
|
|
371
|
+
) {
|
|
372
|
+
if (!validWorkspaceBinding(expectedWorkspaceBinding)) {
|
|
373
|
+
throw workspaceBindingStaleError(
|
|
374
|
+
idOrRef,
|
|
375
|
+
expectedWorkspaceBinding,
|
|
376
|
+
actualWorkspaceBinding,
|
|
377
|
+
"invalid-expected-binding",
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
for (const key of WORKSPACE_BINDING_KEYS) {
|
|
381
|
+
if (expectedWorkspaceBinding[key] !== actualWorkspaceBinding[key]) {
|
|
382
|
+
throw workspaceBindingStaleError(
|
|
383
|
+
idOrRef,
|
|
384
|
+
expectedWorkspaceBinding,
|
|
385
|
+
actualWorkspaceBinding,
|
|
386
|
+
`mismatch:${key}`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function checkpointRestoreHookError(name, reason) {
|
|
393
|
+
const error = new Error(`${name} ${reason}`);
|
|
394
|
+
error.code = "CHECKPOINT_RESTORE_HOOK_INVALID";
|
|
395
|
+
error.hook = name;
|
|
396
|
+
return error;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function assertSynchronousRestoreHook(name, hook) {
|
|
400
|
+
if (hook == null) return;
|
|
401
|
+
if (
|
|
402
|
+
typeof hook !== "function" ||
|
|
403
|
+
hook.constructor?.name === "AsyncFunction"
|
|
404
|
+
) {
|
|
405
|
+
throw checkpointRestoreHookError(name, "must be a synchronous function");
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function callSynchronousRestoreHook(name, hook, payload) {
|
|
410
|
+
if (hook == null) return;
|
|
411
|
+
const result = hook(Object.freeze(payload));
|
|
412
|
+
if (
|
|
413
|
+
result != null &&
|
|
414
|
+
(typeof result === "object" || typeof result === "function") &&
|
|
415
|
+
typeof result.then === "function"
|
|
416
|
+
) {
|
|
417
|
+
throw checkpointRestoreHookError(
|
|
418
|
+
name,
|
|
419
|
+
"must not return a promise or thenable",
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function checkpointSafetyStaleError(safety, reason, actualIdentity = null) {
|
|
425
|
+
const error = new Error(
|
|
426
|
+
`Checkpoint safety state changed before restore: ${safety.id}`,
|
|
427
|
+
);
|
|
428
|
+
error.code = "CHECKPOINT_SAFETY_STALE";
|
|
429
|
+
error.checkpointId = safety.id;
|
|
430
|
+
error.reason = reason;
|
|
431
|
+
error.expectedIdentity = `git:${safety.commit}`;
|
|
432
|
+
error.actualIdentity = actualIdentity;
|
|
433
|
+
return error;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function assertDurableSafetyCheckpoint(root, safety, expectedTree) {
|
|
437
|
+
const actualCommit = readRef(root, safety.ref);
|
|
438
|
+
if (actualCommit !== safety.commit) {
|
|
439
|
+
throw checkpointSafetyStaleError(
|
|
440
|
+
safety,
|
|
441
|
+
"ref-identity-changed",
|
|
442
|
+
actualCommit ? `git:${actualCommit}` : null,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let actualTree = null;
|
|
447
|
+
try {
|
|
448
|
+
actualTree = git(["rev-parse", `${actualCommit}^{tree}`], { cwd: root });
|
|
449
|
+
} catch {
|
|
450
|
+
throw checkpointSafetyStaleError(
|
|
451
|
+
safety,
|
|
452
|
+
"commit-unavailable",
|
|
453
|
+
`git:${actualCommit}`,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
if (safety.tree !== expectedTree || actualTree !== expectedTree) {
|
|
457
|
+
throw checkpointSafetyStaleError(
|
|
458
|
+
safety,
|
|
459
|
+
"prestate-tree-mismatch",
|
|
460
|
+
`git:${actualCommit}`,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function assertRestoreWorkspacePrestate(
|
|
466
|
+
root,
|
|
467
|
+
dir,
|
|
468
|
+
idOrRef,
|
|
469
|
+
expectedWorkspaceBinding,
|
|
470
|
+
{ targetCommit, targetTree },
|
|
471
|
+
) {
|
|
472
|
+
const observedTree = snapshotTree(root, dir);
|
|
473
|
+
if (
|
|
474
|
+
`git-tree:${observedTree}` === expectedWorkspaceBinding.prestateIdentity
|
|
475
|
+
) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const actualWorkspaceBinding = buildWorkspaceBinding(root, dir, {
|
|
479
|
+
targetCommit,
|
|
480
|
+
targetTree,
|
|
481
|
+
currentTree: observedTree,
|
|
482
|
+
});
|
|
483
|
+
throw workspaceBindingStaleError(
|
|
484
|
+
idOrRef,
|
|
485
|
+
expectedWorkspaceBinding,
|
|
486
|
+
actualWorkspaceBinding,
|
|
487
|
+
"mismatch:prestateIdentity",
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
87
491
|
/**
|
|
88
492
|
* Ref-safe session segment. Beyond the charset filter, enforce git's per-
|
|
89
493
|
* component ref rules so a legit-looking name never makes every checkpoint op
|
|
@@ -104,6 +508,56 @@ function sessionPrefix(session) {
|
|
|
104
508
|
return `${REF_NS}/${sanitizeSession(session)}`;
|
|
105
509
|
}
|
|
106
510
|
|
|
511
|
+
function readRef(root, ref) {
|
|
512
|
+
try {
|
|
513
|
+
return (
|
|
514
|
+
git(["rev-parse", "--verify", "--quiet", ref], { cwd: root }) || null
|
|
515
|
+
);
|
|
516
|
+
} catch {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function readRefStrict(root, ref) {
|
|
522
|
+
return (
|
|
523
|
+
git(["for-each-ref", "--format=%(objectname)", ref], { cwd: root }) || null
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function isRefTransactionConflict(error) {
|
|
528
|
+
return /(?:cannot lock ref|unable to create .*\.lock|reference already exists|is at .* but expected)/i.test(
|
|
529
|
+
String(error?.message || error || ""),
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Atomically publish an immutable checkpoint id and advance the session tip.
|
|
535
|
+
*
|
|
536
|
+
* `checkpointRef` uses the all-zero old oid, so an id observed as free can
|
|
537
|
+
* never overwrite a concurrently published checkpoint. `_tip` is updated with
|
|
538
|
+
* the exact oid observed before `commit-tree`; the two updates share one Git
|
|
539
|
+
* ref transaction. Consequently `_tip` is the head of a linear, atomically
|
|
540
|
+
* published session chain, and every successful commit has the previous tip as
|
|
541
|
+
* its parent. A transaction conflict publishes neither ref.
|
|
542
|
+
*/
|
|
543
|
+
function publishCheckpointRefs(
|
|
544
|
+
root,
|
|
545
|
+
{ checkpointRef, tipRef, commit, expectedTip },
|
|
546
|
+
) {
|
|
547
|
+
// Git accepts an all-zero old oid as "this ref must not exist". Match the
|
|
548
|
+
// repository's object format (SHA-1 or SHA-256) via the new commit length.
|
|
549
|
+
const zeroOid = "0".repeat(commit.length);
|
|
550
|
+
const input = [
|
|
551
|
+
"start",
|
|
552
|
+
`update ${checkpointRef} ${commit} ${zeroOid}`,
|
|
553
|
+
`update ${tipRef} ${commit} ${expectedTip || zeroOid}`,
|
|
554
|
+
"prepare",
|
|
555
|
+
"commit",
|
|
556
|
+
"",
|
|
557
|
+
].join("\n");
|
|
558
|
+
git(["update-ref", "--stdin"], { cwd: root, input });
|
|
559
|
+
}
|
|
560
|
+
|
|
107
561
|
// Monotonic per-process counter so two temp-index paths minted in the same
|
|
108
562
|
// millisecond never collide. pid disambiguates across processes; Date.now() +
|
|
109
563
|
// this counter disambiguate within one. A collision would mean two operations
|
|
@@ -170,6 +624,29 @@ function snapshotTree(root, dir) {
|
|
|
170
624
|
}
|
|
171
625
|
}
|
|
172
626
|
|
|
627
|
+
/** Exact path set changed between two trees. Rename detection is disabled so
|
|
628
|
+
* every path that a restore may create/delete is represented independently.
|
|
629
|
+
* NUL framing preserves whitespace and newlines in valid Git path names. */
|
|
630
|
+
function treeDiffPaths(root, treeA, treeB) {
|
|
631
|
+
const out = git(
|
|
632
|
+
[
|
|
633
|
+
"diff",
|
|
634
|
+
"--no-ext-diff",
|
|
635
|
+
"--no-textconv",
|
|
636
|
+
"--no-renames",
|
|
637
|
+
"--name-only",
|
|
638
|
+
"-z",
|
|
639
|
+
treeA,
|
|
640
|
+
treeB,
|
|
641
|
+
"--",
|
|
642
|
+
],
|
|
643
|
+
{ cwd: root, rawOutput: true },
|
|
644
|
+
);
|
|
645
|
+
const paths = out.split("\0").filter((entry) => entry.length > 0);
|
|
646
|
+
paths.sort();
|
|
647
|
+
return paths;
|
|
648
|
+
}
|
|
649
|
+
|
|
173
650
|
/**
|
|
174
651
|
* Capture the current working tree as a shadow commit.
|
|
175
652
|
*
|
|
@@ -238,43 +715,56 @@ export function createCheckpoint(cwd = process.cwd(), opts = {}) {
|
|
|
238
715
|
}
|
|
239
716
|
}
|
|
240
717
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
718
|
+
const message = `cc-checkpoint${label ? `: ${label}` : ""}\n`;
|
|
719
|
+
const prefix = sessionPrefix(session);
|
|
720
|
+
const tipRef = `${prefix}/_tip`;
|
|
721
|
+
let published = null;
|
|
722
|
+
let lastConflict = null;
|
|
723
|
+
|
|
724
|
+
for (let attempt = 1; attempt <= MAX_REF_PUBLISH_ATTEMPTS; attempt += 1) {
|
|
725
|
+
// Chain onto the exact prior tip (or HEAD for the first checkpoint). Both
|
|
726
|
+
// the id and tip are CAS-published below. If another creator wins first,
|
|
727
|
+
// rebuild on its new tip so the session history remains linear.
|
|
728
|
+
const expectedTip = readRef(root, tipRef);
|
|
729
|
+
let parent = expectedTip;
|
|
730
|
+
if (!parent) parent = readRef(root, "HEAD");
|
|
731
|
+
|
|
732
|
+
const commitArgs = ["commit-tree", tree];
|
|
733
|
+
if (parent) commitArgs.push("-p", parent);
|
|
734
|
+
const commit = git(commitArgs, {
|
|
735
|
+
cwd: root,
|
|
736
|
+
input: message,
|
|
737
|
+
env: { ...env, ...CHECKPOINT_IDENTITY },
|
|
738
|
+
});
|
|
739
|
+
const id = nextId(root, session);
|
|
740
|
+
const ref = `${prefix}/${id}`;
|
|
741
|
+
|
|
254
742
|
try {
|
|
255
|
-
|
|
256
|
-
|
|
743
|
+
publishCheckpointRefs(root, {
|
|
744
|
+
checkpointRef: ref,
|
|
745
|
+
tipRef,
|
|
746
|
+
commit,
|
|
747
|
+
expectedTip,
|
|
257
748
|
});
|
|
258
|
-
|
|
259
|
-
|
|
749
|
+
published = { id, ref, commit, parent };
|
|
750
|
+
break;
|
|
751
|
+
} catch (error) {
|
|
752
|
+
if (!isRefTransactionConflict(error)) throw error;
|
|
753
|
+
lastConflict = error;
|
|
260
754
|
}
|
|
261
755
|
}
|
|
262
756
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
757
|
+
if (!published) {
|
|
758
|
+
const error = new Error(
|
|
759
|
+
`Checkpoint refs changed during ${MAX_REF_PUBLISH_ATTEMPTS} publish attempts`,
|
|
760
|
+
lastConflict ? { cause: lastConflict } : undefined,
|
|
761
|
+
);
|
|
762
|
+
error.code = "CHECKPOINT_REF_CONFLICT";
|
|
763
|
+
error.attempts = MAX_REF_PUBLISH_ATTEMPTS;
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
271
766
|
|
|
272
|
-
const id
|
|
273
|
-
const ref = `${sessionPrefix(session)}/${id}`;
|
|
274
|
-
git(["update-ref", ref, commit], { cwd: root });
|
|
275
|
-
git(["update-ref", `${sessionPrefix(session)}/_tip`, commit], {
|
|
276
|
-
cwd: root,
|
|
277
|
-
});
|
|
767
|
+
const { id, ref, commit, parent } = published;
|
|
278
768
|
|
|
279
769
|
// File count in the snapshot (cheap, informative).
|
|
280
770
|
let files = 0;
|
|
@@ -294,17 +784,15 @@ export function createCheckpoint(cwd = process.cwd(), opts = {}) {
|
|
|
294
784
|
// oldest entries). Manual `cc checkpoint create` omits the cap → unbounded.
|
|
295
785
|
if (Number.isFinite(opts.maxPerSession) && opts.maxPerSession > 0) {
|
|
296
786
|
try {
|
|
297
|
-
const rows = listRefs(root, session
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
/* best-effort — a failed prune never affects the new checkpoint */
|
|
304
|
-
}
|
|
305
|
-
}
|
|
787
|
+
const rows = listRefs(root, session, { strict: true });
|
|
788
|
+
pruneCheckpointRows(root, session, rows, opts.maxPerSession, opts, {
|
|
789
|
+
id,
|
|
790
|
+
ref,
|
|
791
|
+
commit,
|
|
792
|
+
});
|
|
306
793
|
} catch {
|
|
307
|
-
|
|
794
|
+
// Verification failure skips the whole prune. The newly published
|
|
795
|
+
// checkpoint remains valid; no possibly retained authority is removed.
|
|
308
796
|
}
|
|
309
797
|
}
|
|
310
798
|
|
|
@@ -327,7 +815,7 @@ function nextId(root, session) {
|
|
|
327
815
|
}
|
|
328
816
|
|
|
329
817
|
/** Raw ref rows for a session (excludes the internal _tip pointer). */
|
|
330
|
-
function listRefs(root, session) {
|
|
818
|
+
function listRefs(root, session, { strict = false } = {}) {
|
|
331
819
|
const prefix = sessionPrefix(session);
|
|
332
820
|
let out = "";
|
|
333
821
|
try {
|
|
@@ -340,7 +828,8 @@ function listRefs(root, session) {
|
|
|
340
828
|
],
|
|
341
829
|
{ cwd: root },
|
|
342
830
|
);
|
|
343
|
-
} catch {
|
|
831
|
+
} catch (error) {
|
|
832
|
+
if (strict) throw error;
|
|
344
833
|
return [];
|
|
345
834
|
}
|
|
346
835
|
if (!out) return [];
|
|
@@ -356,6 +845,123 @@ function listRefs(root, session) {
|
|
|
356
845
|
return rows;
|
|
357
846
|
}
|
|
358
847
|
|
|
848
|
+
function deleteCheckpointRowsExact(root, rows, tip = null) {
|
|
849
|
+
if (rows.length === 0 && !tip) return;
|
|
850
|
+
const tipCommand = tip
|
|
851
|
+
? tip.nextRow
|
|
852
|
+
? `update ${tip.ref} ${tip.nextRow.commit} ${tip.expectedCommit}`
|
|
853
|
+
: `delete ${tip.ref} ${tip.expectedCommit}`
|
|
854
|
+
: null;
|
|
855
|
+
const input = [
|
|
856
|
+
"start",
|
|
857
|
+
...(tip?.nextRow
|
|
858
|
+
? [`verify ${tip.nextRow.ref} ${tip.nextRow.commit}`]
|
|
859
|
+
: []),
|
|
860
|
+
...rows.map((row) => `delete ${row.ref} ${row.commit}`),
|
|
861
|
+
...(tipCommand ? [tipCommand] : []),
|
|
862
|
+
"prepare",
|
|
863
|
+
"commit",
|
|
864
|
+
"",
|
|
865
|
+
].join("\n");
|
|
866
|
+
git(["update-ref", "--stdin"], { cwd: root, input });
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function exactSessionTipRow(rows, tipCommit) {
|
|
870
|
+
if (!tipCommit) return null;
|
|
871
|
+
const matches = rows.filter((row) => row.commit === tipCommit);
|
|
872
|
+
if (matches.length !== 1) {
|
|
873
|
+
throw checkpointRestoreRetentionUnverifiedError(
|
|
874
|
+
null,
|
|
875
|
+
"Git checkpoint session tip does not match one exact checkpoint ref",
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
return matches[0];
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function previousRetainedSessionTip(root, tipCommit, remainingRows) {
|
|
882
|
+
if (remainingRows.length === 0) return null;
|
|
883
|
+
const remainingRowsByCommit = new Map();
|
|
884
|
+
for (const row of remainingRows) {
|
|
885
|
+
const matches = remainingRowsByCommit.get(row.commit) || [];
|
|
886
|
+
matches.push(row);
|
|
887
|
+
remainingRowsByCommit.set(row.commit, matches);
|
|
888
|
+
}
|
|
889
|
+
const history = git(
|
|
890
|
+
[
|
|
891
|
+
"rev-list",
|
|
892
|
+
"--first-parent",
|
|
893
|
+
`--max-count=${MAX_CHECKPOINT_TIP_REWIND_COMMITS + 1}`,
|
|
894
|
+
tipCommit,
|
|
895
|
+
],
|
|
896
|
+
{ cwd: root },
|
|
897
|
+
)
|
|
898
|
+
.split("\n")
|
|
899
|
+
.filter(Boolean);
|
|
900
|
+
for (const commit of history.slice(1)) {
|
|
901
|
+
const matches = remainingRowsByCommit.get(commit) || [];
|
|
902
|
+
if (matches.length > 1) {
|
|
903
|
+
throw checkpointRestoreRetentionUnverifiedError(
|
|
904
|
+
null,
|
|
905
|
+
"Git checkpoint tip predecessor does not match one exact checkpoint ref",
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
if (matches.length === 1) return matches[0];
|
|
909
|
+
}
|
|
910
|
+
throw checkpointRestoreRetentionUnverifiedError(
|
|
911
|
+
null,
|
|
912
|
+
history.length > MAX_CHECKPOINT_TIP_REWIND_COMMITS
|
|
913
|
+
? "Git checkpoint tip rewind exceeded its verification bound"
|
|
914
|
+
: "Git checkpoint tip has no retained checkpoint ancestor",
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function pruneCheckpointRows(root, session, rows, maximum, opts, sessionTip) {
|
|
919
|
+
const excess = rows.length - maximum;
|
|
920
|
+
if (excess <= 0 || rows.length <= 1) return;
|
|
921
|
+
const isSessionTip = (row) =>
|
|
922
|
+
row.id === sessionTip.id &&
|
|
923
|
+
row.ref === sessionTip.ref &&
|
|
924
|
+
row.commit === sessionTip.commit;
|
|
925
|
+
if (!rows.some(isSessionTip)) {
|
|
926
|
+
throw checkpointRestoreRetentionUnverifiedError(
|
|
927
|
+
null,
|
|
928
|
+
"Newly published Git checkpoint tip could not be retained during prune",
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// The exact newly published session tip must remain addressable even when
|
|
933
|
+
// protected older authority means the requested cap cannot be reached.
|
|
934
|
+
const candidates = rows
|
|
935
|
+
.filter((row) => !isSessionTip(row))
|
|
936
|
+
.map((row) => ({
|
|
937
|
+
id: row.id,
|
|
938
|
+
identity: `git:${row.commit}`,
|
|
939
|
+
}));
|
|
940
|
+
withCheckpointRestoreRetention(
|
|
941
|
+
{
|
|
942
|
+
workspaceRoot: canonicalPath(root),
|
|
943
|
+
engine: "git",
|
|
944
|
+
checkpointNamespace: session,
|
|
945
|
+
candidates,
|
|
946
|
+
protectedPolicy: "exclude",
|
|
947
|
+
...checkpointRetentionOptions(opts),
|
|
948
|
+
},
|
|
949
|
+
(snapshot) => {
|
|
950
|
+
const deletable = new Map(
|
|
951
|
+
snapshot.deletableCandidates.map((candidate) => [
|
|
952
|
+
candidate.id,
|
|
953
|
+
candidate.identity,
|
|
954
|
+
]),
|
|
955
|
+
);
|
|
956
|
+
const selected = rows
|
|
957
|
+
.filter((row) => !isSessionTip(row))
|
|
958
|
+
.filter((row) => deletable.get(row.id) === `git:${row.commit}`)
|
|
959
|
+
.slice(0, excess);
|
|
960
|
+
deleteCheckpointRowsExact(root, selected);
|
|
961
|
+
},
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
|
|
359
965
|
/**
|
|
360
966
|
* List checkpoints for a session, newest-first.
|
|
361
967
|
*
|
|
@@ -390,10 +996,31 @@ export function resolveCheckpoint(cwd, idOrRef, opts = {}) {
|
|
|
390
996
|
throw new Error(`Checkpoint not found: ${idOrRef}`);
|
|
391
997
|
}
|
|
392
998
|
|
|
999
|
+
function resolveExpectedCheckpoint(root, idOrRef, session, expectedIdentity) {
|
|
1000
|
+
const resolved = resolveCheckpoint(root, idOrRef, { session });
|
|
1001
|
+
if (expectedIdentity == null) return resolved;
|
|
1002
|
+
const match = /^git:([a-f0-9]{40}|[a-f0-9]{64})$/.exec(
|
|
1003
|
+
String(expectedIdentity),
|
|
1004
|
+
);
|
|
1005
|
+
if (!match || match[1] !== resolved) {
|
|
1006
|
+
const error = new Error(
|
|
1007
|
+
`Checkpoint identity changed before restore: ${idOrRef}`,
|
|
1008
|
+
);
|
|
1009
|
+
error.code = "CHECKPOINT_IDENTITY_STALE";
|
|
1010
|
+
error.checkpointId = idOrRef;
|
|
1011
|
+
error.expectedIdentity = String(expectedIdentity);
|
|
1012
|
+
error.actualIdentity = `git:${resolved}`;
|
|
1013
|
+
throw error;
|
|
1014
|
+
}
|
|
1015
|
+
// Use the captured immutable object even if the named ref changes after the
|
|
1016
|
+
// comparison. Git object ids bind the exact commit/tree that was previewed.
|
|
1017
|
+
return match[1];
|
|
1018
|
+
}
|
|
1019
|
+
|
|
393
1020
|
/**
|
|
394
1021
|
* Compute what differs between a checkpoint and the current working tree.
|
|
395
1022
|
*
|
|
396
|
-
* @returns {{ modified:string[], added:string[], deleted:string[] }}
|
|
1023
|
+
* @returns {{ checkpointIdentity:string, modified:string[], added:string[], deleted:string[] }}
|
|
397
1024
|
* added = exists now, not in the checkpoint (a rewind would delete it)
|
|
398
1025
|
* deleted = in the checkpoint, gone now (a rewind would recreate it)
|
|
399
1026
|
*/
|
|
@@ -401,13 +1028,24 @@ export function statusAgainst(cwd = process.cwd(), idOrRef, opts = {}) {
|
|
|
401
1028
|
const root = repoRoot(cwd);
|
|
402
1029
|
const dir = gitDir(root);
|
|
403
1030
|
const session = sanitizeSession(opts.session);
|
|
404
|
-
const commit =
|
|
1031
|
+
const commit = resolveExpectedCheckpoint(
|
|
1032
|
+
root,
|
|
1033
|
+
idOrRef,
|
|
1034
|
+
session,
|
|
1035
|
+
opts.expectedIdentity,
|
|
1036
|
+
);
|
|
405
1037
|
const targetTree = git(["rev-parse", `${commit}^{tree}`], { cwd: root });
|
|
406
1038
|
const currentTree = snapshotTree(root, dir);
|
|
407
1039
|
return {
|
|
1040
|
+
checkpointIdentity: `git:${commit}`,
|
|
408
1041
|
modified: diffNames(root, targetTree, currentTree, "M"),
|
|
409
1042
|
added: diffNames(root, targetTree, currentTree, "A"),
|
|
410
1043
|
deleted: diffNames(root, targetTree, currentTree, "D"),
|
|
1044
|
+
workspaceBinding: buildWorkspaceBinding(root, dir, {
|
|
1045
|
+
targetCommit: commit,
|
|
1046
|
+
targetTree,
|
|
1047
|
+
currentTree,
|
|
1048
|
+
}),
|
|
411
1049
|
};
|
|
412
1050
|
}
|
|
413
1051
|
|
|
@@ -415,14 +1053,31 @@ export function statusAgainst(cwd = process.cwd(), idOrRef, opts = {}) {
|
|
|
415
1053
|
* Restore the working tree to a checkpoint. Creates a safety checkpoint first
|
|
416
1054
|
* (unless dryRun / skipSafety).
|
|
417
1055
|
*
|
|
418
|
-
* @param {object} [opts]
|
|
419
|
-
*
|
|
1056
|
+
* @param {object} [opts]
|
|
1057
|
+
* { session, dryRun, skipSafety, now, expectedWorkspaceBinding,
|
|
1058
|
+
* onSafetyReady, onMutationStarted, onWorkspaceApplied }
|
|
1059
|
+
* @returns {{ restored, dryRun, target, safetyId, safetyIdentity,
|
|
1060
|
+
* safetyPlanIdentity?:string|null, safetyCoverage?:string,
|
|
1061
|
+
* modified, deleted, recreated }}
|
|
420
1062
|
*/
|
|
421
1063
|
export function rewindTo(cwd = process.cwd(), idOrRef, opts = {}) {
|
|
1064
|
+
for (const hookName of [
|
|
1065
|
+
"onSafetyReady",
|
|
1066
|
+
"onMutationStarted",
|
|
1067
|
+
"onWorkspaceApplied",
|
|
1068
|
+
]) {
|
|
1069
|
+
assertSynchronousRestoreHook(hookName, opts[hookName]);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
422
1072
|
const root = repoRoot(cwd);
|
|
423
1073
|
const dir = gitDir(root);
|
|
424
1074
|
const session = sanitizeSession(opts.session);
|
|
425
|
-
const targetCommit =
|
|
1075
|
+
const targetCommit = resolveExpectedCheckpoint(
|
|
1076
|
+
root,
|
|
1077
|
+
idOrRef,
|
|
1078
|
+
session,
|
|
1079
|
+
opts.expectedIdentity,
|
|
1080
|
+
);
|
|
426
1081
|
const targetTree = git(["rev-parse", `${targetCommit}^{tree}`], {
|
|
427
1082
|
cwd: root,
|
|
428
1083
|
});
|
|
@@ -432,6 +1087,20 @@ export function rewindTo(cwd = process.cwd(), idOrRef, opts = {}) {
|
|
|
432
1087
|
const modified = diffNames(root, targetTree, currentTree, "M");
|
|
433
1088
|
const added = diffNames(root, targetTree, currentTree, "A"); // → delete
|
|
434
1089
|
const recreated = diffNames(root, targetTree, currentTree, "D"); // → recreate
|
|
1090
|
+
const mutationCount = modified.length + added.length + recreated.length;
|
|
1091
|
+
const workspaceBinding = buildWorkspaceBinding(root, dir, {
|
|
1092
|
+
targetCommit,
|
|
1093
|
+
targetTree,
|
|
1094
|
+
currentTree,
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
if (Object.hasOwn(opts, "expectedWorkspaceBinding")) {
|
|
1098
|
+
assertWorkspaceBinding(
|
|
1099
|
+
idOrRef,
|
|
1100
|
+
opts.expectedWorkspaceBinding,
|
|
1101
|
+
workspaceBinding,
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
435
1104
|
|
|
436
1105
|
if (opts.dryRun) {
|
|
437
1106
|
return {
|
|
@@ -439,6 +1108,7 @@ export function rewindTo(cwd = process.cwd(), idOrRef, opts = {}) {
|
|
|
439
1108
|
dryRun: true,
|
|
440
1109
|
target: targetCommit,
|
|
441
1110
|
safetyId: null,
|
|
1111
|
+
safetyIdentity: null,
|
|
442
1112
|
modified: modified.length,
|
|
443
1113
|
deleted: added.length,
|
|
444
1114
|
recreated: recreated.length,
|
|
@@ -447,34 +1117,190 @@ export function rewindTo(cwd = process.cwd(), idOrRef, opts = {}) {
|
|
|
447
1117
|
|
|
448
1118
|
// Safety net — snapshot the current state so this rewind is undoable.
|
|
449
1119
|
let safetyId = null;
|
|
1120
|
+
let safetyIdentity = null;
|
|
1121
|
+
let safety = null;
|
|
450
1122
|
if (!opts.skipSafety) {
|
|
451
|
-
|
|
1123
|
+
safety = createCheckpoint(root, {
|
|
452
1124
|
session,
|
|
453
1125
|
label: `auto: before rewind to ${idOrRef}`,
|
|
454
1126
|
now: opts.now,
|
|
455
|
-
})
|
|
1127
|
+
});
|
|
1128
|
+
safetyId = safety.id;
|
|
1129
|
+
safetyIdentity = `git:${safety.commit}`;
|
|
456
1130
|
}
|
|
457
1131
|
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
|
|
1132
|
+
const checkpointIdentity = `git:${targetCommit}`;
|
|
1133
|
+
const safetyPlanIdentity = workspaceBinding.writePlanIdentity;
|
|
1134
|
+
let restorePhase = "safety-ready";
|
|
461
1135
|
try {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
1136
|
+
if (safety) {
|
|
1137
|
+
assertDurableSafetyCheckpoint(root, safety, currentTree);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// A zero-diff restore has no safety-ready or mutation-started boundary, but
|
|
1141
|
+
// it still has a verified workspace-applied settlement. Keep the historical
|
|
1142
|
+
// safety-checkpoint return behaviour and report that direct no-op boundary
|
|
1143
|
+
// without running checkout-index over an already-settled tree.
|
|
1144
|
+
if (mutationCount === 0) {
|
|
1145
|
+
restorePhase = "workspace-applied";
|
|
1146
|
+
callSynchronousRestoreHook(
|
|
1147
|
+
"onWorkspaceApplied",
|
|
1148
|
+
opts.onWorkspaceApplied,
|
|
1149
|
+
{
|
|
1150
|
+
checkpointId: idOrRef,
|
|
1151
|
+
checkpointIdentity,
|
|
1152
|
+
safetyId,
|
|
1153
|
+
safetyIdentity,
|
|
1154
|
+
safetyCoverage: safety ? "full" : "none",
|
|
1155
|
+
safetyPlanIdentity: safety ? safetyPlanIdentity : null,
|
|
1156
|
+
mutationCount: 0,
|
|
1157
|
+
appliedCount: 0,
|
|
1158
|
+
poststateIdentity: `git-tree:${currentTree}`,
|
|
1159
|
+
},
|
|
1160
|
+
);
|
|
1161
|
+
if (opts.onWorkspaceApplied) {
|
|
1162
|
+
if (safety) {
|
|
1163
|
+
assertDurableSafetyCheckpoint(root, safety, currentTree);
|
|
1164
|
+
}
|
|
1165
|
+
assertRestoreWorkspacePrestate(root, dir, idOrRef, workspaceBinding, {
|
|
1166
|
+
targetCommit,
|
|
1167
|
+
targetTree,
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
return {
|
|
1171
|
+
restored: true,
|
|
1172
|
+
dryRun: false,
|
|
1173
|
+
target: targetCommit,
|
|
1174
|
+
safetyId,
|
|
1175
|
+
safetyIdentity,
|
|
1176
|
+
safetyPlanIdentity: safety ? safetyPlanIdentity : null,
|
|
1177
|
+
safetyCoverage: "checkpoint",
|
|
1178
|
+
modified: 0,
|
|
1179
|
+
deleted: 0,
|
|
1180
|
+
recreated: 0,
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (safety) {
|
|
1185
|
+
callSynchronousRestoreHook("onSafetyReady", opts.onSafetyReady, {
|
|
1186
|
+
checkpointId: idOrRef,
|
|
1187
|
+
checkpointIdentity,
|
|
1188
|
+
safetyId,
|
|
1189
|
+
safetyIdentity,
|
|
1190
|
+
safetyCoverage: "full",
|
|
1191
|
+
safetyPlanIdentity,
|
|
1192
|
+
mutationCount,
|
|
1193
|
+
});
|
|
1194
|
+
// Hooks are extension points. They may not invalidate the durable undo
|
|
1195
|
+
// ref or change the exact workspace state that the write plan covers.
|
|
1196
|
+
assertDurableSafetyCheckpoint(root, safety, currentTree);
|
|
1197
|
+
assertRestoreWorkspacePrestate(root, dir, idOrRef, workspaceBinding, {
|
|
1198
|
+
targetCommit,
|
|
1199
|
+
targetTree,
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const tmpIndex = tempIndexPath(dir);
|
|
1204
|
+
const env = { GIT_INDEX_FILE: tmpIndex };
|
|
1205
|
+
try {
|
|
1206
|
+
git(["read-tree", targetTree], { cwd: root, env });
|
|
1207
|
+
|
|
1208
|
+
callSynchronousRestoreHook("onMutationStarted", opts.onMutationStarted, {
|
|
1209
|
+
checkpointId: idOrRef,
|
|
1210
|
+
checkpointIdentity,
|
|
1211
|
+
safetyId,
|
|
1212
|
+
safetyIdentity,
|
|
1213
|
+
safetyCoverage: safety ? "full" : "none",
|
|
1214
|
+
safetyPlanIdentity: safety ? safetyPlanIdentity : null,
|
|
1215
|
+
mutationCount,
|
|
1216
|
+
});
|
|
1217
|
+
if (opts.onMutationStarted) {
|
|
1218
|
+
// The durable consumer has now recorded the mutation boundary. Treat
|
|
1219
|
+
// any subsequent uncertainty conservatively even though the checks
|
|
1220
|
+
// below still precede the first checkout-index write.
|
|
1221
|
+
restorePhase = "workspace-mutation";
|
|
1222
|
+
}
|
|
1223
|
+
if (safety) {
|
|
1224
|
+
assertDurableSafetyCheckpoint(root, safety, currentTree);
|
|
1225
|
+
}
|
|
1226
|
+
assertRestoreWorkspacePrestate(root, dir, idOrRef, workspaceBinding, {
|
|
1227
|
+
targetCommit,
|
|
1228
|
+
targetTree,
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
restorePhase = "workspace-mutation";
|
|
1232
|
+
git(["checkout-index", "-a", "-f"], { cwd: root, env });
|
|
1233
|
+
} finally {
|
|
1234
|
+
rmQuiet(tmpIndex);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Remove files the target snapshot does not contain (created since). These
|
|
1238
|
+
// paths come from git's tree diff (repo-relative, git rejects `..` in tree
|
|
1239
|
+
// entries), but this is a force-delete over the user's working tree — guard
|
|
1240
|
+
// each resolved path against the repo root before unlinking, as
|
|
1241
|
+
// defense-in-depth, and keep it best-effort so one locked file can't abort
|
|
1242
|
+
// the whole rewind.
|
|
1243
|
+
for (const rel of added) {
|
|
1244
|
+
const abs = path.resolve(root, rel);
|
|
1245
|
+
if (!withinRoot(root, abs)) continue; // never delete outside the repo
|
|
1246
|
+
rmQuiet(abs);
|
|
1247
|
+
}
|
|
1248
|
+
const settledTree = snapshotTree(root, dir);
|
|
1249
|
+
if (settledTree !== targetTree) {
|
|
1250
|
+
const error = new Error(
|
|
1251
|
+
`Checkpoint restore did not settle at the target tree: ${idOrRef}`,
|
|
1252
|
+
);
|
|
1253
|
+
error.code = "CHECKPOINT_RESTORE_INCOMPLETE";
|
|
1254
|
+
error.residualPaths = [
|
|
1255
|
+
...diffNames(root, targetTree, settledTree, "M"),
|
|
1256
|
+
...diffNames(root, targetTree, settledTree, "A"),
|
|
1257
|
+
...diffNames(root, targetTree, settledTree, "D"),
|
|
1258
|
+
];
|
|
1259
|
+
throw error;
|
|
1260
|
+
}
|
|
467
1261
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
1262
|
+
restorePhase = "workspace-applied";
|
|
1263
|
+
callSynchronousRestoreHook("onWorkspaceApplied", opts.onWorkspaceApplied, {
|
|
1264
|
+
checkpointId: idOrRef,
|
|
1265
|
+
checkpointIdentity,
|
|
1266
|
+
safetyId,
|
|
1267
|
+
safetyIdentity,
|
|
1268
|
+
safetyCoverage: safety ? "full" : "none",
|
|
1269
|
+
safetyPlanIdentity: safety ? safetyPlanIdentity : null,
|
|
1270
|
+
mutationCount,
|
|
1271
|
+
appliedCount: mutationCount,
|
|
1272
|
+
poststateIdentity: `git-tree:${settledTree}`,
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
// Do not return a successful restore if the completion hook invalidated
|
|
1276
|
+
// either the recovery ref or the just-verified poststate.
|
|
1277
|
+
if (opts.onWorkspaceApplied) {
|
|
1278
|
+
if (safety) {
|
|
1279
|
+
assertDurableSafetyCheckpoint(root, safety, currentTree);
|
|
1280
|
+
}
|
|
1281
|
+
const postHookTree = snapshotTree(root, dir);
|
|
1282
|
+
if (postHookTree !== targetTree) {
|
|
1283
|
+
const error = new Error(
|
|
1284
|
+
`Checkpoint restore did not remain at the target tree: ${idOrRef}`,
|
|
1285
|
+
);
|
|
1286
|
+
error.code = "CHECKPOINT_RESTORE_INCOMPLETE";
|
|
1287
|
+
error.residualPaths = [
|
|
1288
|
+
...diffNames(root, targetTree, postHookTree, "M"),
|
|
1289
|
+
...diffNames(root, targetTree, postHookTree, "A"),
|
|
1290
|
+
...diffNames(root, targetTree, postHookTree, "D"),
|
|
1291
|
+
];
|
|
1292
|
+
throw error;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
if (error && typeof error === "object") {
|
|
1297
|
+
error.safetyId = safetyId;
|
|
1298
|
+
error.safetyIdentity = safetyIdentity;
|
|
1299
|
+
error.safetyPlanIdentity = safety ? safetyPlanIdentity : null;
|
|
1300
|
+
error.safetyCoverage = "checkpoint";
|
|
1301
|
+
error.restorePhase = restorePhase;
|
|
1302
|
+
}
|
|
1303
|
+
throw error;
|
|
478
1304
|
}
|
|
479
1305
|
|
|
480
1306
|
return {
|
|
@@ -482,17 +1308,718 @@ export function rewindTo(cwd = process.cwd(), idOrRef, opts = {}) {
|
|
|
482
1308
|
dryRun: false,
|
|
483
1309
|
target: targetCommit,
|
|
484
1310
|
safetyId,
|
|
1311
|
+
safetyIdentity,
|
|
1312
|
+
safetyPlanIdentity: safety ? safetyPlanIdentity : null,
|
|
1313
|
+
safetyCoverage: "checkpoint",
|
|
485
1314
|
modified: modified.length,
|
|
486
1315
|
deleted: added.length,
|
|
487
1316
|
recreated: recreated.length,
|
|
488
1317
|
};
|
|
489
1318
|
}
|
|
490
1319
|
|
|
491
|
-
/**
|
|
1320
|
+
/** Strict validation for the durable authority carried by rollback plans. */
|
|
1321
|
+
function validCheckpointReference(value) {
|
|
1322
|
+
return (
|
|
1323
|
+
typeof value === "string" &&
|
|
1324
|
+
value.length > 0 &&
|
|
1325
|
+
value.length <= 4_096 &&
|
|
1326
|
+
value === value.trim() &&
|
|
1327
|
+
!value.includes("\0")
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function validRollbackPaths(paths) {
|
|
1332
|
+
if (!Array.isArray(paths)) return false;
|
|
1333
|
+
let previous = null;
|
|
1334
|
+
for (const entry of paths) {
|
|
1335
|
+
if (
|
|
1336
|
+
typeof entry !== "string" ||
|
|
1337
|
+
entry.length === 0 ||
|
|
1338
|
+
entry.includes("\0") ||
|
|
1339
|
+
entry === "." ||
|
|
1340
|
+
entry.startsWith("/") ||
|
|
1341
|
+
entry.startsWith("../") ||
|
|
1342
|
+
entry.includes("/../") ||
|
|
1343
|
+
entry.endsWith("/..") ||
|
|
1344
|
+
(previous !== null && entry <= previous)
|
|
1345
|
+
) {
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1348
|
+
previous = entry;
|
|
1349
|
+
}
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function validOriginalCheckpointAuthority(authority) {
|
|
1354
|
+
return (
|
|
1355
|
+
hasExactKeys(authority, ORIGINAL_CHECKPOINT_AUTHORITY_KEYS) &&
|
|
1356
|
+
validCheckpointReference(authority.id) &&
|
|
1357
|
+
GIT_CHECKPOINT_IDENTITY_PATTERN.test(authority.identity) &&
|
|
1358
|
+
GIT_TREE_IDENTITY_PATTERN.test(authority.treeIdentity)
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function validSafetyCheckpointAuthority(authority) {
|
|
1363
|
+
return (
|
|
1364
|
+
hasExactKeys(authority, SAFETY_CHECKPOINT_AUTHORITY_KEYS) &&
|
|
1365
|
+
validCheckpointReference(authority.id) &&
|
|
1366
|
+
GIT_CHECKPOINT_IDENTITY_PATTERN.test(authority.identity) &&
|
|
1367
|
+
SHA256_IDENTITY_PATTERN.test(authority.planIdentity) &&
|
|
1368
|
+
GIT_TREE_IDENTITY_PATTERN.test(authority.treeIdentity)
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function originalMutationSetIdentity(paths) {
|
|
1373
|
+
return computeCheckpointRestoreDigest(
|
|
1374
|
+
"cc-checkpoint-restore-original-mutation-set-v1",
|
|
1375
|
+
{ engine: "git", paths },
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function validOriginalPlanAuthority(authority) {
|
|
1380
|
+
return (
|
|
1381
|
+
hasExactKeys(authority, ORIGINAL_PLAN_AUTHORITY_KEYS) &&
|
|
1382
|
+
validCheckpointReference(authority.sourceCheckpointId) &&
|
|
1383
|
+
GIT_CHECKPOINT_IDENTITY_PATTERN.test(authority.sourceCheckpointIdentity) &&
|
|
1384
|
+
SHA256_IDENTITY_PATTERN.test(authority.safetyPlanIdentity) &&
|
|
1385
|
+
SHA256_IDENTITY_PATTERN.test(authority.mutationSetIdentity) &&
|
|
1386
|
+
authority.bindingReconstructable === true
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function rollbackPlanInstanceDigest(plan) {
|
|
1391
|
+
return computeCheckpointRestoreDigest(
|
|
1392
|
+
"cc-checkpoint-restore-rollback-plan-instance-v1",
|
|
1393
|
+
plan,
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function validateCheckpointRollbackPlan(plan) {
|
|
1398
|
+
const invalid = (reason) => {
|
|
1399
|
+
throw checkpointRollbackError(
|
|
1400
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INVALID_ARGUMENT,
|
|
1401
|
+
`Invalid Git checkpoint rollback plan: ${reason}`,
|
|
1402
|
+
{ reason },
|
|
1403
|
+
);
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
if (!hasExactKeys(plan, CHECKPOINT_ROLLBACK_PLAN_KEYS)) {
|
|
1407
|
+
invalid("invalid-plan-shape");
|
|
1408
|
+
}
|
|
1409
|
+
if (
|
|
1410
|
+
plan.schema !== CHECKPOINT_ROLLBACK_PLAN_SCHEMA ||
|
|
1411
|
+
plan.version !== CHECKPOINT_ROLLBACK_PLAN_VERSION ||
|
|
1412
|
+
plan.engine !== "git"
|
|
1413
|
+
) {
|
|
1414
|
+
invalid("invalid-plan-envelope");
|
|
1415
|
+
}
|
|
1416
|
+
if (
|
|
1417
|
+
!validCheckpointReference(plan.checkpointNamespace) ||
|
|
1418
|
+
sanitizeSession(plan.checkpointNamespace) !== plan.checkpointNamespace
|
|
1419
|
+
) {
|
|
1420
|
+
invalid("invalid-checkpoint-namespace");
|
|
1421
|
+
}
|
|
1422
|
+
if (
|
|
1423
|
+
typeof plan.workspaceRoot !== "string" ||
|
|
1424
|
+
!path.isAbsolute(plan.workspaceRoot)
|
|
1425
|
+
) {
|
|
1426
|
+
invalid("invalid-workspace-root");
|
|
1427
|
+
}
|
|
1428
|
+
if (!validOriginalCheckpointAuthority(plan.originalCheckpoint)) {
|
|
1429
|
+
invalid("invalid-original-checkpoint-authority");
|
|
1430
|
+
}
|
|
1431
|
+
if (!validSafetyCheckpointAuthority(plan.safetyCheckpoint)) {
|
|
1432
|
+
invalid("invalid-safety-checkpoint-authority");
|
|
1433
|
+
}
|
|
1434
|
+
if (
|
|
1435
|
+
plan.originalBindingVerification !== "exact-checkpoint-tree-reconstruction"
|
|
1436
|
+
) {
|
|
1437
|
+
invalid("invalid-original-binding-verification");
|
|
1438
|
+
}
|
|
1439
|
+
if (!validOriginalPlanAuthority(plan.originalPlanAuthority)) {
|
|
1440
|
+
invalid("invalid-original-plan-authority");
|
|
1441
|
+
}
|
|
1442
|
+
if (
|
|
1443
|
+
!validRollbackPaths(plan.originalMutationPaths) ||
|
|
1444
|
+
!validRollbackPaths(plan.currentRollbackPaths)
|
|
1445
|
+
) {
|
|
1446
|
+
invalid("invalid-rollback-paths");
|
|
1447
|
+
}
|
|
1448
|
+
if (
|
|
1449
|
+
!Number.isSafeInteger(plan.originalMutationTargetCount) ||
|
|
1450
|
+
plan.originalMutationTargetCount < 0 ||
|
|
1451
|
+
plan.originalMutationTargetCount !== plan.originalMutationPaths.length ||
|
|
1452
|
+
!Number.isSafeInteger(plan.targetCount) ||
|
|
1453
|
+
plan.targetCount < 0 ||
|
|
1454
|
+
plan.targetCount !== plan.currentRollbackPaths.length
|
|
1455
|
+
) {
|
|
1456
|
+
invalid("invalid-target-count");
|
|
1457
|
+
}
|
|
1458
|
+
const originalPathSet = new Set(plan.originalMutationPaths);
|
|
1459
|
+
if (plan.currentRollbackPaths.some((entry) => !originalPathSet.has(entry))) {
|
|
1460
|
+
invalid("rollback-path-outside-original-mutation-set");
|
|
1461
|
+
}
|
|
1462
|
+
if (
|
|
1463
|
+
!isPlainObject(plan.originalWorkspaceBinding) ||
|
|
1464
|
+
!validWorkspaceBinding(plan.originalWorkspaceBinding) ||
|
|
1465
|
+
!isPlainObject(plan.expectedWorkspaceBinding) ||
|
|
1466
|
+
!validWorkspaceBinding(plan.expectedWorkspaceBinding)
|
|
1467
|
+
) {
|
|
1468
|
+
invalid("invalid-workspace-binding");
|
|
1469
|
+
}
|
|
1470
|
+
if (
|
|
1471
|
+
plan.originalWorkspaceBinding.workspaceRoot !== plan.workspaceRoot ||
|
|
1472
|
+
plan.expectedWorkspaceBinding.workspaceRoot !== plan.workspaceRoot ||
|
|
1473
|
+
plan.originalWorkspaceBinding.scopeIdentity !==
|
|
1474
|
+
plan.expectedWorkspaceBinding.scopeIdentity ||
|
|
1475
|
+
plan.originalWorkspaceBinding.prestateIdentity !==
|
|
1476
|
+
plan.safetyCheckpoint.treeIdentity ||
|
|
1477
|
+
plan.originalWorkspaceBinding.targetPoststateIdentity !==
|
|
1478
|
+
plan.originalCheckpoint.treeIdentity ||
|
|
1479
|
+
plan.originalWorkspaceBinding.writePlanIdentity !==
|
|
1480
|
+
plan.safetyCheckpoint.planIdentity ||
|
|
1481
|
+
plan.expectedWorkspaceBinding.targetPoststateIdentity !==
|
|
1482
|
+
plan.safetyCheckpoint.treeIdentity
|
|
1483
|
+
) {
|
|
1484
|
+
invalid("inconsistent-workspace-binding");
|
|
1485
|
+
}
|
|
1486
|
+
if (
|
|
1487
|
+
plan.originalPlanAuthority.sourceCheckpointId !==
|
|
1488
|
+
plan.originalCheckpoint.id ||
|
|
1489
|
+
plan.originalPlanAuthority.sourceCheckpointIdentity !==
|
|
1490
|
+
plan.originalCheckpoint.identity ||
|
|
1491
|
+
plan.originalPlanAuthority.safetyPlanIdentity !==
|
|
1492
|
+
plan.safetyCheckpoint.planIdentity ||
|
|
1493
|
+
plan.originalPlanAuthority.mutationSetIdentity !==
|
|
1494
|
+
originalMutationSetIdentity(plan.originalMutationPaths)
|
|
1495
|
+
) {
|
|
1496
|
+
invalid("inconsistent-original-plan-authority");
|
|
1497
|
+
}
|
|
1498
|
+
if (
|
|
1499
|
+
!SHA256_IDENTITY_PATTERN.test(plan.rollbackPrestateDigest) ||
|
|
1500
|
+
plan.rollbackPrestateDigest !==
|
|
1501
|
+
checkpointRollbackDigest(
|
|
1502
|
+
"cc-checkpoint-restore-rollback-prestate-v1",
|
|
1503
|
+
"git",
|
|
1504
|
+
plan.expectedWorkspaceBinding,
|
|
1505
|
+
plan.expectedWorkspaceBinding.prestateIdentity,
|
|
1506
|
+
)
|
|
1507
|
+
) {
|
|
1508
|
+
invalid("invalid-rollback-prestate-digest");
|
|
1509
|
+
}
|
|
1510
|
+
if (
|
|
1511
|
+
!SHA256_IDENTITY_PATTERN.test(plan.expectedRollbackStateDigest) ||
|
|
1512
|
+
plan.expectedRollbackStateDigest !==
|
|
1513
|
+
checkpointRollbackDigest(
|
|
1514
|
+
"cc-checkpoint-restore-rollback-state-v1",
|
|
1515
|
+
"git",
|
|
1516
|
+
plan.expectedWorkspaceBinding,
|
|
1517
|
+
plan.expectedWorkspaceBinding.targetPoststateIdentity,
|
|
1518
|
+
)
|
|
1519
|
+
) {
|
|
1520
|
+
invalid("invalid-rollback-state-digest");
|
|
1521
|
+
}
|
|
1522
|
+
if (
|
|
1523
|
+
!SHA256_IDENTITY_PATTERN.test(plan.rollbackPlanIdentity) ||
|
|
1524
|
+
plan.rollbackPlanIdentity !==
|
|
1525
|
+
plan.expectedWorkspaceBinding.writePlanIdentity
|
|
1526
|
+
) {
|
|
1527
|
+
invalid("invalid-rollback-plan-identity");
|
|
1528
|
+
}
|
|
1529
|
+
return plan;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function freezeCheckpointRollbackPlan(plan) {
|
|
1533
|
+
return Object.freeze({
|
|
1534
|
+
...plan,
|
|
1535
|
+
currentRollbackPaths: Object.freeze([...plan.currentRollbackPaths]),
|
|
1536
|
+
expectedWorkspaceBinding: freezeWorkspaceBinding(
|
|
1537
|
+
plan.expectedWorkspaceBinding,
|
|
1538
|
+
),
|
|
1539
|
+
originalCheckpoint: freezeCheckpointAuthority(plan.originalCheckpoint),
|
|
1540
|
+
originalMutationPaths: Object.freeze([...plan.originalMutationPaths]),
|
|
1541
|
+
originalPlanAuthority: Object.freeze({ ...plan.originalPlanAuthority }),
|
|
1542
|
+
originalWorkspaceBinding: freezeWorkspaceBinding(
|
|
1543
|
+
plan.originalWorkspaceBinding,
|
|
1544
|
+
),
|
|
1545
|
+
safetyCheckpoint: freezeCheckpointAuthority(plan.safetyCheckpoint),
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function validateCheckpointRollbackPreparationOptions(opts) {
|
|
1550
|
+
if (!isPlainObject(opts)) {
|
|
1551
|
+
throw checkpointRollbackError(
|
|
1552
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INVALID_ARGUMENT,
|
|
1553
|
+
"Git checkpoint rollback options must be a plain object",
|
|
1554
|
+
{ reason: "invalid-options" },
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
if (!GIT_CHECKPOINT_IDENTITY_PATTERN.test(opts.expectedOriginalIdentity)) {
|
|
1558
|
+
throw checkpointRollbackError(
|
|
1559
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INVALID_ARGUMENT,
|
|
1560
|
+
"Git checkpoint rollback requires expectedOriginalIdentity",
|
|
1561
|
+
{ reason: "invalid-original-identity" },
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
if (!GIT_CHECKPOINT_IDENTITY_PATTERN.test(opts.expectedSafetyIdentity)) {
|
|
1565
|
+
throw checkpointRollbackError(
|
|
1566
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INVALID_ARGUMENT,
|
|
1567
|
+
"Git checkpoint rollback requires expectedSafetyIdentity",
|
|
1568
|
+
{ reason: "invalid-safety-identity" },
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
if (!SHA256_IDENTITY_PATTERN.test(opts.expectedSafetyPlanIdentity)) {
|
|
1572
|
+
throw checkpointRollbackError(
|
|
1573
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INVALID_ARGUMENT,
|
|
1574
|
+
"Git checkpoint rollback requires expectedSafetyPlanIdentity",
|
|
1575
|
+
{ reason: "invalid-safety-plan-identity" },
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
if (
|
|
1579
|
+
!Number.isSafeInteger(opts.originalMutationTargetCount) ||
|
|
1580
|
+
opts.originalMutationTargetCount < 0
|
|
1581
|
+
) {
|
|
1582
|
+
throw checkpointRollbackError(
|
|
1583
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INVALID_ARGUMENT,
|
|
1584
|
+
"Git checkpoint rollback requires originalMutationTargetCount",
|
|
1585
|
+
{ reason: "invalid-original-target-count" },
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function resolveRollbackCheckpointAuthority(
|
|
1591
|
+
root,
|
|
1592
|
+
idOrRef,
|
|
1593
|
+
session,
|
|
1594
|
+
expectedIdentity,
|
|
1595
|
+
role,
|
|
1596
|
+
) {
|
|
1597
|
+
try {
|
|
1598
|
+
const commit = resolveExpectedCheckpoint(
|
|
1599
|
+
root,
|
|
1600
|
+
idOrRef,
|
|
1601
|
+
session,
|
|
1602
|
+
expectedIdentity,
|
|
1603
|
+
);
|
|
1604
|
+
const tree = git(["rev-parse", `${commit}^{tree}`], { cwd: root });
|
|
1605
|
+
return { commit, tree };
|
|
1606
|
+
} catch (cause) {
|
|
1607
|
+
throw checkpointRollbackError(
|
|
1608
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.AUTHORITY_STALE,
|
|
1609
|
+
`Git checkpoint rollback ${role} authority is unavailable or stale`,
|
|
1610
|
+
{
|
|
1611
|
+
reason: `${role}-checkpoint-authority-stale`,
|
|
1612
|
+
checkpointId: String(idOrRef),
|
|
1613
|
+
expectedIdentity,
|
|
1614
|
+
},
|
|
1615
|
+
cause,
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
export function prepareCheckpointRollback(
|
|
1621
|
+
cwd = process.cwd(),
|
|
1622
|
+
originalIdOrRef,
|
|
1623
|
+
safetyIdOrRef,
|
|
1624
|
+
opts = {},
|
|
1625
|
+
) {
|
|
1626
|
+
if (
|
|
1627
|
+
!validCheckpointReference(originalIdOrRef) ||
|
|
1628
|
+
!validCheckpointReference(safetyIdOrRef)
|
|
1629
|
+
) {
|
|
1630
|
+
throw checkpointRollbackError(
|
|
1631
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INVALID_ARGUMENT,
|
|
1632
|
+
"Git checkpoint rollback requires original and safety checkpoint ids",
|
|
1633
|
+
{ reason: "invalid-checkpoint-reference" },
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
validateCheckpointRollbackPreparationOptions(opts);
|
|
1637
|
+
|
|
1638
|
+
const root = repoRoot(cwd);
|
|
1639
|
+
const dir = gitDir(root);
|
|
1640
|
+
const workspaceRoot = canonicalPath(root);
|
|
1641
|
+
const checkpointNamespace = sanitizeSession(opts.session);
|
|
1642
|
+
const original = resolveRollbackCheckpointAuthority(
|
|
1643
|
+
root,
|
|
1644
|
+
originalIdOrRef,
|
|
1645
|
+
checkpointNamespace,
|
|
1646
|
+
opts.expectedOriginalIdentity,
|
|
1647
|
+
"original",
|
|
1648
|
+
);
|
|
1649
|
+
const safety = resolveRollbackCheckpointAuthority(
|
|
1650
|
+
root,
|
|
1651
|
+
safetyIdOrRef,
|
|
1652
|
+
checkpointNamespace,
|
|
1653
|
+
opts.expectedSafetyIdentity,
|
|
1654
|
+
"safety",
|
|
1655
|
+
);
|
|
1656
|
+
const originalWorkspaceBinding = buildWorkspaceBinding(root, dir, {
|
|
1657
|
+
targetCommit: original.commit,
|
|
1658
|
+
targetTree: original.tree,
|
|
1659
|
+
currentTree: safety.tree,
|
|
1660
|
+
});
|
|
1661
|
+
if (
|
|
1662
|
+
originalWorkspaceBinding.writePlanIdentity !==
|
|
1663
|
+
opts.expectedSafetyPlanIdentity
|
|
1664
|
+
) {
|
|
1665
|
+
throw checkpointRollbackError(
|
|
1666
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.AUTHORITY_STALE,
|
|
1667
|
+
"Git checkpoint rollback safety plan does not bind the original target and safety tree",
|
|
1668
|
+
{
|
|
1669
|
+
reason: "safety-plan-identity-mismatch",
|
|
1670
|
+
expectedSafetyPlanIdentity: opts.expectedSafetyPlanIdentity,
|
|
1671
|
+
actualSafetyPlanIdentity: originalWorkspaceBinding.writePlanIdentity,
|
|
1672
|
+
},
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
const originalMutationPaths = treeDiffPaths(root, original.tree, safety.tree);
|
|
1677
|
+
if (originalMutationPaths.length !== opts.originalMutationTargetCount) {
|
|
1678
|
+
throw checkpointRollbackError(
|
|
1679
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.AUTHORITY_STALE,
|
|
1680
|
+
"Git checkpoint rollback original mutation count is stale",
|
|
1681
|
+
{
|
|
1682
|
+
reason: "original-mutation-count-mismatch",
|
|
1683
|
+
expectedOriginalMutationTargetCount: opts.originalMutationTargetCount,
|
|
1684
|
+
actualOriginalMutationTargetCount: originalMutationPaths.length,
|
|
1685
|
+
},
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
const currentTree = snapshotTree(root, dir);
|
|
1690
|
+
const currentRollbackPaths = treeDiffPaths(root, safety.tree, currentTree);
|
|
1691
|
+
const originalPathSet = new Set(originalMutationPaths);
|
|
1692
|
+
const outsideOriginalMutationSet = currentRollbackPaths.filter(
|
|
1693
|
+
(entry) => !originalPathSet.has(entry),
|
|
1694
|
+
);
|
|
1695
|
+
if (outsideOriginalMutationSet.length > 0) {
|
|
1696
|
+
throw checkpointRollbackError(
|
|
1697
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.UNSAFE_WORKSPACE,
|
|
1698
|
+
"Git checkpoint rollback found live paths outside the original mutation set",
|
|
1699
|
+
{
|
|
1700
|
+
reason: "rollback-path-outside-original-mutation-set",
|
|
1701
|
+
offendingPaths: outsideOriginalMutationSet,
|
|
1702
|
+
},
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
const pathsDifferentFromOriginal = new Set(
|
|
1706
|
+
treeDiffPaths(root, original.tree, currentTree),
|
|
1707
|
+
);
|
|
1708
|
+
const nonTargetResiduePaths = currentRollbackPaths.filter((entry) =>
|
|
1709
|
+
pathsDifferentFromOriginal.has(entry),
|
|
1710
|
+
);
|
|
1711
|
+
if (nonTargetResiduePaths.length > 0) {
|
|
1712
|
+
throw checkpointRollbackError(
|
|
1713
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.UNSAFE_WORKSPACE,
|
|
1714
|
+
"Git checkpoint rollback found non-target content on original mutation paths",
|
|
1715
|
+
{
|
|
1716
|
+
reason: "non-target-crash-residue",
|
|
1717
|
+
offendingPaths: nonTargetResiduePaths,
|
|
1718
|
+
},
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
const expectedWorkspaceBinding = buildWorkspaceBinding(root, dir, {
|
|
1723
|
+
targetCommit: safety.commit,
|
|
1724
|
+
targetTree: safety.tree,
|
|
1725
|
+
currentTree,
|
|
1726
|
+
});
|
|
1727
|
+
const rollbackPrestateDigest = checkpointRollbackDigest(
|
|
1728
|
+
"cc-checkpoint-restore-rollback-prestate-v1",
|
|
1729
|
+
"git",
|
|
1730
|
+
expectedWorkspaceBinding,
|
|
1731
|
+
expectedWorkspaceBinding.prestateIdentity,
|
|
1732
|
+
);
|
|
1733
|
+
const expectedRollbackStateDigest = checkpointRollbackDigest(
|
|
1734
|
+
"cc-checkpoint-restore-rollback-state-v1",
|
|
1735
|
+
"git",
|
|
1736
|
+
expectedWorkspaceBinding,
|
|
1737
|
+
expectedWorkspaceBinding.targetPoststateIdentity,
|
|
1738
|
+
);
|
|
1739
|
+
const plan = {
|
|
1740
|
+
schema: CHECKPOINT_ROLLBACK_PLAN_SCHEMA,
|
|
1741
|
+
version: CHECKPOINT_ROLLBACK_PLAN_VERSION,
|
|
1742
|
+
engine: "git",
|
|
1743
|
+
workspaceRoot,
|
|
1744
|
+
checkpointNamespace,
|
|
1745
|
+
originalCheckpoint: {
|
|
1746
|
+
id: originalIdOrRef,
|
|
1747
|
+
identity: `git:${original.commit}`,
|
|
1748
|
+
treeIdentity: `git-tree:${original.tree}`,
|
|
1749
|
+
},
|
|
1750
|
+
safetyCheckpoint: {
|
|
1751
|
+
id: safetyIdOrRef,
|
|
1752
|
+
identity: `git:${safety.commit}`,
|
|
1753
|
+
treeIdentity: `git-tree:${safety.tree}`,
|
|
1754
|
+
planIdentity: opts.expectedSafetyPlanIdentity,
|
|
1755
|
+
},
|
|
1756
|
+
originalBindingVerification: "exact-checkpoint-tree-reconstruction",
|
|
1757
|
+
originalPlanAuthority: {
|
|
1758
|
+
sourceCheckpointId: originalIdOrRef,
|
|
1759
|
+
sourceCheckpointIdentity: `git:${original.commit}`,
|
|
1760
|
+
safetyPlanIdentity: opts.expectedSafetyPlanIdentity,
|
|
1761
|
+
mutationSetIdentity: originalMutationSetIdentity(originalMutationPaths),
|
|
1762
|
+
bindingReconstructable: true,
|
|
1763
|
+
},
|
|
1764
|
+
originalWorkspaceBinding,
|
|
1765
|
+
originalMutationPaths,
|
|
1766
|
+
originalMutationTargetCount: originalMutationPaths.length,
|
|
1767
|
+
currentRollbackPaths,
|
|
1768
|
+
targetCount: currentRollbackPaths.length,
|
|
1769
|
+
expectedWorkspaceBinding,
|
|
1770
|
+
rollbackPrestateDigest,
|
|
1771
|
+
rollbackPlanIdentity: expectedWorkspaceBinding.writePlanIdentity,
|
|
1772
|
+
expectedRollbackStateDigest,
|
|
1773
|
+
};
|
|
1774
|
+
validateCheckpointRollbackPlan(plan);
|
|
1775
|
+
return freezeCheckpointRollbackPlan(plan);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
function assertCheckpointRollbackLease(workspaceLease, workspaceRoot) {
|
|
1779
|
+
if (
|
|
1780
|
+
!workspaceLease ||
|
|
1781
|
+
typeof workspaceLease.assertOwned !== "function" ||
|
|
1782
|
+
workspaceLease.assertOwned.constructor?.name === "AsyncFunction"
|
|
1783
|
+
) {
|
|
1784
|
+
throw checkpointRollbackError(
|
|
1785
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INVALID_ARGUMENT,
|
|
1786
|
+
"Git checkpoint rollback requires a synchronous workspace lease",
|
|
1787
|
+
{ reason: "invalid-workspace-lease" },
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
if (workspaceLease.canonicalWorkspaceRoot != null) {
|
|
1791
|
+
let leaseRoot;
|
|
1792
|
+
try {
|
|
1793
|
+
leaseRoot = canonicalPath(workspaceLease.canonicalWorkspaceRoot);
|
|
1794
|
+
} catch (cause) {
|
|
1795
|
+
throw checkpointRollbackError(
|
|
1796
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INVALID_ARGUMENT,
|
|
1797
|
+
"Git checkpoint rollback workspace lease root is invalid",
|
|
1798
|
+
{ reason: "invalid-workspace-lease-root" },
|
|
1799
|
+
cause,
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
if (pathIdentity(leaseRoot) !== pathIdentity(workspaceRoot)) {
|
|
1803
|
+
throw checkpointRollbackError(
|
|
1804
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INVALID_ARGUMENT,
|
|
1805
|
+
"Git checkpoint rollback workspace lease does not match the plan",
|
|
1806
|
+
{ reason: "workspace-lease-root-mismatch" },
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
const result = workspaceLease.assertOwned();
|
|
1811
|
+
let then;
|
|
1812
|
+
try {
|
|
1813
|
+
then =
|
|
1814
|
+
result != null &&
|
|
1815
|
+
(typeof result === "object" || typeof result === "function")
|
|
1816
|
+
? result.then
|
|
1817
|
+
: null;
|
|
1818
|
+
} catch (cause) {
|
|
1819
|
+
throw checkpointRollbackError(
|
|
1820
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INVALID_ARGUMENT,
|
|
1821
|
+
"Git checkpoint rollback workspace lease returned an unreadable thenable",
|
|
1822
|
+
{ reason: "unreadable-workspace-lease-thenable" },
|
|
1823
|
+
cause,
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
if (typeof then === "function") {
|
|
1827
|
+
throw checkpointRollbackError(
|
|
1828
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INVALID_ARGUMENT,
|
|
1829
|
+
"Git checkpoint rollback workspace lease must not return a promise",
|
|
1830
|
+
{ reason: "async-workspace-lease" },
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
function validateCheckpointRollbackExecutionOptions(opts, workspaceRoot) {
|
|
1836
|
+
if (
|
|
1837
|
+
!isPlainObject(opts) ||
|
|
1838
|
+
Object.keys(opts).some((key) => key !== "workspaceLease")
|
|
1839
|
+
) {
|
|
1840
|
+
throw checkpointRollbackError(
|
|
1841
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INVALID_ARGUMENT,
|
|
1842
|
+
"Git checkpoint rollback execution only accepts workspaceLease",
|
|
1843
|
+
{ reason: "invalid-execution-options" },
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
assertCheckpointRollbackLease(opts.workspaceLease, workspaceRoot);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
function attachCheckpointRollbackDiagnostics(error, plan, phase) {
|
|
1850
|
+
if (!error || typeof error !== "object") return error;
|
|
1851
|
+
try {
|
|
1852
|
+
error.checkpointRollbackPhase ||= phase;
|
|
1853
|
+
error.rollbackPlanIdentity ||= plan.rollbackPlanIdentity;
|
|
1854
|
+
error.rollbackPrestateDigest ||= plan.rollbackPrestateDigest;
|
|
1855
|
+
error.expectedRollbackStateDigest ||= plan.expectedRollbackStateDigest;
|
|
1856
|
+
error.rollbackTargetCount ??= plan.targetCount;
|
|
1857
|
+
error.originalMutationTargetCount ??= plan.originalMutationTargetCount;
|
|
1858
|
+
} catch {
|
|
1859
|
+
/* best-effort diagnostics never mask the original failure */
|
|
1860
|
+
}
|
|
1861
|
+
return error;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
export function executeCheckpointRollback(
|
|
1865
|
+
cwd = process.cwd(),
|
|
1866
|
+
plan,
|
|
1867
|
+
opts = {},
|
|
1868
|
+
) {
|
|
1869
|
+
validateCheckpointRollbackPlan(plan);
|
|
1870
|
+
validateCheckpointRollbackExecutionOptions(opts, plan.workspaceRoot);
|
|
1871
|
+
const { workspaceLease } = opts;
|
|
1872
|
+
let phase = "revalidate";
|
|
1873
|
+
|
|
1874
|
+
try {
|
|
1875
|
+
const actualPlan = prepareCheckpointRollback(
|
|
1876
|
+
cwd,
|
|
1877
|
+
plan.originalCheckpoint.id,
|
|
1878
|
+
plan.safetyCheckpoint.id,
|
|
1879
|
+
{
|
|
1880
|
+
session: plan.checkpointNamespace,
|
|
1881
|
+
expectedOriginalIdentity: plan.originalCheckpoint.identity,
|
|
1882
|
+
expectedSafetyIdentity: plan.safetyCheckpoint.identity,
|
|
1883
|
+
expectedSafetyPlanIdentity: plan.safetyCheckpoint.planIdentity,
|
|
1884
|
+
originalMutationTargetCount: plan.originalMutationTargetCount,
|
|
1885
|
+
},
|
|
1886
|
+
);
|
|
1887
|
+
assertCheckpointRollbackLease(workspaceLease, actualPlan.workspaceRoot);
|
|
1888
|
+
|
|
1889
|
+
const expectedPlanInstance = rollbackPlanInstanceDigest(plan);
|
|
1890
|
+
const actualPlanInstance = rollbackPlanInstanceDigest(actualPlan);
|
|
1891
|
+
if (expectedPlanInstance !== actualPlanInstance) {
|
|
1892
|
+
throw checkpointRollbackError(
|
|
1893
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.PLAN_STALE,
|
|
1894
|
+
"Git checkpoint rollback plan changed before execution",
|
|
1895
|
+
{
|
|
1896
|
+
reason: "rollback-plan-revalidation-mismatch",
|
|
1897
|
+
expectedPlanInstance,
|
|
1898
|
+
actualPlanInstance,
|
|
1899
|
+
actualRollbackPlanIdentity: actualPlan.rollbackPlanIdentity,
|
|
1900
|
+
},
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// Revalidation already proved the live tree is exactly safety. Do not run
|
|
1905
|
+
// even the zero-diff rewind path: a zero-target recovery is verification,
|
|
1906
|
+
// not a workspace write operation.
|
|
1907
|
+
if (actualPlan.targetCount === 0) {
|
|
1908
|
+
phase = "postflight";
|
|
1909
|
+
assertCheckpointRollbackLease(workspaceLease, actualPlan.workspaceRoot);
|
|
1910
|
+
return Object.freeze({
|
|
1911
|
+
schema: CHECKPOINT_ROLLBACK_RESULT_SCHEMA,
|
|
1912
|
+
version: CHECKPOINT_ROLLBACK_RESULT_VERSION,
|
|
1913
|
+
engine: "git",
|
|
1914
|
+
rolledBackCount: 0,
|
|
1915
|
+
rollbackStateDigest: actualPlan.expectedRollbackStateDigest,
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
phase = "workspace-mutation";
|
|
1920
|
+
const restore = rewindTo(cwd, actualPlan.safetyCheckpoint.id, {
|
|
1921
|
+
session: actualPlan.checkpointNamespace,
|
|
1922
|
+
skipSafety: true,
|
|
1923
|
+
expectedIdentity: actualPlan.safetyCheckpoint.identity,
|
|
1924
|
+
expectedWorkspaceBinding: actualPlan.expectedWorkspaceBinding,
|
|
1925
|
+
onMutationStarted: () =>
|
|
1926
|
+
assertCheckpointRollbackLease(workspaceLease, actualPlan.workspaceRoot),
|
|
1927
|
+
onWorkspaceApplied: () =>
|
|
1928
|
+
assertCheckpointRollbackLease(workspaceLease, actualPlan.workspaceRoot),
|
|
1929
|
+
});
|
|
1930
|
+
assertCheckpointRollbackLease(workspaceLease, actualPlan.workspaceRoot);
|
|
1931
|
+
|
|
1932
|
+
const restoredCount =
|
|
1933
|
+
Number(restore?.modified) +
|
|
1934
|
+
Number(restore?.deleted) +
|
|
1935
|
+
Number(restore?.recreated);
|
|
1936
|
+
const safetyCommit = actualPlan.safetyCheckpoint.identity.slice(4);
|
|
1937
|
+
if (
|
|
1938
|
+
!isPlainObject(restore) ||
|
|
1939
|
+
restore.restored !== true ||
|
|
1940
|
+
restore.dryRun !== false ||
|
|
1941
|
+
restore.target !== safetyCommit ||
|
|
1942
|
+
restore.safetyId !== null ||
|
|
1943
|
+
restore.safetyIdentity !== null ||
|
|
1944
|
+
restore.safetyPlanIdentity != null ||
|
|
1945
|
+
!Number.isSafeInteger(restore.modified) ||
|
|
1946
|
+
restore.modified < 0 ||
|
|
1947
|
+
!Number.isSafeInteger(restore.deleted) ||
|
|
1948
|
+
restore.deleted < 0 ||
|
|
1949
|
+
!Number.isSafeInteger(restore.recreated) ||
|
|
1950
|
+
restore.recreated < 0 ||
|
|
1951
|
+
!Number.isSafeInteger(restoredCount) ||
|
|
1952
|
+
restoredCount !== actualPlan.targetCount
|
|
1953
|
+
) {
|
|
1954
|
+
throw checkpointRollbackError(
|
|
1955
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INVALID_RESULT,
|
|
1956
|
+
"Git checkpoint rollback primitive returned an invalid result",
|
|
1957
|
+
{ reason: "invalid-rewind-result", actualResult: restore },
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
phase = "postflight";
|
|
1962
|
+
const root = repoRoot(cwd);
|
|
1963
|
+
const dir = gitDir(root);
|
|
1964
|
+
const settledTree = snapshotTree(root, dir);
|
|
1965
|
+
const expectedSafetyTree = actualPlan.safetyCheckpoint.treeIdentity.slice(
|
|
1966
|
+
"git-tree:".length,
|
|
1967
|
+
);
|
|
1968
|
+
if (settledTree !== expectedSafetyTree) {
|
|
1969
|
+
throw checkpointRollbackError(
|
|
1970
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INCOMPLETE,
|
|
1971
|
+
"Git checkpoint rollback did not settle at the exact safety tree",
|
|
1972
|
+
{
|
|
1973
|
+
reason: "rollback-poststate-mismatch",
|
|
1974
|
+
expectedTreeIdentity: actualPlan.safetyCheckpoint.treeIdentity,
|
|
1975
|
+
actualTreeIdentity: `git-tree:${settledTree}`,
|
|
1976
|
+
residualPaths: treeDiffPaths(root, expectedSafetyTree, settledTree),
|
|
1977
|
+
},
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
const rollbackStateDigest = checkpointRollbackDigest(
|
|
1981
|
+
"cc-checkpoint-restore-rollback-state-v1",
|
|
1982
|
+
"git",
|
|
1983
|
+
actualPlan.expectedWorkspaceBinding,
|
|
1984
|
+
`git-tree:${settledTree}`,
|
|
1985
|
+
);
|
|
1986
|
+
if (rollbackStateDigest !== actualPlan.expectedRollbackStateDigest) {
|
|
1987
|
+
throw checkpointRollbackError(
|
|
1988
|
+
CHECKPOINT_ROLLBACK_ERROR_CODES.INCOMPLETE,
|
|
1989
|
+
"Git checkpoint rollback state digest does not match the plan",
|
|
1990
|
+
{
|
|
1991
|
+
reason: "rollback-state-digest-mismatch",
|
|
1992
|
+
expectedRollbackStateDigest: actualPlan.expectedRollbackStateDigest,
|
|
1993
|
+
actualRollbackStateDigest: rollbackStateDigest,
|
|
1994
|
+
},
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
assertCheckpointRollbackLease(workspaceLease, actualPlan.workspaceRoot);
|
|
1998
|
+
|
|
1999
|
+
return Object.freeze({
|
|
2000
|
+
schema: CHECKPOINT_ROLLBACK_RESULT_SCHEMA,
|
|
2001
|
+
version: CHECKPOINT_ROLLBACK_RESULT_VERSION,
|
|
2002
|
+
engine: "git",
|
|
2003
|
+
rolledBackCount: restoredCount,
|
|
2004
|
+
rollbackStateDigest,
|
|
2005
|
+
});
|
|
2006
|
+
} catch (error) {
|
|
2007
|
+
throw attachCheckpointRollbackDiagnostics(error, plan, phase);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
/** name-only diff between two trees filtered by status (A/M/D). */
|
|
492
2012
|
function diffNames(root, treeA, treeB, filter) {
|
|
493
2013
|
try {
|
|
494
2014
|
const out = git(
|
|
495
|
-
[
|
|
2015
|
+
[
|
|
2016
|
+
"diff",
|
|
2017
|
+
"--no-renames",
|
|
2018
|
+
"--name-only",
|
|
2019
|
+
`--diff-filter=${filter}`,
|
|
2020
|
+
treeA,
|
|
2021
|
+
treeB,
|
|
2022
|
+
],
|
|
496
2023
|
{ cwd: root },
|
|
497
2024
|
);
|
|
498
2025
|
return out ? out.split("\n").filter(Boolean) : [];
|
|
@@ -568,20 +2095,58 @@ export function showCheckpoint(cwd = process.cwd(), idOrRef, opts = {}) {
|
|
|
568
2095
|
export function deleteCheckpoint(cwd = process.cwd(), idOrRef, opts = {}) {
|
|
569
2096
|
const root = repoRoot(cwd);
|
|
570
2097
|
const session = sanitizeSession(opts.session);
|
|
571
|
-
let
|
|
2098
|
+
let rows;
|
|
2099
|
+
let tipCommit;
|
|
572
2100
|
try {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
2101
|
+
rows = listRefs(root, session, { strict: true });
|
|
2102
|
+
tipCommit = readRefStrict(root, `${sessionPrefix(session)}/_tip`);
|
|
2103
|
+
} catch (cause) {
|
|
2104
|
+
throw checkpointRestoreRetentionUnverifiedError(
|
|
2105
|
+
cause,
|
|
2106
|
+
"Git checkpoint deletion authority could not be enumerated",
|
|
2107
|
+
);
|
|
576
2108
|
}
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
2109
|
+
const tipRow = exactSessionTipRow(rows, tipCommit);
|
|
2110
|
+
let row = rows.find(
|
|
2111
|
+
(candidate) =>
|
|
2112
|
+
candidate.id === idOrRef ||
|
|
2113
|
+
candidate.ref === idOrRef ||
|
|
2114
|
+
candidate.commit === idOrRef,
|
|
2115
|
+
);
|
|
2116
|
+
if (!row) {
|
|
2117
|
+
let commit;
|
|
2118
|
+
try {
|
|
2119
|
+
commit = resolveCheckpoint(root, idOrRef, { session });
|
|
2120
|
+
} catch {
|
|
2121
|
+
return false;
|
|
2122
|
+
}
|
|
2123
|
+
row = rows.find((candidate) => candidate.commit === commit);
|
|
584
2124
|
}
|
|
2125
|
+
if (!row) return false;
|
|
2126
|
+
const remainingRows = rows.filter((candidate) => candidate.ref !== row.ref);
|
|
2127
|
+
const tipUpdate =
|
|
2128
|
+
tipRow?.ref === row.ref
|
|
2129
|
+
? {
|
|
2130
|
+
ref: `${sessionPrefix(session)}/_tip`,
|
|
2131
|
+
expectedCommit: tipCommit,
|
|
2132
|
+
nextRow: previousRetainedSessionTip(root, tipCommit, remainingRows),
|
|
2133
|
+
}
|
|
2134
|
+
: null;
|
|
2135
|
+
|
|
2136
|
+
return withCheckpointRestoreRetention(
|
|
2137
|
+
{
|
|
2138
|
+
workspaceRoot: canonicalPath(root),
|
|
2139
|
+
engine: "git",
|
|
2140
|
+
checkpointNamespace: session,
|
|
2141
|
+
candidates: [{ id: row.id, identity: `git:${row.commit}` }],
|
|
2142
|
+
protectedPolicy: "reject",
|
|
2143
|
+
...checkpointRetentionOptions(opts),
|
|
2144
|
+
},
|
|
2145
|
+
() => {
|
|
2146
|
+
deleteCheckpointRowsExact(root, [row], tipUpdate);
|
|
2147
|
+
return true;
|
|
2148
|
+
},
|
|
2149
|
+
);
|
|
585
2150
|
}
|
|
586
2151
|
|
|
587
2152
|
/**
|
|
@@ -592,25 +2157,51 @@ export function deleteCheckpoint(cwd = process.cwd(), idOrRef, opts = {}) {
|
|
|
592
2157
|
export function clearCheckpoints(cwd = process.cwd(), opts = {}) {
|
|
593
2158
|
const root = repoRoot(cwd);
|
|
594
2159
|
const session = sanitizeSession(opts.session);
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
git(["update-ref", "-d", r.ref], { cwd: root });
|
|
599
|
-
} catch {
|
|
600
|
-
/* best-effort */
|
|
601
|
-
}
|
|
602
|
-
}
|
|
2160
|
+
const tipRef = `${sessionPrefix(session)}/_tip`;
|
|
2161
|
+
let rows;
|
|
2162
|
+
let tipCommit;
|
|
603
2163
|
try {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
2164
|
+
rows = listRefs(root, session, { strict: true });
|
|
2165
|
+
tipCommit = readRefStrict(root, tipRef);
|
|
2166
|
+
} catch (cause) {
|
|
2167
|
+
throw checkpointRestoreRetentionUnverifiedError(
|
|
2168
|
+
cause,
|
|
2169
|
+
"Git checkpoint clear authority could not be enumerated",
|
|
2170
|
+
);
|
|
607
2171
|
}
|
|
608
|
-
|
|
2172
|
+
exactSessionTipRow(rows, tipCommit);
|
|
2173
|
+
if (rows.length === 0 && !tipCommit) return 0;
|
|
2174
|
+
|
|
2175
|
+
return withCheckpointRestoreRetention(
|
|
2176
|
+
{
|
|
2177
|
+
workspaceRoot: canonicalPath(root),
|
|
2178
|
+
engine: "git",
|
|
2179
|
+
checkpointNamespace: session,
|
|
2180
|
+
candidates: rows.map((row) => ({
|
|
2181
|
+
id: row.id,
|
|
2182
|
+
identity: `git:${row.commit}`,
|
|
2183
|
+
})),
|
|
2184
|
+
protectedPolicy: "reject",
|
|
2185
|
+
...checkpointRetentionOptions(opts),
|
|
2186
|
+
},
|
|
2187
|
+
() => {
|
|
2188
|
+
deleteCheckpointRowsExact(
|
|
2189
|
+
root,
|
|
2190
|
+
rows,
|
|
2191
|
+
tipCommit
|
|
2192
|
+
? { ref: tipRef, expectedCommit: tipCommit, nextRow: null }
|
|
2193
|
+
: null,
|
|
2194
|
+
);
|
|
2195
|
+
return rows.length;
|
|
2196
|
+
},
|
|
2197
|
+
);
|
|
609
2198
|
}
|
|
610
2199
|
|
|
611
2200
|
// Exposed for unit tests / advanced callers.
|
|
612
2201
|
export const _internals = {
|
|
613
2202
|
git,
|
|
2203
|
+
MAX_REF_PUBLISH_ATTEMPTS,
|
|
2204
|
+
publishCheckpointRefs,
|
|
614
2205
|
repoRoot,
|
|
615
2206
|
sanitizeSession,
|
|
616
2207
|
snapshotTree,
|