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.
Files changed (337) hide show
  1. package/README.md +61 -11
  2. package/bin/chainlesschain.js +7 -52
  3. package/completions/_cc +209 -0
  4. package/completions/cc.bash +18 -0
  5. package/completions/cc.fish +13 -0
  6. package/completions/cc.ps1 +226 -0
  7. package/package.json +10 -2
  8. package/src/assets/web-panel/assets/{AIOps-Brr1LVcE.js → AIOps-DBLkaTje.js} +1 -1
  9. package/src/assets/web-panel/assets/{ActionButton-DQU74DKG.js → ActionButton-BAy-Qebo.js} +1 -1
  10. package/src/assets/web-panel/assets/{Analytics-CYp9178y.js → Analytics-Dfb1Mlau.js} +3 -3
  11. package/src/assets/web-panel/assets/{AppLayout-DW4SCYsW.js → AppLayout-BbFkN8m0.js} +5 -5
  12. package/src/assets/web-panel/assets/{Artifacts-CYlD_ed1.js → Artifacts-kM4-cEEH.js} +1 -1
  13. package/src/assets/web-panel/assets/{Audit-CWl1rZOi.js → Audit-BSQfYWf3.js} +1 -1
  14. package/src/assets/web-panel/assets/{BackgroundAgents-BOR-LPsa.js → BackgroundAgents-CWDbI_fG.js} +1 -1
  15. package/src/assets/web-panel/assets/{Backup-DLpG7j1X.js → Backup-BxLPbq_Q.js} +1 -1
  16. package/src/assets/web-panel/assets/{BaseInput-B35K9wdr.js → BaseInput-DuIbdrp6.js} +1 -1
  17. package/src/assets/web-panel/assets/{Chat-CwUJb9gB.js → Chat-NEmjGahU.js} +5 -5
  18. package/src/assets/web-panel/assets/{ChatBubbleRenderer-1v6aMS1m.js → ChatBubbleRenderer-DfJEo-5_.js} +1 -1
  19. package/src/assets/web-panel/assets/{Checkbox-pi3-tPNW.js → Checkbox-CaoeG3Ns.js} +1 -1
  20. package/src/assets/web-panel/assets/{Codegen-FyNF35eL.js → Codegen-BFf_g3xY.js} +1 -1
  21. package/src/assets/web-panel/assets/{Col-tGt_JDEr.js → Col-CNOd8mdQ.js} +1 -1
  22. package/src/assets/web-panel/assets/{Community-CC7LonQX.js → Community-BhpDqz-i.js} +1 -1
  23. package/src/assets/web-panel/assets/{Compact-BGXrHHjE.js → Compact-CQ42T7Z6.js} +1 -1
  24. package/src/assets/web-panel/assets/{Compliance-DSW0TBZ8.js → Compliance-DI5PcLMN.js} +1 -1
  25. package/src/assets/web-panel/assets/{Cowork-Bhk7rbvE.js → Cowork-wYYZe0wR.js} +3 -3
  26. package/src/assets/web-panel/assets/{Cron-DxyiQ2NZ.js → Cron-Dlq03DD_.js} +2 -2
  27. package/src/assets/web-panel/assets/{Crosschain-B2ZkxHok.js → Crosschain-BzqS8PP3.js} +1 -1
  28. package/src/assets/web-panel/assets/{DID-yOJV99eJ.js → DID-It55Yem7.js} +2 -2
  29. package/src/assets/web-panel/assets/{Dashboard-CKsttz7D.js → Dashboard-9HkH6oxn.js} +2 -2
  30. package/src/assets/web-panel/assets/{Dropdown-CVFldXSL.js → Dropdown-00e2YWRB.js} +1 -1
  31. package/src/assets/web-panel/assets/{EmailListRenderer-Bufv638J.js → EmailListRenderer-By-ugRDL.js} +1 -1
  32. package/src/assets/web-panel/assets/{FamilyGuardDashboard-4ncjoIjL.js → FamilyGuardDashboard-C396yu5m.js} +1 -1
  33. package/src/assets/web-panel/assets/{Federation-DsknEDoX.js → Federation-BME5sbZY.js} +1 -1
  34. package/src/assets/web-panel/assets/{FormItemContext-Cqe5jWXA.js → FormItemContext-B4TEZCr-.js} +1 -1
  35. package/src/assets/web-panel/assets/{GenericCardRenderer-NAJaFKxc.js → GenericCardRenderer-_9CX9ieY.js} +1 -1
  36. package/src/assets/web-panel/assets/{Git-CszKQpnl.js → Git-BrS6WVSr.js} +2 -2
  37. package/src/assets/web-panel/assets/{Governance-BXGodXi6.js → Governance-BMdCWnKF.js} +1 -1
  38. package/src/assets/web-panel/assets/{Inference-CkoYuG2c.js → Inference-Bcp3vzcA.js} +1 -1
  39. package/src/assets/web-panel/assets/{KnowledgeGraph-DFlEr_T0.js → KnowledgeGraph-DlLuqGY5.js} +1 -1
  40. package/src/assets/web-panel/assets/{Logs-CVaENddA.js → Logs-D-W_6x09.js} +2 -2
  41. package/src/assets/web-panel/assets/MarkdownRenderer-DElGPxCZ.js +1 -0
  42. package/src/assets/web-panel/assets/{Marketplace-CDlLT0fw.js → Marketplace-Dzn2guvI.js} +1 -1
  43. package/src/assets/web-panel/assets/{McpTools-DdwOyszN.js → McpTools-uPict-KL.js} +6 -6
  44. package/src/assets/web-panel/assets/{Memory-DKQnkKuG.js → Memory-BCw43kOb.js} +2 -2
  45. package/src/assets/web-panel/assets/{MobileBridge-DGseqlzJ.js → MobileBridge-DmV8Wv7F.js} +1 -1
  46. package/src/assets/web-panel/assets/{MobileProjects-COBbEOlj.js → MobileProjects-C-HaCmSM.js} +1 -1
  47. package/src/assets/web-panel/assets/{Mtc-O61Vwyli.js → Mtc-C6jxJ34Y.js} +5 -5
  48. package/src/assets/web-panel/assets/{MtcAudit-Dw2SyasS.js → MtcAudit-Za8ExwhQ.js} +5 -5
  49. package/src/assets/web-panel/assets/{Multisig-hSu_l8kf.js → Multisig-KKDbuEgb.js} +3 -3
  50. package/src/assets/web-panel/assets/{NLProgramming-CX1Epurr.js → NLProgramming-Dj97zbCk.js} +1 -1
  51. package/src/assets/web-panel/assets/{Notes-R9xX8o8_.js → Notes-DBXvMraU.js} +4 -4
  52. package/src/assets/web-panel/assets/{NotificationSettings-jtSNLCuK.js → NotificationSettings-DOxITLYl.js} +1 -1
  53. package/src/assets/web-panel/assets/OrderTableRenderer-7DVjZwMM.js +1 -0
  54. package/src/assets/web-panel/assets/{Organization-Pu2WO-U3.js → Organization-D1i73mjb.js} +4 -4
  55. package/src/assets/web-panel/assets/{Overflow-BjnqUyFp.js → Overflow-kW6Q3rvg.js} +1 -1
  56. package/src/assets/web-panel/assets/{P2P-jjkvz44c.js → P2P-C-v_8p2v.js} +2 -2
  57. package/src/assets/web-panel/assets/PdhVaultBrowser-tUbbY64W.js +7 -0
  58. package/src/assets/web-panel/assets/{Permissions-D4yDpivd.js → Permissions-DkHxEZ58.js} +4 -4
  59. package/src/assets/web-panel/assets/{PersonalDataHub-0qsL6iH1.js → PersonalDataHub-DWuMPGrK.js} +5 -5
  60. package/src/assets/web-panel/assets/{Pipeline-Cx7Ybrk2.js → Pipeline-30IYXWWm.js} +1 -1
  61. package/src/assets/web-panel/assets/{Privacy-PbfR_M_V.js → Privacy-DZNQ4RrH.js} +1 -1
  62. package/src/assets/web-panel/assets/{ProjectInit-CyV6Gsub.js → ProjectInit-De2MCElf.js} +2 -2
  63. package/src/assets/web-panel/assets/{ProjectSettings-C7KZ2Wdk.js → ProjectSettings-5uHk9wsC.js} +2 -2
  64. package/src/assets/web-panel/assets/{Projects-DQDjO-yK.js → Projects-XjDfrFLb.js} +1 -1
  65. package/src/assets/web-panel/assets/{Providers-DyFVBBMX.js → Providers-Bgwj6gcE.js} +1 -1
  66. package/src/assets/web-panel/assets/{QrScannerModal-CzB1BC9_.js → QrScannerModal-gKql9umv.js} +1 -1
  67. package/src/assets/web-panel/assets/{QuickAsk-Bk636mC1.js → QuickAsk-BWvmwkh7.js} +1 -1
  68. package/src/assets/web-panel/assets/{Recommend-DoWxHIT3.js → Recommend-Uok4hZPd.js} +1 -1
  69. package/src/assets/web-panel/assets/{RemoteSession-oOVIv9wq.js → RemoteSession-DLJzTk9a.js} +3 -3
  70. package/src/assets/web-panel/assets/{Reputation-BucoReGG.js → Reputation-ZVwIPMSO.js} +1 -1
  71. package/src/assets/web-panel/assets/{Row-DzmOdJU7.js → Row-CN04HxWR.js} +1 -1
  72. package/src/assets/web-panel/assets/{RssFeed-C1tK7KGU.js → RssFeed-BdSrEG4g.js} +3 -3
  73. package/src/assets/web-panel/assets/{Search-CSnInOqk.js → Search-5PwVd0IR.js} +1 -1
  74. package/src/assets/web-panel/assets/{Security-dlejr15l.js → Security-DdBo4zjN.js} +3 -3
  75. package/src/assets/web-panel/assets/{Services-BioPZuNh.js → Services-D3letU7M.js} +2 -2
  76. package/src/assets/web-panel/assets/{Skeleton-DWMibwpC.js → Skeleton--Jb83f1v.js} +1 -1
  77. package/src/assets/web-panel/assets/{Skills-BAAnx1B2.js → Skills-Bg-CulHX.js} +1 -1
  78. package/src/assets/web-panel/assets/{Sla-jWNp4hNO.js → Sla-7QOa7S-X.js} +1 -1
  79. package/src/assets/web-panel/assets/{SpeechSettings-y5kqKha-.js → SpeechSettings-DQwppjRw.js} +1 -1
  80. package/src/assets/web-panel/assets/{SyncSettings-B9GNsc3e.js → SyncSettings-dNPQGL-s.js} +2 -2
  81. package/src/assets/web-panel/assets/{Tasks-BDBYKRen.js → Tasks-Ggs-Z48q.js} +1 -1
  82. package/src/assets/web-panel/assets/{Templates-DRiE5eaN.js → Templates-78TYHgRO.js} +1 -1
  83. package/src/assets/web-panel/assets/{Tenant-BGlAvExF.js → Tenant-xHwwv9vO.js} +1 -1
  84. package/src/assets/web-panel/assets/{Terminal-B8uzrbNY.js → Terminal-BJXSJfm4.js} +2 -2
  85. package/src/assets/web-panel/assets/{TimelineRenderer-Df48iveY.js → TimelineRenderer-fLbjRhJw.js} +1 -1
  86. package/src/assets/web-panel/assets/{Tokens-CcmHndN6.js → Tokens-oMSBSBmB.js} +1 -1
  87. package/src/assets/web-panel/assets/{Trigger-DPkWxmQ2.js → Trigger-CfnY_wX_.js} +1 -1
  88. package/src/assets/web-panel/assets/{Trust-DTb5a5AV.js → Trust-AdyTgYKO.js} +1 -1
  89. package/src/assets/web-panel/assets/{UkeySign-CBmg5vch.js → UkeySign-C5K_81dS.js} +1 -1
  90. package/src/assets/web-panel/assets/{VideoEditing-B0mZrz4O.js → VideoEditing-vcGVDqki.js} +1 -1
  91. package/src/assets/web-panel/assets/{Wallet-Bxs7_mpd.js → Wallet-hhOc3hlY.js} +4 -4
  92. package/src/assets/web-panel/assets/{WebAuthn-C8x3cMxU.js → WebAuthn-CVloC11s.js} +3 -3
  93. package/src/assets/web-panel/assets/{WorkflowEditor-DPrNznrP.js → WorkflowEditor-CYeE32R5.js} +1 -1
  94. package/src/assets/web-panel/assets/{chat-D3bYWu7S.js → chat-CNROR5bN.js} +1 -1
  95. package/src/assets/web-panel/assets/{colors-DY-PwMu4.js → colors-Cjx-cnwU.js} +1 -1
  96. package/src/assets/web-panel/assets/{compact-item-LDk10Kw6.js → compact-item-DOIfelvD.js} +1 -1
  97. package/src/assets/web-panel/assets/{createContext-C-2MZnFq.js → createContext-DfA7UZ8u.js} +1 -1
  98. package/src/assets/web-panel/assets/devWarning-ChcCBGs7.js +1 -0
  99. package/src/assets/web-panel/assets/{hasIn-BQIBjcnB.js → hasIn-BMSCzcfM.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CQVb2dWp.js → index-5-HXgOX5.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BdXnYMCZ.js → index-AWg-0M1S.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Cx6D_dx2.js → index-B4iYzpkL.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-_HRO_cIu.js → index-B5Udeb_6.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DlRYqt3S.js → index-B7gf77CJ.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BrnRO9yE.js → index-BEMwBBHk.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-VCh5zNJo.js → index-BM1y_9hM.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BRI3ngNg.js → index-BUonmKSE.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-C3GNvExG.js → index-BbQ2eQ8H.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DSvo_bp0.js → index-BemJ2u_E.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-L6qixyLc.js → index-BolaOQxx.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BI4KgcYC.js → index-BxOk9vkG.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CY4KnaO9.js → index-By61ojUx.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-L3smcoQX.js → index-C2K9SNVX.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BX2b9Orc.js → index-C3L5gdiD.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Cz4b6Z_7.js → index-C7xn0Nub.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-COG61sRv.js → index-CAnvhwg6.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-C7FC8cP2.js → index-CEKMcOyU.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BN-xdSWZ.js → index-CNC5onm4.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DY2FIrmz.js → index-CVL27kXC.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CiTvBAr-.js → index-CVm1_i3t.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DhRt9cYJ.js → index-CdYBye8B.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-B5pgtZnY.js → index-ClNMrqEY.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-7xKu7Fns.js → index-Cs6l7uRu.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-D4K1weKc.js → index-CshQbilz.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-k7C_oDCO.js → index-Cyweh0Hy.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-D3Oq-DLL.js → index-DBpWXFed.js} +3 -3
  127. package/src/assets/web-panel/assets/{index-DYyLJPlF.js → index-DHtpHZAQ.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Dh-tkpKQ.js → index-DLmn0BoK.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-8JC9BJa1.js → index-DPAWBrMf.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-Dd2Km97U.js → index-DjEsxO5h.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-KFZO_lcx.js → index-FBc_CJBd.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DTIZpPtG.js → index-Icdoqmzt.js} +1 -1
  133. package/src/assets/web-panel/assets/index-Q2IajL54.js +1 -0
  134. package/src/assets/web-panel/assets/index-bRkHgOMc.js +1 -0
  135. package/src/assets/web-panel/assets/{index-lmm9p-jm.js → index-dCd8cSUq.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-HasPt4-h.js → index-m2vG9gG_.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-CaslYch6.js → index-mTpHoEzr.js} +1 -1
  138. package/src/assets/web-panel/assets/{index-CbR6ROOa.js → index-ti_lnTWl.js} +1 -1
  139. package/src/assets/web-panel/assets/{initDefaultProps-zQPSHf2C.js → initDefaultProps-Bu8k7H1Y.js} +1 -1
  140. package/src/assets/web-panel/assets/{motion-CKCStGVk.js → motion-s3iVK41k.js} +1 -1
  141. package/src/assets/web-panel/assets/{move-BufVDGYF.js → move-B6B-E5Jz.js} +1 -1
  142. package/src/assets/web-panel/assets/{mtc-parser-DcaFxy5D.js → mtc-parser-BiYgTidA.js} +1 -1
  143. package/src/assets/web-panel/assets/{omit-EScpkGdo.js → omit-pU1RdUDA.js} +1 -1
  144. package/src/assets/web-panel/assets/{pickAttrs-72hG_qfT.js → pickAttrs-Cdoq_SeM.js} +1 -1
  145. package/src/assets/web-panel/assets/{placementArrow-CMFWiH20.js → placementArrow-DIuvAn2K.js} +1 -1
  146. package/src/assets/web-panel/assets/{responsiveObserve-DKQHS41p.js → responsiveObserve-8w16YR2D.js} +1 -1
  147. package/src/assets/web-panel/assets/{slide-QVAFHTIQ.js → slide-VZ01cZe3.js} +1 -1
  148. package/src/assets/web-panel/assets/{statusUtils-DIz38e2G.js → statusUtils-lh8YMN8Z.js} +1 -1
  149. package/src/assets/web-panel/assets/{styleChecker-zK5cyLby.js → styleChecker-CmDyreec.js} +1 -1
  150. package/src/assets/web-panel/assets/{useFlexGapSupport-DhZngk62.js → useFlexGapSupport-hiJWMU5y.js} +1 -1
  151. package/src/assets/web-panel/assets/{useFs-BvV2cvkg.js → useFs-Thnu-Iaj.js} +1 -1
  152. package/src/assets/web-panel/assets/{usePersonalDataHub-CRmxb9l3.js → usePersonalDataHub-LBdOxjwY.js} +1 -1
  153. package/src/assets/web-panel/assets/{vnode-CgQIbobP.js → vnode-Cg6lFY_X.js} +1 -1
  154. package/src/assets/web-panel/assets/{zoom-D4XrrO0R.js → zoom-DVfsR1YO.js} +1 -1
  155. package/src/assets/web-panel/index.html +1 -1
  156. package/src/cli-help.js +181 -0
  157. package/src/command-help-index.json +181 -0
  158. package/src/command-manifest.json +1107 -175
  159. package/src/command-surface-policy.js +197 -0
  160. package/src/commands/agent.js +98 -33
  161. package/src/commands/artifacts.js +400 -2
  162. package/src/commands/background-session.js +41 -3
  163. package/src/commands/checkpoint-managed.js +1059 -0
  164. package/src/commands/checkpoint-restore-recovery.js +1476 -0
  165. package/src/commands/checkpoint.js +950 -22
  166. package/src/commands/compact.js +101 -25
  167. package/src/commands/config.js +506 -20
  168. package/src/commands/doctor.js +4 -1
  169. package/src/commands/hub.js +1 -1
  170. package/src/commands/mcp.js +688 -24
  171. package/src/commands/pack.js +16 -2
  172. package/src/commands/plugin.js +20 -12
  173. package/src/commands/session-mcp-recovery.js +218 -0
  174. package/src/commands/session.js +470 -110
  175. package/src/commands/setup.js +36 -10
  176. package/src/commands/skill.js +181 -110
  177. package/src/commands/status.js +21 -5
  178. package/src/commands/team-distributed.js +3324 -0
  179. package/src/commands/team.js +1130 -74
  180. package/src/commands/update.js +68 -6
  181. package/src/constants.js +11 -0
  182. package/src/data/changelog.json +26 -0
  183. package/src/gateways/ws/action-protocol.js +10 -0
  184. package/src/gateways/ws/session-protocol.js +555 -27
  185. package/src/gateways/ws/ws-agent-handler.js +541 -14
  186. package/src/gateways/ws/ws-session-gateway.js +129 -22
  187. package/src/gateways/ws/ws-session-state.js +431 -0
  188. package/src/harness/background-task-manager.js +233 -36
  189. package/src/harness/jsonl-session-store.js +2551 -167
  190. package/src/harness/mcp-client.js +802 -61
  191. package/src/harness/prompt-compressor.js +227 -27
  192. package/src/harness/session-index.js +89 -38
  193. package/src/harness/session-list-index.js +254 -0
  194. package/src/harness/structured-handoff.js +410 -0
  195. package/src/lazy-dispatch.js +747 -84
  196. package/src/lib/__tests__/logger.test.js +50 -2
  197. package/src/lib/advisor-runtime.js +1047 -0
  198. package/src/lib/agent-core.js +2 -0
  199. package/src/lib/agent-sandbox.js +120 -26
  200. package/src/lib/agent-session-export.js +20 -0
  201. package/src/lib/agent-team/task-lease.js +271 -3
  202. package/src/lib/agent-team/team-adjudication.js +1501 -0
  203. package/src/lib/agent-team/team-budget.js +4 -1
  204. package/src/lib/agent-team/team-control-store.js +582 -0
  205. package/src/lib/agent-team/team-distributed-queue.js +5246 -0
  206. package/src/lib/agent-team/team-process-checkpoint.js +211 -0
  207. package/src/lib/agent-team/team-runner.js +543 -24
  208. package/src/lib/agent-team/team-worktree.js +1403 -95
  209. package/src/lib/artifact-store.js +131 -8
  210. package/src/lib/background-agent-supervisor.js +185 -13
  211. package/src/lib/background-interaction-journal.js +37 -9
  212. package/src/lib/background-launch-profile.js +1058 -0
  213. package/src/lib/checkpoint-restore-already-completed-controller.js +902 -0
  214. package/src/lib/checkpoint-restore-orchestrator.js +987 -0
  215. package/src/lib/checkpoint-restore-partial-rollback-controller.js +1953 -0
  216. package/src/lib/checkpoint-restore-recovery-controller.js +689 -0
  217. package/src/lib/checkpoint-restore-recovery.js +884 -0
  218. package/src/lib/checkpoint-restore-retention.js +126 -0
  219. package/src/lib/checkpoint-restore-saga.js +5079 -0
  220. package/src/lib/checkpoint-restore-session-recovery.js +1178 -0
  221. package/src/lib/checkpoint-restore-workspace-target-verifier.js +270 -0
  222. package/src/lib/checkpoint-store.js +1688 -97
  223. package/src/lib/checkpoint-timeline-authority.js +522 -0
  224. package/src/lib/checkpoint-timeline.js +642 -0
  225. package/src/lib/config-keys.js +18 -119
  226. package/src/lib/config-manager.js +446 -27
  227. package/src/lib/config-redaction.js +57 -0
  228. package/src/lib/config-schema.js +853 -0
  229. package/src/lib/cowork-mcp-tools.js +5 -0
  230. package/src/lib/cowork-task-runner.js +458 -63
  231. package/src/lib/delivery-coordinator.js +1234 -0
  232. package/src/lib/delivery-evidence.js +450 -0
  233. package/src/lib/doctor-checkup.js +199 -14
  234. package/src/lib/executable-path.js +34 -0
  235. package/src/lib/fatal-handler.js +16 -0
  236. package/src/lib/file-checkpoint.js +4909 -186
  237. package/src/lib/file-lines.js +125 -0
  238. package/src/lib/headless-config-command.js +16 -5
  239. package/src/lib/hook-runner.cjs +139 -38
  240. package/src/lib/hooks-v2-workspace-context.js +10 -0
  241. package/src/lib/impacted-gate-selector.js +424 -0
  242. package/src/lib/interaction-adapter.js +62 -0
  243. package/src/lib/jsonl-session-store.js +1 -35
  244. package/src/lib/logger.js +66 -14
  245. package/src/lib/managed-checkpoint-render.js +60 -0
  246. package/src/lib/managed-tool-checkpoint.js +194 -0
  247. package/src/lib/mcp-call-ledger-store.js +1069 -0
  248. package/src/lib/mcp-call-ledger.js +747 -0
  249. package/src/lib/mcp-client.js +5 -0
  250. package/src/lib/mcp-conflict-scheduler.js +468 -0
  251. package/src/lib/mcp-effect-contract.js +102 -0
  252. package/src/lib/mcp-headers-helper-trust.js +145 -0
  253. package/src/lib/mcp-headers-helper.js +616 -0
  254. package/src/lib/mcp-host-recovery-runtime.js +131 -0
  255. package/src/lib/mcp-ledger-recovery-admission.js +1065 -0
  256. package/src/lib/mcp-recovery-adjudication.js +443 -0
  257. package/src/lib/output-context.js +86 -0
  258. package/src/lib/packer/file-identity.js +170 -0
  259. package/src/lib/packer/index.js +3 -0
  260. package/src/lib/packer/native-update-state.js +277 -0
  261. package/src/lib/packer/pack-update-applier.js +1983 -136
  262. package/src/lib/packer/pack-update-checker.js +34 -0
  263. package/src/lib/packer/pack-update-downloader.js +875 -86
  264. package/src/lib/packer/pack-update-signature.js +86 -0
  265. package/src/lib/packer/pkg-config-generator.js +6 -0
  266. package/src/lib/paths.js +65 -10
  267. package/src/lib/plan-mode.js +831 -14
  268. package/src/lib/plan-persistence.js +540 -0
  269. package/src/lib/plugin-runtime/hooks.js +227 -69
  270. package/src/lib/plugin-runtime/plugin-options.js +189 -44
  271. package/src/lib/pr-automation-policy.js +146 -8
  272. package/src/lib/process-execution-broker/credential-agent.js +7 -0
  273. package/src/lib/process-execution-broker/index.js +491 -13
  274. package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +28 -1
  275. package/src/lib/process-execution-broker/linux-generic-bwrap.js +28 -4
  276. package/src/lib/process-execution-broker/platform-sandbox.js +92 -70
  277. package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
  278. package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
  279. package/src/lib/process-execution-broker/windows-sandbox.cs +284 -19
  280. package/src/lib/process-execution-broker/workspace-transaction.js +4459 -0
  281. package/src/lib/project-instructions.js +165 -29
  282. package/src/lib/project-mcp-trust.js +67 -0
  283. package/src/lib/public-ide-capability-manifest.js +272 -0
  284. package/src/lib/public-ide-capability-validation.js +351 -0
  285. package/src/lib/secret-store.js +267 -43
  286. package/src/lib/secure-file-identity.js +433 -0
  287. package/src/lib/secure-fs.js +1089 -0
  288. package/src/lib/security-audit.js +58 -0
  289. package/src/lib/service-manager.js +34 -13
  290. package/src/lib/session-budget-runtime.js +1452 -0
  291. package/src/lib/session-host-snapshot.js +388 -0
  292. package/src/lib/session-message-provenance.js +429 -0
  293. package/src/lib/session-projection.js +804 -0
  294. package/src/lib/session-resource-budget.js +1495 -0
  295. package/src/lib/settings-hooks.cjs +138 -22
  296. package/src/lib/settings-loader.cjs +25 -2
  297. package/src/lib/side-effect-ledger-store.js +45 -23
  298. package/src/lib/skill-execution-identity.js +217 -0
  299. package/src/lib/skill-loader.js +299 -18
  300. package/src/lib/sub-agent-context.js +236 -28
  301. package/src/lib/sub-agent-registry.js +5 -0
  302. package/src/lib/subagent-contract.js +94 -0
  303. package/src/lib/todo-manager.js +185 -18
  304. package/src/lib/todo-persistence.js +417 -0
  305. package/src/lib/turn-binding-store.js +41 -23
  306. package/src/lib/turn-binding.js +160 -2
  307. package/src/lib/with-file-lock.js +165 -24
  308. package/src/lib/ws-chat-handler.js +21 -0
  309. package/src/program-base.js +18 -0
  310. package/src/repl/advisor-command.js +103 -0
  311. package/src/repl/agent-repl.js +2175 -146
  312. package/src/repl/btw-command.js +93 -10
  313. package/src/repl/clipboard-image.js +119 -0
  314. package/src/repl/config-summary.js +64 -108
  315. package/src/repl/prompt-editor.js +233 -0
  316. package/src/repl/prompt-interactions.js +344 -0
  317. package/src/repl/prompt-stash.js +354 -0
  318. package/src/repl/prompt-suggestions.js +312 -0
  319. package/src/repl/repl-keybindings.js +227 -0
  320. package/src/repl/session-recap.js +217 -0
  321. package/src/repl/slash-command-registry.js +22 -0
  322. package/src/repl/terminal-layout.js +88 -0
  323. package/src/runtime/agent-core.js +1630 -447
  324. package/src/runtime/coding-agent-contract-shared.cjs +13 -1
  325. package/src/runtime/coding-agent-managed-tool-policy.cjs +7 -2
  326. package/src/runtime/diagnostics.js +32 -13
  327. package/src/runtime/headless-runner.js +310 -118
  328. package/src/runtime/headless-stream.js +356 -95
  329. package/src/runtime/mcp-config.js +148 -14
  330. package/src/runtime/policies/agent-policy.js +14 -0
  331. package/src/runtime/status-diagnostics-lite.js +137 -0
  332. package/src/assets/web-panel/assets/MarkdownRenderer-7iWGwZ-r.js +0 -1
  333. package/src/assets/web-panel/assets/OrderTableRenderer-BU2Yqq5H.js +0 -1
  334. package/src/assets/web-panel/assets/PdhVaultBrowser-69ca_WeR.js +0 -7
  335. package/src/assets/web-panel/assets/devWarning-B4dXIJqD.js +0 -1
  336. package/src/assets/web-panel/assets/index-C-C7DaG6.js +0 -1
  337. 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 { rmSync } from "node:fs";
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
- env: env ? { ...process.env, ...env } : process.env,
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
- return (res.stdout || "").toString().trim();
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
- // Chain onto the prior tip (or HEAD) so checkpoints form a readable history.
242
- let parent = null;
243
- try {
244
- parent = git(
245
- ["rev-parse", "--verify", "--quiet", `${sessionPrefix(session)}/_tip`],
246
- {
247
- cwd: root,
248
- },
249
- );
250
- } catch {
251
- /* no tip yet */
252
- }
253
- if (!parent) {
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
- parent = git(["rev-parse", "--verify", "--quiet", "HEAD"], {
256
- cwd: root,
743
+ publishCheckpointRefs(root, {
744
+ checkpointRef: ref,
745
+ tipRef,
746
+ commit,
747
+ expectedTip,
257
748
  });
258
- } catch {
259
- /* fresh repo, root commit */
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
- const message = `cc-checkpoint${label ? `: ${label}` : ""}\n`;
264
- const commitArgs = ["commit-tree", tree];
265
- if (parent) commitArgs.push("-p", parent);
266
- const commit = git(commitArgs, {
267
- cwd: root,
268
- input: message,
269
- env: { ...env, ...CHECKPOINT_IDENTITY },
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 = nextId(root, session);
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); // oldest-first (creatordate)
298
- const excess = rows.length - opts.maxPerSession;
299
- for (let i = 0; i < excess; i++) {
300
- try {
301
- git(["update-ref", "-d", rows[i].ref], { cwd: root });
302
- } catch {
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
- /* pruning is entirely best-effort */
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 = resolveCheckpoint(root, idOrRef, { session });
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] { session, dryRun, skipSafety, now }
419
- * @returns {{ restored, dryRun, target, safetyId, modified, deleted, recreated }}
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 = resolveCheckpoint(root, idOrRef, { session });
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
- safetyId = createCheckpoint(root, {
1123
+ safety = createCheckpoint(root, {
452
1124
  session,
453
1125
  label: `auto: before rewind to ${idOrRef}`,
454
1126
  now: opts.now,
455
- }).id;
1127
+ });
1128
+ safetyId = safety.id;
1129
+ safetyIdentity = `git:${safety.commit}`;
456
1130
  }
457
1131
 
458
- // Write the target tree over the working tree via a temp index.
459
- const tmpIndex = tempIndexPath(dir);
460
- const env = { GIT_INDEX_FILE: tmpIndex };
1132
+ const checkpointIdentity = `git:${targetCommit}`;
1133
+ const safetyPlanIdentity = workspaceBinding.writePlanIdentity;
1134
+ let restorePhase = "safety-ready";
461
1135
  try {
462
- git(["read-tree", targetTree], { cwd: root, env });
463
- git(["checkout-index", "-a", "-f"], { cwd: root, env });
464
- } finally {
465
- rmQuiet(tmpIndex);
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
- // Remove files the target snapshot does not contain (created since). These
469
- // paths come from git's tree diff (repo-relative, git rejects `..` in tree
470
- // entries), but this is a force-delete over the user's working tree — guard
471
- // each resolved path against the repo root before unlinking, as
472
- // defense-in-depth, and keep it best-effort so one locked file can't abort
473
- // the whole rewind.
474
- for (const rel of added) {
475
- const abs = path.resolve(root, rel);
476
- if (!withinRoot(root, abs)) continue; // never delete outside the repo
477
- rmQuiet(abs);
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
- /** name-only diff between two trees filtered by status (A/M/D…). */
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
- ["diff", "--name-only", `--diff-filter=${filter}`, treeA, treeB],
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 commit;
2098
+ let rows;
2099
+ let tipCommit;
572
2100
  try {
573
- commit = resolveCheckpoint(root, idOrRef, { session });
574
- } catch {
575
- return false;
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 row = listRefs(root, session).find((r) => r.commit === commit);
578
- const ref = row ? row.ref : `${sessionPrefix(session)}/${idOrRef}`;
579
- try {
580
- git(["update-ref", "-d", ref], { cwd: root });
581
- return true;
582
- } catch {
583
- return false;
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 rows = listRefs(root, session);
596
- for (const r of rows) {
597
- try {
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
- git(["update-ref", "-d", `${sessionPrefix(session)}/_tip`], { cwd: root });
605
- } catch {
606
- /* _tip may not exist */
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
- return rows.length;
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,