chainlesschain 0.162.186 → 0.162.187
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-DTXiO-TB.js → AIOps-CETaO3s8.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BbmDW-Pz.js → ActionButton-CAe7E9TW.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CSfgXJuD.js → Analytics-BGr2mzwO.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BS0FFLb7.js → AppLayout-BEoY-NcT.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-B19WFPK2.js → Artifacts-DUZivCgt.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CQhZBi7n.js → Audit-C3yBs68k.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BOLZCpGH.js → BackgroundAgents-C9KwiY3O.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Dz3R_ePD.js → Backup-BoSWD_C8.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CYfgMpax.js → BaseInput-Dp09GkxL.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-5CymMr1j.js → Chat-5t9d-Ja0.js} +4 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DmVku4GH.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-CDXHVYcu.js → Checkbox-8QJFlkqI.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-uq9Z9oiS.js → Codegen-BJVFQ3lL.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BuW1K_65.js → Col-6hdLD0oB.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Bbg-PvRY.js → Community-B8opo0uY.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-co0pPPTw.js → Compact-Dv8HudiN.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-B-pFAv7z.js → Compliance-DhxuCS5N.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-54ZQS4gC.js → Cowork-Dggy6VIL.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-B1BF2Seq.js → Cron-DSmDqJTT.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-Dey6qQTj.js → Crosschain-7bZq0j5f.js} +1 -1
- package/src/assets/web-panel/assets/{DID-ltERvfAj.js → DID-BWY4kh-H.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DuWxOcqN.js → Dashboard-bOzPZxlu.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CrZ8ekTo.js → Dropdown-BTFJHqQu.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-fIyHXhUS.js → EmailListRenderer-BC3MOdtT.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BtRyrRPm.js → FamilyGuardDashboard-CFHAdTTY.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-EUC6Wydj.js → Federation-DZ_4aF_n.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BOpka2G-.js → FormItemContext-BZje6UTj.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BXtrrYHn.js → GenericCardRenderer-D7QfCvjN.js} +1 -1
- package/src/assets/web-panel/assets/{Git-8WMb7PiR.js → Git-DuG7qANG.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DNWOeDTV.js → Governance-BDmYAgTh.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Bne9b_Lb.js → Inference-DSXAfc1Q.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Cp-6Us9b.js → KnowledgeGraph-DusleniP.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Ck-uSPAx.js → Logs-CpImmA8p.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-DxjsdAXw.js → MarkdownRenderer-BZ6LJBkk.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-Vdq7brrJ.js → Marketplace-DXS8CXkE.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-ggLnw4hY.js → McpTools-D151UfUI.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-BKIjNVCU.js → Memory-DKDxnoA1.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BElCY25a.js → MobileBridge-BFZ95iKM.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-CDAR-grF.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-bNdrF_Zh.js → Mtc-OGJ95xJs.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-Dw8w1Cqk.js → MtcAudit-BmGTo1XC.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CfGKvlqc.js → Multisig-XumJRqOa.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-D6OJf7YN.js → NLProgramming-DHhhRZed.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B3n_LzIS.js → Notes-B9rGxNNr.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-Uu1V_ICM.js → NotificationSettings-DClLbMYV.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-aGyWwWcp.js → OrderTableRenderer-I8OohT7N.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-qjFFmuwK.js → Organization-M6WzaYVa.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-J4ZgnwZz.js → Overflow-Cb3B5KtM.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-YEZF-uVH.js → P2P-BGk57kya.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-yqx7fwqk.js → PdhVaultBrowser-CXcKyPqK.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DdEjlZ02.js → Permissions-hsfJ-Y0e.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DbAPYvns.js → PersonalDataHub-BSBii4js.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-Bxlk_ZJU.js → Pipeline-BNw52rey.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CpE-05tz.js → Privacy-DUpfue4E.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-DWlhcGHT.js → ProjectInit-C68NsP3h.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BnH6BiTQ.js → ProjectSettings-BJ2It9v4.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CHw2fXDB.js → Projects-D2FHLUa-.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-Bdz_sTyb.js → Providers-EnU4VWwS.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BHh2b3pH.js → QrScannerModal-BRt5-B_I.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-D84M09pe.js → QuickAsk-Jd5L9UL2.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Czc30K6M.js → Recommend-BNDhoVIu.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-q_TU5qxt.js → RemoteSession-BII41Mel.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-CnTce4WM.js → Reputation-D1wQPUNv.js} +1 -1
- package/src/assets/web-panel/assets/{Row-C60UFLay.js → Row-DnjRegul.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BOUFEu9_.js → RssFeed-C_T4I-nF.js} +3 -3
- package/src/assets/web-panel/assets/{Search-D4__blE-.js → Search-C4lrxlLh.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BoOm7_JK.js → Security-ByYT-VzP.js} +4 -4
- package/src/assets/web-panel/assets/{Services-AvzrW6HA.js → Services-BHpyEqB5.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BZlrHozi.js → Skeleton-C0I23vG-.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CKaMLuk0.js → Skills-DGZXz22K.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DYCNJF8h.js → Sla-DL_urP9A.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-6x-hUNQ_.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-B6tW8dVO.js → SyncSettings-Crk5TCXq.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-oNC_2NVz.js → Tasks-rZGq5PZs.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DvLPvvjL.js → Templates-DT5el8Q8.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BDt9UBn_.js → Tenant-mlgeyMK8.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-ddCUnH-X.js → Terminal-C8HEA-lM.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CA4sDL47.js → TimelineRenderer-DvlqBayB.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-By03hnVs.js → Tokens-C7lDcNmT.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dz2mH-ZU.js → Trigger-CM8nCnIJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DShSWHeJ.js → Trust-BQDLUQ9x.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-JN8lP3gt.js → UkeySign-CVqM0OFE.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BgPy4gV3.js → VideoEditing-BvAcQpzT.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CQGEsXw_.js → Wallet-zFVFaAZ2.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-D8xR-ds_.js → WebAuthn-Cc522ums.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-Df61_p1p.js → WorkflowEditor-Bg_wxpDo.js} +1 -1
- package/src/assets/web-panel/assets/{chat-fdPby427.js → chat-jg0QwC8K.js} +1 -1
- package/src/assets/web-panel/assets/{colors-Dimqhufo.js → colors-ALzYz1gO.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DHayKcqZ.js → compact-item-DXHYUoqi.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-hZUhypRH.js → createContext-BsGsBM5m.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Dy1GJA4S.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Dw7zC_dV.js → hasIn-BccMLzxA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIxxvaGN.js → index-08diKcTm.js} +1 -1
- package/src/assets/web-panel/assets/{index-BVHdkz8F.js → index-2VstcImq.js} +17 -17
- package/src/assets/web-panel/assets/index-8zbMbbuf.js +1 -0
- package/src/assets/web-panel/assets/{index-DAjvkQ4K.js → index-BBbbr1YB.js} +1 -1
- package/src/assets/web-panel/assets/{index-DX1DwDFI.js → index-BTTeAiOP.js} +1 -1
- package/src/assets/web-panel/assets/{index-JE6Udz2r.js → index-BcJzphrP.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYdNwhkv.js → index-BdCmJ_bG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTiVrnpR.js → index-BfF0OGKU.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1yMYKi-.js → index-BfV8RDlg.js} +1 -1
- package/src/assets/web-panel/assets/{index-DxoPzPHy.js → index-BvsII10i.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyEhrvpm.js → index-C0AbXsDy.js} +1 -1
- package/src/assets/web-panel/assets/{index-lPjBQJCy.js → index-C3Rf8wIM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CPZ3U5iH.js → index-C3k3ZwkQ.js} +1 -1
- package/src/assets/web-panel/assets/index-C97f71oi.js +1 -0
- package/src/assets/web-panel/assets/{index-DJP7TjWm.js → index-CBVIFX-x.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSlda4-6.js → index-CJg6Wsjc.js} +1 -1
- package/src/assets/web-panel/assets/{index-DzJXkoZs.js → index-CLzPzN75.js} +1 -1
- package/src/assets/web-panel/assets/{index-cEnkyHtX.js → index-CQ1ORdp-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bhmpp3cm.js → index-CSFESurl.js} +1 -1
- package/src/assets/web-panel/assets/{index-BwnUluxu.js → index-CWLWBgfe.js} +1 -1
- package/src/assets/web-panel/assets/{index-CIOE8tt6.js → index-CahDve8M.js} +1 -1
- package/src/assets/web-panel/assets/{index-CBQK-LEF.js → index-Cg9cOVe9.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7jQEwB6.js → index-Cmg_Npx5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuMSdwno.js → index-CrZHUsOZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DnwDYBnG.js → index-Cz_s25m3.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVM_KgzF.js → index-D9oV2iRR.js} +1 -1
- package/src/assets/web-panel/assets/{index--DVpUpz3.js → index-DgjQ-kdT.js} +1 -1
- package/src/assets/web-panel/assets/{index-y5LHQ5Ck.js → index-Dmk7dD-T.js} +1 -1
- package/src/assets/web-panel/assets/{index-Urt2R5aE.js → index-Dxqiu9-F.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvKHpaao.js → index-JS2LXdd0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CsCo_A0I.js → index-TeCof1s3.js} +1 -1
- package/src/assets/web-panel/assets/{index-ulvBurtw.js → index-UQXedWQz.js} +1 -1
- package/src/assets/web-panel/assets/{index-BM5BNqp9.js → index-koT8Q86Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHJUptSx.js → index-mIQUoUx1.js} +1 -1
- package/src/assets/web-panel/assets/{index-C8XXPz2n.js → index-oWltafBn.js} +1 -1
- package/src/assets/web-panel/assets/{index-DC2R4zvM.js → index-rIy96PeM.js} +1 -1
- package/src/assets/web-panel/assets/{index-j0pxzOu7.js → index-rcwgNMc0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CuaVa8tX.js → index-srqW5_-X.js} +1 -1
- package/src/assets/web-panel/assets/{index-CcpDboUd.js → index-uzykuL_R.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-B4pR3px5.js → initDefaultProps-xafkqrW3.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CE7dWPPs.js → motion-BrtyQXLk.js} +1 -1
- package/src/assets/web-panel/assets/{move-Hw8cUObc.js → move-BunUyg59.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CvRjE4Fa.js → mtc-parser-BPwjBeuE.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CkFeLVre.js → omit-BpyqZlNO.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-5ZMqmbbb.js → pickAttrs-DDrV0XjE.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BZ4oeZ0q.js → placementArrow-1A18Pcl2.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-lwoiX_Us.js → responsiveObserve-B8Fnlmso.js} +1 -1
- package/src/assets/web-panel/assets/{slide-D_as37l8.js → slide-x-Fwb2Zm.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BsA-4Bs7.js → statusUtils-aAG5yJl_.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BVar4sLQ.js → styleChecker-BRQSTMRL.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-j1IwVpGd.js → useFlexGapSupport-D5X_KozY.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-7Q9bEnUp.js → useFs-DN_YIjP-.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BBwTIRMd.js → usePersonalDataHub-BizxAvjw.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-C7Dk-M8w.js → vnode-B5oAxN93.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-D3UHaTFB.js → zoom-DJhvyzKv.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/team.js +1805 -473
- package/src/data/changelog.json +13 -0
- package/src/harness/worktree-isolator.js +671 -79
- package/src/lib/agent-team/task-lease.js +378 -78
- package/src/lib/agent-team/team-agent-stream.js +488 -0
- package/src/lib/agent-team/team-budget.js +216 -20
- package/src/lib/agent-team/team-mailbox.js +389 -15
- package/src/lib/agent-team/team-run-state-lock.js +209 -0
- package/src/lib/agent-team/team-runner.js +547 -63
- package/src/lib/agent-team/team-scope-lock.js +409 -0
- package/src/lib/agent-team/team-task-contract.js +277 -0
- package/src/lib/agent-team/team-worktree.js +1171 -62
- package/src/lib/agent-worktree.js +19 -20
- package/src/lib/collaboration-run-store.js +983 -68
- package/src/lib/with-file-lock.js +190 -24
- package/src/lib/worktree-isolator.js +3 -0
- package/src/lib/worktree-sparse.js +23 -0
- package/src/runtime/headless-stream.js +6 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-B2A_TJtl.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-DpLgovdH.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-BvCSU6d2.js +0 -1
- package/src/assets/web-panel/assets/devWarning-ePxoBp92.js +0 -1
- package/src/assets/web-panel/assets/index-BTBDdCoo.js +0 -1
- package/src/assets/web-panel/assets/index-CfOGx8Ns.js +0 -1
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
* integration test exercises the actual worktree/commit/merge path.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
18
20
|
import {
|
|
19
21
|
createWorktree as _createWorktree,
|
|
22
|
+
assertManagedWorktreePath as _assertManagedWorktreePath,
|
|
23
|
+
removeManagedDependencyLinks as _removeManagedDependencyLinks,
|
|
20
24
|
removeWorktree as _removeWorktree,
|
|
21
25
|
previewWorktreeMerge as _previewWorktreeMerge,
|
|
22
26
|
mergeWorktree as _mergeWorktree,
|
|
@@ -24,12 +28,30 @@ import {
|
|
|
24
28
|
import { isGitRepo as _isGitRepo } from "../../lib/git-integration.js";
|
|
25
29
|
import { normalizeSparsePaths } from "../../lib/worktree-sparse.js";
|
|
26
30
|
import executionBroker from "../../lib/process-execution-broker/index.js";
|
|
31
|
+
import { stopBackgroundAgentChildTree } from "../../lib/background-agent-supervisor.js";
|
|
32
|
+
import { redactSecrets } from "../../lib/secret-scan.js";
|
|
27
33
|
|
|
28
34
|
export const _processDeps = {
|
|
29
35
|
spawn: executionBroker.spawn.bind(executionBroker),
|
|
30
36
|
execFileSync: executionBroker.execFileSync.bind(executionBroker),
|
|
37
|
+
killProcessTree: (child) => {
|
|
38
|
+
const pid = Number(child?.pid);
|
|
39
|
+
if (Number.isSafeInteger(pid) && pid > 0 && pid !== process.pid) {
|
|
40
|
+
try {
|
|
41
|
+
return stopBackgroundAgentChildTree(pid);
|
|
42
|
+
} catch {
|
|
43
|
+
/* fall through to the direct handle */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return child?.kill?.() ?? false;
|
|
47
|
+
},
|
|
31
48
|
};
|
|
32
49
|
|
|
50
|
+
export const MAX_TEAM_WORKTREE_STDERR_BYTES = 64 * 1024;
|
|
51
|
+
const TEAM_WORKTREE_SNAPSHOT_VERSION = 3;
|
|
52
|
+
const MAX_RUN_ID_BYTES = 256;
|
|
53
|
+
const MAX_RECORD_VALUE_BYTES = 4096;
|
|
54
|
+
|
|
33
55
|
/**
|
|
34
56
|
* Resolve the sparse-checkout / dependency-symlink options for a task, letting a
|
|
35
57
|
* per-task value (from the plan) override the coordinator-wide default. Returns
|
|
@@ -41,35 +63,142 @@ function resolveWorktreeOptions(task, defaults) {
|
|
|
41
63
|
const sparse = normalizeSparsePaths(
|
|
42
64
|
t.sparsePaths ?? t.metadata?.sparsePaths ?? defaults.sparsePaths ?? null,
|
|
43
65
|
);
|
|
44
|
-
const
|
|
45
|
-
t.symlinkDirectories ??
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
66
|
+
const requestedSymlink =
|
|
67
|
+
t.symlinkDirectories ?? t.metadata?.symlinkDirectories ?? null;
|
|
68
|
+
const approvedSymlink = defaults.symlinkDirectories ?? null;
|
|
69
|
+
let symlink = approvedSymlink;
|
|
70
|
+
if (requestedSymlink != null) {
|
|
71
|
+
const requested = Array.isArray(requestedSymlink)
|
|
72
|
+
? requestedSymlink
|
|
73
|
+
: [requestedSymlink];
|
|
74
|
+
const approved = new Set(
|
|
75
|
+
(Array.isArray(approvedSymlink)
|
|
76
|
+
? approvedSymlink
|
|
77
|
+
: approvedSymlink == null
|
|
78
|
+
? []
|
|
79
|
+
: [approvedSymlink]
|
|
80
|
+
).map(String),
|
|
81
|
+
);
|
|
82
|
+
if (requested.some((entry) => !approved.has(String(entry)))) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"task symlinkDirectories must be explicitly approved by --symlink-dirs",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
symlink = requestedSymlink;
|
|
88
|
+
}
|
|
49
89
|
const opts = {};
|
|
50
90
|
if (sparse) opts.sparsePaths = sparse;
|
|
51
91
|
if (symlink != null) opts.symlinkDirectories = symlink;
|
|
52
92
|
return Object.keys(opts).length ? opts : undefined;
|
|
53
93
|
}
|
|
54
94
|
|
|
55
|
-
function
|
|
95
|
+
function safeProcessError(value, fallback) {
|
|
96
|
+
const text = redactSecrets(String(value || "")).trim();
|
|
97
|
+
return (text || fallback).slice(0, 4096);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function requireAdjudication(error, code) {
|
|
101
|
+
const failure =
|
|
102
|
+
error instanceof Error ? error : new Error(String(error || code));
|
|
103
|
+
failure.code = failure.code || code;
|
|
104
|
+
failure.retryable = false;
|
|
105
|
+
return failure;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function defaultRunShell(command, cwd, { signal = null } = {}) {
|
|
56
109
|
return new Promise((resolve, reject) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
110
|
+
let child;
|
|
111
|
+
try {
|
|
112
|
+
child = _processDeps.spawn(command, [], {
|
|
113
|
+
cwd,
|
|
114
|
+
shell: true,
|
|
115
|
+
env: process.env,
|
|
116
|
+
origin: "team-worktree:task-command",
|
|
117
|
+
policy: "allow",
|
|
118
|
+
scope: "team-worktree",
|
|
119
|
+
detached: process.platform !== "win32",
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let settled = false;
|
|
127
|
+
let stderrBytes = 0;
|
|
128
|
+
const stderrChunks = [];
|
|
129
|
+
let terminationError = null;
|
|
130
|
+
let terminationTimer = null;
|
|
131
|
+
let abortListener = null;
|
|
132
|
+
const finish = (error, value) => {
|
|
133
|
+
if (settled) return;
|
|
134
|
+
settled = true;
|
|
135
|
+
if (terminationTimer) clearTimeout(terminationTimer);
|
|
136
|
+
if (abortListener) {
|
|
137
|
+
signal?.removeEventListener?.("abort", abortListener);
|
|
138
|
+
}
|
|
139
|
+
if (error) reject(error);
|
|
140
|
+
else resolve(value);
|
|
141
|
+
};
|
|
142
|
+
const terminate = () => {
|
|
143
|
+
if (settled || terminationError) return;
|
|
144
|
+
terminationError = new Error("Team worktree shell task was cancelled");
|
|
145
|
+
terminationError.code = "TEAM_WORKTREE_SHELL_ABORTED";
|
|
146
|
+
try {
|
|
147
|
+
_processDeps.killProcessTree(child);
|
|
148
|
+
} catch {
|
|
149
|
+
/* wait for close or the bounded grace timer */
|
|
150
|
+
}
|
|
151
|
+
terminationTimer = setTimeout(() => finish(terminationError), 5000);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// A piped stdout that nobody consumes can fill its OS buffer and deadlock a
|
|
155
|
+
// task before it exits. Worktree commands do not return stdout, so drain it.
|
|
156
|
+
if (typeof child.stdout?.resume === "function") {
|
|
157
|
+
child.stdout.resume();
|
|
158
|
+
} else {
|
|
159
|
+
child.stdout?.on?.("data", () => {});
|
|
160
|
+
}
|
|
161
|
+
child.stderr?.on("data", (data) => {
|
|
162
|
+
if (stderrBytes >= MAX_TEAM_WORKTREE_STDERR_BYTES) return;
|
|
163
|
+
const chunk = Buffer.isBuffer(data)
|
|
164
|
+
? data
|
|
165
|
+
: Buffer.from(String(data), "utf8");
|
|
166
|
+
const remaining = MAX_TEAM_WORKTREE_STDERR_BYTES - stderrBytes;
|
|
167
|
+
const retained = chunk.subarray(0, remaining);
|
|
168
|
+
if (retained.length > 0) {
|
|
169
|
+
stderrChunks.push(retained);
|
|
170
|
+
stderrBytes += retained.length;
|
|
171
|
+
}
|
|
64
172
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
173
|
+
child.once("error", (error) => {
|
|
174
|
+
if (!terminationError) {
|
|
175
|
+
finish(
|
|
176
|
+
new Error(
|
|
177
|
+
safeProcessError(
|
|
178
|
+
error?.message || error,
|
|
179
|
+
"command failed to start",
|
|
180
|
+
),
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
child.once("close", (code) => {
|
|
186
|
+
if (terminationError) {
|
|
187
|
+
finish(terminationError);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (code === 0) {
|
|
191
|
+
finish(null, { code });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const stderr = Buffer.concat(stderrChunks, stderrBytes).toString("utf8");
|
|
195
|
+
finish(new Error(safeProcessError(stderr, `command exited ${code}`)));
|
|
196
|
+
});
|
|
197
|
+
if (signal) {
|
|
198
|
+
abortListener = terminate;
|
|
199
|
+
if (signal.aborted) terminate();
|
|
200
|
+
else signal.addEventListener?.("abort", abortListener, { once: true });
|
|
201
|
+
}
|
|
73
202
|
});
|
|
74
203
|
}
|
|
75
204
|
|
|
@@ -84,41 +213,298 @@ function runTeamGit(args, cwd) {
|
|
|
84
213
|
});
|
|
85
214
|
}
|
|
86
215
|
|
|
216
|
+
function resolveGitRef(cwd, ref = "HEAD") {
|
|
217
|
+
const value = _processDeps.execFileSync(
|
|
218
|
+
"git",
|
|
219
|
+
["rev-parse", "--verify", ref],
|
|
220
|
+
{
|
|
221
|
+
cwd,
|
|
222
|
+
encoding: "utf8",
|
|
223
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
224
|
+
origin: "team-worktree:recovery",
|
|
225
|
+
policy: "allow",
|
|
226
|
+
scope: "team-worktree",
|
|
227
|
+
shell: false,
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
const oid = String(value || "").trim();
|
|
231
|
+
if (!/^[a-f0-9]{40,64}$/i.test(oid)) {
|
|
232
|
+
throw new Error(`invalid Git object id for ${ref}`);
|
|
233
|
+
}
|
|
234
|
+
return oid.toLowerCase();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function resolveGitBranch(cwd) {
|
|
238
|
+
try {
|
|
239
|
+
const value = _processDeps.execFileSync(
|
|
240
|
+
"git",
|
|
241
|
+
["symbolic-ref", "--quiet", "--short", "HEAD"],
|
|
242
|
+
{
|
|
243
|
+
cwd,
|
|
244
|
+
encoding: "utf8",
|
|
245
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
246
|
+
origin: "team-worktree:recovery",
|
|
247
|
+
policy: "allow",
|
|
248
|
+
scope: "team-worktree",
|
|
249
|
+
shell: false,
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
const branch = String(value || "").trim();
|
|
253
|
+
if (!isGitSafeBranch(branch)) {
|
|
254
|
+
throw new Error("invalid current Git branch");
|
|
255
|
+
}
|
|
256
|
+
return branch;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (error?.status === 1 || error?.code === 1) return null;
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function isGitAncestor(cwd, ancestor, descendant = "HEAD") {
|
|
264
|
+
try {
|
|
265
|
+
_processDeps.execFileSync(
|
|
266
|
+
"git",
|
|
267
|
+
["merge-base", "--is-ancestor", ancestor, descendant],
|
|
268
|
+
{
|
|
269
|
+
cwd,
|
|
270
|
+
stdio: "ignore",
|
|
271
|
+
origin: "team-worktree:recovery",
|
|
272
|
+
policy: "allow",
|
|
273
|
+
scope: "team-worktree",
|
|
274
|
+
shell: false,
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
return true;
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (error?.status === 1 || error?.code === 1) return false;
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isWorktreeClean(cwd, { includeIgnored = false } = {}) {
|
|
285
|
+
const args = [
|
|
286
|
+
"status",
|
|
287
|
+
"--porcelain=v1",
|
|
288
|
+
"--untracked-files=all",
|
|
289
|
+
"--ignore-submodules=none",
|
|
290
|
+
];
|
|
291
|
+
if (includeIgnored) args.push("--ignored=matching");
|
|
292
|
+
const value = _processDeps.execFileSync("git", args, {
|
|
293
|
+
cwd,
|
|
294
|
+
encoding: "utf8",
|
|
295
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
296
|
+
origin: "team-worktree:cleanup",
|
|
297
|
+
policy: "allow",
|
|
298
|
+
scope: "team-worktree",
|
|
299
|
+
shell: false,
|
|
300
|
+
});
|
|
301
|
+
return String(value || "").trim() === "";
|
|
302
|
+
}
|
|
303
|
+
|
|
87
304
|
/** Stage + commit everything in a worktree. Returns true if a commit was made. */
|
|
88
305
|
function defaultCommit(worktreePath, message) {
|
|
89
306
|
runTeamGit(["add", "-A"], worktreePath);
|
|
307
|
+
|
|
90
308
|
try {
|
|
91
|
-
runTeamGit(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"commit",
|
|
98
|
-
"-m",
|
|
99
|
-
message,
|
|
100
|
-
],
|
|
101
|
-
worktreePath,
|
|
102
|
-
);
|
|
103
|
-
return true;
|
|
104
|
-
} catch {
|
|
105
|
-
return false; // nothing to commit (no changes)
|
|
309
|
+
runTeamGit(["diff", "--cached", "--quiet", "--exit-code"], worktreePath);
|
|
310
|
+
return false;
|
|
311
|
+
} catch (error) {
|
|
312
|
+
// `git diff --quiet` uses status 1 exclusively to report a non-empty diff.
|
|
313
|
+
// Any other status is an actual Git failure and must not be hidden.
|
|
314
|
+
if (error?.status !== 1 && error?.code !== 1) throw error;
|
|
106
315
|
}
|
|
316
|
+
|
|
317
|
+
// The staged diff was known to be non-empty. A commit failure here may be a
|
|
318
|
+
// hook, lock, identity, or repository problem, so always surface it.
|
|
319
|
+
runTeamGit(
|
|
320
|
+
[
|
|
321
|
+
"-c",
|
|
322
|
+
"user.email=team@chainlesschain.local",
|
|
323
|
+
"-c",
|
|
324
|
+
"user.name=cc team",
|
|
325
|
+
"commit",
|
|
326
|
+
"-m",
|
|
327
|
+
message,
|
|
328
|
+
],
|
|
329
|
+
worktreePath,
|
|
330
|
+
);
|
|
331
|
+
return true;
|
|
107
332
|
}
|
|
108
333
|
|
|
109
334
|
export const _deps = {
|
|
110
335
|
createWorktree: _createWorktree,
|
|
336
|
+
assertManagedWorktreePath: _assertManagedWorktreePath,
|
|
337
|
+
removeManagedDependencyLinks: _removeManagedDependencyLinks,
|
|
111
338
|
removeWorktree: _removeWorktree,
|
|
112
339
|
previewWorktreeMerge: _previewWorktreeMerge,
|
|
113
340
|
mergeWorktree: _mergeWorktree,
|
|
114
341
|
isGitRepo: _isGitRepo,
|
|
115
342
|
runShell: defaultRunShell,
|
|
116
343
|
commit: defaultCommit,
|
|
344
|
+
resolveGitRef,
|
|
345
|
+
resolveGitBranch,
|
|
346
|
+
isGitAncestor,
|
|
347
|
+
isWorktreeClean,
|
|
348
|
+
pathExists: existsSync,
|
|
117
349
|
};
|
|
118
350
|
|
|
351
|
+
function normalizeRunId(value) {
|
|
352
|
+
if (value == null) return null;
|
|
353
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
354
|
+
throw new TypeError("team worktree runId must be a non-empty string");
|
|
355
|
+
}
|
|
356
|
+
if (Buffer.byteLength(value, "utf8") > MAX_RUN_ID_BYTES) {
|
|
357
|
+
throw new RangeError(
|
|
358
|
+
`team worktree runId exceeds ${MAX_RUN_ID_BYTES} bytes`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
return value;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function safeGitSegment(value, fallback) {
|
|
365
|
+
const raw = String(value);
|
|
366
|
+
const digest = createHash("sha256")
|
|
367
|
+
.update(raw, "utf8")
|
|
368
|
+
.digest("hex")
|
|
369
|
+
.slice(0, 16);
|
|
370
|
+
let slug = raw
|
|
371
|
+
.normalize("NFKC")
|
|
372
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
373
|
+
.replace(/\.{2,}/g, "-")
|
|
374
|
+
.replace(/^[.-]+|[.-]+$/g, "")
|
|
375
|
+
.slice(0, 48)
|
|
376
|
+
.replace(/[.-]+$/g, "");
|
|
377
|
+
if (!slug) slug = fallback;
|
|
378
|
+
return `${slug}-${digest}`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function isGitSafeBranch(branch) {
|
|
382
|
+
if (
|
|
383
|
+
typeof branch !== "string" ||
|
|
384
|
+
branch.length === 0 ||
|
|
385
|
+
Buffer.byteLength(branch, "utf8") > MAX_RECORD_VALUE_BYTES ||
|
|
386
|
+
branch === "@" ||
|
|
387
|
+
branch.startsWith("/") ||
|
|
388
|
+
branch.endsWith("/") ||
|
|
389
|
+
branch.endsWith(".") ||
|
|
390
|
+
branch.includes("..") ||
|
|
391
|
+
branch.includes("@{") ||
|
|
392
|
+
branch.includes("//") ||
|
|
393
|
+
Array.from(branch).some((char) => {
|
|
394
|
+
const code = char.codePointAt(0);
|
|
395
|
+
return code <= 0x20 || code === 0x7f;
|
|
396
|
+
}) ||
|
|
397
|
+
/[~^:?*[\]\\]/u.test(branch)
|
|
398
|
+
) {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
return branch
|
|
402
|
+
.split("/")
|
|
403
|
+
.every(
|
|
404
|
+
(part) =>
|
|
405
|
+
part.length > 0 && !part.startsWith(".") && !part.endsWith(".lock"),
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function requireRecordString(value, field) {
|
|
410
|
+
if (
|
|
411
|
+
typeof value !== "string" ||
|
|
412
|
+
value.length === 0 ||
|
|
413
|
+
Buffer.byteLength(value, "utf8") > MAX_RECORD_VALUE_BYTES ||
|
|
414
|
+
value.includes("\0")
|
|
415
|
+
) {
|
|
416
|
+
throw new TypeError(`invalid team worktree record ${field}`);
|
|
417
|
+
}
|
|
418
|
+
return value;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function requireGitOid(value, field) {
|
|
422
|
+
if (typeof value !== "string" || !/^[a-f0-9]{40,64}$/i.test(value)) {
|
|
423
|
+
throw new TypeError(`invalid team worktree record ${field}`);
|
|
424
|
+
}
|
|
425
|
+
return value.toLowerCase();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function emptyIntegrationState() {
|
|
429
|
+
return {
|
|
430
|
+
previewed: false,
|
|
431
|
+
clean: null,
|
|
432
|
+
merged: false,
|
|
433
|
+
baseCommit: null,
|
|
434
|
+
mergeCommit: null,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function normalizeIntegrationState(value, key) {
|
|
439
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
440
|
+
throw new TypeError(`invalid team worktree integration state for "${key}"`);
|
|
441
|
+
}
|
|
442
|
+
if (
|
|
443
|
+
typeof value.previewed !== "boolean" ||
|
|
444
|
+
(value.clean !== null && typeof value.clean !== "boolean") ||
|
|
445
|
+
typeof value.merged !== "boolean" ||
|
|
446
|
+
(value.baseCommit !== null &&
|
|
447
|
+
!/^[a-f0-9]{40,64}$/i.test(value.baseCommit)) ||
|
|
448
|
+
(value.mergeCommit !== null &&
|
|
449
|
+
!/^[a-f0-9]{40,64}$/i.test(value.mergeCommit)) ||
|
|
450
|
+
(!value.previewed &&
|
|
451
|
+
(value.clean !== null ||
|
|
452
|
+
value.merged ||
|
|
453
|
+
value.baseCommit !== null ||
|
|
454
|
+
value.mergeCommit !== null)) ||
|
|
455
|
+
(value.previewed &&
|
|
456
|
+
(typeof value.clean !== "boolean" || value.baseCommit == null)) ||
|
|
457
|
+
(!value.merged && value.mergeCommit !== null) ||
|
|
458
|
+
(value.merged && (value.clean !== true || value.mergeCommit == null))
|
|
459
|
+
) {
|
|
460
|
+
throw new TypeError(`invalid team worktree integration state for "${key}"`);
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
previewed: value.previewed,
|
|
464
|
+
clean: value.clean,
|
|
465
|
+
merged: value.merged,
|
|
466
|
+
baseCommit: value.baseCommit?.toLowerCase() || null,
|
|
467
|
+
mergeCommit: value.mergeCommit?.toLowerCase() || null,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function normalizeBaseTarget(value) {
|
|
472
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
473
|
+
throw new TypeError("invalid team worktree base target");
|
|
474
|
+
}
|
|
475
|
+
const branch = value.branch;
|
|
476
|
+
if (branch !== null && !isGitSafeBranch(branch)) {
|
|
477
|
+
throw new TypeError("invalid team worktree base branch");
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
branch,
|
|
481
|
+
commitOid: requireGitOid(value.commitOid, "baseTarget.commitOid"),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function normalizeManagedLinks(value, key) {
|
|
486
|
+
if (value == null) return [];
|
|
487
|
+
if (!Array.isArray(value)) {
|
|
488
|
+
throw new TypeError(`invalid team worktree managed links for "${key}"`);
|
|
489
|
+
}
|
|
490
|
+
const seen = new Set();
|
|
491
|
+
const links = [];
|
|
492
|
+
for (const item of value) {
|
|
493
|
+
const link = requireRecordString(item, "managedLinks");
|
|
494
|
+
if (seen.has(link)) {
|
|
495
|
+
throw new TypeError(`duplicate team worktree managed link for "${key}"`);
|
|
496
|
+
}
|
|
497
|
+
seen.add(link);
|
|
498
|
+
links.push(link);
|
|
499
|
+
}
|
|
500
|
+
return links;
|
|
501
|
+
}
|
|
502
|
+
|
|
119
503
|
export class TeamWorktreeCoordinator {
|
|
120
504
|
constructor(repoDir, options = {}) {
|
|
121
505
|
this.repoDir = repoDir;
|
|
506
|
+
this.runId = normalizeRunId(options.runId);
|
|
507
|
+
this._baseTarget = null;
|
|
122
508
|
this._created = new Map(); // key → { branch, path, committed }
|
|
123
509
|
// Coordinator-wide sparse-checkout / dependency-symlink defaults; a per-task
|
|
124
510
|
// value (task.sparsePaths / task.metadata.sparsePaths) overrides these.
|
|
@@ -128,14 +514,247 @@ export class TeamWorktreeCoordinator {
|
|
|
128
514
|
};
|
|
129
515
|
this._onWorktree =
|
|
130
516
|
typeof options.onWorktree === "function" ? options.onWorktree : null;
|
|
517
|
+
const recovered =
|
|
518
|
+
options.snapshot ??
|
|
519
|
+
options.recover ??
|
|
520
|
+
options.seedRecords ??
|
|
521
|
+
options.seed ??
|
|
522
|
+
null;
|
|
523
|
+
if (recovered) {
|
|
524
|
+
if (Array.isArray(recovered)) this.seed(recovered);
|
|
525
|
+
else this.restore(recovered);
|
|
526
|
+
}
|
|
527
|
+
if (
|
|
528
|
+
this._baseTarget == null &&
|
|
529
|
+
(this._created.size > 0 || _deps.isGitRepo(this.repoDir))
|
|
530
|
+
) {
|
|
531
|
+
this._captureBaseTarget();
|
|
532
|
+
}
|
|
131
533
|
}
|
|
132
534
|
|
|
133
535
|
isGitRepo() {
|
|
134
536
|
return _deps.isGitRepo(this.repoDir);
|
|
135
537
|
}
|
|
136
538
|
|
|
539
|
+
_captureBaseTarget() {
|
|
540
|
+
this._baseTarget = {
|
|
541
|
+
branch: _deps.resolveGitBranch(this.repoDir),
|
|
542
|
+
commitOid: requireGitOid(
|
|
543
|
+
_deps.resolveGitRef(this.repoDir, "HEAD"),
|
|
544
|
+
"baseTarget.commitOid",
|
|
545
|
+
),
|
|
546
|
+
};
|
|
547
|
+
return { ...this._baseTarget };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
_assertBaseTarget({ merge = false } = {}) {
|
|
551
|
+
if (this._baseTarget == null) this._captureBaseTarget();
|
|
552
|
+
const currentBranch = _deps.resolveGitBranch(this.repoDir);
|
|
553
|
+
const currentHead = requireGitOid(
|
|
554
|
+
_deps.resolveGitRef(this.repoDir, "HEAD"),
|
|
555
|
+
"baseTarget.currentHead",
|
|
556
|
+
);
|
|
557
|
+
if (currentBranch !== this._baseTarget.branch) {
|
|
558
|
+
throw requireAdjudication(
|
|
559
|
+
new Error(
|
|
560
|
+
`team worktree base target changed from ${
|
|
561
|
+
this._baseTarget.branch || "detached HEAD"
|
|
562
|
+
} to ${currentBranch || "detached HEAD"}`,
|
|
563
|
+
),
|
|
564
|
+
"TEAM_WORKTREE_BASE_ADJUDICATION_REQUIRED",
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
if (currentBranch == null) {
|
|
568
|
+
if (merge) {
|
|
569
|
+
throw requireAdjudication(
|
|
570
|
+
new Error(
|
|
571
|
+
"refusing to merge team worktrees into a detached HEAD; check out the persisted base branch first",
|
|
572
|
+
),
|
|
573
|
+
"TEAM_WORKTREE_BASE_ADJUDICATION_REQUIRED",
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
if (currentHead !== this._baseTarget.commitOid) {
|
|
577
|
+
throw requireAdjudication(
|
|
578
|
+
new Error(
|
|
579
|
+
"detached team worktree base HEAD moved after run creation",
|
|
580
|
+
),
|
|
581
|
+
"TEAM_WORKTREE_BASE_ADJUDICATION_REQUIRED",
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
} else if (
|
|
585
|
+
currentHead !== this._baseTarget.commitOid &&
|
|
586
|
+
!_deps.isGitAncestor(
|
|
587
|
+
this.repoDir,
|
|
588
|
+
this._baseTarget.commitOid,
|
|
589
|
+
currentHead,
|
|
590
|
+
)
|
|
591
|
+
) {
|
|
592
|
+
throw requireAdjudication(
|
|
593
|
+
new Error(
|
|
594
|
+
`team worktree base branch "${currentBranch}" no longer descends from its persisted start commit`,
|
|
595
|
+
),
|
|
596
|
+
"TEAM_WORKTREE_BASE_ADJUDICATION_REQUIRED",
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
return { branch: currentBranch, currentHead };
|
|
600
|
+
}
|
|
601
|
+
|
|
137
602
|
branchFor(key) {
|
|
138
|
-
return `team/${key}`;
|
|
603
|
+
if (this.runId == null) return `team/${key}`;
|
|
604
|
+
requireRecordString(key, "key");
|
|
605
|
+
return `team/${safeGitSegment(this.runId, "run")}/${safeGitSegment(
|
|
606
|
+
key,
|
|
607
|
+
"task",
|
|
608
|
+
)}`;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** Seed validated worktree records after recovering team state. */
|
|
612
|
+
seed(records) {
|
|
613
|
+
if (!Array.isArray(records)) {
|
|
614
|
+
throw new TypeError("team worktree seed must be an array");
|
|
615
|
+
}
|
|
616
|
+
const normalized = [];
|
|
617
|
+
const keys = new Set(this._created.keys());
|
|
618
|
+
for (const record of records) {
|
|
619
|
+
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
|
620
|
+
throw new TypeError("invalid team worktree record");
|
|
621
|
+
}
|
|
622
|
+
const key = requireRecordString(record.key, "key");
|
|
623
|
+
const branch = requireRecordString(record.branch, "branch");
|
|
624
|
+
const worktreePath = _deps.assertManagedWorktreePath(
|
|
625
|
+
this.repoDir,
|
|
626
|
+
requireRecordString(record.path, "path"),
|
|
627
|
+
{ branchName: branch },
|
|
628
|
+
);
|
|
629
|
+
if (!isGitSafeBranch(branch)) {
|
|
630
|
+
throw new TypeError(`invalid team worktree record branch for "${key}"`);
|
|
631
|
+
}
|
|
632
|
+
if (branch !== this.branchFor(key)) {
|
|
633
|
+
throw new Error(`team worktree branch does not match key "${key}"`);
|
|
634
|
+
}
|
|
635
|
+
if (keys.has(key)) {
|
|
636
|
+
throw new Error(`duplicate team worktree record "${key}"`);
|
|
637
|
+
}
|
|
638
|
+
const committed = record.committed;
|
|
639
|
+
const completed = record.completed ?? true;
|
|
640
|
+
const cleanupPrepared = record.cleanupPrepared;
|
|
641
|
+
const cleaned = record.cleaned;
|
|
642
|
+
if (
|
|
643
|
+
typeof committed !== "boolean" ||
|
|
644
|
+
typeof completed !== "boolean" ||
|
|
645
|
+
typeof cleanupPrepared !== "boolean" ||
|
|
646
|
+
typeof cleaned !== "boolean"
|
|
647
|
+
) {
|
|
648
|
+
throw new TypeError(
|
|
649
|
+
`invalid team worktree completion state for "${key}"`,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
const commitOid = completed
|
|
653
|
+
? requireGitOid(record.commitOid, "commitOid")
|
|
654
|
+
: record.commitOid == null
|
|
655
|
+
? null
|
|
656
|
+
: requireGitOid(record.commitOid, "commitOid");
|
|
657
|
+
const integration = normalizeIntegrationState(record.integration, key);
|
|
658
|
+
const managedLinks = normalizeManagedLinks(record.managedLinks, key);
|
|
659
|
+
if (
|
|
660
|
+
!completed &&
|
|
661
|
+
(committed ||
|
|
662
|
+
commitOid !== null ||
|
|
663
|
+
integration.previewed ||
|
|
664
|
+
cleanupPrepared ||
|
|
665
|
+
cleaned)
|
|
666
|
+
) {
|
|
667
|
+
throw new TypeError(
|
|
668
|
+
`invalid unfinished team worktree recovery state for "${key}"`,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
if (
|
|
672
|
+
cleanupPrepared &&
|
|
673
|
+
(!integration.previewed || integration.clean !== true)
|
|
674
|
+
) {
|
|
675
|
+
throw new TypeError(
|
|
676
|
+
`prepared team worktree "${key}" has no clean integration record`,
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
if (cleaned && !cleanupPrepared) {
|
|
680
|
+
throw new TypeError(
|
|
681
|
+
`cleaned team worktree "${key}" has no cleanup authorization`,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
keys.add(key);
|
|
685
|
+
normalized.push({
|
|
686
|
+
key,
|
|
687
|
+
info: {
|
|
688
|
+
branch,
|
|
689
|
+
path: worktreePath,
|
|
690
|
+
committed,
|
|
691
|
+
completed,
|
|
692
|
+
commitOid,
|
|
693
|
+
integration,
|
|
694
|
+
managedLinks,
|
|
695
|
+
cleanupPrepared,
|
|
696
|
+
cleaned,
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
for (const { key, info } of normalized) this._created.set(key, info);
|
|
701
|
+
return this;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** Return a detached, JSON-safe recovery snapshot. */
|
|
705
|
+
snapshot() {
|
|
706
|
+
return {
|
|
707
|
+
version: TEAM_WORKTREE_SNAPSHOT_VERSION,
|
|
708
|
+
runId: this.runId,
|
|
709
|
+
baseTarget: this._baseTarget ? { ...this._baseTarget } : null,
|
|
710
|
+
records: Array.from(this._created, ([key, info]) => ({
|
|
711
|
+
key,
|
|
712
|
+
branch: info.branch,
|
|
713
|
+
path: info.path,
|
|
714
|
+
committed: info.committed === true,
|
|
715
|
+
completed: info.completed === true,
|
|
716
|
+
commitOid: info.commitOid || null,
|
|
717
|
+
integration: { ...info.integration },
|
|
718
|
+
managedLinks: [...(info.managedLinks || [])],
|
|
719
|
+
cleanupPrepared: info.cleanupPrepared === true,
|
|
720
|
+
cleaned: info.cleaned === true,
|
|
721
|
+
})),
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** Restore a snapshot into an empty coordinator. */
|
|
726
|
+
restore(snapshot) {
|
|
727
|
+
if (this._created.size > 0) {
|
|
728
|
+
throw new Error(
|
|
729
|
+
"cannot restore team worktrees into a non-empty coordinator",
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
if (
|
|
733
|
+
!snapshot ||
|
|
734
|
+
typeof snapshot !== "object" ||
|
|
735
|
+
Array.isArray(snapshot) ||
|
|
736
|
+
snapshot.version !== TEAM_WORKTREE_SNAPSHOT_VERSION ||
|
|
737
|
+
!snapshot.baseTarget ||
|
|
738
|
+
!Array.isArray(snapshot.records)
|
|
739
|
+
) {
|
|
740
|
+
throw new TypeError("invalid team worktree snapshot");
|
|
741
|
+
}
|
|
742
|
+
const snapshotRunId = normalizeRunId(snapshot.runId);
|
|
743
|
+
const previousRunId = this.runId;
|
|
744
|
+
const previousBaseTarget = this._baseTarget;
|
|
745
|
+
if (this.runId == null) {
|
|
746
|
+
this.runId = snapshotRunId;
|
|
747
|
+
} else if (snapshotRunId !== this.runId) {
|
|
748
|
+
throw new Error("team worktree snapshot runId mismatch");
|
|
749
|
+
}
|
|
750
|
+
this._baseTarget = normalizeBaseTarget(snapshot.baseTarget);
|
|
751
|
+
try {
|
|
752
|
+
return this.seed(snapshot.records);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
this.runId = previousRunId;
|
|
755
|
+
this._baseTarget = previousBaseTarget;
|
|
756
|
+
throw error;
|
|
757
|
+
}
|
|
139
758
|
}
|
|
140
759
|
|
|
141
760
|
/**
|
|
@@ -147,7 +766,9 @@ export class TeamWorktreeCoordinator {
|
|
|
147
766
|
* non-zero command / agent exit so retry/cancel still work.
|
|
148
767
|
*/
|
|
149
768
|
makeRunTask({ runInWorktree = null } = {}) {
|
|
150
|
-
return async (
|
|
769
|
+
return async (context) => {
|
|
770
|
+
const { key, task, holder } = context;
|
|
771
|
+
this._assertBaseTarget();
|
|
151
772
|
const branch = this.branchFor(key);
|
|
152
773
|
// A retry (the prior attempt failed and TaskLeaseRegistry re-queued the
|
|
153
774
|
// task) would collide with its own leftover worktree: createWorktree
|
|
@@ -158,38 +779,192 @@ export class TeamWorktreeCoordinator {
|
|
|
158
779
|
// so a retry starts clean.
|
|
159
780
|
const prior = this._created.get(key);
|
|
160
781
|
if (prior) {
|
|
782
|
+
if (prior.completed) {
|
|
783
|
+
throw requireAdjudication(
|
|
784
|
+
new Error(`task "${key}" already has completed worktree output`),
|
|
785
|
+
"TEAM_WORKTREE_RETRY_ADJUDICATION_REQUIRED",
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
const priorPath = _deps.assertManagedWorktreePath(
|
|
789
|
+
this.repoDir,
|
|
790
|
+
prior.path,
|
|
791
|
+
{ branchName: prior.branch },
|
|
792
|
+
);
|
|
793
|
+
const priorBranchHead = _deps.resolveGitRef(this.repoDir, prior.branch);
|
|
794
|
+
const baseHead = _deps.resolveGitRef(this.repoDir, "HEAD");
|
|
795
|
+
if (priorBranchHead !== baseHead) {
|
|
796
|
+
throw requireAdjudication(
|
|
797
|
+
new Error(`task "${key}" has an unsettled worktree commit`),
|
|
798
|
+
"TEAM_WORKTREE_RETRY_ADJUDICATION_REQUIRED",
|
|
799
|
+
);
|
|
800
|
+
}
|
|
161
801
|
try {
|
|
162
|
-
_deps.
|
|
802
|
+
if (_deps.pathExists(priorPath)) {
|
|
803
|
+
if (_deps.resolveGitRef(priorPath, "HEAD") !== priorBranchHead) {
|
|
804
|
+
throw new Error("worktree HEAD moved");
|
|
805
|
+
}
|
|
806
|
+
_deps.removeManagedDependencyLinks(
|
|
807
|
+
this.repoDir,
|
|
808
|
+
priorPath,
|
|
809
|
+
prior.managedLinks || [],
|
|
810
|
+
);
|
|
811
|
+
if (!_deps.isWorktreeClean(priorPath, { includeIgnored: true })) {
|
|
812
|
+
throw new Error("worktree has unsettled or ignored files");
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
_deps.removeWorktree(this.repoDir, priorPath, {
|
|
163
816
|
deleteBranch: true,
|
|
817
|
+
branchName: prior.branch,
|
|
818
|
+
expectedBranchOid: priorBranchHead,
|
|
819
|
+
force: false,
|
|
164
820
|
});
|
|
165
|
-
} catch {
|
|
166
|
-
|
|
821
|
+
} catch (error) {
|
|
822
|
+
throw requireAdjudication(
|
|
823
|
+
new Error(
|
|
824
|
+
`task "${key}" retry would discard unsettled worktree changes: ${
|
|
825
|
+
error instanceof Error ? error.message : String(error)
|
|
826
|
+
}`,
|
|
827
|
+
),
|
|
828
|
+
"TEAM_WORKTREE_RETRY_ADJUDICATION_REQUIRED",
|
|
829
|
+
);
|
|
167
830
|
}
|
|
168
831
|
this._created.delete(key);
|
|
169
832
|
}
|
|
170
833
|
const wtOptions = resolveWorktreeOptions(task, this._worktreeDefaults);
|
|
171
|
-
const
|
|
834
|
+
const created = _deps.createWorktree(
|
|
172
835
|
this.repoDir,
|
|
173
836
|
branch,
|
|
174
837
|
null,
|
|
175
838
|
wtOptions,
|
|
176
839
|
);
|
|
177
|
-
|
|
840
|
+
const createdPath = created.path;
|
|
841
|
+
const worktreePath = _deps.assertManagedWorktreePath(
|
|
842
|
+
this.repoDir,
|
|
843
|
+
createdPath,
|
|
844
|
+
{ branchName: branch },
|
|
845
|
+
);
|
|
846
|
+
this._created.set(key, {
|
|
847
|
+
branch,
|
|
848
|
+
path: worktreePath,
|
|
849
|
+
committed: false,
|
|
850
|
+
completed: false,
|
|
851
|
+
commitOid: null,
|
|
852
|
+
integration: emptyIntegrationState(),
|
|
853
|
+
managedLinks: normalizeManagedLinks(
|
|
854
|
+
created.symlinkedDirectories || [],
|
|
855
|
+
key,
|
|
856
|
+
),
|
|
857
|
+
cleanupPrepared: false,
|
|
858
|
+
cleaned: false,
|
|
859
|
+
});
|
|
178
860
|
this._onWorktree?.({ key, branch, path: worktreePath, holder });
|
|
179
|
-
if (typeof
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
861
|
+
if (typeof context.renew === "function") {
|
|
862
|
+
const fence = await context.renew();
|
|
863
|
+
if (!fence?.ok) {
|
|
864
|
+
throw requireAdjudication(
|
|
865
|
+
new Error(
|
|
866
|
+
`task "${key}" lost its lease while creating its worktree`,
|
|
867
|
+
),
|
|
868
|
+
"TEAM_WORKTREE_CREATE_ADJUDICATION_REQUIRED",
|
|
186
869
|
);
|
|
187
870
|
}
|
|
188
|
-
await _deps.runShell(command, worktreePath);
|
|
189
871
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
872
|
+
if (
|
|
873
|
+
context.signal?.aborted ||
|
|
874
|
+
context.budget?.reason?.() === "max-wall-ms"
|
|
875
|
+
) {
|
|
876
|
+
throw requireAdjudication(
|
|
877
|
+
new Error(`task "${key}" was cancelled before execution`),
|
|
878
|
+
"TEAM_WORKTREE_EXECUTION_CANCELLED",
|
|
879
|
+
);
|
|
880
|
+
}
|
|
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) {
|
|
891
|
+
throw new Error(
|
|
892
|
+
`task "${key}" has no \`command\` to run in a worktree`,
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
await _deps.runShell(command, worktreePath, {
|
|
896
|
+
signal: context.signal || null,
|
|
897
|
+
});
|
|
898
|
+
}
|
|
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) {
|
|
936
|
+
throw requireAdjudication(
|
|
937
|
+
new Error(
|
|
938
|
+
`task "${key}" lost its lease after committing worktree output`,
|
|
939
|
+
),
|
|
940
|
+
"TEAM_WORKTREE_POST_COMMIT_ADJUDICATION_REQUIRED",
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (context.budget?.reason?.() === "max-wall-ms") {
|
|
945
|
+
throw requireAdjudication(
|
|
946
|
+
new Error(
|
|
947
|
+
`task "${key}" exceeded the team wall-clock budget during commit`,
|
|
948
|
+
),
|
|
949
|
+
"TEAM_WORKTREE_POST_COMMIT_ADJUDICATION_REQUIRED",
|
|
950
|
+
);
|
|
951
|
+
}
|
|
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
|
+
};
|
|
193
968
|
};
|
|
194
969
|
}
|
|
195
970
|
|
|
@@ -202,9 +977,147 @@ export class TeamWorktreeCoordinator {
|
|
|
202
977
|
* @returns {Array<{key,branch,committed,clean,merged,conflicts,error?}>}
|
|
203
978
|
*/
|
|
204
979
|
integrate({ merge = false } = {}) {
|
|
980
|
+
this._assertBaseTarget({ merge });
|
|
205
981
|
const results = [];
|
|
206
982
|
for (const [key, info] of this._created) {
|
|
983
|
+
const target = this._assertBaseTarget({ merge });
|
|
984
|
+
if (info.completed === false) {
|
|
985
|
+
results.push({
|
|
986
|
+
key,
|
|
987
|
+
branch: info.branch,
|
|
988
|
+
committed: false,
|
|
989
|
+
clean: false,
|
|
990
|
+
merged: false,
|
|
991
|
+
conflicts: [],
|
|
992
|
+
note: "task did not complete",
|
|
993
|
+
});
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
if (info.integration.merged) {
|
|
997
|
+
try {
|
|
998
|
+
if (
|
|
999
|
+
!_deps.isGitAncestor(
|
|
1000
|
+
this.repoDir,
|
|
1001
|
+
info.commitOid,
|
|
1002
|
+
target.currentHead,
|
|
1003
|
+
)
|
|
1004
|
+
) {
|
|
1005
|
+
throw new Error(
|
|
1006
|
+
`merged worktree commit for "${key}" is no longer in HEAD`,
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
results.push({
|
|
1010
|
+
key,
|
|
1011
|
+
branch: info.branch,
|
|
1012
|
+
committed: info.committed,
|
|
1013
|
+
clean: true,
|
|
1014
|
+
merged: true,
|
|
1015
|
+
conflicts: [],
|
|
1016
|
+
recovered: true,
|
|
1017
|
+
});
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
results.push({
|
|
1020
|
+
key,
|
|
1021
|
+
branch: info.branch,
|
|
1022
|
+
committed: info.committed,
|
|
1023
|
+
clean: false,
|
|
1024
|
+
merged: false,
|
|
1025
|
+
conflicts: [],
|
|
1026
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const worktreeMissing = !_deps.pathExists(info.path);
|
|
1032
|
+
if (info.cleaned || (info.cleanupPrepared && worktreeMissing)) {
|
|
1033
|
+
try {
|
|
1034
|
+
const branchHead = _deps.resolveGitRef(this.repoDir, info.branch);
|
|
1035
|
+
if (branchHead !== info.commitOid) {
|
|
1036
|
+
throw new Error(
|
|
1037
|
+
`worktree branch "${info.branch}" moved after task settlement`,
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
if (
|
|
1041
|
+
!info.integration.previewed ||
|
|
1042
|
+
info.integration.clean !== true ||
|
|
1043
|
+
(merge && info.committed) ||
|
|
1044
|
+
(info.committed &&
|
|
1045
|
+
info.integration.baseCommit !== target.currentHead)
|
|
1046
|
+
) {
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
`persisted merge preview for "${key}" is stale or incomplete`,
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
results.push({
|
|
1052
|
+
key,
|
|
1053
|
+
branch: info.branch,
|
|
1054
|
+
committed: info.committed,
|
|
1055
|
+
clean: true,
|
|
1056
|
+
merged: false,
|
|
1057
|
+
conflicts: [],
|
|
1058
|
+
recovered: true,
|
|
1059
|
+
note: "reused persisted merge preview",
|
|
1060
|
+
});
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
results.push({
|
|
1063
|
+
key,
|
|
1064
|
+
branch: info.branch,
|
|
1065
|
+
committed: info.committed,
|
|
1066
|
+
clean: false,
|
|
1067
|
+
merged: false,
|
|
1068
|
+
conflicts: [],
|
|
1069
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
let branchHead;
|
|
1075
|
+
let baseHead;
|
|
1076
|
+
try {
|
|
1077
|
+
branchHead = _deps.resolveGitRef(this.repoDir, info.branch);
|
|
1078
|
+
if (branchHead !== info.commitOid) {
|
|
1079
|
+
throw new Error(
|
|
1080
|
+
`worktree branch "${info.branch}" moved after task settlement`,
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
if (worktreeMissing) {
|
|
1084
|
+
throw new Error(
|
|
1085
|
+
`worktree for "${key}" is missing before integration`,
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
const worktreeHead = _deps.resolveGitRef(info.path, "HEAD");
|
|
1089
|
+
if (worktreeHead !== info.commitOid) {
|
|
1090
|
+
throw new Error(
|
|
1091
|
+
`worktree HEAD for "${key}" moved after task settlement`,
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
if (!_deps.isWorktreeClean(info.path)) {
|
|
1095
|
+
throw new Error(
|
|
1096
|
+
`worktree "${key}" has unsettled tracked or untracked changes`,
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
baseHead = target.currentHead;
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
results.push({
|
|
1102
|
+
key,
|
|
1103
|
+
branch: info.branch,
|
|
1104
|
+
committed: info.committed,
|
|
1105
|
+
clean: false,
|
|
1106
|
+
merged: false,
|
|
1107
|
+
conflicts: [],
|
|
1108
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1109
|
+
});
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
207
1112
|
if (!info.committed) {
|
|
1113
|
+
const recovered = info.integration.previewed === true;
|
|
1114
|
+
info.integration = {
|
|
1115
|
+
previewed: true,
|
|
1116
|
+
clean: true,
|
|
1117
|
+
merged: false,
|
|
1118
|
+
baseCommit: info.integration.baseCommit || baseHead,
|
|
1119
|
+
mergeCommit: null,
|
|
1120
|
+
};
|
|
208
1121
|
results.push({
|
|
209
1122
|
key,
|
|
210
1123
|
branch: info.branch,
|
|
@@ -212,10 +1125,36 @@ export class TeamWorktreeCoordinator {
|
|
|
212
1125
|
clean: true,
|
|
213
1126
|
merged: false,
|
|
214
1127
|
conflicts: [],
|
|
1128
|
+
recovered,
|
|
215
1129
|
note: "no changes to merge",
|
|
216
1130
|
});
|
|
217
1131
|
continue;
|
|
218
1132
|
}
|
|
1133
|
+
// A crash may happen after Git merged a branch but before the updated
|
|
1134
|
+
// integration record was persisted. Prove ancestry and settle it without
|
|
1135
|
+
// replaying the merge.
|
|
1136
|
+
if (
|
|
1137
|
+
merge &&
|
|
1138
|
+
_deps.isGitAncestor(this.repoDir, info.commitOid, target.currentHead)
|
|
1139
|
+
) {
|
|
1140
|
+
info.integration = {
|
|
1141
|
+
previewed: true,
|
|
1142
|
+
clean: true,
|
|
1143
|
+
merged: true,
|
|
1144
|
+
baseCommit: info.integration.baseCommit || baseHead,
|
|
1145
|
+
mergeCommit: baseHead,
|
|
1146
|
+
};
|
|
1147
|
+
results.push({
|
|
1148
|
+
key,
|
|
1149
|
+
branch: info.branch,
|
|
1150
|
+
committed: info.committed,
|
|
1151
|
+
clean: true,
|
|
1152
|
+
merged: true,
|
|
1153
|
+
conflicts: [],
|
|
1154
|
+
recovered: true,
|
|
1155
|
+
});
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
219
1158
|
let preview;
|
|
220
1159
|
try {
|
|
221
1160
|
preview = _deps.previewWorktreeMerge(this.repoDir, info.branch);
|
|
@@ -231,8 +1170,41 @@ export class TeamWorktreeCoordinator {
|
|
|
231
1170
|
});
|
|
232
1171
|
continue;
|
|
233
1172
|
}
|
|
1173
|
+
try {
|
|
1174
|
+
const postPreviewTarget = this._assertBaseTarget({ merge });
|
|
1175
|
+
const postPreviewBranch = _deps.resolveGitRef(
|
|
1176
|
+
this.repoDir,
|
|
1177
|
+
info.branch,
|
|
1178
|
+
);
|
|
1179
|
+
if (
|
|
1180
|
+
postPreviewTarget.currentHead !== baseHead ||
|
|
1181
|
+
postPreviewBranch !== info.commitOid
|
|
1182
|
+
) {
|
|
1183
|
+
throw new Error(
|
|
1184
|
+
`Git refs moved while previewing worktree "${key}"; retry integration`,
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
results.push({
|
|
1189
|
+
key,
|
|
1190
|
+
branch: info.branch,
|
|
1191
|
+
committed: true,
|
|
1192
|
+
clean: false,
|
|
1193
|
+
merged: false,
|
|
1194
|
+
conflicts: [],
|
|
1195
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1196
|
+
});
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
234
1199
|
const clean = preview?.success === true;
|
|
235
1200
|
const conflicts = preview?.conflicts || [];
|
|
1201
|
+
info.integration = {
|
|
1202
|
+
previewed: true,
|
|
1203
|
+
clean,
|
|
1204
|
+
merged: false,
|
|
1205
|
+
baseCommit: baseHead,
|
|
1206
|
+
mergeCommit: null,
|
|
1207
|
+
};
|
|
236
1208
|
let merged = false;
|
|
237
1209
|
if (clean && merge) {
|
|
238
1210
|
// mergeWorktree does NOT throw on a failed merge — it catches internally
|
|
@@ -244,15 +1216,46 @@ export class TeamWorktreeCoordinator {
|
|
|
244
1216
|
// throw path — a malformed branch ref.)
|
|
245
1217
|
let mergeResult;
|
|
246
1218
|
try {
|
|
247
|
-
|
|
1219
|
+
this._assertBaseTarget({ merge: true });
|
|
1220
|
+
mergeResult = _deps.mergeWorktree(this.repoDir, info.commitOid, {
|
|
248
1221
|
message: `Merge team task ${key}`,
|
|
1222
|
+
deleteBranch: false,
|
|
249
1223
|
});
|
|
250
1224
|
} catch (err) {
|
|
251
1225
|
mergeResult = { success: false, message: err.message };
|
|
252
1226
|
}
|
|
253
1227
|
if (mergeResult?.success === true) {
|
|
254
|
-
|
|
1228
|
+
try {
|
|
1229
|
+
const mergedTarget = this._assertBaseTarget({ merge: true });
|
|
1230
|
+
const mergeCommit = mergedTarget.currentHead;
|
|
1231
|
+
if (
|
|
1232
|
+
!_deps.isGitAncestor(this.repoDir, info.commitOid, mergeCommit)
|
|
1233
|
+
) {
|
|
1234
|
+
throw new Error(
|
|
1235
|
+
`merged worktree commit for "${key}" is not in HEAD`,
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
merged = true;
|
|
1239
|
+
info.integration = {
|
|
1240
|
+
...info.integration,
|
|
1241
|
+
merged: true,
|
|
1242
|
+
mergeCommit,
|
|
1243
|
+
};
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
info.integration.clean = false;
|
|
1246
|
+
results.push({
|
|
1247
|
+
key,
|
|
1248
|
+
branch: info.branch,
|
|
1249
|
+
committed: true,
|
|
1250
|
+
clean: false,
|
|
1251
|
+
merged: false,
|
|
1252
|
+
conflicts: [],
|
|
1253
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1254
|
+
});
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
255
1257
|
} else {
|
|
1258
|
+
info.integration.clean = false;
|
|
256
1259
|
results.push({
|
|
257
1260
|
key,
|
|
258
1261
|
branch: info.branch,
|
|
@@ -279,18 +1282,124 @@ export class TeamWorktreeCoordinator {
|
|
|
279
1282
|
return results;
|
|
280
1283
|
}
|
|
281
1284
|
|
|
282
|
-
/**
|
|
1285
|
+
/**
|
|
1286
|
+
* Durable phase one of cleanup. The caller must persist the resulting
|
|
1287
|
+
* snapshot before deleting anything. A recovered `cleanupPrepared` record is
|
|
1288
|
+
* sufficient proof that a missing worktree may be finalized without rerunning
|
|
1289
|
+
* its task or replaying integration.
|
|
1290
|
+
*/
|
|
1291
|
+
prepareCleanupAll({ requireMerged = false } = {}) {
|
|
1292
|
+
const results = [];
|
|
1293
|
+
for (const [key, info] of this._created) {
|
|
1294
|
+
if (info.cleaned || info.cleanupPrepared) {
|
|
1295
|
+
results.push({
|
|
1296
|
+
key,
|
|
1297
|
+
ok: true,
|
|
1298
|
+
alreadyPrepared: true,
|
|
1299
|
+
});
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
if (!info.completed) {
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
`refusing to prepare unfinished worktree "${key}" for cleanup`,
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
if (!info.integration.previewed || info.integration.clean !== true) {
|
|
1308
|
+
throw new Error(
|
|
1309
|
+
`refusing to prepare worktree "${key}" before clean integration`,
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
if (requireMerged && info.committed && !info.integration.merged) {
|
|
1313
|
+
throw new Error(
|
|
1314
|
+
`refusing to prepare unmerged worktree "${key}" for cleanup`,
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
info.cleanupPrepared = true;
|
|
1318
|
+
results.push({ key, ok: true });
|
|
1319
|
+
}
|
|
1320
|
+
return results;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Remove every worktree created for this run (branches left intact).
|
|
1325
|
+
* Failed removals remain in the recovery snapshot and are returned to the
|
|
1326
|
+
* caller; they are never hidden by clearing the entire in-memory manifest.
|
|
1327
|
+
*/
|
|
283
1328
|
cleanupAll({ deleteBranch = false } = {}) {
|
|
284
|
-
|
|
1329
|
+
const results = [];
|
|
1330
|
+
for (const [key, info] of Array.from(this._created)) {
|
|
1331
|
+
if (info.cleaned) {
|
|
1332
|
+
results.push({
|
|
1333
|
+
key,
|
|
1334
|
+
ok: true,
|
|
1335
|
+
path: info.path,
|
|
1336
|
+
alreadyCleaned: true,
|
|
1337
|
+
});
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
285
1340
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
1341
|
+
if (!info.cleanupPrepared) {
|
|
1342
|
+
throw new Error(
|
|
1343
|
+
`refusing to clean worktree "${key}" before durable preparation`,
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
if (deleteBranch && info.committed && !info.integration.merged) {
|
|
1347
|
+
throw new Error(
|
|
1348
|
+
`refusing to delete unmerged worktree branch "${key}"`,
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
const branchHead = _deps.resolveGitRef(this.repoDir, info.branch);
|
|
1352
|
+
if (branchHead !== info.commitOid) {
|
|
1353
|
+
throw new Error(
|
|
1354
|
+
`refusing to clean worktree "${key}" after its branch moved`,
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
const worktreePath = _deps.assertManagedWorktreePath(
|
|
1358
|
+
this.repoDir,
|
|
1359
|
+
info.path,
|
|
1360
|
+
{ branchName: info.branch },
|
|
1361
|
+
);
|
|
1362
|
+
if (_deps.pathExists(worktreePath)) {
|
|
1363
|
+
const worktreeHead = _deps.resolveGitRef(worktreePath, "HEAD");
|
|
1364
|
+
if (worktreeHead !== info.commitOid) {
|
|
1365
|
+
throw new Error(
|
|
1366
|
+
`refusing to clean worktree "${key}" after its HEAD moved`,
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
_deps.removeManagedDependencyLinks(
|
|
1370
|
+
this.repoDir,
|
|
1371
|
+
worktreePath,
|
|
1372
|
+
info.managedLinks || [],
|
|
1373
|
+
);
|
|
1374
|
+
if (!_deps.isWorktreeClean(worktreePath, { includeIgnored: true })) {
|
|
1375
|
+
throw new Error(
|
|
1376
|
+
`refusing to clean worktree "${key}" with unsettled or ignored files`,
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
_deps.removeWorktree(this.repoDir, worktreePath, {
|
|
1381
|
+
deleteBranch,
|
|
1382
|
+
branchName: info.branch,
|
|
1383
|
+
expectedBranchOid: info.commitOid,
|
|
1384
|
+
force: false,
|
|
1385
|
+
});
|
|
1386
|
+
info.cleaned = true;
|
|
1387
|
+
results.push({ key, ok: true, path: worktreePath });
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
results.push({
|
|
1390
|
+
key,
|
|
1391
|
+
ok: false,
|
|
1392
|
+
path: info.path,
|
|
1393
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1394
|
+
});
|
|
289
1395
|
}
|
|
290
1396
|
}
|
|
1397
|
+
return results;
|
|
291
1398
|
}
|
|
292
1399
|
|
|
293
1400
|
branches() {
|
|
294
|
-
return Array.from(this._created.values())
|
|
1401
|
+
return Array.from(this._created.values())
|
|
1402
|
+
.filter((info) => !info.cleaned)
|
|
1403
|
+
.map((info) => info.branch);
|
|
295
1404
|
}
|
|
296
1405
|
}
|