chainlesschain 0.162.188 → 0.162.189

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 (188) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-Brr1LVcE.js → AIOps-DbZft8w0.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-DQU74DKG.js → ActionButton-DoOZpwfN.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-CYp9178y.js → Analytics-B0J0KLTW.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-DW4SCYsW.js → AppLayout-D9eIR0qq.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-CYlD_ed1.js → Artifacts-BnZXEVeo.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-CWl1rZOi.js → Audit-X1ce4Bjn.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-BOR-LPsa.js → BackgroundAgents-BvWR-y5v.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-DLpG7j1X.js → Backup-BublSGEd.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-B35K9wdr.js → BaseInput-CR_77Hso.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-CwUJb9gB.js → Chat-DixW52tq.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-1v6aMS1m.js → ChatBubbleRenderer-DDHDpBBA.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-pi3-tPNW.js → Checkbox-CCv1C87K.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-FyNF35eL.js → Codegen-B03Cc3dd.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-tGt_JDEr.js → Col-CA06-6pU.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-CC7LonQX.js → Community-Bj881J52.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-BGXrHHjE.js → Compact-UZLTPoaL.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-DSW0TBZ8.js → Compliance-kGwzRb_0.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Bhk7rbvE.js → Cowork-KundoYM8.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-DxyiQ2NZ.js → Cron-BYZyoqDe.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-B2ZkxHok.js → Crosschain-BhrrppBX.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-yOJV99eJ.js → DID-CdC6EguZ.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-CKsttz7D.js → Dashboard-CcQx3J4y.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-CVFldXSL.js → Dropdown-BpijtEzQ.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-Bufv638J.js → EmailListRenderer-DfZjeZTe.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-4ncjoIjL.js → FamilyGuardDashboard-BFYSf69C.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-DsknEDoX.js → Federation-Okdi3pJm.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Cqe5jWXA.js → FormItemContext-mH-HebKW.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-NAJaFKxc.js → GenericCardRenderer-xivV69x7.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-CszKQpnl.js → Git-1rh_YBwg.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-BXGodXi6.js → Governance-eAXkNxdY.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CkoYuG2c.js → Inference-CS6dMVTN.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-DFlEr_T0.js → KnowledgeGraph-Bq_TnkvD.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-CVaENddA.js → Logs-KchHtiHt.js} +2 -2
  35. package/src/assets/web-panel/assets/MarkdownRenderer-TabqHW9B.js +1 -0
  36. package/src/assets/web-panel/assets/{Marketplace-CDlLT0fw.js → Marketplace-CsmGcX5X.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-DdwOyszN.js → McpTools-BaPFnnyz.js} +5 -5
  38. package/src/assets/web-panel/assets/{Memory-DKQnkKuG.js → Memory-2m-sUUf7.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-DGseqlzJ.js → MobileBridge-D5nVZBRu.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-Bo8MLZvT.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-O61Vwyli.js → Mtc-3GDvamGa.js} +6 -6
  42. package/src/assets/web-panel/assets/{MtcAudit-Dw2SyasS.js → MtcAudit-D2zGuZCH.js} +4 -4
  43. package/src/assets/web-panel/assets/{Multisig-hSu_l8kf.js → Multisig-BBVQ4t6t.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-CX1Epurr.js → NLProgramming-BIOCyOGp.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-R9xX8o8_.js → Notes-DVwBGpzc.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-jtSNLCuK.js → NotificationSettings-BDnHfIfX.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-BU2Yqq5H.js → OrderTableRenderer-C7Ek20ag.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-Pu2WO-U3.js → Organization-D3SRKaa7.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-BjnqUyFp.js → Overflow-C2B8ahyn.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-jjkvz44c.js → P2P-2iwQzG65.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-69ca_WeR.js → PdhVaultBrowser-CoPiWqMN.js} +4 -4
  52. package/src/assets/web-panel/assets/{Permissions-D4yDpivd.js → Permissions-yV5XNXo5.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-0qsL6iH1.js → PersonalDataHub-uCDJhMUF.js} +5 -5
  54. package/src/assets/web-panel/assets/{Pipeline-Cx7Ybrk2.js → Pipeline-hN7ucMOA.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-PbfR_M_V.js → Privacy-CgU0ptN4.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-CyV6Gsub.js → ProjectInit-CfYPiT6j.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-C7KZ2Wdk.js → ProjectSettings-B0A2C4nw.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-DQDjO-yK.js → Projects-BE5C0ajn.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-DyFVBBMX.js → Providers-CstQPf8W.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-CzB1BC9_.js → QrScannerModal-Co3z_ynk.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-Bk636mC1.js → QuickAsk-DMTS6pQ6.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-DoWxHIT3.js → Recommend-ETw4odUa.js} +1 -1
  63. package/src/assets/web-panel/assets/RemoteSession-B0rkE09d.js +5 -0
  64. package/src/assets/web-panel/assets/{Reputation-BucoReGG.js → Reputation-BTVqQgKq.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-DzmOdJU7.js → Row-CBKKwwtO.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-C1tK7KGU.js → RssFeed-BHUQ438I.js} +2 -2
  67. package/src/assets/web-panel/assets/{Search-CSnInOqk.js → Search-B4NqeUsF.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-dlejr15l.js → Security-BRjoYG5_.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-BioPZuNh.js → Services-CxQzu3p3.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-DWMibwpC.js → Skeleton-BKzwwmWO.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-BAAnx1B2.js → Skills-DrsFcgth.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-jWNp4hNO.js → Sla-D21tmXCH.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-y5kqKha-.js → SpeechSettings-CjyjLfp8.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-B9GNsc3e.js → SyncSettings-Cu45-tnx.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-BDBYKRen.js → Tasks-oMcILYOj.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-DRiE5eaN.js → Templates-Bh-Zcr5R.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-BGlAvExF.js → Tenant-68ns63aI.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-B8uzrbNY.js → Terminal-BLJkFykE.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-Df48iveY.js → TimelineRenderer-xcrJLq-O.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-CcmHndN6.js → Tokens-C6PpFJtf.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DPkWxmQ2.js → Trigger-BaKA7tdM.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-DTb5a5AV.js → Trust-BzhA57Jc.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-CBmg5vch.js → UkeySign-4_U3JkIR.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-B0mZrz4O.js → VideoEditing-162AANxX.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-Bxs7_mpd.js → Wallet-B1viuql_.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-C8x3cMxU.js → WebAuthn-ZVWazhCy.js} +4 -4
  87. package/src/assets/web-panel/assets/{WorkflowEditor-DPrNznrP.js → WorkflowEditor-tzdyUk-1.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-D3bYWu7S.js → chat-DmrHNuwq.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-DY-PwMu4.js → colors-DAJ_M-PK.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-LDk10Kw6.js → compact-item-uIzmYwLs.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-C-2MZnFq.js → createContext-D_ZmuLwb.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-DIrncrV1.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-BQIBjcnB.js → hasIn-CDt_shTL.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-8JC9BJa1.js → index-BAvSmJXW.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-KFZO_lcx.js → index-BDF6EQBs.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CiTvBAr-.js → index-BHvlm0In.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-COG61sRv.js → index-BRa4kJ_S.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-lmm9p-jm.js → index-BTCXf1t-.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-DTIZpPtG.js → index-Bbxjfstq.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-_HRO_cIu.js → index-Bx4d6c1k.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BRI3ngNg.js → index-ByzGvpR9.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-C3GNvExG.js → index-C16fYKFD.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D3Oq-DLL.js → index-C5WcdJH2.js} +3 -3
  104. package/src/assets/web-panel/assets/{index-L3smcoQX.js → index-CG0lY4h3.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Dh-tkpKQ.js → index-CKyd472S.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CQVb2dWp.js → index-CaelyMgF.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DY2FIrmz.js → index-CbD_WW_6.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DYyLJPlF.js → index-CcZ9xJI9.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-C7FC8cP2.js → index-CeUpKjvi.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BN-xdSWZ.js → index-Cvak6Oe2.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DSvo_bp0.js → index-CzXqvMuP.js} +1 -1
  112. package/src/assets/web-panel/assets/index-D-Swwqld.js +1 -0
  113. package/src/assets/web-panel/assets/{index-BdXnYMCZ.js → index-D4n1lGTB.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-L6qixyLc.js → index-D9OE8B3d.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-VCh5zNJo.js → index-DBFmcaCQ.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DlRYqt3S.js → index-DdUMcwRB.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-k7C_oDCO.js → index-DeGBoLQA.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-7xKu7Fns.js → index-DfSSy9uy.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CY4KnaO9.js → index-DqDSF2RW.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-HasPt4-h.js → index-DvFhhe6r.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CaslYch6.js → index-DwfvPe-j.js} +1 -1
  122. package/src/assets/web-panel/assets/index-FMZAMKy8.js +1 -0
  123. package/src/assets/web-panel/assets/{index-B5pgtZnY.js → index-FyplHuNt.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-Cz4b6Z_7.js → index-GcKOHD_c.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BI4KgcYC.js → index-OzTxFIJP.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BX2b9Orc.js → index-QsgfSZrV.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CbR6ROOa.js → index-Z8P4BexE.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BrnRO9yE.js → index-clSX4PEc.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Dd2Km97U.js → index-ifxmu8SY.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-DhRt9cYJ.js → index-rS3gl_68.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-D4K1weKc.js → index-rXdwUeE3.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-Cx6D_dx2.js → index-saf3VNeP.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-zQPSHf2C.js → initDefaultProps-pmfaD-aK.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-CKCStGVk.js → motion-CaQ2CpYF.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-BufVDGYF.js → move-CkKTqNbI.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-DcaFxy5D.js → mtc-parser-Dd4xsgRI.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-EScpkGdo.js → omit-BFuA5YEs.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-72hG_qfT.js → pickAttrs-DDfxOAJc.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-CMFWiH20.js → placementArrow-Be595JCp.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-DKQHS41p.js → responsiveObserve-7byljUM3.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-QVAFHTIQ.js → slide-CTWfOTaY.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-DIz38e2G.js → statusUtils-CNZOuIxM.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-zK5cyLby.js → styleChecker-DwOcBV78.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DhZngk62.js → useFlexGapSupport-B6MNwAO9.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-BvV2cvkg.js → useFs-CzyQjH9r.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-CRmxb9l3.js → usePersonalDataHub-Dxh3KiVA.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-CgQIbobP.js → vnode-DVfZiS5N.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-D4XrrO0R.js → zoom-BYme7fbY.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agent.js +22 -0
  151. package/src/commands/checkpoint-managed.js +1059 -0
  152. package/src/commands/checkpoint.js +3 -0
  153. package/src/commands/team-distributed.js +3324 -0
  154. package/src/commands/team.js +1130 -74
  155. package/src/data/changelog.json +13 -0
  156. package/src/lib/agent-core.js +1 -0
  157. package/src/lib/agent-team/task-lease.js +271 -3
  158. package/src/lib/agent-team/team-adjudication.js +1501 -0
  159. package/src/lib/agent-team/team-budget.js +4 -1
  160. package/src/lib/agent-team/team-control-store.js +582 -0
  161. package/src/lib/agent-team/team-distributed-queue.js +5246 -0
  162. package/src/lib/agent-team/team-process-checkpoint.js +211 -0
  163. package/src/lib/agent-team/team-runner.js +319 -22
  164. package/src/lib/agent-team/team-worktree.js +1403 -95
  165. package/src/lib/managed-checkpoint-render.js +60 -0
  166. package/src/lib/managed-tool-checkpoint.js +194 -0
  167. package/src/lib/process-execution-broker/index.js +438 -5
  168. package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +28 -1
  169. package/src/lib/process-execution-broker/linux-generic-bwrap.js +28 -4
  170. package/src/lib/process-execution-broker/platform-sandbox.js +92 -70
  171. package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
  172. package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
  173. package/src/lib/process-execution-broker/windows-sandbox.cs +284 -19
  174. package/src/lib/process-execution-broker/workspace-transaction.js +3556 -0
  175. package/src/lib/secure-file-identity.js +433 -0
  176. package/src/lib/turn-binding.js +160 -2
  177. package/src/lib/with-file-lock.js +140 -23
  178. package/src/repl/agent-repl.js +19 -1
  179. package/src/runtime/agent-core.js +366 -122
  180. package/src/runtime/headless-runner.js +12 -0
  181. package/src/runtime/headless-stream.js +3 -0
  182. package/src/runtime/policies/agent-policy.js +14 -0
  183. package/src/assets/web-panel/assets/MarkdownRenderer-7iWGwZ-r.js +0 -1
  184. package/src/assets/web-panel/assets/MobileProjects-COBbEOlj.js +0 -1
  185. package/src/assets/web-panel/assets/RemoteSession-oOVIv9wq.js +0 -5
  186. package/src/assets/web-panel/assets/devWarning-B4dXIJqD.js +0 -1
  187. package/src/assets/web-panel/assets/index-C-C7DaG6.js +0 -1
  188. package/src/assets/web-panel/assets/index-DkfWzWtd.js +0 -1
@@ -48,9 +48,32 @@ export const _processDeps = {
48
48
  };
49
49
 
50
50
  export const MAX_TEAM_WORKTREE_STDERR_BYTES = 64 * 1024;
51
- const TEAM_WORKTREE_SNAPSHOT_VERSION = 3;
51
+ const TEAM_WORKTREE_SNAPSHOT_VERSION = 5;
52
+ const PUBLISHED_TEAM_WORKTREE_SNAPSHOT_VERSION = 3;
53
+ const LEGACY_TEAM_WORKTREE_SNAPSHOT_VERSION = 4;
52
54
  const MAX_RUN_ID_BYTES = 256;
53
55
  const MAX_RECORD_VALUE_BYTES = 4096;
56
+ const CHECKPOINT_DIGEST = /^sha256:[a-f0-9]{64}$/;
57
+ const CHECKPOINT_COVERAGE = new Set(["full", "partial", "none"]);
58
+ const CHECKPOINT_STATES = new Set([
59
+ "preparing",
60
+ "prepared",
61
+ "running",
62
+ "rollback_required",
63
+ "committed",
64
+ "rolled_back",
65
+ "rollback_failed",
66
+ "restoring",
67
+ "restored",
68
+ "restore_failed",
69
+ "aborted",
70
+ ]);
71
+ const CHECKPOINT_TERMINAL_STATES = new Set([
72
+ "committed",
73
+ "rolled_back",
74
+ "restored",
75
+ "aborted",
76
+ ]);
54
77
 
55
78
  /**
56
79
  * Resolve the sparse-checkout / dependency-symlink options for a task, letting a
@@ -102,10 +125,42 @@ function requireAdjudication(error, code) {
102
125
  error instanceof Error ? error : new Error(String(error || code));
103
126
  failure.code = failure.code || code;
104
127
  failure.retryable = false;
128
+ failure.adjudication ||= {
129
+ code: failure.code,
130
+ reason: failure.message,
131
+ evidenceDigest: null,
132
+ };
133
+ return failure;
134
+ }
135
+
136
+ function abortReason(signal, code, message) {
137
+ const reason = signal?.reason;
138
+ if (
139
+ reason instanceof Error &&
140
+ (reason.name !== "AbortError" ||
141
+ typeof reason.code === "string" ||
142
+ reason.adjudication ||
143
+ reason.interruptEvidence)
144
+ ) {
145
+ return reason;
146
+ }
147
+ const failure = new Error(
148
+ reason == null ? message : safeProcessError(reason, message),
149
+ );
150
+ failure.code = code;
151
+ if (reason && (typeof reason === "object" || typeof reason === "function")) {
152
+ for (const field of ["adjudication", "interruptEvidence", "requestId"]) {
153
+ if (reason[field] !== undefined) failure[field] = reason[field];
154
+ }
155
+ }
105
156
  return failure;
106
157
  }
107
158
 
108
- function defaultRunShell(command, cwd, { signal = null } = {}) {
159
+ function defaultRunShell(
160
+ command,
161
+ cwd,
162
+ { signal = null, managedCheckpoint = false } = {},
163
+ ) {
109
164
  return new Promise((resolve, reject) => {
110
165
  let child;
111
166
  try {
@@ -116,7 +171,8 @@ function defaultRunShell(command, cwd, { signal = null } = {}) {
116
171
  origin: "team-worktree:task-command",
117
172
  policy: "allow",
118
173
  scope: "team-worktree",
119
- detached: process.platform !== "win32",
174
+ detached: managedCheckpoint ? false : process.platform !== "win32",
175
+ ...(managedCheckpoint ? { requiredBoundaries: ["process-tree"] } : {}),
120
176
  });
121
177
  } catch (error) {
122
178
  reject(error instanceof Error ? error : new Error(String(error)));
@@ -129,6 +185,7 @@ function defaultRunShell(command, cwd, { signal = null } = {}) {
129
185
  let terminationError = null;
130
186
  let terminationTimer = null;
131
187
  let abortListener = null;
188
+ let processError = null;
132
189
  const finish = (error, value) => {
133
190
  if (settled) return;
134
191
  settled = true;
@@ -141,14 +198,29 @@ function defaultRunShell(command, cwd, { signal = null } = {}) {
141
198
  };
142
199
  const terminate = () => {
143
200
  if (settled || terminationError) return;
144
- terminationError = new Error("Team worktree shell task was cancelled");
145
- terminationError.code = "TEAM_WORKTREE_SHELL_ABORTED";
201
+ terminationError = abortReason(
202
+ signal,
203
+ "TEAM_WORKTREE_SHELL_ABORTED",
204
+ "Team worktree shell task was cancelled",
205
+ );
146
206
  try {
147
207
  _processDeps.killProcessTree(child);
148
208
  } catch {
149
209
  /* wait for close or the bounded grace timer */
150
210
  }
