chainlesschain 0.162.187 → 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.
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-CETaO3s8.js → AIOps-DbZft8w0.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CAe7E9TW.js → ActionButton-DoOZpwfN.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BGr2mzwO.js → Analytics-B0J0KLTW.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BEoY-NcT.js → AppLayout-D9eIR0qq.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-DUZivCgt.js → Artifacts-BnZXEVeo.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-C3yBs68k.js → Audit-X1ce4Bjn.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-C9KwiY3O.js → BackgroundAgents-BvWR-y5v.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BoSWD_C8.js → Backup-BublSGEd.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-Dp09GkxL.js → BaseInput-CR_77Hso.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-5t9d-Ja0.js → Chat-DixW52tq.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DmVku4GH.js → ChatBubbleRenderer-DDHDpBBA.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-8QJFlkqI.js → Checkbox-CCv1C87K.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BJVFQ3lL.js → Codegen-B03Cc3dd.js} +1 -1
- package/src/assets/web-panel/assets/{Col-6hdLD0oB.js → Col-CA06-6pU.js} +1 -1
- package/src/assets/web-panel/assets/{Community-B8opo0uY.js → Community-Bj881J52.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Dv8HudiN.js → Compact-UZLTPoaL.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DhxuCS5N.js → Compliance-kGwzRb_0.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Dggy6VIL.js → Cowork-KundoYM8.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DSmDqJTT.js → Cron-BYZyoqDe.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-7bZq0j5f.js → Crosschain-BhrrppBX.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BWY4kh-H.js → DID-CdC6EguZ.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-bOzPZxlu.js → Dashboard-CcQx3J4y.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BTFJHqQu.js → Dropdown-BpijtEzQ.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BC3MOdtT.js → EmailListRenderer-DfZjeZTe.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CFHAdTTY.js → FamilyGuardDashboard-BFYSf69C.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DZ_4aF_n.js → Federation-Okdi3pJm.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BZje6UTj.js → FormItemContext-mH-HebKW.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-D7QfCvjN.js → GenericCardRenderer-xivV69x7.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DuG7qANG.js → Git-1rh_YBwg.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BDmYAgTh.js → Governance-eAXkNxdY.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DSXAfc1Q.js → Inference-CS6dMVTN.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DusleniP.js → KnowledgeGraph-Bq_TnkvD.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CpImmA8p.js → Logs-KchHtiHt.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-BZ6LJBkk.js → MarkdownRenderer-TabqHW9B.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-DXS8CXkE.js → Marketplace-CsmGcX5X.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-D151UfUI.js → McpTools-BaPFnnyz.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-DKDxnoA1.js → Memory-2m-sUUf7.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BFZ95iKM.js → MobileBridge-D5nVZBRu.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-CDAR-grF.js → MobileProjects-Bo8MLZvT.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-OGJ95xJs.js → Mtc-3GDvamGa.js} +6 -6
- package/src/assets/web-panel/assets/{MtcAudit-BmGTo1XC.js → MtcAudit-D2zGuZCH.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-XumJRqOa.js → Multisig-BBVQ4t6t.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DHhhRZed.js → NLProgramming-BIOCyOGp.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B9rGxNNr.js → Notes-DVwBGpzc.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-DClLbMYV.js → NotificationSettings-BDnHfIfX.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-I8OohT7N.js → OrderTableRenderer-C7Ek20ag.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-M6WzaYVa.js → Organization-D3SRKaa7.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Cb3B5KtM.js → Overflow-C2B8ahyn.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BGk57kya.js → P2P-2iwQzG65.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-CXcKyPqK.js → PdhVaultBrowser-CoPiWqMN.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-hsfJ-Y0e.js → Permissions-yV5XNXo5.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BSBii4js.js → PersonalDataHub-uCDJhMUF.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-BNw52rey.js → Pipeline-hN7ucMOA.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DUpfue4E.js → Privacy-CgU0ptN4.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-C68NsP3h.js → ProjectInit-CfYPiT6j.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BJ2It9v4.js → ProjectSettings-B0A2C4nw.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BE5C0ajn.js +1 -0
- package/src/assets/web-panel/assets/{Providers-EnU4VWwS.js → Providers-CstQPf8W.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BRt5-B_I.js → QrScannerModal-Co3z_ynk.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-Jd5L9UL2.js → QuickAsk-DMTS6pQ6.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BNDhoVIu.js → Recommend-ETw4odUa.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-B0rkE09d.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-D1wQPUNv.js → Reputation-BTVqQgKq.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DnjRegul.js → Row-CBKKwwtO.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-C_T4I-nF.js → RssFeed-BHUQ438I.js} +2 -2
- package/src/assets/web-panel/assets/{Search-C4lrxlLh.js → Search-B4NqeUsF.js} +1 -1
- package/src/assets/web-panel/assets/{Security-ByYT-VzP.js → Security-BRjoYG5_.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BHpyEqB5.js → Services-CxQzu3p3.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-C0I23vG-.js → Skeleton-BKzwwmWO.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DGZXz22K.js → Skills-DrsFcgth.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DL_urP9A.js → Sla-D21tmXCH.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-6x-hUNQ_.js → SpeechSettings-CjyjLfp8.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-Crk5TCXq.js → SyncSettings-Cu45-tnx.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-rZGq5PZs.js → Tasks-oMcILYOj.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DT5el8Q8.js → Templates-Bh-Zcr5R.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-mlgeyMK8.js → Tenant-68ns63aI.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-C8HEA-lM.js → Terminal-BLJkFykE.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-DvlqBayB.js → TimelineRenderer-xcrJLq-O.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-C7lDcNmT.js → Tokens-C6PpFJtf.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-CM8nCnIJ.js → Trigger-BaKA7tdM.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BQDLUQ9x.js → Trust-BzhA57Jc.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CVqM0OFE.js → UkeySign-4_U3JkIR.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BvAcQpzT.js → VideoEditing-162AANxX.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-zFVFaAZ2.js → Wallet-B1viuql_.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Cc522ums.js → WebAuthn-ZVWazhCy.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-Bg_wxpDo.js → WorkflowEditor-tzdyUk-1.js} +1 -1
- package/src/assets/web-panel/assets/{chat-jg0QwC8K.js → chat-DmrHNuwq.js} +1 -1
- package/src/assets/web-panel/assets/{colors-ALzYz1gO.js → colors-DAJ_M-PK.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DXHYUoqi.js → compact-item-uIzmYwLs.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BsGsBM5m.js → createContext-D_ZmuLwb.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DIrncrV1.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BccMLzxA.js → hasIn-CDt_shTL.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cg9cOVe9.js → index-BAvSmJXW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BTTeAiOP.js → index-BDF6EQBs.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3k3ZwkQ.js → index-BHvlm0In.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQ1ORdp-.js → index-BRa4kJ_S.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcJzphrP.js → index-BTCXf1t-.js} +1 -1
- package/src/assets/web-panel/assets/{index-rIy96PeM.js → index-Bbxjfstq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSFESurl.js → index-Bx4d6c1k.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cmg_Npx5.js → index-ByzGvpR9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CahDve8M.js → index-C16fYKFD.js} +1 -1
- package/src/assets/web-panel/assets/{index-2VstcImq.js → index-C5WcdJH2.js} +3 -3
- package/src/assets/web-panel/assets/{index-C3Rf8wIM.js → index-CG0lY4h3.js} +1 -1
- package/src/assets/web-panel/assets/{index-uzykuL_R.js → index-CKyd472S.js} +1 -1
- package/src/assets/web-panel/assets/{index-CBVIFX-x.js → index-CaelyMgF.js} +1 -1
- package/src/assets/web-panel/assets/{index-TeCof1s3.js → index-CbD_WW_6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfF0OGKU.js → index-CcZ9xJI9.js} +1 -1
- package/src/assets/web-panel/assets/{index-oWltafBn.js → index-CeUpKjvi.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWLWBgfe.js → index-Cvak6Oe2.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0AbXsDy.js → index-CzXqvMuP.js} +1 -1
- package/src/assets/web-panel/assets/index-D-Swwqld.js +1 -0
- package/src/assets/web-panel/assets/{index-BfV8RDlg.js → index-D4n1lGTB.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dxqiu9-F.js → index-D9OE8B3d.js} +1 -1
- package/src/assets/web-panel/assets/{index-BdCmJ_bG.js → index-DBFmcaCQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DgjQ-kdT.js → index-DdUMcwRB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBbbr1YB.js → index-DeGBoLQA.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dmk7dD-T.js → index-DfSSy9uy.js} +1 -1
- package/src/assets/web-panel/assets/{index-JS2LXdd0.js → index-DqDSF2RW.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9oV2iRR.js → index-DvFhhe6r.js} +1 -1
- package/src/assets/web-panel/assets/{index-BvsII10i.js → index-DwfvPe-j.js} +1 -1
- package/src/assets/web-panel/assets/index-FMZAMKy8.js +1 -0
- package/src/assets/web-panel/assets/{index-CrZHUsOZ.js → index-FyplHuNt.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLzPzN75.js → index-GcKOHD_c.js} +1 -1
- package/src/assets/web-panel/assets/{index-koT8Q86Z.js → index-OzTxFIJP.js} +1 -1
- package/src/assets/web-panel/assets/{index-08diKcTm.js → index-QsgfSZrV.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJg6Wsjc.js → index-Z8P4BexE.js} +1 -1
- package/src/assets/web-panel/assets/{index-srqW5_-X.js → index-clSX4PEc.js} +1 -1
- package/src/assets/web-panel/assets/{index-mIQUoUx1.js → index-ifxmu8SY.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cz_s25m3.js → index-rS3gl_68.js} +1 -1
- package/src/assets/web-panel/assets/{index-UQXedWQz.js → index-rXdwUeE3.js} +1 -1
- package/src/assets/web-panel/assets/{index-rcwgNMc0.js → index-saf3VNeP.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-xafkqrW3.js → initDefaultProps-pmfaD-aK.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BrtyQXLk.js → motion-CaQ2CpYF.js} +1 -1
- package/src/assets/web-panel/assets/{move-BunUyg59.js → move-CkKTqNbI.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BPwjBeuE.js → mtc-parser-Dd4xsgRI.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BpyqZlNO.js → omit-BFuA5YEs.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DDrV0XjE.js → pickAttrs-DDfxOAJc.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-1A18Pcl2.js → placementArrow-Be595JCp.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-B8Fnlmso.js → responsiveObserve-7byljUM3.js} +1 -1
- package/src/assets/web-panel/assets/{slide-x-Fwb2Zm.js → slide-CTWfOTaY.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-aAG5yJl_.js → statusUtils-CNZOuIxM.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BRQSTMRL.js → styleChecker-DwOcBV78.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-D5X_KozY.js → useFlexGapSupport-B6MNwAO9.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DN_YIjP-.js → useFs-CzyQjH9r.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BizxAvjw.js → usePersonalDataHub-Dxh3KiVA.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-B5oAxN93.js → vnode-DVfZiS5N.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DJhvyzKv.js → zoom-BYme7fbY.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +22 -0
- package/src/commands/auto-mode.js +47 -0
- package/src/commands/checkpoint-managed.js +1059 -0
- package/src/commands/checkpoint.js +3 -0
- package/src/commands/team-distributed.js +3324 -0
- package/src/commands/team.js +1130 -74
- package/src/data/auto-mode-safety-eval-v1.json +3294 -0
- package/src/data/changelog.json +26 -0
- package/src/lib/agent-core.js +1 -0
- package/src/lib/agent-team/task-lease.js +271 -3
- package/src/lib/agent-team/team-adjudication.js +1501 -0
- package/src/lib/agent-team/team-budget.js +4 -1
- package/src/lib/agent-team/team-control-store.js +582 -0
- package/src/lib/agent-team/team-distributed-queue.js +5246 -0
- package/src/lib/agent-team/team-process-checkpoint.js +211 -0
- package/src/lib/agent-team/team-runner.js +319 -22
- package/src/lib/agent-team/team-worktree.js +1403 -95
- package/src/lib/auto-mode-safety-classifier.js +2034 -0
- package/src/lib/auto-mode-safety-eval.js +1022 -0
- package/src/lib/managed-checkpoint-render.js +60 -0
- package/src/lib/managed-tool-checkpoint.js +194 -0
- package/src/lib/process-execution-broker/index.js +438 -5
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +28 -1
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +28 -4
- package/src/lib/process-execution-broker/platform-sandbox.js +92 -70
- package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
- package/src/lib/process-execution-broker/windows-sandbox.cs +284 -19
- package/src/lib/process-execution-broker/workspace-transaction.js +3556 -0
- package/src/lib/secure-file-identity.js +433 -0
- package/src/lib/turn-binding.js +160 -2
- package/src/lib/with-file-lock.js +140 -23
- package/src/repl/agent-repl.js +19 -1
- package/src/runtime/agent-core.js +366 -122
- package/src/runtime/headless-runner.js +12 -0
- package/src/runtime/headless-stream.js +3 -0
- package/src/runtime/policies/agent-policy.js +14 -0
- package/src/assets/web-panel/assets/Projects-D2FHLUa-.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-BII41Mel.js +0 -5
- package/src/assets/web-panel/assets/devWarning-Dy1GJA4S.js +0 -1
- package/src/assets/web-panel/assets/index-8zbMbbuf.js +0 -1
- package/src/assets/web-panel/assets/index-C97f71oi.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 =
|
|
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(
|
|
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 =
|
|
145
|
-
|
|
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(() =>
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
1901
|
+
"task commit no longer contains its verified baseline",
|
|
893
1902
|
);
|
|
894
1903
|
}
|
|
895
|
-
|
|
896
|
-
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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}"
|
|
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
|
-
|
|
945
|
-
|
|
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}"
|
|
2038
|
+
`task "${key}" could not prepare its workspace checkpoint: ${
|
|
2039
|
+
error instanceof Error ? error.message : String(error)
|
|
2040
|
+
}`,
|
|
948
2041
|
),
|
|
949
|
-
"
|
|
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
|
-
*
|
|
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)
|