chainlesschain 0.162.170 → 0.162.171

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 (166) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-BTXN24I3.js → AIOps-Cf8yb18C.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BBB1VF4j.js → ActionButton-CsFT6FMt.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-BVCXq0k6.js → Analytics-DQe_hXMA.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-C3EOo2s5.js → AppLayout-Cl9Pp7jk.js} +4 -4
  6. package/src/assets/web-panel/assets/{Artifacts-CsJTZSMv.js → Artifacts-CBEzLVjF.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-CP0AllJ_.js → Audit-BS2Un14C.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-BCAjs08m.js → BackgroundAgents-DmcDH8OK.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-BxQI5XH4.js → Backup-DvzjTkkn.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-Bu-PNzC5.js → BaseInput-DnbpuyME.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DjPQynup.js → Chat-BJRRCm2P.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CygkRopu.js → ChatBubbleRenderer-Bgpxyqd_.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-BxL6xm7Z.js → Checkbox-DFOrWmvm.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CcyB7h_6.js → Codegen-BIQ2kU6Q.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-8NQiKJJx.js → Col-BI3R-N4I.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-B31qy5OS.js → Community-CPiAwsHT.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-C-5fAsZ4.js → Compact-Bv4cslSK.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-BTSccvF0.js → Compliance-CSlDG1NZ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-BXctU9IK.js → Cowork-CFcRumXf.js} +4 -4
  20. package/src/assets/web-panel/assets/{Cron-CxVFX9NG.js → Cron-DKYKMmOn.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DEF3dq8M.js → Crosschain-CxPMByp2.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BmKcE9he.js → DID-DJqiYw1C.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-CECBq_wG.js → Dashboard-D2dLMxqy.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-U-iuPB0n.js → Dropdown-BnJc384A.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-D8BIlSPL.js → EmailListRenderer-DctFvm-J.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BzQGfUH-.js → FamilyGuardDashboard-J_xg-Qop.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-4_WlOqLZ.js → Federation-BckL07Uw.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-gtrW-DdF.js → FormItemContext-B8s5ccSO.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-BLcvJJB6.js → GenericCardRenderer-hPMBK2No.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-Bk0janGD.js → Git-CfMpYLo7.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CcfyMlV8.js → Governance-Bt3OcrC7.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CBLrAf4M.js → Inference-BlKc336N.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-ClguhteZ.js → KnowledgeGraph-B6p7og2c.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-Dp_oeKfJ.js → Logs-uv5Re6YR.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-RQ1HbTY5.js → MarkdownRenderer-CId5JWjX.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-BHk50smM.js → Marketplace-CfNbntHt.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-BAY3CV6J.js → McpTools-YRXt74Yk.js} +5 -5
  38. package/src/assets/web-panel/assets/{Memory-w0pG2AKx.js → Memory-BfQQs-Jd.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-C0VkDUE3.js → MobileBridge-BuGC-4S2.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-BCvQ80I9.js → MobileProjects-Czieqw1m.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-DSIl41Tt.js → Mtc-DHWHOVCO.js} +5 -5
  42. package/src/assets/web-panel/assets/{MtcAudit-D1mzUlIr.js → MtcAudit-DWSEbMW-.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-CeJz4VSS.js → Multisig-DrhEv1vq.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-_O3aweu8.js → NLProgramming-D8wKCu7E.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-Dt7_pO86.js → Notes-AFeqq0I_.js} +4 -4
  46. package/src/assets/web-panel/assets/{NotificationSettings-Dhg3Nktv.js → NotificationSettings-D6qenYUo.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-QsAPcTb2.js → OrderTableRenderer-CF0X4fyB.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-BuiaUPkv.js → Organization-DdfbNqf6.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-C3jb6knl.js → Overflow-BFEvnLfd.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-BOMyrLkb.js → P2P-DtL6wbkz.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-UvLStCgS.js → PdhVaultBrowser-CQPriTud.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-B9cN0dJH.js → Permissions-Bj25ANmR.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-DNjW5qro.js → PersonalDataHub-BNj2Oux0.js} +3 -3
  54. package/src/assets/web-panel/assets/{Pipeline-CFLyZ6Sh.js → Pipeline-Dyj8UEaG.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-D3w6vOxf.js → Privacy-BBUrWRFz.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-cwcVsn8K.js → ProjectInit-BdX7uQn6.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-aUoAroc-.js → ProjectSettings-BghtW1zr.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-CrzQv7Zv.js → Projects-9VTquZCz.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-jZaPizKR.js → Providers-D-0K_Eza.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-DNzmwcfg.js → QrScannerModal-DDaN30_D.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-m-5Gwt70.js → QuickAsk-DfHlifOj.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-CrrAjNxP.js → Recommend-CgLIvYE2.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-BDCMjRFe.js → RemoteSession-DpOPrwE4.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-CPCt1Jc8.js → Reputation-wMexlbCE.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-C-RorxHs.js → Row-D9aD_nrc.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-CnQs4TuY.js → RssFeed-Drg7l6zu.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-B1jkeFZY.js → Search-BKO1nrwN.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-CGKI3vvd.js → Security-G3Kyk-TG.js} +3 -3
  69. package/src/assets/web-panel/assets/{Services-DQjesYF8.js → Services-xJReTOna.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-C9Ipj0A6.js → Skeleton-ldVDVTPG.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-D2j54i9O.js → Skills-emwNYV0j.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-xhrVjTeb.js → Sla-CZYwGp1K.js} +1 -1
  73. package/src/assets/web-panel/assets/SpeechSettings-CyaPrzOF.js +1 -0
  74. package/src/assets/web-panel/assets/{SyncSettings-CmIPU5wR.js → SyncSettings-Dcs5Hhj6.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-B-T1J42r.js → Tasks-CerFAo26.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-Aq669sjw.js → Templates-BdZUbOg3.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-BKWUWNXB.js → Tenant-B4c_7Bsw.js} +1 -1
  78. package/src/assets/web-panel/assets/Terminal-VVFLS3Ol.js +3 -0
  79. package/src/assets/web-panel/assets/{TimelineRenderer-C465ySdn.js → TimelineRenderer-tc6sp0TV.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-BgVqF-bX.js → Tokens-Bw_vkMJq.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-BMNHmxwH.js → Trigger-DkbJnpvz.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-B_adk89L.js → Trust-CIf9kqN1.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-Biw38mog.js → UkeySign-Cjwllnea.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-AC-eaw5k.js → VideoEditing-kgfkY7AN.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-8lQ5_t0X.js → Wallet-CYWXt4u9.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-JDmH_-DF.js → WebAuthn-QgPwyWCO.js} +5 -5
  87. package/src/assets/web-panel/assets/{WorkflowEditor-DHQAdITG.js → WorkflowEditor-Co0lha18.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-C3qauUVv.js → chat-CI__Ox-r.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-DLP70JGe.js → colors-Csv0eYYd.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-DjsoqA5t.js → compact-item-e53UCdHy.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-CfEdl1in.js → createContext-BMPGRDhV.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-BIulobq7.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DWwHHlBG.js → hasIn-Dz68qIAq.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-DbFnfyKh.js → index--7tHEadk.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-DKsWujXP.js → index-53ljQo_x.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-ArZCi1rK.js → index-5iicv5af.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DTEcPJGf.js → index-B1s1B8Iy.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BcDeMCUK.js → index-B4lKPJy1.js} +1 -1
  99. package/src/assets/web-panel/assets/index-BGkiYnET.js +1 -0
  100. package/src/assets/web-panel/assets/{index-Ds1BfT_C.js → index-BHkDRNA5.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CNeLedtU.js → index-BOpz2qS1.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Dfka-vzJ.js → index-BT3CV4Nh.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BqPuyN4p.js → index-BW2dImbg.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-C4k6JGm_.js → index-C-NbcAbP.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-D-te-S9f.js → index-C53o-MdJ.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CsEk9s04.js → index-C5hLtxVz.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-OfMwkFIo.js → index-CNtZMPKV.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-Bp_WNm2_.js → index-CXuON9OG.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-CgruUvB3.js → index-CYUxs6J1.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-C8wrUFzY.js → index-CoGSbW5J.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BydBh7oT.js → index-CpTzqJ0Z.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-nuZaMsMy.js → index-CrsoSnQF.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-B-rD2rhi.js → index-CvLBQk_M.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CbF27GqZ.js → index-CyLUaP2i.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DTDpF8MS.js → index-D0bg74e1.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DAcBi6F-.js → index-DCl4Xb4N.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-D5VL0NYy.js → index-DJqQL6so.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-UEbYl2pu.js → index-DVCd2aMX.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-C0xMzvk8.js → index-DctYrVMq.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-C47EiQ7d.js → index-Dk9Gi8xd.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CnhJbWsl.js → index-DnXm3Qsh.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-T5emAyFT.js → index-Dtjm1K1a.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DgQ92xdO.js → index-DyqcHiL1.js} +1 -1
  124. package/src/assets/web-panel/assets/index-Eyhrr_3u.js +1 -0
  125. package/src/assets/web-panel/assets/{index-VtCz3A64.js → index-HbhQRrM2.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-B4gCFHtB.js → index-K5E_wjut.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DbjUePgq.js → index-N9DkR1i7.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Dn2rNMmv.js → index-_LmPlyeV.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Lh0e2afo.js → index-ajz0E0pK.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-Dmt1HDWZ.js → index-t6f80uqL.js} +17 -17
  131. package/src/assets/web-panel/assets/{index-_g5q2hF3.js → index-wcCouSNq.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-gu8zBhtp.js → index-ygfUVVFv.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-C_pMmi75.js → initDefaultProps-DVCeQJrT.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-BvKgS96o.js → motion-chBCR_8v.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-C-1U4R1I.js → move-BziPlrh6.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-D6BC968-.js → mtc-parser-Cenk0OD7.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-DxOZvcGQ.js → omit-D-AiRsei.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-LpzmHdkw.js → pickAttrs-CHw3Fvqt.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-Bm--qom1.js → placementArrow-g8I-rCad.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-CVZZlhJf.js → responsiveObserve-BWffAr9k.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-DD1xINGx.js → slide-BmUQqhkb.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-LrjHAy63.js → statusUtils-B89gCO-y.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-BqknYxTe.js → styleChecker-x1bnCF73.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-Dljy3diT.js → useFlexGapSupport-B-k1_Ggw.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-OedUtPLc.js → useFs-Bc3PUqQk.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-Cz-kxbP_.js → usePersonalDataHub-D8qFDX1I.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-C8ZuTk8W.js → vnode-UaL5-WGg.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-CjbgJYXQ.js → zoom-Dv7bsbLs.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agent.js +39 -14
  151. package/src/commands/background-session.js +12 -1
  152. package/src/gateways/ws/ws-agent-handler.js +26 -1
  153. package/src/gateways/ws/ws-approval-gate.js +131 -0
  154. package/src/lib/agent-worktree.js +173 -5
  155. package/src/lib/background-agent-supervisor.js +193 -4
  156. package/src/lib/interaction-adapter.js +26 -5
  157. package/src/lib/repl-rewind.js +62 -14
  158. package/src/lib/turn-binding.js +134 -0
  159. package/src/repl/agent-repl.js +75 -3
  160. package/src/repl/repl-turn-binding.js +91 -0
  161. package/src/runtime/headless-runner.js +59 -0
  162. package/src/assets/web-panel/assets/SpeechSettings-C6KMOUCa.js +0 -1
  163. package/src/assets/web-panel/assets/Terminal-DEeAvA8L.js +0 -3
  164. package/src/assets/web-panel/assets/devWarning-Cwuuqc_f.js +0 -1
  165. package/src/assets/web-panel/assets/index-BkTk-zJK.js +0 -1
  166. package/src/assets/web-panel/assets/index-C-BSTEhe.js +0 -1
@@ -26,6 +26,7 @@ import { runnableTaskModel } from "../../lib/runnable-provider.js";
26
26
  import { PlanState } from "../../lib/plan-mode.js";
27
27
  import { CLISlotFiller } from "../../lib/slot-filler.js";
28
28
  import { createAbortError, isAbortError } from "../../lib/abort-utils.js";
29
+ import { createWsApprovalGate } from "./ws-approval-gate.js";
29
30
 
30
31
  export class WSAgentHandler {
31
32
  /**
@@ -34,7 +35,7 @@ export class WSAgentHandler {
34
35
  * @param {import("../../lib/interaction-adapter.js").WebSocketInteractionAdapter} options.interaction
35
36
  * @param {object} [options.db]
36
37
  */
37
- constructor({ session, interaction, db }) {
38
+ constructor({ session, interaction, db, approvalGate }) {
38
39
  this.session = session;
39
40
  this.interaction = interaction;
40
41
  this.db = db || null;
@@ -44,6 +45,27 @@ export class WSAgentHandler {
44
45
  // P0-2: monotonic sequence for crash-safe side-effect ledger op ids. The
45
46
  // session id + a per-op nonce keep ids unique across turns and processes.
46
47
  this._sideEffectSeq = 0;
48
+ // P0 authority: permission-gate-over-WS. An explicitly injected gate wins
49
+ // (tests / embedders); otherwise one is built lazily on the first turn
50
+ // when CC_WS_APPROVAL_GATE=1 — its confirmer raises confirm questions
51
+ // over the interaction adapter WITH an approval binding the client must
52
+ // echo back on approve (mismatch → deny). Default (env unset, nothing
53
+ // injected): approvalGate stays null and loopOptions are byte-identical.
54
+ this._approvalGate = approvalGate || null;
55
+ this._approvalGateInit = Boolean(approvalGate);
56
+ }
57
+
58
+ /** Resolve the session's approval gate (lazy, opt-in; null when disabled). */
59
+ async _ensureApprovalGate() {
60
+ if (this._approvalGateInit) return this._approvalGate;
61
+ this._approvalGateInit = true;
62
+ if (process.env.CC_WS_APPROVAL_GATE === "1") {
63
+ this._approvalGate = await createWsApprovalGate({
64
+ sessionId: this.session.id,
65
+ interaction: this.interaction,
66
+ });
67
+ }
68
+ return this._approvalGate;
47
69
  }
48
70
 
49
71
  /**
@@ -127,6 +149,9 @@ export class WSAgentHandler {
127
149
  slotFiller,
128
150
  interaction: this.interaction,
129
151
  signal: abortController.signal,
152
+ // P0 authority: null unless opted in (byte-identical default — agent
153
+ // core already defaults `options.approvalGate || null`).
154
+ approvalGate: await this._ensureApprovalGate(),
130
155
  };
131
156
 
132
157
  // P0-2: crash-safe side-effect ledger for the IDE/bridge path — mirrors
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Permission-gate-over-WS — the approval-BINDING producer for the WS path.
3
+ *
4
+ * The consumer side has been wired for a while: WebSocketInteractionAdapter
5
+ * stores `options.binding` on a pending request, rides it out on the message,
6
+ * and `_resolvePending` rejects an approve whose echoed binding mismatches
7
+ * (deny, fail closed). What was missing is the PRODUCER: nothing on the WS
8
+ * path ever raised an approval request WITH a binding, because the WS agent
9
+ * handler wired no ApprovalGate at all (CONFIRM-tier decisions fell closed).
10
+ *
11
+ * This module supplies both halves:
12
+ * - `buildWsApprovalConfirmer` — the gate confirmer that computes
13
+ * `approvalBindingDigest({toolCallId, args, policyDigest})` (mirroring the
14
+ * headless-stream `interactiveConfirm` producer) and asks the WS client
15
+ * over the interaction adapter with the binding attached. Timeout /
16
+ * disconnect / any transport error → false (fail closed).
17
+ * - `createWsApprovalGate` — a session-scoped gate whose CONFIRM decisions
18
+ * route to that confirmer. Policy tiers stay authoritative on the shared
19
+ * singleton gate (so `sessions.policy.set` over WS keeps working and no
20
+ * global confirmer is ever mutated — a per-session confirmer on the
21
+ * process-wide singleton would race across concurrent WS sessions).
22
+ *
23
+ * Default byte-identical: the WS handler only builds this when
24
+ * `CC_WS_APPROVAL_GATE=1` (or a gate is injected); otherwise loopOptions
25
+ * carry `approvalGate: null` exactly as before.
26
+ */
27
+
28
+ import { approvalBindingDigest } from "../../lib/agent-authority.js";
29
+
30
+ /**
31
+ * Build the binding-attaching confirmer for one WS session.
32
+ *
33
+ * @param {object} opts
34
+ * @param {{askConfirm: Function}} opts.interaction - the session's WS adapter
35
+ * @param {string} opts.sessionId
36
+ * @returns {(ctx?: object) => Promise<boolean>}
37
+ */
38
+ export function buildWsApprovalConfirmer({ interaction, sessionId }) {
39
+ let approvalSeq = 0;
40
+ return async (ctx = {}) => {
41
+ const command = ctx.command ?? ctx.args?.command ?? null;
42
+ const risk = ctx.riskLevel || ctx.risk || "unknown";
43
+ // Bind this approval to the exact call it authorizes (same recipe as the
44
+ // headless-stream producer): request identity + normalized args + the
45
+ // policy/rule in force. The client must echo it back on approve.
46
+ const binding = approvalBindingDigest({
47
+ toolCallId: `${sessionId}:appr-${++approvalSeq}`,
48
+ args: ctx.args ?? (command != null ? { command } : null),
49
+ policyDigest: ctx.rule || ctx.riskLevel || ctx.risk || null,
50
+ });
51
+ const what = ctx.tool || ctx.toolName || "tool";
52
+ const detail = command != null ? `: ${String(command).slice(0, 200)}` : "";
53
+ try {
54
+ const ok = await interaction.askConfirm(
55
+ `Approve ${what}${detail}? (risk: ${risk})`,
56
+ {
57
+ default: false,
58
+ binding,
59
+ // Structured context so UIs can render a proper approval card
60
+ // instead of parsing the question text.
61
+ approval: {
62
+ tool: what,
63
+ command,
64
+ risk,
65
+ rule: ctx.rule || null,
66
+ },
67
+ },
68
+ );
69
+ return ok === true;
70
+ } catch {
71
+ // timeout / disconnect / interrupt → fail closed, never approve
72
+ return false;
73
+ }
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Create a session-scoped approval gate for the WS agent handler.
79
+ *
80
+ * Policy lookups delegate to the persistent singleton gate (per-session tiers
81
+ * set via `sessions.policy.set` stay live); CONFIRM decisions route to the
82
+ * binding-attaching WS confirmer. Returns null on any load failure so the
83
+ * caller degrades to the legacy no-gate path (fail closed at CONFIRM).
84
+ *
85
+ * @param {object} opts
86
+ * @param {string} opts.sessionId
87
+ * @param {{askConfirm: Function}} opts.interaction
88
+ * @param {object} [opts.deps] - test seams: `loadSingletons`, `loadSessionCore`
89
+ * @returns {Promise<object|null>}
90
+ */
91
+ export async function createWsApprovalGate({
92
+ sessionId,
93
+ interaction,
94
+ deps = {},
95
+ }) {
96
+ try {
97
+ const loadSingletons =
98
+ deps.loadSingletons ||
99
+ (() => import("../../lib/session-core-singletons.js"));
100
+ const loadSessionCore =
101
+ deps.loadSessionCore || (() => import("@chainlesschain/session-core"));
102
+ const [singletons, core] = await Promise.all([
103
+ loadSingletons(),
104
+ loadSessionCore(),
105
+ ]);
106
+ const inner = await singletons.getApprovalGate();
107
+ const confirmer =
108
+ deps.confirmer || buildWsApprovalConfirmer({ interaction, sessionId });
109
+ const gate = new core.ApprovalGate({
110
+ defaultPolicy: core.APPROVAL_POLICY?.STRICT || "strict",
111
+ confirm: confirmer,
112
+ });
113
+ return {
114
+ // Explicit ctx.policy always wins inside decide(); resolve it from the
115
+ // singleton on every call so mid-session `sessions.policy.set` applies.
116
+ decide: (ctx = {}) =>
117
+ gate.decide({
118
+ ...ctx,
119
+ policy:
120
+ ctx.policy || inner.getSessionPolicy(ctx.sessionId || sessionId),
121
+ }),
122
+ hasConfirmer: () => gate.hasConfirmer(),
123
+ setConfirmer: (fn) => gate.setConfirmer(fn),
124
+ getSessionPolicy: (sid) => inner.getSessionPolicy(sid || sessionId),
125
+ setSessionPolicy: (sid, policy) =>
126
+ inner.setSessionPolicy(sid || sessionId, policy),
127
+ };
128
+ } catch {
129
+ return null; // degrade to the legacy no-gate path
130
+ }
131
+ }
@@ -11,11 +11,18 @@
11
11
  * a merge hint. Removal failures fail-open to "kept".
12
12
  */
13
13
 
14
- import { execSync } from "child_process";
14
+ import { execFileSync, execSync } from "child_process";
15
+ import { existsSync, realpathSync } from "node:fs";
16
+ import { resolve } from "node:path";
15
17
  import { createWorktree, removeWorktree } from "./worktree-isolator.js";
16
18
  import { findProjectRoot } from "./project-instructions.js";
17
19
 
18
- export const _deps = { execSync, createWorktree, removeWorktree };
20
+ export const _deps = {
21
+ execFileSync,
22
+ execSync,
23
+ createWorktree,
24
+ removeWorktree,
25
+ };
19
26
 
20
27
  function git(cmd, cwd, deps) {
21
28
  return (deps.execSync || execSync)(`git ${cmd}`, {
@@ -25,6 +32,132 @@ function git(cmd, cwd, deps) {
25
32
  });
26
33
  }
27
34
 
35
+ function canonicalPath(value) {
36
+ const absolute = resolve(String(value || ""));
37
+ const real = realpathSync.native
38
+ ? realpathSync.native(absolute)
39
+ : realpathSync(absolute);
40
+ return process.platform === "win32" ? real.toLowerCase() : real;
41
+ }
42
+
43
+ function parseRegisteredWorktrees(text) {
44
+ const entries = [];
45
+ let current = null;
46
+ for (const line of String(text || "").split(/\r?\n/)) {
47
+ if (line.startsWith("worktree ")) {
48
+ if (current) entries.push(current);
49
+ current = { path: line.slice("worktree ".length) };
50
+ continue;
51
+ }
52
+ if (!current) continue;
53
+ if (line.startsWith("HEAD ")) current.head = line.slice("HEAD ".length);
54
+ if (line.startsWith("branch ")) {
55
+ current.branch = line.slice("branch ".length);
56
+ }
57
+ }
58
+ if (current) entries.push(current);
59
+ return entries;
60
+ }
61
+
62
+ /**
63
+ * Verify that persisted worktree metadata still identifies the same linked
64
+ * worktree. Cleanup callers use this as a fail-closed identity check before
65
+ * invoking any removal primitive: a missing/corrupt path must never turn into
66
+ * a recursive delete of the repository checkout.
67
+ *
68
+ * @returns {{valid:boolean, reason:string}}
69
+ */
70
+ export function validateAgentWorktree(info, { deps = _deps } = {}) {
71
+ const worktreePath = info?.path || info?.worktreePath;
72
+ if (!worktreePath || !info?.repoRoot || !info?.branch || !info?.baseSha) {
73
+ return { valid: false, reason: "incomplete worktree metadata" };
74
+ }
75
+ if (!/^[0-9a-f]{40,64}$/i.test(String(info.baseSha))) {
76
+ return { valid: false, reason: "invalid worktree base SHA" };
77
+ }
78
+ if (!existsSync(worktreePath) || !existsSync(info.repoRoot)) {
79
+ return { valid: false, reason: "worktree or repository path is missing" };
80
+ }
81
+
82
+ try {
83
+ const repo = canonicalPath(info.repoRoot);
84
+ const worktree = canonicalPath(worktreePath);
85
+ if (repo === worktree) {
86
+ return {
87
+ valid: false,
88
+ reason: "refusing to treat the main checkout as an agent worktree",
89
+ };
90
+ }
91
+
92
+ const repoTop = canonicalPath(
93
+ git("rev-parse --show-toplevel", info.repoRoot, deps).trim(),
94
+ );
95
+ const worktreeTop = canonicalPath(
96
+ git("rev-parse --show-toplevel", worktreePath, deps).trim(),
97
+ );
98
+ if (repoTop !== repo || worktreeTop !== worktree) {
99
+ return { valid: false, reason: "worktree path identity mismatch" };
100
+ }
101
+
102
+ const registered = parseRegisteredWorktrees(
103
+ git("worktree list --porcelain", info.repoRoot, deps),
104
+ ).find((entry) => {
105
+ try {
106
+ return canonicalPath(entry.path) === worktree;
107
+ } catch {
108
+ return false;
109
+ }
110
+ });
111
+ if (!registered) {
112
+ return {
113
+ valid: false,
114
+ reason: "path is not a registered worktree of the repository",
115
+ };
116
+ }
117
+ if (registered.branch !== `refs/heads/${info.branch}`) {
118
+ return {
119
+ valid: false,
120
+ reason: "registered worktree branch does not match persisted metadata",
121
+ };
122
+ }
123
+ return { valid: true, reason: "verified" };
124
+ } catch (error) {
125
+ return {
126
+ valid: false,
127
+ reason: `worktree identity could not be verified: ${error.message}`,
128
+ };
129
+ }
130
+ }
131
+
132
+ function removeVerifiedAgentWorktree(info, deps) {
133
+ // Unit seams historically inject removeWorktree directly. Keep that seam,
134
+ // while production uses a non-forced git removal: if the tree changed
135
+ // between inspection and deletion, git refuses instead of discarding it.
136
+ if (deps.removeWorktree && deps.removeWorktree !== removeWorktree) {
137
+ deps.removeWorktree(info.repoRoot, info.path);
138
+ return null;
139
+ }
140
+
141
+ const run = deps.execFileSync || execFileSync;
142
+ run("git", ["worktree", "remove", info.path], {
143
+ cwd: info.repoRoot,
144
+ encoding: "utf8",
145
+ stdio: ["ignore", "pipe", "pipe"],
146
+ });
147
+ try {
148
+ run("git", ["branch", "-D", "--", info.branch], {
149
+ cwd: info.repoRoot,
150
+ encoding: "utf8",
151
+ stdio: ["ignore", "pipe", "pipe"],
152
+ });
153
+ return null;
154
+ } catch (error) {
155
+ // The worktree itself is already gone. Preserve an honest warning rather
156
+ // than claiming the whole cleanup failed and that the path was kept.
157
+ return `branch cleanup failed: ${error.message}`;
158
+ }
159
+ }
160
+
28
161
  /**
29
162
  * @returns {{ path, branch, repoRoot, baseSha }}
30
163
  */
@@ -51,22 +184,57 @@ export function setupAgentWorktree({
51
184
  export function finishAgentWorktree(info, { deps = _deps } = {}) {
52
185
  if (!info) return { removed: false, kept: false, reason: "no worktree" };
53
186
 
187
+ // Production cleanup validates the persisted path against git's own
188
+ // worktree registry. Custom dependency bundles are test seams and retain the
189
+ // historical pure mocked behavior unless they provide validateWorktree.
190
+ const validation =
191
+ typeof deps.validateWorktree === "function"
192
+ ? deps.validateWorktree(info)
193
+ : deps === _deps
194
+ ? validateAgentWorktree(info, { deps })
195
+ : { valid: true, reason: "injected validation seam" };
196
+ if (!validation?.valid) {
197
+ return {
198
+ removed: false,
199
+ kept: true,
200
+ reason: `unverifiable worktree: ${validation?.reason || "unknown reason"}`,
201
+ };
202
+ }
203
+
54
204
  let dirty = true;
55
205
  let headSha = null;
206
+ let readable = false;
56
207
  try {
57
208
  dirty = git("status --porcelain", info.path, deps).trim().length > 0;
58
209
  headSha = git("rev-parse HEAD", info.path, deps).trim();
210
+ readable = true;
59
211
  } catch {
60
212
  /* unreadable → keep, never destroy work we cannot verify */
61
213
  }
62
214
 
215
+ if (!readable) {
216
+ return {
217
+ removed: false,
218
+ kept: true,
219
+ reason: "unverifiable worktree: git state could not be read",
220
+ };
221
+ }
222
+
63
223
  const hasCommits = headSha !== null && headSha !== info.baseSha;
64
224
  if (!dirty && !hasCommits) {
65
225
  try {
66
- (deps.removeWorktree || removeWorktree)(info.repoRoot, info.path);
67
- return { removed: true, kept: false, reason: "no changes" };
226
+ const warning = removeVerifiedAgentWorktree(info, deps);
227
+ return {
228
+ removed: true,
229
+ kept: false,
230
+ reason: warning ? `no changes; ${warning}` : "no changes",
231
+ };
68
232
  } catch (err) {
69
- return { removed: false, kept: true, reason: `cleanup failed: ${err.message}` };
233
+ return {
234
+ removed: false,
235
+ kept: true,
236
+ reason: `cleanup failed: ${err.message}`,
237
+ };
70
238
  }
71
239
  }
72
240
  return {
@@ -6,16 +6,21 @@ import {
6
6
  openSync,
7
7
  readFileSync,
8
8
  readdirSync,
9
+ realpathSync,
9
10
  renameSync,
10
11
  rmSync,
11
12
  statSync,
12
13
  writeFileSync,
13
14
  } from "node:fs";
14
15
  import { randomBytes } from "node:crypto";
15
- import { join } from "node:path";
16
+ import { join, resolve } from "node:path";
16
17
  import { fileURLToPath } from "node:url";
17
18
  import { getHomeDir } from "./paths.js";
18
19
  import { deriveSessionState } from "./session-lifecycle.js";
20
+ import {
21
+ finishAgentWorktree,
22
+ validateAgentWorktree,
23
+ } from "./agent-worktree.js";
19
24
 
20
25
  export const DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
21
26
  export const DEFAULT_HEARTBEAT_STALE_MS = 120000;
@@ -280,6 +285,20 @@ export function writeBackgroundAgentState(state) {
280
285
  if (Number(current.pinnedAt || 0) > Number(next.pinnedAt || 0)) {
281
286
  next = { ...next, pinned: current.pinned, pinnedAt: current.pinnedAt };
282
287
  }
288
+ // A background worktree is immutable session identity, just like the
289
+ // conversation id. Heartbeat/finalize/rename writers may race with a stale
290
+ // snapshot, but no same-id transition is allowed to move the run back to
291
+ // the main checkout or drop the cleanup metadata.
292
+ if (current.worktreePath) {
293
+ next = {
294
+ ...next,
295
+ cwd: current.worktreePath,
296
+ repoRoot: current.repoRoot,
297
+ worktreePath: current.worktreePath,
298
+ baseSha: current.baseSha,
299
+ branch: current.branch,
300
+ };
301
+ }
283
302
  }
284
303
  const tmp = `${target}.${process.pid}.tmp`;
285
304
  writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
@@ -500,7 +519,7 @@ export function renameBackgroundAgent(id, title, options = {}) {
500
519
  * conversation session is NOT touched — that's `cc session delete`.
501
520
  */
502
521
  export function removeBackgroundAgent(id, options = {}) {
503
- const state = effectiveBackgroundAgentState(readBackgroundAgentState(id), {
522
+ let state = effectiveBackgroundAgentState(readBackgroundAgentState(id), {
504
523
  now: options.now,
505
524
  heartbeatStaleMs: options.heartbeatStaleMs,
506
525
  });
@@ -513,15 +532,28 @@ export function removeBackgroundAgent(id, options = {}) {
513
532
  }
514
533
  try {
515
534
  stopBackgroundAgent(id);
535
+ state =
536
+ effectiveBackgroundAgentState(readBackgroundAgentState(id), {
537
+ now: options.now,
538
+ heartbeatStaleMs: options.heartbeatStaleMs,
539
+ }) || state;
516
540
  } catch {
517
541
  /* best-effort — the record removal below is the point of rm --force */
518
542
  }
519
543
  }
544
+ const worktree = cleanupBackgroundAgentWorktree(state, {
545
+ keepWorktree: options.keepWorktree === true,
546
+ });
520
547
  rmSync(statePath(id), { force: true });
521
548
  if (options.keepLog !== true) {
522
549
  rmSync(logPath(id), { force: true });
523
550
  }
524
- return { id, removed: true, status: state.status };
551
+ return {
552
+ id,
553
+ removed: true,
554
+ status: state.status,
555
+ ...(worktree ? { worktree } : {}),
556
+ };
525
557
  }
526
558
 
527
559
  /**
@@ -598,6 +630,68 @@ export function buildFollowUpArgv(argv, opts = {}) {
598
630
  return out;
599
631
  }
600
632
 
633
+ function canonicalPath(value) {
634
+ const absolute = resolve(String(value || ""));
635
+ try {
636
+ const real = realpathSync.native
637
+ ? realpathSync.native(absolute)
638
+ : realpathSync(absolute);
639
+ return process.platform === "win32" ? real.toLowerCase() : real;
640
+ } catch {
641
+ return process.platform === "win32" ? absolute.toLowerCase() : absolute;
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Normalize and verify worktree metadata before it enters a background job.
647
+ * The metadata is persisted as top-level state fields for compatibility with
648
+ * status/IDE consumers: repoRoot/worktreePath/baseSha/branch.
649
+ */
650
+ export function normalizeBackgroundWorktree(worktree, cwd) {
651
+ if (!worktree) return null;
652
+ const normalized = {
653
+ repoRoot: resolve(String(worktree.repoRoot || "")),
654
+ worktreePath: resolve(String(worktree.worktreePath || worktree.path || "")),
655
+ baseSha: String(worktree.baseSha || ""),
656
+ branch: String(worktree.branch || ""),
657
+ };
658
+ if (
659
+ !normalized.repoRoot ||
660
+ !normalized.worktreePath ||
661
+ !normalized.baseSha ||
662
+ !normalized.branch
663
+ ) {
664
+ throw new Error(
665
+ "Cannot launch background agent: incomplete worktree metadata",
666
+ );
667
+ }
668
+ if (canonicalPath(cwd) !== canonicalPath(normalized.worktreePath)) {
669
+ throw new Error(
670
+ "Cannot launch background agent: cwd does not match the persisted worktree path",
671
+ );
672
+ }
673
+ const validation = validateAgentWorktree({
674
+ ...normalized,
675
+ path: normalized.worktreePath,
676
+ });
677
+ if (!validation.valid) {
678
+ throw new Error(
679
+ `Cannot launch background agent: worktree metadata is not usable (${validation.reason})`,
680
+ );
681
+ }
682
+ return normalized;
683
+ }
684
+
685
+ function persistedWorktree(state) {
686
+ if (!state?.worktreePath) return null;
687
+ return {
688
+ repoRoot: state.repoRoot,
689
+ worktreePath: state.worktreePath,
690
+ baseSha: state.baseSha,
691
+ branch: state.branch,
692
+ };
693
+ }
694
+
601
695
  /**
602
696
  * Continue a finished/crashed background session as a NEW background session
603
697
  * on the same conversation (`cc daemon resume <id> <prompt>`). The original
@@ -622,14 +716,27 @@ export function resumeBackgroundAgent(id, prompt, options = {}) {
622
716
  }
623
717
  const text = String(prompt || "").trim();
624
718
  if (!text) throw new Error("resume requires a prompt");
719
+ const worktree = persistedWorktree(state);
720
+ if (
721
+ worktree &&
722
+ options.cwd &&
723
+ canonicalPath(options.cwd) !== canonicalPath(worktree.worktreePath)
724
+ ) {
725
+ throw new Error(
726
+ `Background agent ${id} is bound to worktree "${worktree.worktreePath}" and cannot resume in a different cwd`,
727
+ );
728
+ }
625
729
  const argv = ["agent", "--session", state.sessionId, "-p", text];
626
730
  return launchBackgroundAgent({
627
731
  argv,
628
- cwd: options.cwd || state.cwd || process.cwd(),
732
+ // Worktree sessions fail closed: never fall back to process.cwd(), which
733
+ // could silently resume a write task in the main checkout.
734
+ cwd: worktree?.worktreePath || options.cwd || state.cwd || process.cwd(),
629
735
  sessionId: state.sessionId,
630
736
  title: options.title || state.title || text.slice(0, 100),
631
737
  cliEntry: options.cliEntry,
632
738
  followUpArgv: ["agent", "--session", state.sessionId],
739
+ worktree,
633
740
  });
634
741
  }
635
742
 
@@ -667,8 +774,10 @@ export function launchBackgroundAgent({
667
774
  title,
668
775
  cliEntry,
669
776
  followUpArgv,
777
+ worktree,
670
778
  }) {
671
779
  assertUsableCwd(cwd);
780
+ const worktreeState = normalizeBackgroundWorktree(worktree, cwd);
672
781
  const id = createBackgroundAgentId();
673
782
  const dir = backgroundAgentsDir();
674
783
  const jobFile = join(dir, `${id}.job.${process.pid}.json`);
@@ -683,6 +792,7 @@ export function launchBackgroundAgent({
683
792
  title: title || "Background agent",
684
793
  cliEntry: cliEntry || process.argv[1],
685
794
  logFile: logPath(id),
795
+ ...(worktreeState || {}),
686
796
  // Present = interactive attach can start follow-up turns; absent = the
687
797
  // transport rejects prompts (log-only session).
688
798
  ...(Array.isArray(followUpArgv) ? { followUpArgv } : {}),
@@ -706,6 +816,7 @@ export function launchBackgroundAgent({
706
816
  endedAt: null,
707
817
  exitCode: null,
708
818
  logFile: job.logFile,
819
+ ...(worktreeState || {}),
709
820
  };
710
821
  writeBackgroundAgentState(state);
711
822
  let child;
@@ -866,6 +977,84 @@ export function stopBackgroundAgent(id) {
866
977
  return { ...next, stopped: true };
867
978
  }
868
979
 
980
+ function activeWorktreeProcesses(state) {
981
+ const active = [];
982
+ if (
983
+ Number.isInteger(Number(state?.pid)) &&
984
+ Number(state.pid) > 0 &&
985
+ isSameProcess(state.pid, state.startedAt)
986
+ ) {
987
+ active.push("worker");
988
+ }
989
+ if (
990
+ Number.isInteger(Number(state?.agentPid)) &&
991
+ Number(state.agentPid) > 0 &&
992
+ isSameProcess(state.agentPid, state.agentStartedAt)
993
+ ) {
994
+ active.push("agent");
995
+ }
996
+ return active;
997
+ }
998
+
999
+ /**
1000
+ * Explicit background-record cleanup owns the worktree teardown. Normal
1001
+ * completion, kill, lost-worker reconciliation, attach and resume all retain
1002
+ * it so a later resume continues in the exact same isolated checkout.
1003
+ */
1004
+ export function cleanupBackgroundAgentWorktree(state, options = {}) {
1005
+ const worktree = persistedWorktree(state);
1006
+ if (!worktree) return null;
1007
+ if (options.keepWorktree === true) {
1008
+ return {
1009
+ removed: false,
1010
+ kept: true,
1011
+ reason: "kept by explicit request",
1012
+ path: worktree.worktreePath,
1013
+ };
1014
+ }
1015
+
1016
+ const blockers = [];
1017
+ const active = activeWorktreeProcesses(state);
1018
+ if (active.length) blockers.push(`active ${active.join("+")} process`);
1019
+ if (Number(state?.pendingApprovals || 0) > 0) {
1020
+ blockers.push("pending approval");
1021
+ }
1022
+ if (state?.pendingQuestion) blockers.push("pending user input");
1023
+ if (
1024
+ Number(state?.uncertainSideEffects || 0) > 0 ||
1025
+ state?.phase === "uncertain_side_effect" ||
1026
+ state?.phase === "uncertain-side-effect"
1027
+ ) {
1028
+ blockers.push("unfinished or uncertain side effect");
1029
+ }
1030
+ if (blockers.length) {
1031
+ throw new Error(
1032
+ `Refusing to clean background worktree "${worktree.worktreePath}": ${blockers.join(", ")}`,
1033
+ );
1034
+ }
1035
+
1036
+ if (!existsSync(worktree.worktreePath)) {
1037
+ return {
1038
+ removed: false,
1039
+ kept: false,
1040
+ reason: "worktree path already missing",
1041
+ path: worktree.worktreePath,
1042
+ };
1043
+ }
1044
+
1045
+ const result = finishAgentWorktree({
1046
+ ...worktree,
1047
+ path: worktree.worktreePath,
1048
+ });
1049
+ if (!result.removed) {
1050
+ throw new Error(
1051
+ `Refusing to remove background agent record while its worktree is kept (${result.reason}). ` +
1052
+ "Resolve or preserve the work first, or pass --keep-worktree to remove only the record.",
1053
+ );
1054
+ }
1055
+ return { ...result, path: worktree.worktreePath };
1056
+ }
1057
+
869
1058
  export function openBackgroundLogFile(id) {
870
1059
  const fd = openSync(logPath(id), "a", 0o600);
871
1060
  return { fd, close: () => closeSync(fd) };