151
- terminationTimer = setTimeout(() => finish(terminationError), 5000);
211
+ terminationTimer = setTimeout(() => {
212
+ if (managedCheckpoint) {
213
+ // A managed checkpoint may not begin rollback while any writer could
214
+ // still be alive. Retry termination, but keep waiting for `close`.
215
+ try {
216
+ _processDeps.killProcessTree(child);
217
+ } catch {
218
+ /* the durable transaction remains running/fail-closed */
219
+ }
220
+ return;
221
+ }
222
+ finish(terminationError);
223
+ }, 5000);
152
224
  };
153
225
 
154
226
  // A piped stdout that nobody consumes can fill its OS buffer and deadlock a
@@ -172,14 +244,11 @@ function defaultRunShell(command, cwd, { signal = null } = {}) {
172
244
  });
173
245
  child.once("error", (error) => {
174
246
  if (!terminationError) {
175
- finish(
176
- new Error(
177
- safeProcessError(
178
- error?.message || error,
179
- "command failed to start",
180
- ),
181
- ),
247
+ const failure = new Error(
248
+ safeProcessError(error?.message || error, "command failed to start"),
182
249
  );
250
+ if (managedCheckpoint) processError = failure;
251
+ else finish(failure);
183
252
  }
184
253
  });
185
254
  child.once("close", (code) => {
@@ -187,6 +256,10 @@ function defaultRunShell(command, cwd, { signal = null } = {}) {
187
256
  finish(terminationError);
188
257
  return;
189
258
  }
259
+ if (processError) {
260
+ finish(processError);
261
+ return;
262
+ }
190
263
  if (code === 0) {
191
264
  finish(null, { code });
192
265
  return;
@@ -500,6 +573,232 @@ function normalizeManagedLinks(value, key) {
500
573
  return links;
501
574
  }
502
575
 
576
+ function normalizeDependencyKeys(task, key) {
577
+ const value = task?.dependsOn ?? task?.metadata?.dependsOn ?? [];
578
+ if (!Array.isArray(value)) {
579
+ throw new TypeError(`invalid dependencies for team worktree "${key}"`);
580
+ }
581
+ const seen = new Set();
582
+ const dependencies = [];
583
+ for (const item of value) {
584
+ const dependency = requireRecordString(item, "dependency key");
585
+ if (dependency === key) {
586
+ throw new TypeError(`team worktree "${key}" cannot depend on itself`);
587
+ }
588
+ if (seen.has(dependency)) {
589
+ throw new TypeError(
590
+ `duplicate dependency "${dependency}" for team worktree "${key}"`,
591
+ );
592
+ }
593
+ seen.add(dependency);
594
+ dependencies.push(dependency);
595
+ }
596
+ return dependencies.sort();
597
+ }
598
+
599
+ function normalizeDependencyCommits(value, key) {
600
+ if (!Array.isArray(value)) {
601
+ throw new TypeError(
602
+ `invalid team worktree dependency commits for "${key}"`,
603
+ );
604
+ }
605
+ const seen = new Set();
606
+ const commits = value.map((item) => {
607
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
608
+ throw new TypeError(
609
+ `invalid team worktree dependency commit for "${key}"`,
610
+ );
611
+ }
612
+ const dependencyKey = requireRecordString(item.key, "dependency key");
613
+ if (dependencyKey === key || seen.has(dependencyKey)) {
614
+ throw new TypeError(
615
+ `invalid team worktree dependency order for "${key}"`,
616
+ );
617
+ }
618
+ seen.add(dependencyKey);
619
+ const branch = requireRecordString(item.branch, "dependency branch");
620
+ if (!isGitSafeBranch(branch)) {
621
+ throw new TypeError(
622
+ `invalid team worktree dependency branch for "${key}"`,
623
+ );
624
+ }
625
+ return {
626
+ key: dependencyKey,
627
+ branch,
628
+ commitOid: requireGitOid(
629
+ item.commitOid,
630
+ `dependencyCommits.${dependencyKey}.commitOid`,
631
+ ),
632
+ };
633
+ });
634
+ const stable = [...commits].sort((left, right) =>
635
+ left.key < right.key ? -1 : left.key > right.key ? 1 : 0,
636
+ );
637
+ if (stable.some((item, index) => item.key !== commits[index].key)) {
638
+ throw new TypeError(`unstable team worktree dependency order for "${key}"`);
639
+ }
640
+ return commits;
641
+ }
642
+
643
+ function optionalCheckpointDigest(value, field, key) {
644
+ if (value == null) return null;
645
+ if (typeof value !== "string" || !CHECKPOINT_DIGEST.test(value)) {
646
+ throw new TypeError(
647
+ `invalid team worktree checkpoint ${field} for "${key}"`,
648
+ );
649
+ }
650
+ return value;
651
+ }
652
+
653
+ function normalizeCheckpointPaths(value, key) {
654
+ if (!Array.isArray(value)) {
655
+ throw new TypeError(
656
+ `invalid team worktree checkpoint uncoveredPaths for "${key}"`,
657
+ );
658
+ }
659
+ const paths = value.map((item) =>
660
+ requireRecordString(item, "checkpoint.uncoveredPaths"),
661
+ );
662
+ if (new Set(paths).size !== paths.length) {
663
+ throw new TypeError(
664
+ `duplicate team worktree checkpoint uncovered path for "${key}"`,
665
+ );
666
+ }
667
+ return [...paths];
668
+ }
669
+
670
+ function normalizeWorktreeCheckpoint(value, key) {
671
+ if (value == null) return null;
672
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
673
+ throw new TypeError(`invalid team worktree checkpoint for "${key}"`);
674
+ }
675
+ const state = requireRecordString(value.state, "checkpoint.state");
676
+ const coverage = requireRecordString(value.coverage, "checkpoint.coverage");
677
+ const fileCoverage = requireRecordString(
678
+ value.fileCoverage,
679
+ "checkpoint.fileCoverage",
680
+ );
681
+ if (
682
+ !CHECKPOINT_STATES.has(state) ||
683
+ !CHECKPOINT_COVERAGE.has(coverage) ||
684
+ !CHECKPOINT_COVERAGE.has(fileCoverage) ||
685
+ value.requestedCoverage !== "partial" ||
686
+ typeof value.externalSideEffects !== "boolean"
687
+ ) {
688
+ throw new TypeError(
689
+ `invalid team worktree checkpoint coverage/state for "${key}"`,
690
+ );
691
+ }
692
+ const uncoveredPaths = normalizeCheckpointPaths(value.uncoveredPaths, key);
693
+ if (
694
+ coverage === "full" &&
695
+ (value.externalSideEffects || uncoveredPaths.length > 0)
696
+ ) {
697
+ throw new TypeError(
698
+ `team worktree checkpoint overstates full coverage for "${key}"`,
699
+ );
700
+ }
701
+ const updatedAt = requireRecordString(
702
+ value.updatedAt,
703
+ "checkpoint.updatedAt",
704
+ );
705
+ if (!Number.isFinite(Date.parse(updatedAt))) {
706
+ throw new TypeError(
707
+ `invalid team worktree checkpoint timestamp for "${key}"`,
708
+ );
709
+ }
710
+ const taskKey = requireRecordString(value.taskKey, "checkpoint.taskKey");
711
+ if (taskKey !== key) {
712
+ throw new TypeError(
713
+ `team worktree checkpoint task binding changed for "${key}"`,
714
+ );
715
+ }
716
+ return {
717
+ transactionId: requireRecordString(
718
+ value.transactionId,
719
+ "checkpoint.transactionId",
720
+ ),
721
+ checkpointId: requireRecordString(
722
+ value.checkpointId,
723
+ "checkpoint.checkpointId",
724
+ ),
725
+ runId: requireRecordString(value.runId, "checkpoint.runId"),
726
+ taskKey,
727
+ workspaceRoot: requireRecordString(
728
+ value.workspaceRoot,
729
+ "checkpoint.workspaceRoot",
730
+ ),
731
+ state,
732
+ stateDir: requireRecordString(value.stateDir, "checkpoint.stateDir"),
733
+ writerIsolation: requireRecordString(
734
+ value.writerIsolation,
735
+ "checkpoint.writerIsolation",
736
+ ),
737
+ requestedCoverage: "partial",
738
+ coverage,
739
+ fileCoverage,
740
+ externalSideEffects: value.externalSideEffects,
741
+ uncoveredPaths,
742
+ checkpointDigest: optionalCheckpointDigest(
743
+ value.checkpointDigest,
744
+ "checkpointDigest",
745
+ key,
746
+ ),
747
+ writeManifestDigest: optionalCheckpointDigest(
748
+ value.writeManifestDigest,
749
+ "writeManifestDigest",
750
+ key,
751
+ ),
752
+ evidenceDigest: optionalCheckpointDigest(
753
+ value.evidenceDigest,
754
+ "evidenceDigest",
755
+ key,
756
+ ),
757
+ updatedAt,
758
+ recoveryRequired: !CHECKPOINT_TERMINAL_STATES.has(state),
759
+ failureCode:
760
+ value.failureCode == null
761
+ ? null
762
+ : requireRecordString(value.failureCode, "checkpoint.failureCode"),
763
+ };
764
+ }
765
+
766
+ export function summarizeWorktreeCheckpoint(snapshot, key) {
767
+ if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
768
+ throw new TypeError(
769
+ `invalid workspace transaction snapshot for team task "${key}"`,
770
+ );
771
+ }
772
+ const latestFailure = Array.isArray(snapshot.failureEvidence)
773
+ ? snapshot.failureEvidence.at(-1)
774
+ : null;
775
+ return normalizeWorktreeCheckpoint(
776
+ {
777
+ transactionId: snapshot.id,
778
+ checkpointId: snapshot.checkpointId,
779
+ runId: snapshot.runId,
780
+ taskKey: snapshot.taskKey,
781
+ workspaceRoot: snapshot.workspaceRoot,
782
+ state: snapshot.state,
783
+ stateDir: snapshot.stateDir,
784
+ writerIsolation: snapshot.writerIsolation || "unknown",
785
+ requestedCoverage: "partial",
786
+ coverage: snapshot.coverage || "none",
787
+ fileCoverage: snapshot.fileCoverage || "none",
788
+ externalSideEffects: snapshot.externalSideEffects !== false,
789
+ uncoveredPaths: Array.isArray(snapshot.uncoveredPaths)
790
+ ? snapshot.uncoveredPaths
791
+ : [],
792
+ checkpointDigest: snapshot.checkpoint?.digest || null,
793
+ writeManifestDigest: snapshot.writeManifest?.digest || null,
794
+ evidenceDigest: snapshot.evidence?.evidenceDigest || null,
795
+ updatedAt: snapshot.updatedAt,
796
+ failureCode: latestFailure?.code || null,
797
+ },
798
+ key,
799
+ );
800
+ }
801
+
503
802
  export class TeamWorktreeCoordinator {
504
803
  constructor(repoDir, options = {}) {
505
804
  this.repoDir = repoDir;
@@ -514,6 +813,12 @@ export class TeamWorktreeCoordinator {
514
813
  };
515
814
  this._onWorktree =
516
815
  typeof options.onWorktree === "function" ? options.onWorktree : null;
816
+ this._checkpointBroker = options.checkpointBroker || null;
817
+ this._onCheckpoint =
818
+ typeof options.onCheckpoint === "function" ? options.onCheckpoint : null;
819
+ this._checkpointExclusions = Array.isArray(options.checkpointExclusions)
820
+ ? [...options.checkpointExclusions]
821
+ : [];
517
822
  const recovered =
518
823
  options.snapshot ??
519
824
  options.recover ??
@@ -608,6 +913,422 @@ export class TeamWorktreeCoordinator {
608
913
  )}`;
609
914
  }
610
915
 
916
+ _checkpointRecord(key, record) {
917
+ return {
918
+ key,
919
+ branch: record.branch,
920
+ path: record.path,
921
+ committed: record.committed === true,
922
+ completed: record.completed === true,
923
+ commitOid: record.commitOid || null,
924
+ dependencyCommits: record.dependencyCommits.map((dependency) => ({
925
+ ...dependency,
926
+ })),
927
+ baselineCommitOid: record.baselineCommitOid,
928
+ imported: record.imported === true,
929
+ integration: { ...record.integration },
930
+ managedLinks: [...(record.managedLinks || [])],
931
+ workspaceCheckpoint: record.workspaceCheckpoint
932
+ ? {
933
+ ...record.workspaceCheckpoint,
934
+ uncoveredPaths: [...record.workspaceCheckpoint.uncoveredPaths],
935
+ }
936
+ : null,
937
+ cleanupPrepared: record.cleanupPrepared === true,
938
+ cleaned: record.cleaned === true,
939
+ };
940
+ }
941
+
942
+ _publishCheckpoint(key, record, phase, details = {}) {
943
+ if (!this._onCheckpoint) return;
944
+ this._onCheckpoint({
945
+ key,
946
+ phase,
947
+ ...details,
948
+ checkpoint: record.workspaceCheckpoint
949
+ ? {
950
+ ...record.workspaceCheckpoint,
951
+ uncoveredPaths: [...record.workspaceCheckpoint.uncoveredPaths],
952
+ }
953
+ : null,
954
+ worktree: this._checkpointRecord(key, record),
955
+ });
956
+ }
957
+
958
+ _updateCheckpoint(key, record, guard, phase, details = {}) {
959
+ record.workspaceCheckpoint = summarizeWorktreeCheckpoint(
960
+ guard.snapshot(),
961
+ key,
962
+ );
963
+ this._publishCheckpoint(key, record, phase, details);
964
+ return record.workspaceCheckpoint;
965
+ }
966
+
967
+ /**
968
+ * Import a completed task result produced by another worker process. Callers
969
+ * register dependencies in topological order before running a downstream
970
+ * task. The same branch/path/OID checks used by snapshot recovery apply, so a
971
+ * stale or replaced worker result cannot become merge authority.
972
+ */
973
+ registerCompletedDependency(key, result) {
974
+ requireRecordString(key, "key");
975
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
976
+ throw new TypeError(`invalid completed dependency result for "${key}"`);
977
+ }
978
+ const record = {
979
+ key,
980
+ branch: result.branch,
981
+ path: result.worktreePath,
982
+ committed: result.committed,
983
+ completed: true,
984
+ commitOid: result.commitOid,
985
+ dependencyCommits: result.dependencyCommits || [],
986
+ baselineCommitOid: result.baselineCommitOid,
987
+ imported: true,
988
+ integration: emptyIntegrationState(),
989
+ managedLinks: result.managedLinks || [],
990
+ workspaceCheckpoint: result.workspaceCheckpoint || null,
991
+ cleanupPrepared: false,
992
+ cleaned: false,
993
+ };
994
+ const existing = this._created.get(key);
995
+ if (existing) {
996
+ const same =
997
+ existing.completed === true &&
998
+ existing.branch === record.branch &&
999
+ existing.path === record.path &&
1000
+ existing.committed === record.committed &&
1001
+ existing.commitOid === record.commitOid &&
1002
+ existing.baselineCommitOid === record.baselineCommitOid &&
1003
+ existing.imported === true &&
1004
+ JSON.stringify(existing.dependencyCommits) ===
1005
+ JSON.stringify(record.dependencyCommits) &&
1006
+ JSON.stringify(existing.managedLinks) ===
1007
+ JSON.stringify(record.managedLinks) &&
1008
+ JSON.stringify(existing.workspaceCheckpoint) ===
1009
+ JSON.stringify(record.workspaceCheckpoint);
1010
+ if (!same) {
1011
+ throw requireAdjudication(
1012
+ new Error(`completed dependency result for "${key}" changed`),
1013
+ "TEAM_WORKTREE_DEPENDENCY_ADJUDICATION_REQUIRED",
1014
+ );
1015
+ }
1016
+ const branchHead = _deps.resolveGitRef(this.repoDir, existing.branch);
1017
+ const worktreeHead = _deps.resolveGitRef(existing.path, "HEAD");
1018
+ if (
1019
+ branchHead !== existing.commitOid ||
1020
+ worktreeHead !== existing.commitOid ||
1021
+ !_deps.isWorktreeClean(existing.path)
1022
+ ) {
1023
+ throw requireAdjudication(
1024
+ new Error(`completed dependency result for "${key}" drifted`),
1025
+ "TEAM_WORKTREE_DEPENDENCY_ADJUDICATION_REQUIRED",
1026
+ );
1027
+ }
1028
+ return this;
1029
+ }
1030
+ this.seed([record]);
1031
+ const imported = this._created.get(key);
1032
+ try {
1033
+ const branchHead = _deps.resolveGitRef(this.repoDir, imported.branch);
1034
+ const worktreeHead = _deps.resolveGitRef(imported.path, "HEAD");
1035
+ if (
1036
+ branchHead !== imported.commitOid ||
1037
+ worktreeHead !== imported.commitOid ||
1038
+ !_deps.isWorktreeClean(imported.path) ||
1039
+ (imported.baselineCommitOid !== imported.commitOid &&
1040
+ !_deps.isGitAncestor(
1041
+ this.repoDir,
1042
+ imported.baselineCommitOid,
1043
+ imported.commitOid,
1044
+ ))
1045
+ ) {
1046
+ throw new Error("completed dependency result could not be verified");
1047
+ }
1048
+ } catch (error) {
1049
+ this._created.delete(key);
1050
+ throw this._dependencyAdjudication(key, key, error);
1051
+ }
1052
+ return this;
1053
+ }
1054
+
1055
+ _dependencyAdjudication(key, dependency, error, mergeResult = null) {
1056
+ if (error?.code === "TEAM_WORKTREE_DEPENDENCY_ADJUDICATION_REQUIRED") {
1057
+ return error;
1058
+ }
1059
+ const reason =
1060
+ error instanceof Error ? error.message : String(error || "unknown error");
1061
+ const failure = requireAdjudication(
1062
+ new Error(
1063
+ `task "${key}" dependency "${dependency}" requires adjudication: ${reason}`,
1064
+ ),
1065
+ "TEAM_WORKTREE_DEPENDENCY_ADJUDICATION_REQUIRED",
1066
+ );
1067
+ failure.dependency = dependency;
1068
+ if (mergeResult?.conflicts?.length) {
1069
+ failure.conflicts = mergeResult.conflicts;
1070
+ }
1071
+ return failure;
1072
+ }
1073
+
1074
+ _assertDependencyBinding(key, binding) {
1075
+ const info = this._created.get(binding.key);
1076
+ if (!info) {
1077
+ const branchHead = _deps.resolveGitRef(this.repoDir, binding.branch);
1078
+ if (branchHead !== binding.commitOid) {
1079
+ throw new Error(
1080
+ `dependency branch "${binding.branch}" moved after settlement`,
1081
+ );
1082
+ }
1083
+ return null;
1084
+ }
1085
+ if (info.completed !== true || info.commitOid == null) {
1086
+ throw new Error("completed dependency worktree output is unavailable");
1087
+ }
1088
+ if (
1089
+ info.branch !== binding.branch ||
1090
+ info.commitOid !== binding.commitOid
1091
+ ) {
1092
+ throw new Error("persisted dependency commit binding changed");
1093
+ }
1094
+ const branchHead = _deps.resolveGitRef(this.repoDir, binding.branch);
1095
+ if (branchHead !== binding.commitOid) {
1096
+ throw new Error(
1097
+ `dependency branch "${binding.branch}" moved after settlement`,
1098
+ );
1099
+ }
1100
+ if (_deps.pathExists(info.path)) {
1101
+ const worktreeHead = _deps.resolveGitRef(info.path, "HEAD");
1102
+ if (worktreeHead !== binding.commitOid) {
1103
+ throw new Error(
1104
+ `dependency worktree HEAD for "${binding.key}" moved after settlement`,
1105
+ );
1106
+ }
1107
+ if (!_deps.isWorktreeClean(info.path)) {
1108
+ throw new Error(
1109
+ `dependency worktree "${binding.key}" has unsettled changes`,
1110
+ );
1111
+ }
1112
+ } else if (!info.cleaned) {
1113
+ throw new Error(`dependency worktree "${binding.key}" is missing`);
1114
+ }
1115
+ if (
1116
+ info.baselineCommitOid !== binding.commitOid &&
1117
+ !_deps.isGitAncestor(
1118
+ this.repoDir,
1119
+ info.baselineCommitOid,
1120
+ binding.commitOid,
1121
+ )
1122
+ ) {
1123
+ throw new Error(
1124
+ `dependency commit for "${binding.key}" no longer descends from its persisted baseline`,
1125
+ );
1126
+ }
1127
+ return info;
1128
+ }
1129
+
1130
+ _assertDependencyBindings(key, bindings, { baselineCommitOid = null } = {}) {
1131
+ for (const binding of bindings) {
1132
+ try {
1133
+ this._assertDependencyBinding(key, binding);
1134
+ if (
1135
+ baselineCommitOid != null &&
1136
+ baselineCommitOid !== binding.commitOid &&
1137
+ !_deps.isGitAncestor(
1138
+ this.repoDir,
1139
+ binding.commitOid,
1140
+ baselineCommitOid,
1141
+ )
1142
+ ) {
1143
+ throw new Error(
1144
+ `dependency commit "${binding.commitOid}" is absent from the persisted task baseline`,
1145
+ );
1146
+ }
1147
+ } catch (error) {
1148
+ throw this._dependencyAdjudication(key, binding.key, error);
1149
+ }
1150
+ }
1151
+ }
1152
+
1153
+ _planDependencyCommits(key, task) {
1154
+ let dependencies;
1155
+ try {
1156
+ dependencies = normalizeDependencyKeys(task, key);
1157
+ } catch (error) {
1158
+ throw this._dependencyAdjudication(key, key, error);
1159
+ }
1160
+ const bindings = dependencies.map((dependency) => {
1161
+ const info = this._created.get(dependency);
1162
+ if (!info || info.completed !== true || info.commitOid == null) {
1163
+ throw this._dependencyAdjudication(
1164
+ key,
1165
+ dependency,
1166
+ new Error("completed dependency worktree output is unavailable"),
1167
+ );
1168
+ }
1169
+ return {
1170
+ key: dependency,
1171
+ branch: info.branch,
1172
+ commitOid: info.commitOid,
1173
+ };
1174
+ });
1175
+ this._assertDependencyBindings(key, bindings);
1176
+ return bindings;
1177
+ }
1178
+
1179
+ _composeDependencyBaseline(key, record) {
1180
+ let baselineCommitOid = _deps.resolveGitRef(record.path, "HEAD");
1181
+ const branchHead = _deps.resolveGitRef(this.repoDir, record.branch);
1182
+ if (branchHead !== baselineCommitOid) {
1183
+ throw this._dependencyAdjudication(
1184
+ key,
1185
+ key,
1186
+ new Error("new task branch and worktree HEAD do not match"),
1187
+ );
1188
+ }
1189
+ record.baselineCommitOid = baselineCommitOid;
1190
+
1191
+ for (const binding of record.dependencyCommits) {
1192
+ this._assertDependencyBindings(key, [binding]);
1193
+ if (
1194
+ baselineCommitOid === binding.commitOid ||
1195
+ _deps.isGitAncestor(this.repoDir, binding.commitOid, baselineCommitOid)
1196
+ ) {
1197
+ continue;
1198
+ }
1199
+ const previousBaseline = baselineCommitOid;
1200
+ let mergeResult;
1201
+ try {
1202
+ mergeResult = _deps.mergeWorktree(record.path, binding.commitOid, {
1203
+ message: `Compose dependency ${binding.key} for team task ${key}`,
1204
+ deleteBranch: false,
1205
+ });
1206
+ } catch (error) {
1207
+ throw this._dependencyAdjudication(key, binding.key, error);
1208
+ }
1209
+ if (mergeResult?.success !== true) {
1210
+ throw this._dependencyAdjudication(
1211
+ key,
1212
+ binding.key,
1213
+ new Error(
1214
+ `dependency merge failed: ${
1215
+ mergeResult?.message || "unknown error"
1216
+ }`,
1217
+ ),
1218
+ mergeResult,
1219
+ );
1220
+ }
1221
+ baselineCommitOid = _deps.resolveGitRef(record.path, "HEAD");
1222
+ const postMergeBranch = _deps.resolveGitRef(this.repoDir, record.branch);
1223
+ if (
1224
+ postMergeBranch !== baselineCommitOid ||
1225
+ (previousBaseline !== baselineCommitOid &&
1226
+ !_deps.isGitAncestor(
1227
+ this.repoDir,
1228
+ previousBaseline,
1229
+ baselineCommitOid,
1230
+ )) ||
1231
+ (binding.commitOid !== baselineCommitOid &&
1232
+ !_deps.isGitAncestor(
1233
+ this.repoDir,
1234
+ binding.commitOid,
1235
+ baselineCommitOid,
1236
+ )) ||
1237
+ !_deps.isWorktreeClean(record.path)
1238
+ ) {
1239
+ throw this._dependencyAdjudication(
1240
+ key,
1241
+ binding.key,
1242
+ new Error("dependency merge result could not be verified"),
1243
+ );
1244
+ }
1245
+ record.baselineCommitOid = baselineCommitOid;
1246
+ }
1247
+
1248
+ this._assertDependencyBindings(key, record.dependencyCommits, {
1249
+ baselineCommitOid,
1250
+ });
1251
+ return baselineCommitOid;
1252
+ }
1253
+
1254
+ _assertTaskBaseline(key, record) {
1255
+ const branchHead = _deps.resolveGitRef(this.repoDir, record.branch);
1256
+ const worktreeHead = _deps.resolveGitRef(record.path, "HEAD");
1257
+ if (
1258
+ branchHead !== record.baselineCommitOid ||
1259
+ worktreeHead !== record.baselineCommitOid ||
1260
+ !_deps.isWorktreeClean(record.path)
1261
+ ) {
1262
+ throw this._dependencyAdjudication(
1263
+ key,
1264
+ key,
1265
+ new Error("task dependency baseline moved before execution"),
1266
+ );
1267
+ }
1268
+ this._assertDependencyBindings(key, record.dependencyCommits, {
1269
+ baselineCommitOid: record.baselineCommitOid,
1270
+ });
1271
+ }
1272
+
1273
+ _validateDependencyGraph(keys) {
1274
+ for (const key of keys) {
1275
+ const info = this._created.get(key);
1276
+ for (const binding of info.dependencyCommits) {
1277
+ const dependency = this._created.get(binding.key);
1278
+ if (!dependency && info.imported) continue;
1279
+ if (
1280
+ !dependency ||
1281
+ dependency.completed !== true ||
1282
+ dependency.branch !== binding.branch ||
1283
+ dependency.commitOid !== binding.commitOid
1284
+ ) {
1285
+ throw new TypeError(
1286
+ `invalid persisted dependency binding for "${key}"`,
1287
+ );
1288
+ }
1289
+ }
1290
+ }
1291
+
1292
+ const visiting = new Set();
1293
+ const visited = new Set();
1294
+ const visit = (key) => {
1295
+ if (visited.has(key)) return;
1296
+ if (visiting.has(key)) {
1297
+ throw new TypeError("cyclic team worktree dependency snapshot");
1298
+ }
1299
+ visiting.add(key);
1300
+ for (const binding of this._created.get(key)?.dependencyCommits || []) {
1301
+ if (this._created.has(binding.key)) visit(binding.key);
1302
+ }
1303
+ visiting.delete(key);
1304
+ visited.add(key);
1305
+ };
1306
+ for (const key of keys) visit(key);
1307
+
1308
+ for (const key of keys) {
1309
+ const info = this._created.get(key);
1310
+ if (info.dependencyCommits.length === 0 || info.cleaned) continue;
1311
+ this._assertDependencyBindings(key, info.dependencyCommits, {
1312
+ baselineCommitOid: info.baselineCommitOid,
1313
+ });
1314
+ if (
1315
+ info.completed &&
1316
+ info.baselineCommitOid !== info.commitOid &&
1317
+ !_deps.isGitAncestor(
1318
+ this.repoDir,
1319
+ info.baselineCommitOid,
1320
+ info.commitOid,
1321
+ )
1322
+ ) {
1323
+ throw this._dependencyAdjudication(
1324
+ key,
1325
+ key,
1326
+ new Error("settled task commit no longer contains its baseline"),
1327
+ );
1328
+ }
1329
+ }
1330
+ }
1331
+
611
1332
  /** Seed validated worktree records after recovering team state. */
612
1333
  seed(records) {
613
1334
  if (!Array.isArray(records)) {
@@ -639,11 +1360,13 @@ export class TeamWorktreeCoordinator {
639
1360
  const completed = record.completed ?? true;
640
1361
  const cleanupPrepared = record.cleanupPrepared;
641
1362
  const cleaned = record.cleaned;
1363
+ const imported = record.imported ?? false;
642
1364
  if (
643
1365
  typeof committed !== "boolean" ||
644
1366
  typeof completed !== "boolean" ||
645
1367
  typeof cleanupPrepared !== "boolean" ||
646
- typeof cleaned !== "boolean"
1368
+ typeof cleaned !== "boolean" ||
1369
+ typeof imported !== "boolean"
647
1370
  ) {
648
1371
  throw new TypeError(
649
1372
  `invalid team worktree completion state for "${key}"`,
@@ -656,6 +1379,27 @@ export class TeamWorktreeCoordinator {
656
1379
  : requireGitOid(record.commitOid, "commitOid");
657
1380
  const integration = normalizeIntegrationState(record.integration, key);
658
1381
  const managedLinks = normalizeManagedLinks(record.managedLinks, key);
1382
+ const workspaceCheckpoint = normalizeWorktreeCheckpoint(
1383
+ record.workspaceCheckpoint,
1384
+ key,
1385
+ );
1386
+ const dependencyCommits = normalizeDependencyCommits(
1387
+ record.dependencyCommits,
1388
+ key,
1389
+ );
1390
+ const baselineCommitOid = requireGitOid(
1391
+ record.baselineCommitOid,
1392
+ "baselineCommitOid",
1393
+ );
1394
+ if (
1395
+ workspaceCheckpoint &&
1396
+ (workspaceCheckpoint.workspaceRoot !== worktreePath ||
1397
+ workspaceCheckpoint.runId !== (this.runId || "team-worktree"))
1398
+ ) {
1399
+ throw new TypeError(
1400
+ `team worktree checkpoint authority changed for "${key}"`,
1401
+ );
1402
+ }
659
1403
  if (
660
1404
  !completed &&
661
1405
  (committed ||
@@ -665,7 +1409,23 @@ export class TeamWorktreeCoordinator {
665
1409
  cleaned)
666
1410
  ) {
667
1411
  throw new TypeError(
668
- `invalid unfinished team worktree recovery state for "${key}"`,
1412
+ `invalid unfinished team worktree recovery state for "${key}"`,
1413
+ );
1414
+ }
1415
+ if (imported && !completed) {
1416
+ throw new TypeError(
1417
+ `invalid imported team worktree recovery state for "${key}"`,
1418
+ );
1419
+ }
1420
+ if (
1421
+ completed &&
1422
+ workspaceCheckpoint &&
1423
+ (workspaceCheckpoint.state !== "committed" ||
1424
+ !workspaceCheckpoint.writeManifestDigest ||
1425
+ !workspaceCheckpoint.evidenceDigest)
1426
+ ) {
1427
+ throw new TypeError(
1428
+ `completed team worktree "${key}" has no committed checkpoint evidence`,
669
1429
  );
670
1430
  }
671
1431
  if (
@@ -690,14 +1450,24 @@ export class TeamWorktreeCoordinator {
690
1450
  committed,
691
1451
  completed,
692
1452
  commitOid,
1453
+ dependencyCommits,
1454
+ baselineCommitOid,
1455
+ imported,
693
1456
  integration,
694
1457
  managedLinks,
1458
+ workspaceCheckpoint,
695
1459
  cleanupPrepared,
696
1460
  cleaned,
697
1461
  },
698
1462
  });
699
1463
  }
700
1464
  for (const { key, info } of normalized) this._created.set(key, info);
1465
+ try {
1466
+ this._validateDependencyGraph(normalized.map(({ key }) => key));
1467
+ } catch (error) {
1468
+ for (const { key } of normalized) this._created.delete(key);
1469
+ throw error;
1470
+ }
701
1471
  return this;
702
1472
  }
703
1473
 
@@ -714,14 +1484,118 @@ export class TeamWorktreeCoordinator {
714
1484
  committed: info.committed === true,
715
1485
  completed: info.completed === true,
716
1486
  commitOid: info.commitOid || null,
1487
+ dependencyCommits: info.dependencyCommits.map((dependency) => ({
1488
+ ...dependency,
1489
+ })),
1490
+ baselineCommitOid: info.baselineCommitOid,
1491
+ imported: info.imported === true,
717
1492
  integration: { ...info.integration },
718
1493
  managedLinks: [...(info.managedLinks || [])],
1494
+ ...(info.workspaceCheckpoint
1495
+ ? {
1496
+ workspaceCheckpoint: {
1497
+ ...info.workspaceCheckpoint,
1498
+ uncoveredPaths: [...info.workspaceCheckpoint.uncoveredPaths],
1499
+ },
1500
+ }
1501
+ : {}),
719
1502
  cleanupPrepared: info.cleanupPrepared === true,
720
1503
  cleaned: info.cleaned === true,
721
1504
  })),
722
1505
  };
723
1506
  }
724
1507
 
1508
+ _migratePublishedV3Records(records) {
1509
+ const baselineCommitOid = this._baseTarget.commitOid;
1510
+ return records.map((record) => {
1511
+ if (!record || typeof record !== "object" || Array.isArray(record)) {
1512
+ throw new TypeError("invalid v3 team worktree record");
1513
+ }
1514
+ if (record.workspaceCheckpoint != null) {
1515
+ throw new TypeError(
1516
+ "v3 team worktree record cannot carry checkpoint authority",
1517
+ );
1518
+ }
1519
+ const key = requireRecordString(record.key, "key");
1520
+ const branch = requireRecordString(record.branch, "branch");
1521
+ const worktreePath = _deps.assertManagedWorktreePath(
1522
+ this.repoDir,
1523
+ requireRecordString(record.path, "path"),
1524
+ { branchName: branch },
1525
+ );
1526
+ const completed = record.completed ?? true;
1527
+ const commitOid = completed
1528
+ ? requireGitOid(record.commitOid, "commitOid")
1529
+ : null;
1530
+ const integration = normalizeIntegrationState(record.integration, key);
1531
+ const cleanupPrepared = record.cleanupPrepared === true;
1532
+ const cleaned = record.cleaned === true;
1533
+ if (completed) {
1534
+ if (
1535
+ baselineCommitOid !== commitOid &&
1536
+ !_deps.isGitAncestor(this.repoDir, baselineCommitOid, commitOid)
1537
+ ) {
1538
+ throw new TypeError(
1539
+ `v3 team worktree "${key}" commit does not descend from its run base`,
1540
+ );
1541
+ }
1542
+ if (integration.merged) {
1543
+ const currentHead = _deps.resolveGitRef(this.repoDir, "HEAD");
1544
+ if (
1545
+ commitOid !== currentHead &&
1546
+ !_deps.isGitAncestor(this.repoDir, commitOid, currentHead)
1547
+ ) {
1548
+ throw new TypeError(
1549
+ `v3 merged team worktree "${key}" is absent from current HEAD`,
1550
+ );
1551
+ }
1552
+ } else {
1553
+ const branchHead = _deps.resolveGitRef(this.repoDir, branch);
1554
+ if (branchHead !== commitOid) {
1555
+ throw new TypeError(
1556
+ `v3 team worktree "${key}" branch no longer matches its commit`,
1557
+ );
1558
+ }
1559
+ }
1560
+ if (_deps.pathExists(worktreePath)) {
1561
+ if (
1562
+ _deps.resolveGitRef(worktreePath, "HEAD") !== commitOid ||
1563
+ !_deps.isWorktreeClean(worktreePath, {
1564
+ includeIgnored: true,
1565
+ })
1566
+ ) {
1567
+ throw new TypeError(
1568
+ `v3 team worktree "${key}" cannot prove a clean committed tree`,
1569
+ );
1570
+ }
1571
+ } else if (!cleanupPrepared && !cleaned && !integration.merged) {
1572
+ throw new TypeError(
1573
+ `v3 team worktree "${key}" disappeared before durable cleanup`,
1574
+ );
1575
+ }
1576
+ } else {
1577
+ const branchHead = _deps.resolveGitRef(this.repoDir, branch);
1578
+ if (
1579
+ !_deps.pathExists(worktreePath) ||
1580
+ branchHead !== baselineCommitOid ||
1581
+ _deps.resolveGitRef(worktreePath, "HEAD") !== baselineCommitOid ||
1582
+ !_deps.isWorktreeClean(worktreePath, { includeIgnored: true })
1583
+ ) {
1584
+ throw new TypeError(
1585
+ `v3 unfinished team worktree "${key}" has an unverifiable baseline`,
1586
+ );
1587
+ }
1588
+ }
1589
+ return {
1590
+ ...record,
1591
+ path: worktreePath,
1592
+ dependencyCommits: [],
1593
+ baselineCommitOid,
1594
+ imported: false,
1595
+ };
1596
+ });
1597
+ }
1598
+
725
1599
  /** Restore a snapshot into an empty coordinator. */
726
1600
  restore(snapshot) {
727
1601
  if (this._created.size > 0) {
@@ -733,7 +1607,11 @@ export class TeamWorktreeCoordinator {
733
1607
  !snapshot ||
734
1608
  typeof snapshot !== "object" ||
735
1609
  Array.isArray(snapshot) ||
736
- snapshot.version !== TEAM_WORKTREE_SNAPSHOT_VERSION ||
1610
+ ![
1611
+ PUBLISHED_TEAM_WORKTREE_SNAPSHOT_VERSION,
1612
+ LEGACY_TEAM_WORKTREE_SNAPSHOT_VERSION,
1613
+ TEAM_WORKTREE_SNAPSHOT_VERSION,
1614
+ ].includes(snapshot.version) ||
737
1615
  !snapshot.baseTarget ||
738
1616
  !Array.isArray(snapshot.records)
739
1617
  ) {
@@ -749,7 +1627,11 @@ export class TeamWorktreeCoordinator {
749
1627
  }
750
1628
  this._baseTarget = normalizeBaseTarget(snapshot.baseTarget);
751
1629
  try {
752
- return this.seed(snapshot.records);
1630
+ const records =
1631
+ snapshot.version === PUBLISHED_TEAM_WORKTREE_SNAPSHOT_VERSION
1632
+ ? this._migratePublishedV3Records(snapshot.records)
1633
+ : snapshot.records;
1634
+ return this.seed(records);
753
1635
  } catch (error) {
754
1636
  this.runId = previousRunId;
755
1637
  this._baseTarget = previousBaseTarget;
@@ -768,8 +1650,44 @@ export class TeamWorktreeCoordinator {
768
1650
  makeRunTask({ runInWorktree = null } = {}) {
769
1651
  return async (context) => {
770
1652
  const { key, task, holder } = context;
1653
+ const executionLease = task?.lease || task?.metadata?.lease || null;
1654
+ const checkpointEvent = (details = {}) => ({
1655
+ ...(executionLease
1656
+ ? {
1657
+ lease: {
1658
+ holder: executionLease.holder,
1659
+ leaseId: executionLease.leaseId,
1660
+ ownerPid: executionLease.ownerPid,
1661
+ fencingToken: executionLease.fencingToken,
1662
+ },
1663
+ }
1664
+ : {}),
1665
+ ...details,
1666
+ });
1667
+ const assertWallFence = (phase) => {
1668
+ if (
1669
+ context.signal?.aborted !== true &&
1670
+ context.budget?.reason?.() !== "max-wall-ms"
1671
+ ) {
1672
+ return;
1673
+ }
1674
+ const message = `task "${key}" crossed the team wall-clock budget ${phase}`;
1675
+ const failure =
1676
+ context.signal?.aborted === true
1677
+ ? abortReason(
1678
+ context.signal,
1679
+ "TEAM_WORKTREE_POST_COMMIT_ADJUDICATION_REQUIRED",
1680
+ message,
1681
+ )
1682
+ : new Error(message);
1683
+ throw requireAdjudication(
1684
+ failure,
1685
+ "TEAM_WORKTREE_POST_COMMIT_ADJUDICATION_REQUIRED",
1686
+ );
1687
+ };
771
1688
  this._assertBaseTarget();
772
1689
  const branch = this.branchFor(key);
1690
+ const dependencyCommits = this._planDependencyCommits(key, task);
773
1691
  // A retry (the prior attempt failed and TaskLeaseRegistry re-queued the
774
1692
  // task) would collide with its own leftover worktree: createWorktree
775
1693
  // derives a DETERMINISTIC path from the branch and throws "Worktree
@@ -785,14 +1703,34 @@ export class TeamWorktreeCoordinator {
785
1703
  "TEAM_WORKTREE_RETRY_ADJUDICATION_REQUIRED",
786
1704
  );
787
1705
  }
1706
+ if (
1707
+ prior.workspaceCheckpoint &&
1708
+ !["rolled_back", "aborted"].includes(prior.workspaceCheckpoint.state)
1709
+ ) {
1710
+ throw requireAdjudication(
1711
+ new Error(
1712
+ `task "${key}" has unsettled workspace checkpoint ${prior.workspaceCheckpoint.transactionId} (${prior.workspaceCheckpoint.state})`,
1713
+ ),
1714
+ "TEAM_WORKTREE_CHECKPOINT_RECOVERY_REQUIRED",
1715
+ );
1716
+ }
1717
+ if (
1718
+ JSON.stringify(prior.dependencyCommits) !==
1719
+ JSON.stringify(dependencyCommits)
1720
+ ) {
1721
+ throw requireAdjudication(
1722
+ new Error(`task "${key}" dependency commits changed before retry`),
1723
+ "TEAM_WORKTREE_RETRY_ADJUDICATION_REQUIRED",
1724
+ );
1725
+ }
1726
+ this._assertDependencyBindings(key, prior.dependencyCommits);
788
1727
  const priorPath = _deps.assertManagedWorktreePath(
789
1728
  this.repoDir,
790
1729
  prior.path,
791
1730
  { branchName: prior.branch },
792
1731
  );
793
1732
  const priorBranchHead = _deps.resolveGitRef(this.repoDir, prior.branch);
794
- const baseHead = _deps.resolveGitRef(this.repoDir, "HEAD");
795
- if (priorBranchHead !== baseHead) {
1733
+ if (priorBranchHead !== prior.baselineCommitOid) {
796
1734
  throw requireAdjudication(
797
1735
  new Error(`task "${key}" has an unsettled worktree commit`),
798
1736
  "TEAM_WORKTREE_RETRY_ADJUDICATION_REQUIRED",
@@ -834,7 +1772,7 @@ export class TeamWorktreeCoordinator {
834
1772
  const created = _deps.createWorktree(
835
1773
  this.repoDir,
836
1774
  branch,
837
- null,
1775
+ this._baseTarget.commitOid,
838
1776
  wtOptions,
839
1777
  );
840
1778
  const createdPath = created.path;
@@ -843,21 +1781,49 @@ export class TeamWorktreeCoordinator {
843
1781
  createdPath,
844
1782
  { branchName: branch },
845
1783
  );
1784
+ let initialBaselineCommitOid;
1785
+ try {
1786
+ initialBaselineCommitOid = requireGitOid(
1787
+ _deps.resolveGitRef(worktreePath, "HEAD"),
1788
+ "baselineCommitOid",
1789
+ );
1790
+ } catch (error) {
1791
+ throw requireAdjudication(
1792
+ error,
1793
+ "TEAM_WORKTREE_CREATE_ADJUDICATION_REQUIRED",
1794
+ );
1795
+ }
846
1796
  this._created.set(key, {
847
1797
  branch,
848
1798
  path: worktreePath,
849
1799
  committed: false,
850
1800
  completed: false,
851
1801
  commitOid: null,
1802
+ dependencyCommits,
1803
+ baselineCommitOid: initialBaselineCommitOid,
1804
+ imported: false,
852
1805
  integration: emptyIntegrationState(),
853
1806
  managedLinks: normalizeManagedLinks(
854
1807
  created.symlinkedDirectories || [],
855
1808
  key,
856
1809
  ),
1810
+ workspaceCheckpoint: null,
857
1811
  cleanupPrepared: false,
858
1812
  cleaned: false,
859
1813
  });
1814
+ const record = this._created.get(key);
860
1815
  this._onWorktree?.({ key, branch, path: worktreePath, holder });
1816
+ this._composeDependencyBaseline(key, record);
1817
+ this._assertTaskBaseline(key, record);
1818
+ if (record.dependencyCommits.length > 0) {
1819
+ this._onWorktree?.({
1820
+ key,
1821
+ branch,
1822
+ path: worktreePath,
1823
+ holder,
1824
+ baselineComposed: true,
1825
+ });
1826
+ }
861
1827
  if (typeof context.renew === "function") {
862
1828
  const fence = await context.renew();
863
1829
  if (!fence?.ok) {
@@ -878,93 +1844,326 @@ export class TeamWorktreeCoordinator {
878
1844
  "TEAM_WORKTREE_EXECUTION_CANCELLED",
879
1845
  );
880
1846
  }
881
- let executionResult = null;
882
- try {
883
- if (typeof runInWorktree === "function") {
884
- executionResult = await runInWorktree({
885
- ...context,
886
- cwd: worktreePath,
887
- });
888
- } else {
889
- const command = task.metadata?.command || task?.command;
890
- if (!command) {
1847
+ const executeAndValidate = async (managedCheckpoint) => {
1848
+ let executionResult = null;
1849
+ try {
1850
+ if (typeof runInWorktree === "function") {
1851
+ executionResult = await runInWorktree({
1852
+ ...context,
1853
+ cwd: worktreePath,
1854
+ managedCheckpoint,
1855
+ });
1856
+ } else {
1857
+ const command = task.metadata?.command || task?.command;
1858
+ if (!command) {
1859
+ throw new Error(
1860
+ `task "${key}" has no \`command\` to run in a worktree`,
1861
+ );
1862
+ }
1863
+ await _deps.runShell(command, worktreePath, {
1864
+ signal: context.signal || null,
1865
+ managedCheckpoint,
1866
+ });
1867
+ }
1868
+ } catch (error) {
1869
+ if (task.metadata?.retrySafe === true) throw error;
1870
+ throw requireAdjudication(
1871
+ error,
1872
+ "TEAM_WORKTREE_EXECUTION_ADJUDICATION_REQUIRED",
1873
+ );
1874
+ }
1875
+ if (context.signal?.aborted) {
1876
+ throw requireAdjudication(
1877
+ new Error(`task "${key}" was cancelled before commit`),
1878
+ "TEAM_WORKTREE_COMMIT_CANCELLED",
1879
+ );
1880
+ }
1881
+ let committed;
1882
+ let commitOid;
1883
+ try {
1884
+ committed = _deps.commit(worktreePath, `team task ${key}`);
1885
+ commitOid = requireGitOid(
1886
+ _deps.resolveGitRef(worktreePath, "HEAD"),
1887
+ "commitOid",
1888
+ );
1889
+ const branchHead = _deps.resolveGitRef(this.repoDir, branch);
1890
+ if (
1891
+ branchHead !== commitOid ||
1892
+ (record.baselineCommitOid !== commitOid &&
1893
+ !_deps.isGitAncestor(
1894
+ this.repoDir,
1895
+ record.baselineCommitOid,
1896
+ commitOid,
1897
+ )) ||
1898
+ !_deps.isWorktreeClean(worktreePath)
1899
+ ) {
891
1900
  throw new Error(
892
- `task "${key}" has no \`command\` to run in a worktree`,
1901
+ "task commit no longer contains its verified baseline",
893
1902
  );
894
1903
  }
895
- await _deps.runShell(command, worktreePath, {
896
- signal: context.signal || null,
1904
+ this._assertDependencyBindings(key, record.dependencyCommits, {
1905
+ baselineCommitOid: commitOid,
897
1906
  });
1907
+ } catch (error) {
1908
+ throw requireAdjudication(
1909
+ error,
1910
+ "TEAM_WORKTREE_COMMIT_ADJUDICATION_REQUIRED",
1911
+ );
898
1912
  }
899
- } catch (error) {
900
- if (task.metadata?.retrySafe === true) throw error;
901
- throw requireAdjudication(
902
- error,
903
- "TEAM_WORKTREE_EXECUTION_ADJUDICATION_REQUIRED",
904
- );
905
- }
906
- if (context.signal?.aborted) {
907
- throw requireAdjudication(
908
- new Error(`task "${key}" was cancelled before commit`),
909
- "TEAM_WORKTREE_COMMIT_CANCELLED",
910
- );
911
- }
912
- let committed;
913
- let commitOid;
914
- try {
915
- committed = _deps.commit(worktreePath, `team task ${key}`);
916
- commitOid = _deps.resolveGitRef(worktreePath, "HEAD");
917
- } catch (error) {
918
- throw requireAdjudication(
919
- error,
920
- "TEAM_WORKTREE_COMMIT_ADJUDICATION_REQUIRED",
921
- );
922
- }
923
- // Git commit/ref resolution are synchronous today. Yield once so a wall
924
- // timer, fatal peer failure, or cancellation that became ready while Git
925
- // held the event loop can fence this output before it is authorized.
926
- await new Promise((resolveYield) => setImmediate(resolveYield));
927
- if (context.signal?.aborted) {
928
- throw requireAdjudication(
929
- new Error(`task "${key}" was cancelled after commit`),
930
- "TEAM_WORKTREE_POST_COMMIT_ADJUDICATION_REQUIRED",
931
- );
932
- }
933
- if (typeof context.renew === "function") {
934
- const fence = await context.renew();
935
- if (!fence?.ok) {
1913
+ // Git commit/ref resolution are synchronous today. Yield once so a wall
1914
+ // timer, fatal peer failure, or cancellation that became ready while
1915
+ // Git held the event loop can fence this output before authorization.
1916
+ await new Promise((resolveYield) => setImmediate(resolveYield));
1917
+ if (context.signal?.aborted) {
1918
+ throw requireAdjudication(
1919
+ new Error(`task "${key}" was cancelled after commit`),
1920
+ "TEAM_WORKTREE_POST_COMMIT_ADJUDICATION_REQUIRED",
1921
+ );
1922
+ }
1923
+ if (typeof context.renew === "function") {
1924
+ const fence = await context.renew();
1925
+ if (!fence?.ok) {
1926
+ throw requireAdjudication(
1927
+ new Error(
1928
+ `task "${key}" lost its lease after committing worktree output`,
1929
+ ),
1930
+ "TEAM_WORKTREE_POST_COMMIT_ADJUDICATION_REQUIRED",
1931
+ );
1932
+ }
1933
+ }
1934
+ if (context.budget?.reason?.() === "max-wall-ms") {
936
1935
  throw requireAdjudication(
937
1936
  new Error(
938
- `task "${key}" lost its lease after committing worktree output`,
1937
+ `task "${key}" exceeded the team wall-clock budget during commit`,
939
1938
  ),
940
1939
  "TEAM_WORKTREE_POST_COMMIT_ADJUDICATION_REQUIRED",
941
1940
  );
942
1941
  }
1942
+ return { executionResult, committed, commitOid };
1943
+ };
1944
+
1945
+ const completeRecord = ({ executionResult, committed, commitOid }) => {
1946
+ record.committed = committed;
1947
+ record.completed = true;
1948
+ record.commitOid = commitOid;
1949
+ return {
1950
+ branch: record.branch,
1951
+ worktreePath: record.path,
1952
+ committed: record.committed,
1953
+ commitOid: record.commitOid,
1954
+ baselineCommitOid: record.baselineCommitOid,
1955
+ dependencyCommits: record.dependencyCommits.map((dependency) => ({
1956
+ ...dependency,
1957
+ })),
1958
+ managedLinks: [...(record.managedLinks || [])],
1959
+ ...(record.workspaceCheckpoint
1960
+ ? {
1961
+ workspaceCheckpoint: {
1962
+ ...record.workspaceCheckpoint,
1963
+ uncoveredPaths: [
1964
+ ...record.workspaceCheckpoint.uncoveredPaths,
1965
+ ],
1966
+ },
1967
+ }
1968
+ : {}),
1969
+ ...(executionResult?.usage ? { usage: executionResult.usage } : {}),
1970
+ ...(executionResult?.provider
1971
+ ? { provider: executionResult.provider }
1972
+ : {}),
1973
+ ...(executionResult?.model ? { model: executionResult.model } : {}),
1974
+ ...(executionResult?.usageRecords
1975
+ ? { usageRecords: executionResult.usageRecords }
1976
+ : {}),
1977
+ };
1978
+ };
1979
+
1980
+ if (!this._checkpointBroker) {
1981
+ if (task.metadata?.managedCheckpointRequired === true) {
1982
+ throw requireAdjudication(
1983
+ new Error(`task "${key}" requires a managed workspace checkpoint`),
1984
+ "TEAM_WORKTREE_CHECKPOINT_REQUIRED",
1985
+ );
1986
+ }
1987
+ return completeRecord(await executeAndValidate(false));
943
1988
  }
944
- if (context.budget?.reason?.() === "max-wall-ms") {
945
- throw requireAdjudication(
1989
+
1990
+ let guard;
1991
+ try {
1992
+ guard = this._checkpointBroker.beginTask({
1993
+ runId: this.runId || "team-worktree",
1994
+ taskKey: key,
1995
+ workspaceRoot: worktreePath,
1996
+ coverageTarget: "partial",
1997
+ writerIsolation: "unknown",
1998
+ externalSideEffects: true,
1999
+ exclusions: this._checkpointExclusions,
2000
+ });
2001
+ this._updateCheckpoint(
2002
+ key,
2003
+ record,
2004
+ guard,
2005
+ "prepared",
2006
+ checkpointEvent(),
2007
+ );
2008
+ guard.markRunning();
2009
+ this._updateCheckpoint(
2010
+ key,
2011
+ record,
2012
+ guard,
2013
+ "running",
2014
+ checkpointEvent(),
2015
+ );
2016
+ } catch (error) {
2017
+ if (guard) {
2018
+ try {
2019
+ guard.rollback({
2020
+ reason: "team checkpoint preparation/persistence failed",
2021
+ });
2022
+ record.workspaceCheckpoint = summarizeWorktreeCheckpoint(
2023
+ guard.snapshot(),
2024
+ key,
2025
+ );
2026
+ this._publishCheckpoint(
2027
+ key,
2028
+ record,
2029
+ "preparation-rolled-back",
2030
+ checkpointEvent(),
2031
+ );
2032
+ } catch {
2033
+ /* the durable checkpoint remains fail-closed for recovery */
2034
+ }
2035
+ }
2036
+ const failure = requireAdjudication(
946
2037
  new Error(
947
- `task "${key}" exceeded the team wall-clock budget during commit`,
2038
+ `task "${key}" could not prepare its workspace checkpoint: ${
2039
+ error instanceof Error ? error.message : String(error)
2040
+ }`,
948
2041
  ),
949
- "TEAM_WORKTREE_POST_COMMIT_ADJUDICATION_REQUIRED",
2042
+ "TEAM_WORKTREE_CHECKPOINT_PREPARE_FAILED",
950
2043
  );
2044
+ failure.workspaceCheckpoint = record.workspaceCheckpoint;
2045
+ throw failure;
2046
+ }
2047
+
2048
+ let accepted = false;
2049
+ let verifiedCommitOid = null;
2050
+ try {
2051
+ const attempt = await executeAndValidate(true);
2052
+ verifiedCommitOid = attempt.commitOid;
2053
+ this._publishCheckpoint(
2054
+ key,
2055
+ record,
2056
+ "validated",
2057
+ checkpointEvent({ verifiedCommitOid: attempt.commitOid }),
2058
+ );
2059
+ assertWallFence("before checkpoint acceptance");
2060
+ guard.accept();
2061
+ accepted = true;
2062
+ assertWallFence("during checkpoint acceptance");
2063
+ this._updateCheckpoint(
2064
+ key,
2065
+ record,
2066
+ guard,
2067
+ "committed",
2068
+ checkpointEvent({ verifiedCommitOid: attempt.commitOid }),
2069
+ );
2070
+ assertWallFence("while publishing the committed checkpoint");
2071
+ const result = completeRecord(attempt);
2072
+ assertWallFence("while materializing the completed worktree result");
2073
+ this._publishCheckpoint(
2074
+ key,
2075
+ record,
2076
+ "completed",
2077
+ checkpointEvent({ verifiedCommitOid: attempt.commitOid }),
2078
+ );
2079
+ assertWallFence("while publishing checkpoint completion");
2080
+ return result;
2081
+ } catch (caughtError) {
2082
+ const taskError =
2083
+ caughtError instanceof Error
2084
+ ? caughtError
2085
+ : new Error(String(caughtError));
2086
+ if (accepted) {
2087
+ try {
2088
+ record.workspaceCheckpoint = summarizeWorktreeCheckpoint(
2089
+ guard.snapshot(),
2090
+ key,
2091
+ );
2092
+ if (verifiedCommitOid && record.completed !== true) {
2093
+ this._publishCheckpoint(
2094
+ key,
2095
+ record,
2096
+ "committed",
2097
+ checkpointEvent({ verifiedCommitOid }),
2098
+ );
2099
+ }
2100
+ } catch {
2101
+ /* the durable transaction store remains authoritative */
2102
+ }
2103
+ const failure = requireAdjudication(
2104
+ new Error(
2105
+ `task "${key}" committed its workspace checkpoint but final result persistence could not be proven: ${taskError.message}`,
2106
+ ),
2107
+ "TEAM_WORKTREE_CHECKPOINT_SETTLEMENT_FAILED",
2108
+ );
2109
+ failure.taskError = taskError;
2110
+ failure.workspaceCheckpoint = record.workspaceCheckpoint;
2111
+ throw failure;
2112
+ }
2113
+ try {
2114
+ guard.rollback({
2115
+ reason:
2116
+ context.signal?.aborted === true
2117
+ ? "team worktree task interrupted"
2118
+ : "team worktree task failed",
2119
+ });
2120
+ record.workspaceCheckpoint = summarizeWorktreeCheckpoint(
2121
+ guard.snapshot(),
2122
+ key,
2123
+ );
2124
+ this._publishCheckpoint(
2125
+ key,
2126
+ record,
2127
+ "rolled-back",
2128
+ checkpointEvent(),
2129
+ );
2130
+ taskError.workspaceCheckpoint = {
2131
+ ...record.workspaceCheckpoint,
2132
+ uncoveredPaths: [...record.workspaceCheckpoint.uncoveredPaths],
2133
+ };
2134
+ throw taskError;
2135
+ } catch (rollbackError) {
2136
+ if (rollbackError === taskError) throw taskError;
2137
+ try {
2138
+ record.workspaceCheckpoint = summarizeWorktreeCheckpoint(
2139
+ guard.snapshot(),
2140
+ key,
2141
+ );
2142
+ this._publishCheckpoint(
2143
+ key,
2144
+ record,
2145
+ "rollback-recovery-required",
2146
+ checkpointEvent(),
2147
+ );
2148
+ } catch {
2149
+ /* the underlying transaction remains the recovery authority */
2150
+ }
2151
+ const failure = requireAdjudication(
2152
+ new Error(
2153
+ `task "${key}" failed and its workspace checkpoint rollback could not be proven: ${
2154
+ rollbackError instanceof Error
2155
+ ? rollbackError.message
2156
+ : String(rollbackError)
2157
+ }`,
2158
+ ),
2159
+ "TEAM_WORKTREE_CHECKPOINT_ROLLBACK_FAILED",
2160
+ );
2161
+ failure.taskError = taskError;
2162
+ failure.rollbackError = rollbackError;
2163
+ failure.workspaceCheckpoint = record.workspaceCheckpoint;
2164
+ throw failure;
2165
+ }
951
2166
  }
952
- const record = this._created.get(key);
953
- record.committed = committed;
954
- record.completed = true;
955
- record.commitOid = commitOid;
956
- return {
957
- branch,
958
- committed,
959
- ...(executionResult?.usage ? { usage: executionResult.usage } : {}),
960
- ...(executionResult?.provider
961
- ? { provider: executionResult.provider }
962
- : {}),
963
- ...(executionResult?.model ? { model: executionResult.model } : {}),
964
- ...(executionResult?.usageRecords
965
- ? { usageRecords: executionResult.usageRecords }
966
- : {}),
967
- };
968
2167
  };
969
2168
  }
970
2169
 
@@ -973,13 +2172,27 @@ export class TeamWorktreeCoordinator {
973
2172
  * base. Merging is in-order, so a later branch that conflicts with an
974
2173
  * already-merged one is reported as a conflict rather than silently clobbering.
975
2174
  *
976
- * @param {object} [opts] { merge:boolean } actually merge clean branches
2175
+ * onProgress runs synchronously before and after each worktree. Returning a
2176
+ * Promise is rejected before the corresponding operation so callers cannot
2177
+ * accidentally release their lease while Git continues in the background.
2178
+ *
2179
+ * @param {object} [opts] { merge:boolean, onProgress?:Function }
977
2180
  * @returns {Array<{key,branch,committed,clean,merged,conflicts,error?}>}
978
2181
  */
979
- integrate({ merge = false } = {}) {
2182
+ integrate({ merge = false, onProgress = null } = {}) {
2183
+ if (onProgress == null) return this._integrate({ merge });
2184
+ return this._runWithWorktreeProgress({
2185
+ phase: "integrate",
2186
+ onProgress,
2187
+ run: (key) => this._integrate({ merge, onlyKey: key }),
2188
+ });
2189
+ }
2190
+
2191
+ _integrate({ merge = false, onlyKey = null } = {}) {
980
2192
  this._assertBaseTarget({ merge });
981
2193
  const results = [];
982
2194
  for (const [key, info] of this._created) {
2195
+ if (onlyKey != null && key !== onlyKey) continue;
983
2196
  const target = this._assertBaseTarget({ merge });
984
2197
  if (info.completed === false) {
985
2198
  results.push({
@@ -993,6 +2206,34 @@ export class TeamWorktreeCoordinator {
993
2206
  });
994
2207
  continue;
995
2208
  }
2209
+ try {
2210
+ this._assertDependencyBindings(key, info.dependencyCommits, {
2211
+ baselineCommitOid: info.baselineCommitOid,
2212
+ });
2213
+ if (
2214
+ info.baselineCommitOid !== info.commitOid &&
2215
+ !_deps.isGitAncestor(
2216
+ this.repoDir,
2217
+ info.baselineCommitOid,
2218
+ info.commitOid,
2219
+ )
2220
+ ) {
2221
+ throw new Error(
2222
+ `worktree commit for "${key}" no longer contains its dependency baseline`,
2223
+ );
2224
+ }
2225
+ } catch (error) {
2226
+ results.push({
2227
+ key,
2228
+ branch: info.branch,
2229
+ committed: info.committed,
2230
+ clean: false,
2231
+ merged: false,
2232
+ conflicts: error?.conflicts || [],
2233
+ error: error instanceof Error ? error.message : String(error),
2234
+ });
2235
+ continue;
2236
+ }
996
2237
  if (info.integration.merged) {
997
2238
  try {
998
2239
  if (
@@ -1288,9 +2529,21 @@ export class TeamWorktreeCoordinator {
1288
2529
  * sufficient proof that a missing worktree may be finalized without rerunning
1289
2530
  * its task or replaying integration.
1290
2531
  */
1291
- prepareCleanupAll({ requireMerged = false } = {}) {
2532
+ prepareCleanupAll({ requireMerged = false, onProgress = null } = {}) {
2533
+ if (onProgress == null) {
2534
+ return this._prepareCleanupAll({ requireMerged });
2535
+ }
2536
+ return this._runWithWorktreeProgress({
2537
+ phase: "prepare-cleanup",
2538
+ onProgress,
2539
+ run: (key) => this._prepareCleanupAll({ requireMerged, onlyKey: key }),
2540
+ });
2541
+ }
2542
+
2543
+ _prepareCleanupAll({ requireMerged = false, onlyKey = null } = {}) {
1292
2544
  const results = [];
1293
2545
  for (const [key, info] of this._created) {
2546
+ if (onlyKey != null && key !== onlyKey) continue;
1294
2547
  if (info.cleaned || info.cleanupPrepared) {
1295
2548
  results.push({
1296
2549
  key,
@@ -1325,9 +2578,19 @@ export class TeamWorktreeCoordinator {
1325
2578
  * Failed removals remain in the recovery snapshot and are returned to the
1326
2579
  * caller; they are never hidden by clearing the entire in-memory manifest.
1327
2580
  */
1328
- cleanupAll({ deleteBranch = false } = {}) {
2581
+ cleanupAll({ deleteBranch = false, onProgress = null } = {}) {
2582
+ if (onProgress == null) return this._cleanupAll({ deleteBranch });
2583
+ return this._runWithWorktreeProgress({
2584
+ phase: "cleanup",
2585
+ onProgress,
2586
+ run: (key) => this._cleanupAll({ deleteBranch, onlyKey: key }),
2587
+ });
2588
+ }
2589
+
2590
+ _cleanupAll({ deleteBranch = false, onlyKey = null } = {}) {
1329
2591
  const results = [];
1330
2592
  for (const [key, info] of Array.from(this._created)) {
2593
+ if (onlyKey != null && key !== onlyKey) continue;
1331
2594
  if (info.cleaned) {
1332
2595
  results.push({
1333
2596
  key,
@@ -1397,6 +2660,51 @@ export class TeamWorktreeCoordinator {
1397
2660
  return results;
1398
2661
  }
1399
2662
 
2663
+ _runWithWorktreeProgress({ phase, onProgress, run }) {
2664
+ if (typeof onProgress !== "function") {
2665
+ throw new TypeError("onProgress must be a function");
2666
+ }
2667
+ const entries = Array.from(this._created);
2668
+ const total = entries.length;
2669
+ const results = [];
2670
+ for (const [offset, [key, info]] of entries.entries()) {
2671
+ const progress = {
2672
+ phase,
2673
+ worktree: info.path,
2674
+ task: key,
2675
+ branch: info.branch,
2676
+ index: offset + 1,
2677
+ total,
2678
+ };
2679
+ this._callWorktreeProgress(onProgress, {
2680
+ ...progress,
2681
+ timing: "before",
2682
+ });
2683
+ results.push(...run(key));
2684
+ this._callWorktreeProgress(onProgress, {
2685
+ ...progress,
2686
+ timing: "after",
2687
+ });
2688
+ }
2689
+ return results;
2690
+ }
2691
+
2692
+ _callWorktreeProgress(onProgress, event) {
2693
+ const returned = onProgress(event);
2694
+ if (
2695
+ returned != null &&
2696
+ (typeof returned === "object" || typeof returned === "function") &&
2697
+ typeof returned.then === "function"
2698
+ ) {
2699
+ // Consume a native/foreign rejected thenable so rejecting unsupported
2700
+ // async callbacks cannot become an unhandled-rejection side channel.
2701
+ Promise.resolve(returned).catch(() => {});
2702
+ const error = new TypeError("onProgress callback must be synchronous");
2703
+ error.code = "TEAM_WORKTREE_ASYNC_PROGRESS_UNSUPPORTED";
2704
+ throw error;
2705
+ }
2706
+ }
2707
+
1400
2708
  branches() {
1401
2709
  return Array.from(this._created.values())
1402
2710
  .filter((info) => !info.cleaned)