chainlesschain 0.162.185 → 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.
Files changed (184) hide show
  1. package/bin/chainlesschain.js +21 -5
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-Co4ipAKx.js → AIOps-CETaO3s8.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-BpsoDrDq.js → ActionButton-CAe7E9TW.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-DMUqeBik.js → Analytics-BGr2mzwO.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-C6fD_dXr.js → AppLayout-BEoY-NcT.js} +5 -5
  7. package/src/assets/web-panel/assets/{Artifacts-DW0YlQlV.js → Artifacts-DUZivCgt.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-DXtHiiFK.js → Audit-C3yBs68k.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-BgmF62U-.js → BackgroundAgents-C9KwiY3O.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-D8vhVLQC.js → Backup-BoSWD_C8.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-D7nCtY2X.js → BaseInput-Dp09GkxL.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-CBZLEV2G.js → Chat-5t9d-Ja0.js} +4 -4
  13. package/src/assets/web-panel/assets/ChatBubbleRenderer-DmVku4GH.js +1 -0
  14. package/src/assets/web-panel/assets/{Checkbox-DJLkeD4M.js → Checkbox-8QJFlkqI.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-Cs6xO8Fd.js → Codegen-BJVFQ3lL.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-Dtx9wxfT.js → Col-6hdLD0oB.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-DiigJg4n.js → Community-B8opo0uY.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-B67oMDDL.js → Compact-Dv8HudiN.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-BBDl-Jos.js → Compliance-DhxuCS5N.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-Ba98hSsa.js → Cowork-Dggy6VIL.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-DBWQZIz7.js → Cron-DSmDqJTT.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-OcrKzxAt.js → Crosschain-7bZq0j5f.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-mW2eSbjM.js → DID-BWY4kh-H.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-BmsDPmKJ.js → Dashboard-bOzPZxlu.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-CVzmCyRx.js → Dropdown-BTFJHqQu.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-vVgqKzhh.js → EmailListRenderer-BC3MOdtT.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CWY5t_ZJ.js → FamilyGuardDashboard-CFHAdTTY.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-ZGomhjxr.js → Federation-DZ_4aF_n.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-DyaknbLr.js → FormItemContext-BZje6UTj.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-CmIvN1qr.js → GenericCardRenderer-D7QfCvjN.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-DQOJIYkG.js → Git-DuG7qANG.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-CMiY-EEp.js → Governance-BDmYAgTh.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-D6IXuucf.js → Inference-DSXAfc1Q.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-6jkTL0hV.js → KnowledgeGraph-DusleniP.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-N0jD-349.js → Logs-CpImmA8p.js} +2 -2
  36. package/src/assets/web-panel/assets/MarkdownRenderer-BZ6LJBkk.js +1 -0
  37. package/src/assets/web-panel/assets/{Marketplace-WhYuhBxm.js → Marketplace-DXS8CXkE.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-DTeO28xy.js → McpTools-D151UfUI.js} +5 -5
  39. package/src/assets/web-panel/assets/{Memory-MSHzPSE-.js → Memory-DKDxnoA1.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-Cyqs3J3W.js → MobileBridge-BFZ95iKM.js} +1 -1
  41. package/src/assets/web-panel/assets/MobileProjects-CDAR-grF.js +1 -0
  42. package/src/assets/web-panel/assets/{Mtc-MWBnezNM.js → Mtc-OGJ95xJs.js} +5 -5
  43. package/src/assets/web-panel/assets/{MtcAudit-DQu35Nu-.js → MtcAudit-BmGTo1XC.js} +2 -2
  44. package/src/assets/web-panel/assets/{Multisig-CQS_M-DP.js → Multisig-XumJRqOa.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-WD16vQvY.js → NLProgramming-DHhhRZed.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-DqsiSwOq.js → Notes-B9rGxNNr.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-D3LoxO-P.js → NotificationSettings-DClLbMYV.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-C5jPoTd6.js → OrderTableRenderer-I8OohT7N.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-48aMBJso.js → Organization-M6WzaYVa.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-j-a2XkhR.js → Overflow-Cb3B5KtM.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-BYpBNYnh.js → P2P-BGk57kya.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-CYCsrrLg.js → PdhVaultBrowser-CXcKyPqK.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-ZRXGXDFb.js → Permissions-hsfJ-Y0e.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-6OWUPHyL.js → PersonalDataHub-BSBii4js.js} +5 -5
  55. package/src/assets/web-panel/assets/{Pipeline-FcjlDK9E.js → Pipeline-BNw52rey.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-vD_6Tldp.js → Privacy-DUpfue4E.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-B_CHOLXp.js → ProjectInit-C68NsP3h.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-rEiqQ2da.js → ProjectSettings-BJ2It9v4.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-D2FHLUa-.js +1 -0
  60. package/src/assets/web-panel/assets/{Providers-Dgtt97B4.js → Providers-EnU4VWwS.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-DaGVylZO.js → QrScannerModal-BRt5-B_I.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-DP2_2x3u.js → QuickAsk-Jd5L9UL2.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-D29yj4ki.js → Recommend-BNDhoVIu.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-Ue2wnBLp.js → RemoteSession-BII41Mel.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-CrxIHqeC.js → Reputation-D1wQPUNv.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-BgSzJPCn.js → Row-DnjRegul.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-AwhYOrP9.js → RssFeed-C_T4I-nF.js} +2 -2
  68. package/src/assets/web-panel/assets/{Search-D2shpEcm.js → Search-C4lrxlLh.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-BtKzhTbT.js → Security-ByYT-VzP.js} +4 -4
  70. package/src/assets/web-panel/assets/{Services-BY0DsUSm.js → Services-BHpyEqB5.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-B3Sb0UI9.js → Skeleton-C0I23vG-.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-Ne5w0bDs.js → Skills-DGZXz22K.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-C8VkaP4T.js → Sla-DL_urP9A.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-BQdIEYP9.js → SpeechSettings-6x-hUNQ_.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-CwFafrUg.js → SyncSettings-Crk5TCXq.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-yJuKoDlR.js → Tasks-rZGq5PZs.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-DYn9qCmx.js → Templates-DT5el8Q8.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-DkKReHvn.js → Tenant-mlgeyMK8.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-r6bd-owt.js → Terminal-C8HEA-lM.js} +2 -2
  80. package/src/assets/web-panel/assets/TimelineRenderer-DvlqBayB.js +1 -0
  81. package/src/assets/web-panel/assets/{Tokens-BnMw7MRi.js → Tokens-C7lDcNmT.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-CTzKp4o9.js → Trigger-CM8nCnIJ.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-BBrSZm9U.js → Trust-BQDLUQ9x.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-D9fDvEMq.js → UkeySign-CVqM0OFE.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-DUi5ve-0.js → VideoEditing-BvAcQpzT.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-C8Fi1P8_.js → Wallet-zFVFaAZ2.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-s1LppYi2.js → WebAuthn-Cc522ums.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-DJL2C4lj.js → WorkflowEditor-Bg_wxpDo.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-D3F2ndOB.js → chat-jg0QwC8K.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-B7739-e3.js → colors-ALzYz1gO.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-CcJbFTF4.js → compact-item-DXHYUoqi.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-CuP_DEFS.js → createContext-BsGsBM5m.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-Dy1GJA4S.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-6YEXfIik.js → hasIn-BccMLzxA.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-fWgUQZ9d.js → index-08diKcTm.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CxradkS6.js → index-2VstcImq.js} +19 -19
  97. package/src/assets/web-panel/assets/index-8zbMbbuf.js +1 -0
  98. package/src/assets/web-panel/assets/{index-DDRlBaDt.js → index-BBbbr1YB.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Dox44j29.js → index-BTTeAiOP.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BYYSLEjn.js → index-BcJzphrP.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CRuoRapN.js → index-BdCmJ_bG.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BxSqVMGh.js → index-BfF0OGKU.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D6Ij_2uw.js → index-BfV8RDlg.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-Dsy7_x4s.js → index-BvsII10i.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BsdSnHQE.js → index-C0AbXsDy.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-mfWQRSLn.js → index-C3Rf8wIM.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BIID4XcP.js → index-C3k3ZwkQ.js} +1 -1
  108. package/src/assets/web-panel/assets/index-C97f71oi.js +1 -0
  109. package/src/assets/web-panel/assets/{index-4h3lpWJ3.js → index-CBVIFX-x.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DHvxXXNA.js → index-CJg6Wsjc.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DjI9MkId.js → index-CLzPzN75.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-D0zdPyTD.js → index-CQ1ORdp-.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-D0K_rFC1.js → index-CSFESurl.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-b9cuvFEv.js → index-CWLWBgfe.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Nz-MUHD1.js → index-CahDve8M.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DXdnfDVK.js → index-Cg9cOVe9.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Bh4hcNWW.js → index-Cmg_Npx5.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-Bru_kpZl.js → index-CrZHUsOZ.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-yOZYSNmG.js → index-Cz_s25m3.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DlSvpNMf.js → index-D9oV2iRR.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-LfRlSmqe.js → index-DgjQ-kdT.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BOHUo3aM.js → index-Dmk7dD-T.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-D3haorUt.js → index-Dxqiu9-F.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DV11jpeO.js → index-JS2LXdd0.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CY0zd_jc.js → index-TeCof1s3.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DpEKmjoa.js → index-UQXedWQz.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-C3pctdCb.js → index-koT8Q86Z.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-D_75jYWk.js → index-mIQUoUx1.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Gxw09e9R.js → index-oWltafBn.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BhIKnQAW.js → index-rIy96PeM.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-C3ZA_h1j.js → index-rcwgNMc0.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DGsE7MQz.js → index-srqW5_-X.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-eR6H6NR7.js → index-uzykuL_R.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-CJ1Bq2L3.js → initDefaultProps-xafkqrW3.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-rYJN_vzf.js → motion-BrtyQXLk.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-DnDK2gTP.js → move-BunUyg59.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-CKnXy7ES.js → mtc-parser-BPwjBeuE.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-3nRc3hWT.js → omit-BpyqZlNO.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-Ddjj3sLu.js → pickAttrs-DDrV0XjE.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-CbsbphQY.js → placementArrow-1A18Pcl2.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-D9aZ4VDJ.js → responsiveObserve-B8Fnlmso.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-D4pvdFj0.js → slide-x-Fwb2Zm.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-C2W-vY-S.js → statusUtils-aAG5yJl_.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-Cbq5150d.js → styleChecker-BRQSTMRL.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-BA2B7R82.js → useFlexGapSupport-D5X_KozY.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-CLrwg3xg.js → useFs-DN_YIjP-.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-DQeAI5qs.js → usePersonalDataHub-BizxAvjw.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-8BDGy-YU.js → vnode-B5oAxN93.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-B2SzFprl.js → zoom-DJhvyzKv.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/commands/eval.js +7 -0
  152. package/src/commands/status.js +55 -0
  153. package/src/commands/team.js +1805 -448
  154. package/src/data/changelog.json +26 -0
  155. package/src/harness/worktree-isolator.js +671 -79
  156. package/src/lazy-dispatch.js +12 -1
  157. package/src/lib/agent-team/task-lease.js +378 -78
  158. package/src/lib/agent-team/team-agent-stream.js +488 -0
  159. package/src/lib/agent-team/team-budget.js +216 -20
  160. package/src/lib/agent-team/team-mailbox.js +389 -15
  161. package/src/lib/agent-team/team-run-state-lock.js +209 -0
  162. package/src/lib/agent-team/team-runner.js +547 -63
  163. package/src/lib/agent-team/team-scope-lock.js +409 -0
  164. package/src/lib/agent-team/team-task-contract.js +277 -0
  165. package/src/lib/agent-team/team-worktree.js +1171 -62
  166. package/src/lib/agent-worktree.js +19 -20
  167. package/src/lib/collaboration-run-store.js +983 -68
  168. package/src/lib/observability/index.js +191 -19
  169. package/src/lib/observability/otlp-exporter.js +1209 -207
  170. package/src/lib/otlp-exporter.js +8 -36
  171. package/src/lib/with-file-lock.js +190 -24
  172. package/src/lib/worktree-isolator.js +3 -0
  173. package/src/lib/worktree-sparse.js +23 -0
  174. package/src/program-base.js +9 -2
  175. package/src/runtime/headless-runner.js +45 -21
  176. package/src/runtime/headless-stream.js +6 -1
  177. package/src/assets/web-panel/assets/ChatBubbleRenderer-CtDb-2Qu.js +0 -1
  178. package/src/assets/web-panel/assets/MarkdownRenderer-BL1IVXm-.js +0 -1
  179. package/src/assets/web-panel/assets/MobileProjects-7IN2hWHg.js +0 -1
  180. package/src/assets/web-panel/assets/Projects-Bg8obRAM.js +0 -1
  181. package/src/assets/web-panel/assets/TimelineRenderer-Mq2juUC3.js +0 -1
  182. package/src/assets/web-panel/assets/devWarning-Cj_36GHq.js +0 -1
  183. package/src/assets/web-panel/assets/index-CezFrAmD.js +0 -1
  184. package/src/assets/web-panel/assets/index-Cxgguez6.js +0 -1
@@ -7,8 +7,17 @@
7
7
  * Feature-flag gated: WORKTREE_ISOLATION
8
8
  */
9
9
 
10
- import { existsSync, rmSync, symlinkSync, mkdirSync } from "node:fs";
11
- import { resolve, dirname } from "node:path";
10
+ import {
11
+ existsSync,
12
+ lstatSync,
13
+ realpathSync,
14
+ rmdirSync,
15
+ symlinkSync,
16
+ unlinkSync,
17
+ mkdirSync,
18
+ rmSync,
19
+ } from "node:fs";
20
+ import { resolve, dirname, relative, isAbsolute, basename } from "node:path";
12
21
  import {
13
22
  isGitRepo,
14
23
  gitExec,
@@ -24,6 +33,347 @@ import {
24
33
 
25
34
  const WORKTREE_DIR = ".worktrees";
26
35
 
36
+ function normalizeFilesystemPath(value) {
37
+ const absolute = resolve(value);
38
+ return process.platform === "win32" ? absolute.toLowerCase() : absolute;
39
+ }
40
+
41
+ function nativeRealpath(value) {
42
+ return realpathSync.native ? realpathSync.native(value) : realpathSync(value);
43
+ }
44
+
45
+ /**
46
+ * Resolve filesystem aliases without requiring the complete target to exist.
47
+ * `realpathSync()` alone cannot validate a not-yet-created worktree, while a
48
+ * lexical `resolve()` treats macOS' `/var` and `/private/var` (or a repository
49
+ * directory symlink) as different locations. Resolve the deepest existing
50
+ * prefix physically, then append only the still-missing path segments.
51
+ */
52
+ function canonicalExistingPrefixPath(value) {
53
+ let cursor = resolve(value);
54
+ const missing = [];
55
+
56
+ for (;;) {
57
+ try {
58
+ return resolve(nativeRealpath(cursor), ...missing);
59
+ } catch (error) {
60
+ if (error?.code !== "ENOENT") throw error;
61
+ const parent = dirname(cursor);
62
+ if (parent === cursor) throw error;
63
+ missing.unshift(basename(cursor));
64
+ cursor = parent;
65
+ }
66
+ }
67
+ }
68
+
69
+ function sameFilesystemPath(left, right) {
70
+ return (
71
+ normalizeFilesystemPath(canonicalExistingPrefixPath(left)) ===
72
+ normalizeFilesystemPath(canonicalExistingPrefixPath(right))
73
+ );
74
+ }
75
+
76
+ function assertNotSymlink(target, label) {
77
+ let stat;
78
+ try {
79
+ stat = lstatSync(target);
80
+ } catch (error) {
81
+ if (error?.code === "ENOENT") return;
82
+ throw error;
83
+ }
84
+ if (stat.isSymbolicLink()) {
85
+ throw new Error(`${label} must not be a symbolic link or junction`);
86
+ }
87
+ }
88
+
89
+ function gitRepositoryIdentity(repoDir) {
90
+ const [topLevel, commonDir, ...unexpected] = gitExecArgs(
91
+ ["rev-parse", "--show-toplevel", "--git-common-dir"],
92
+ repoDir,
93
+ ).split(/\r?\n/);
94
+ if (!topLevel || !commonDir || unexpected.length > 0) {
95
+ throw new Error("unable to determine git repository identity");
96
+ }
97
+ const absoluteCommonDir = isAbsolute(commonDir)
98
+ ? commonDir
99
+ : resolve(repoDir, commonDir);
100
+ return {
101
+ topLevel,
102
+ topLevelIdentity: normalizeFilesystemPath(
103
+ canonicalExistingPrefixPath(topLevel),
104
+ ),
105
+ commonDirIdentity: normalizeFilesystemPath(
106
+ canonicalExistingPrefixPath(absoluteCommonDir),
107
+ ),
108
+ };
109
+ }
110
+
111
+ function assertSameGitRepository(repoDir, expectedCommonDir, expectedRoot) {
112
+ const identity = gitRepositoryIdentity(repoDir);
113
+ if (
114
+ identity.commonDirIdentity !== expectedCommonDir ||
115
+ identity.topLevelIdentity !== expectedRoot
116
+ ) {
117
+ throw new Error("worktree does not belong to the expected git repository");
118
+ }
119
+ }
120
+
121
+ function parseRegisteredWorktrees(output) {
122
+ const worktrees = [];
123
+ let current = {};
124
+
125
+ for (const line of output.split(/\r?\n/)) {
126
+ if (line.startsWith("worktree ")) {
127
+ if (current.path) worktrees.push(current);
128
+ current = { path: line.slice(9) };
129
+ } else if (line.startsWith("HEAD ")) {
130
+ current.head = line.slice(5);
131
+ } else if (line.startsWith("branch ")) {
132
+ current.branch = line.slice(7).replace("refs/heads/", "");
133
+ } else if (line === "bare") {
134
+ current.bare = true;
135
+ } else if (line === "detached") {
136
+ current.detached = true;
137
+ } else if (line === "locked") {
138
+ current.locked = true;
139
+ } else if (line.startsWith("locked ")) {
140
+ current.locked = true;
141
+ current.lockReason = line.slice(7);
142
+ } else if (line === "prunable") {
143
+ current.prunable = true;
144
+ } else if (line.startsWith("prunable ")) {
145
+ current.prunable = true;
146
+ current.prunableReason = line.slice(9);
147
+ }
148
+ }
149
+ if (current.path) worktrees.push(current);
150
+ return worktrees;
151
+ }
152
+
153
+ function readRegisteredWorktrees(repoDir) {
154
+ return parseRegisteredWorktrees(
155
+ gitExec("worktree list --porcelain", repoDir),
156
+ );
157
+ }
158
+
159
+ function repositoryManagementContext(repoDir) {
160
+ const repository = gitRepositoryIdentity(repoDir);
161
+ const registeredWorktrees = readRegisteredWorktrees(repoDir);
162
+ const invocationRegistered = registeredWorktrees.some((worktree) => {
163
+ if (worktree.bare || !worktree.path) return false;
164
+ try {
165
+ return (
166
+ normalizeFilesystemPath(canonicalExistingPrefixPath(worktree.path)) ===
167
+ repository.topLevelIdentity
168
+ );
169
+ } catch {
170
+ return false;
171
+ }
172
+ });
173
+ if (!invocationRegistered) {
174
+ throw new Error("repository checkout is not a registered git worktree");
175
+ }
176
+
177
+ // Git lists the primary checkout first. It is the sole management anchor:
178
+ // calls made from a linked worktree still create a sibling below the primary
179
+ // checkout's `.worktrees`, never below an arbitrary externally-located
180
+ // registered checkout.
181
+ const primary = registeredWorktrees[0];
182
+ if (!primary || primary.bare || !primary.path || !existsSync(primary.path)) {
183
+ throw new Error("primary git worktree is unavailable");
184
+ }
185
+ const primaryPath = canonicalExistingPrefixPath(primary.path);
186
+ const primaryIdentity = normalizeFilesystemPath(primaryPath);
187
+ assertSameGitRepository(
188
+ primary.path,
189
+ repository.commonDirIdentity,
190
+ primaryIdentity,
191
+ );
192
+
193
+ return {
194
+ repository,
195
+ registeredWorktrees,
196
+ primaryPath,
197
+ primaryIdentity,
198
+ managedRoot: resolve(primaryPath, WORKTREE_DIR),
199
+ };
200
+ }
201
+
202
+ function assertPhysicalDescendant(target, root, label) {
203
+ const fromRoot = relative(realpathSync(root), realpathSync(target));
204
+ if (
205
+ fromRoot === "" ||
206
+ isAbsolute(fromRoot) ||
207
+ fromRoot === ".." ||
208
+ fromRoot.startsWith(`..${pathSeparator()}`)
209
+ ) {
210
+ throw new Error(`${label} escapes its physical root`);
211
+ }
212
+ }
213
+
214
+ function assertNoSymlinkAncestors(root, target, label) {
215
+ const rootPath = resolve(root);
216
+ const targetPath = resolve(target);
217
+ const fromRoot = relative(rootPath, targetPath);
218
+ if (
219
+ isAbsolute(fromRoot) ||
220
+ fromRoot === ".." ||
221
+ fromRoot.startsWith(`..${pathSeparator()}`)
222
+ ) {
223
+ throw new Error(`${label} escapes its root`);
224
+ }
225
+ assertNotSymlink(rootPath, `${label} root`);
226
+ let cursor = rootPath;
227
+ for (const segment of fromRoot.split(/[\\/]/).filter(Boolean)) {
228
+ cursor = resolve(cursor, segment);
229
+ assertNotSymlink(cursor, label);
230
+ }
231
+ }
232
+
233
+ export function managedWorktreePath(repoDir, branchName) {
234
+ assertSafeGitRef(branchName, "branch name");
235
+ const { managedRoot } = repositoryManagementContext(repoDir);
236
+ return resolve(managedRoot, branchName.replace(/\//g, "-"));
237
+ }
238
+
239
+ /**
240
+ * Validate a path before it reaches Git cleanup or recursive filesystem
241
+ * removal. Managed worktrees are direct children of `<repo>/.worktrees` and,
242
+ * when a branch is known, have exactly the deterministic path created for it.
243
+ */
244
+ export function assertManagedWorktreePath(
245
+ repoDir,
246
+ worktreePath,
247
+ { branchName = null } = {},
248
+ ) {
249
+ if (
250
+ typeof repoDir !== "string" ||
251
+ repoDir.length === 0 ||
252
+ typeof worktreePath !== "string" ||
253
+ worktreePath.length === 0 ||
254
+ worktreePath.includes("\0")
255
+ ) {
256
+ throw new TypeError("managed worktree path must be a non-empty string");
257
+ }
258
+ if (branchName != null) {
259
+ assertSafeGitRef(branchName, "branch name");
260
+ }
261
+
262
+ const context = repositoryManagementContext(repoDir);
263
+ const candidate = resolve(worktreePath);
264
+ const candidatePath = canonicalExistingPrefixPath(candidate);
265
+ const candidateIdentity = normalizeFilesystemPath(candidatePath);
266
+ const registered = context.registeredWorktrees.find((worktree) => {
267
+ try {
268
+ return sameFilesystemPath(worktree.path, candidatePath);
269
+ } catch {
270
+ return false;
271
+ }
272
+ });
273
+ const managedRootPath = canonicalExistingPrefixPath(context.managedRoot);
274
+ const managedRootIdentity = normalizeFilesystemPath(managedRootPath);
275
+ if (
276
+ candidateIdentity === context.primaryIdentity ||
277
+ candidateIdentity === context.repository.topLevelIdentity ||
278
+ normalizeFilesystemPath(dirname(candidateIdentity)) !==
279
+ normalizeFilesystemPath(managedRootIdentity)
280
+ ) {
281
+ throw new Error("refusing unmanaged worktree path");
282
+ }
283
+
284
+ if (registered && branchName && registered.branch !== branchName) {
285
+ throw new Error(
286
+ "registered worktree branch does not match cleanup request",
287
+ );
288
+ }
289
+ const effectiveBranch = branchName || registered?.branch || null;
290
+ if (effectiveBranch != null) {
291
+ assertSafeGitRef(effectiveBranch, "branch name");
292
+ }
293
+ if (
294
+ effectiveBranch != null &&
295
+ !sameFilesystemPath(
296
+ candidatePath,
297
+ resolve(managedRootPath, effectiveBranch.replace(/\//g, "-")),
298
+ )
299
+ ) {
300
+ throw new Error("managed worktree path does not match its branch");
301
+ }
302
+
303
+ if (registered && existsSync(candidatePath)) {
304
+ assertSameGitRepository(
305
+ candidatePath,
306
+ context.repository.commonDirIdentity,
307
+ candidateIdentity,
308
+ );
309
+ }
310
+
311
+ assertNotSymlink(context.managedRoot, "managed worktree root");
312
+ assertNotSymlink(candidate, "managed worktree");
313
+ assertNotSymlink(candidatePath, "managed worktree");
314
+ return candidatePath;
315
+ }
316
+
317
+ function pathSeparator() {
318
+ return process.platform === "win32" ? "\\" : "/";
319
+ }
320
+
321
+ function validateDependencySource(repoDir, entry) {
322
+ if (!existsSync(entry.source)) return false;
323
+ assertNoSymlinkAncestors(repoDir, entry.source, "dependency source");
324
+ if (!lstatSync(entry.source).isDirectory()) {
325
+ throw new Error(`dependency source is not a directory: ${entry.name}`);
326
+ }
327
+ assertPhysicalDescendant(entry.source, repoDir, "dependency source");
328
+ return true;
329
+ }
330
+
331
+ /**
332
+ * Remove only dependency links that this module previously planned and
333
+ * verified. Ordinary directories are never removed: if a link was replaced
334
+ * after creation, cleanup fails closed and leaves the worktree for inspection.
335
+ */
336
+ export function removeManagedDependencyLinks(
337
+ repoDir,
338
+ worktreePath,
339
+ directories = [],
340
+ ) {
341
+ const plan = planSymlinkDirectories(directories, {
342
+ repoDir,
343
+ worktreePath,
344
+ });
345
+ const removed = [];
346
+ for (const entry of plan) {
347
+ let stat;
348
+ try {
349
+ stat = lstatSync(entry.dest);
350
+ } catch (error) {
351
+ if (error?.code === "ENOENT") continue;
352
+ throw error;
353
+ }
354
+ assertNoSymlinkAncestors(
355
+ worktreePath,
356
+ dirname(entry.dest),
357
+ "dependency destination",
358
+ );
359
+ if (!stat.isSymbolicLink()) {
360
+ throw new Error(
361
+ `managed dependency link was replaced by a real path: ${entry.name}`,
362
+ );
363
+ }
364
+ if (
365
+ !validateDependencySource(repoDir, entry) ||
366
+ !sameFilesystemPath(realpathSync(entry.dest), realpathSync(entry.source))
367
+ ) {
368
+ throw new Error(`managed dependency link target changed: ${entry.name}`);
369
+ }
370
+ if (process.platform === "win32") rmdirSync(entry.dest);
371
+ else unlinkSync(entry.dest);
372
+ removed.push(entry.name);
373
+ }
374
+ return removed;
375
+ }
376
+
27
377
  export function createWorktree(repoDir, branchName, baseBranch, options = {}) {
28
378
  assertSafeGitRef(branchName, "branch name");
29
379
  if (baseBranch) assertSafeGitRef(baseBranch, "base branch");
@@ -31,83 +381,323 @@ export function createWorktree(repoDir, branchName, baseBranch, options = {}) {
31
381
  throw new Error("Not a git repository");
32
382
  }
33
383
 
34
- const worktreePath = resolve(
384
+ const worktreePath = assertManagedWorktreePath(
35
385
  repoDir,
36
- WORKTREE_DIR,
37
- branchName.replace(/\//g, "-"),
386
+ managedWorktreePath(repoDir, branchName),
387
+ { branchName },
38
388
  );
39
389
 
40
390
  if (existsSync(worktreePath)) {
41
391
  throw new Error(`Worktree already exists: ${worktreePath}`);
42
392
  }
43
393
 
44
- const base = baseBranch || "HEAD";
45
- gitExec(`worktree add "${worktreePath}" -b "${branchName}" ${base}`, repoDir);
46
-
47
- const result = { path: worktreePath, branch: branchName };
48
-
49
- // Sparse checkout: only materialize the packages this task needs. For a
50
- // large monorepo this cuts worktree creation time and disk. Cone mode wants
51
- // directory paths — our sparsePaths are package dirs. Fail-closed on unsafe
52
- // paths (normalizeSparsePaths drops them); empty/absent → full checkout,
53
- // byte-identical to the no-sparse path.
394
+ // Validate all task-controlled configuration before creating a branch. A bad
395
+ // plan must not leave an untracked worktree behind.
54
396
  const sparsePaths = normalizeSparsePaths(options.sparsePaths);
55
397
  if (sparsePaths) {
56
- for (const p of sparsePaths) assertSafeGitPath(p, "sparse path");
57
- gitExecArgs(["sparse-checkout", "set", ...sparsePaths], worktreePath);
58
- result.sparsePaths = sparsePaths;
398
+ for (const item of sparsePaths) {
399
+ assertSafeGitPath(item, "sparse path");
400
+ }
59
401
  }
402
+ const symlinkPlan =
403
+ options.symlinkDirectories == null
404
+ ? []
405
+ : planSymlinkDirectories(options.symlinkDirectories, {
406
+ repoDir,
407
+ worktreePath,
408
+ });
409
+ const availableSymlinkPlan = symlinkPlan.filter((entry) =>
410
+ validateDependencySource(repoDir, entry),
411
+ );
412
+
413
+ const base = baseBranch || "HEAD";
414
+ gitExecArgs(
415
+ ["worktree", "add", worktreePath, "-b", branchName, base],
416
+ repoDir,
417
+ );
418
+
419
+ const result = { path: worktreePath, branch: branchName };
420
+ let createdBranchOid = null;
60
421
 
61
- // Dependency reuse: link explicitly-approved directories (e.g. node_modules)
62
- // from the main checkout into the worktree so the task does not reinstall.
63
- // planSymlinkDirectories throws (fail-closed) on any junction/symlink escape.
64
- if (options.symlinkDirectories != null) {
65
- const plan = planSymlinkDirectories(options.symlinkDirectories, {
422
+ try {
423
+ createdBranchOid = gitExecArgs(
424
+ ["rev-parse", "--verify", `refs/heads/${branchName}^{commit}`],
66
425
  repoDir,
67
- worktreePath,
68
- });
69
- const linked = [];
70
- for (const { name, source, dest } of plan) {
71
- if (!existsSync(source)) continue; // approved but absent nothing to reuse
72
- if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
73
- mkdirSync(dirname(dest), { recursive: true });
74
- // 'junction' on Windows needs no admin and works for directories;
75
- // 'dir' elsewhere. Both resolve to an absolute source we validated.
76
- symlinkSync(
77
- source,
78
- dest,
79
- process.platform === "win32" ? "junction" : "dir",
426
+ )
427
+ .trim()
428
+ .toLowerCase();
429
+ assertManagedWorktreePath(repoDir, worktreePath, { branchName });
430
+ // Sparse checkout: only materialize the packages this task needs. For a
431
+ // large monorepo this cuts worktree creation time and disk. Cone mode wants
432
+ // directory paths our sparsePaths are package dirs. Fail-closed on unsafe
433
+ // paths (normalizeSparsePaths drops them); empty/absent full checkout,
434
+ // byte-identical to the no-sparse path.
435
+ if (sparsePaths) {
436
+ gitExecArgs(["sparse-checkout", "set", ...sparsePaths], worktreePath);
437
+ result.sparsePaths = sparsePaths;
438
+ }
439
+
440
+ // Dependency reuse: link explicitly-approved directories (e.g. node_modules)
441
+ // from the main checkout into the worktree so the task does not reinstall.
442
+ // planSymlinkDirectories throws (fail-closed) on any junction/symlink escape.
443
+ if (availableSymlinkPlan.length > 0) {
444
+ const linked = [];
445
+ for (const { name, source, dest } of availableSymlinkPlan) {
446
+ assertNoSymlinkAncestors(
447
+ worktreePath,
448
+ dirname(dest),
449
+ "dependency destination",
450
+ );
451
+ assertNotSymlink(dest, "dependency destination");
452
+ if (existsSync(dest)) {
453
+ throw new Error(
454
+ `dependency destination already exists; refusing replacement: ${name}`,
455
+ );
456
+ }
457
+ mkdirSync(dirname(dest), { recursive: true });
458
+ // 'junction' on Windows needs no admin and works for directories;
459
+ // 'dir' elsewhere. Both resolve to an absolute source we validated.
460
+ symlinkSync(
461
+ source,
462
+ dest,
463
+ process.platform === "win32" ? "junction" : "dir",
464
+ );
465
+ // Register immediately after creation. If verification below fails,
466
+ // rollback must know a reparse point exists and must never send an
467
+ // unrecorded junction into force-removal.
468
+ linked.push(name);
469
+ result.symlinkedDirectories = [...linked];
470
+ if (
471
+ !lstatSync(dest).isSymbolicLink() ||
472
+ !sameFilesystemPath(realpathSync(dest), realpathSync(source))
473
+ ) {
474
+ throw new Error(`dependency link verification failed: ${name}`);
475
+ }
476
+ assertManagedWorktreePath(repoDir, worktreePath, { branchName });
477
+ }
478
+ }
479
+ } catch (error) {
480
+ try {
481
+ removeManagedDependencyLinks(
482
+ repoDir,
483
+ worktreePath,
484
+ result.symlinkedDirectories || [],
80
485
  );
81
- linked.push(name);
486
+ removeWorktree(repoDir, worktreePath, {
487
+ deleteBranch: true,
488
+ branchName,
489
+ ...(createdBranchOid ? { expectedBranchOid: createdBranchOid } : {}),
490
+ force: true,
491
+ });
492
+ } catch (cleanupError) {
493
+ error.cleanupError =
494
+ cleanupError instanceof Error
495
+ ? cleanupError.message
496
+ : String(cleanupError);
497
+ error.retainedWorktree = { path: worktreePath, branch: branchName };
82
498
  }
83
- if (linked.length > 0) result.symlinkedDirectories = linked;
499
+ throw error;
84
500
  }
85
501
 
86
502
  return result;
87
503
  }
88
504
 
505
+ function findRegisteredWorktreeByPath(worktrees, worktreePath) {
506
+ return (
507
+ worktrees.find((worktree) =>
508
+ sameFilesystemPath(worktree.path, worktreePath),
509
+ ) || null
510
+ );
511
+ }
512
+
513
+ function validateWorktreeRemovalTarget(repoDir, worktreePath, branchName) {
514
+ const validatedPath = assertManagedWorktreePath(repoDir, worktreePath, {
515
+ branchName,
516
+ });
517
+ const registered = findRegisteredWorktreeByPath(
518
+ readRegisteredWorktrees(repoDir),
519
+ validatedPath,
520
+ );
521
+ if (existsSync(validatedPath) && !registered) {
522
+ throw new Error("refusing to remove an unregistered worktree path");
523
+ }
524
+ if (registered?.locked) {
525
+ const reason = registered.lockReason ? `: ${registered.lockReason}` : "";
526
+ throw new Error(`refusing to remove a locked worktree${reason}`);
527
+ }
528
+ if (registered && branchName && registered.branch !== branchName) {
529
+ throw new Error(
530
+ "registered worktree branch does not match cleanup request",
531
+ );
532
+ }
533
+ return { validatedPath, registered };
534
+ }
535
+
536
+ function assertWorktreeCleanForRemoval(worktreePath) {
537
+ const status = gitExecArgs(
538
+ [
539
+ "status",
540
+ "--porcelain=v1",
541
+ "--untracked-files=all",
542
+ "--ignored=matching",
543
+ "--ignore-submodules=none",
544
+ ],
545
+ worktreePath,
546
+ );
547
+ if (status.trim() !== "") {
548
+ throw new Error(
549
+ "refusing to remove a worktree with tracked, untracked, or ignored files",
550
+ );
551
+ }
552
+ }
553
+
554
+ function readLocalBranchOid(repoDir, branch) {
555
+ assertSafeGitRef(branch, "branch name");
556
+ const ref = `refs/heads/${branch}`;
557
+ const output = gitExecArgs(
558
+ ["for-each-ref", "--format=%(refname) %(objectname)", ref],
559
+ repoDir,
560
+ ).trim();
561
+ if (output === "") return null;
562
+ const prefix = `${ref} `;
563
+ const exactMatches = output
564
+ .split(/\r?\n/)
565
+ .filter((line) => line.startsWith(prefix));
566
+ if (
567
+ exactMatches.length !== 1 ||
568
+ !/^[a-f0-9]{40,64}$/i.test(exactMatches[0].slice(prefix.length))
569
+ ) {
570
+ if (exactMatches.length === 0) return null;
571
+ throw new Error(`unable to verify worktree branch: ${branch}`);
572
+ }
573
+ return exactMatches[0].slice(prefix.length).toLowerCase();
574
+ }
575
+
89
576
  export function removeWorktree(repoDir, worktreePath, options = {}) {
90
577
  const deleteBranch = options.deleteBranch !== false;
578
+ const force = options.force === true;
579
+ const expectedBranchOid =
580
+ options.expectedBranchOid == null
581
+ ? null
582
+ : String(options.expectedBranchOid).toLowerCase();
583
+ if (
584
+ expectedBranchOid != null &&
585
+ !/^[a-f0-9]{40,64}$/i.test(expectedBranchOid)
586
+ ) {
587
+ throw new TypeError("expected worktree branch OID is invalid");
588
+ }
589
+ const initialTarget = validateWorktreeRemovalTarget(
590
+ repoDir,
591
+ worktreePath,
592
+ options.branchName || null,
593
+ );
594
+ const validatedPath = initialTarget.validatedPath;
595
+ const registered = initialTarget.registered;
596
+ const initiallySettled = registered == null && !existsSync(validatedPath);
597
+ if (!force && existsSync(validatedPath)) {
598
+ assertWorktreeCleanForRemoval(validatedPath);
599
+ }
91
600
 
92
- let branch = null;
601
+ let branch =
602
+ registered?.branch ||
603
+ (deleteBranch && expectedBranchOid && options.branchName
604
+ ? options.branchName
605
+ : null);
93
606
  if (deleteBranch) {
94
607
  try {
95
- branch = gitExecArgs(
96
- ["rev-parse", "--abbrev-ref", "HEAD"],
97
- worktreePath,
98
- ).trim();
608
+ branch =
609
+ branch ||
610
+ gitExecArgs(
611
+ ["rev-parse", "--abbrev-ref", "HEAD"],
612
+ validatedPath,
613
+ ).trim();
99
614
  } catch (_e) {
100
615
  // Can't determine branch, skip branch deletion.
101
616
  }
102
617
  }
618
+ let removalBranchOid = expectedBranchOid;
619
+ if (branch && (deleteBranch || removalBranchOid != null)) {
620
+ const currentBranchOid = readLocalBranchOid(repoDir, branch);
621
+ if (currentBranchOid == null) {
622
+ if (initiallySettled) return;
623
+ throw new Error(`Failed to verify worktree branch: ${branch}`);
624
+ }
625
+ removalBranchOid = removalBranchOid || currentBranchOid;
626
+ if (currentBranchOid !== removalBranchOid) {
627
+ throw new Error(
628
+ `refusing to remove worktree after branch moved: ${branch}`,
629
+ );
630
+ }
631
+ }
103
632
 
104
- try {
105
- gitExec(`worktree remove "${worktreePath}" --force`, repoDir);
106
- } catch (_e) {
107
- if (existsSync(worktreePath)) {
108
- rmSync(worktreePath, { recursive: true, force: true });
633
+ // Missing + unregistered is the idempotent recovery state after a crash
634
+ // between physical removal and snapshot settlement.
635
+ if (registered || existsSync(validatedPath)) {
636
+ if (!force && existsSync(validatedPath)) {
637
+ assertWorktreeCleanForRemoval(validatedPath);
638
+ }
639
+ if (branch && removalBranchOid) {
640
+ const currentBranchOid = readLocalBranchOid(repoDir, branch);
641
+ if (currentBranchOid == null) {
642
+ throw new Error(`Failed to verify worktree branch: ${branch}`);
643
+ }
644
+ if (currentBranchOid !== removalBranchOid) {
645
+ throw new Error(
646
+ `refusing to remove worktree after branch moved: ${branch}`,
647
+ );
648
+ }
649
+ }
650
+
651
+ // Re-read every path/registry/branch/lock authority as the final operation
652
+ // before deletion. In particular, persisted metadata is not deletion
653
+ // authority if it disappeared from Git's registry, became locked, or
654
+ // stopped identifying the same common repository.
655
+ const currentTarget = validateWorktreeRemovalTarget(
656
+ repoDir,
657
+ validatedPath,
658
+ options.branchName || registered?.branch || null,
659
+ );
660
+ const currentPath = currentTarget.validatedPath;
661
+ if (currentTarget.registered || existsSync(currentPath)) {
662
+ try {
663
+ if (process.platform === "win32") {
664
+ // Git for Windows follows directory junctions during
665
+ // `worktree remove --force`. Node 22's rmSync unlinks reparse points
666
+ // without traversing their targets, so remove the already-canonical,
667
+ // physically-verified checkout and then settle only stale, unlocked
668
+ // registry metadata. Exact-path verification below remains
669
+ // authoritative.
670
+ if (existsSync(currentPath)) {
671
+ rmSync(currentPath, {
672
+ recursive: true,
673
+ force: true,
674
+ maxRetries: 2,
675
+ retryDelay: 100,
676
+ });
677
+ }
678
+ gitExecArgs(["worktree", "prune", "--expire", "now"], repoDir);
679
+ } else {
680
+ gitExecArgs(
681
+ ["worktree", "remove", currentPath, ...(force ? ["--force"] : [])],
682
+ repoDir,
683
+ );
684
+ }
685
+ } catch (error) {
686
+ throw new Error(`Failed to remove managed worktree: ${validatedPath}`, {
687
+ cause: error,
688
+ });
689
+ }
690
+
691
+ const remaining = findRegisteredWorktreeByPath(
692
+ readRegisteredWorktrees(repoDir),
693
+ currentPath,
694
+ );
695
+ if (remaining || existsSync(currentPath)) {
696
+ throw new Error(
697
+ `Failed to verify managed worktree removal: ${validatedPath}`,
698
+ );
699
+ }
109
700
  }
110
- gitExec("worktree prune", repoDir);
111
701
  }
112
702
 
113
703
  if (
@@ -118,9 +708,24 @@ export function removeWorktree(repoDir, worktreePath, options = {}) {
118
708
  !branch.startsWith("master")
119
709
  ) {
120
710
  try {
121
- gitExec(`branch -D "${branch}"`, repoDir);
122
- } catch (_e) {
123
- // Branch might already be deleted.
711
+ assertSafeGitRef(branch, "branch name");
712
+ const currentBranchOid = readLocalBranchOid(repoDir, branch);
713
+ if (currentBranchOid == null) return;
714
+ if (removalBranchOid && currentBranchOid !== removalBranchOid) {
715
+ throw new Error(`worktree branch moved during removal: ${branch}`);
716
+ }
717
+ gitExecArgs(["branch", "-D", branch], repoDir);
718
+ } catch (error) {
719
+ try {
720
+ if (readLocalBranchOid(repoDir, branch) == null) return;
721
+ } catch (probeError) {
722
+ throw new Error(`Failed to verify worktree branch: ${branch}`, {
723
+ cause: probeError,
724
+ });
725
+ }
726
+ throw new Error(`Failed to delete worktree branch: ${branch}`, {
727
+ cause: error,
728
+ });
124
729
  }
125
730
  }
126
731
  }
@@ -129,25 +734,7 @@ export function listWorktrees(repoDir) {
129
734
  if (!isGitRepo(repoDir)) return [];
130
735
 
131
736
  try {
132
- const output = gitExec("worktree list --porcelain", repoDir);
133
- const worktrees = [];
134
- let current = {};
135
-
136
- for (const line of output.split("\n")) {
137
- if (line.startsWith("worktree ")) {
138
- if (current.path) worktrees.push(current);
139
- current = { path: line.slice(9) };
140
- } else if (line.startsWith("HEAD ")) {
141
- current.head = line.slice(5);
142
- } else if (line.startsWith("branch ")) {
143
- current.branch = line.slice(7).replace("refs/heads/", "");
144
- } else if (line === "bare") {
145
- current.bare = true;
146
- }
147
- }
148
- if (current.path) worktrees.push(current);
149
-
150
- return worktrees;
737
+ return readRegisteredWorktrees(repoDir);
151
738
  } catch (_e) {
152
739
  return [];
153
740
  }
@@ -157,7 +744,7 @@ export function pruneWorktrees(repoDir) {
157
744
  if (!isGitRepo(repoDir)) return 0;
158
745
 
159
746
  const before = listWorktrees(repoDir).length;
160
- gitExec("worktree prune", repoDir);
747
+ gitExecArgs(["worktree", "prune"], repoDir);
161
748
  const after = listWorktrees(repoDir).length;
162
749
  return before - after;
163
750
  }
@@ -167,12 +754,8 @@ export async function isolateTask(repoDir, taskId, fn, options = {}) {
167
754
  // options (large-monorepo): { sparsePaths, symlinkDirectories } forwarded to
168
755
  // createWorktree so an isolated task only materializes the packages it needs
169
756
  // and reuses approved dep dirs. Empty/absent → full checkout (byte-identical).
170
- const { path: worktreePath } = createWorktree(
171
- repoDir,
172
- branchName,
173
- undefined,
174
- options || {},
175
- );
757
+ const created = createWorktree(repoDir, branchName, undefined, options || {});
758
+ const worktreePath = created.path;
176
759
 
177
760
  try {
178
761
  const result = await fn(worktreePath);
@@ -187,8 +770,14 @@ export async function isolateTask(repoDir, taskId, fn, options = {}) {
187
770
  } finally {
188
771
  try {
189
772
  const hasCommits = _hasBranchCommits(repoDir, branchName);
773
+ removeManagedDependencyLinks(
774
+ repoDir,
775
+ worktreePath,
776
+ created.symlinkedDirectories || [],
777
+ );
190
778
  removeWorktree(repoDir, worktreePath, {
191
779
  deleteBranch: !hasCommits,
780
+ force: false,
192
781
  });
193
782
  } catch (_e) {
194
783
  // Best-effort cleanup.
@@ -290,7 +879,10 @@ export function cleanupAgentWorktrees(repoDir, options = {}) {
290
879
  if (!decision.safeToRemove) continue; // keep work we cannot safely lose
291
880
  }
292
881
  try {
293
- removeWorktree(repoDir, wt.path, { deleteBranch: true });
882
+ removeWorktree(repoDir, wt.path, {
883
+ deleteBranch: true,
884
+ force,
885
+ });
294
886
  cleaned++;
295
887
  } catch (_e) {
296
888
  // Skip if cleanup fails.