chainlesschain 0.162.166 → 0.162.168

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 (185) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-LbDx7jL6.js → AIOps-CDzzuWZV.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-DnurtK34.js → ActionButton-Dy-8MmQi.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-DXSsV1am.js → Analytics-BtJ_VWkx.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-dqRpzNuD.js → AppLayout-DkjiI4VS.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-BwdmYepK.js → Artifacts-h8PyDO86.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-oSX0kOUG.js → Audit-DiDV0C6l.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-C2KxJahT.js → BackgroundAgents-DTw_aOaf.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-B4TkxWkC.js → Backup-CihFuUO9.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CX5-KxeU.js → BaseInput-CqP2QNoR.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-O_HzD-PL.js → Chat-tm-ygX-p.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-Br8saHQA.js → ChatBubbleRenderer-BVKEhj_y.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-ChepDpC1.js → Checkbox-BFMyRN3Q.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-D9z8kc7q.js → Codegen-B1gTeoAS.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-B-H5eoNw.js → Col-1GP8MvKL.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-Ccyp_Kk5.js → Community-BLY938F6.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-BNvBznp_.js → Compact-DVt_p36I.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-D1FjRd8n.js → Compliance-DxqEBXVG.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-CV9B9u-8.js → Cowork-CyWrM-T8.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-BowJ4VIx.js → Cron-Domu6aI9.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-BJc6jzRF.js → Crosschain-C8LKpjHJ.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BEp8NV6D.js → DID-XBy15RZ5.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Dls0B3YB.js → Dashboard-DDAyPD-V.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-DQ7zXVtD.js → Dropdown-BHV0MZ13.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-B9q79tOl.js → EmailListRenderer-DmFGW4PX.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CD8Ie-88.js → FamilyGuardDashboard-CqYdbKvd.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-5o7XU5Vf.js → Federation-YP5-Puv5.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-DeKbMvOs.js → FormItemContext-BOmcIcTe.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-CZiQvz_z.js → GenericCardRenderer-CvTZdoc4.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-CTvFxVV8.js → Git-Q2oxua0L.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-DwzFIjHt.js → Governance-D-WpyWEU.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CJBk59c-.js → Inference-NUtsYHQo.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-DfsaXN2z.js → KnowledgeGraph-BHQLa8fr.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-DVRYf18S.js → Logs-6WsR5Q1G.js} +2 -2
  35. package/src/assets/web-panel/assets/MarkdownRenderer-DuWoDwMx.js +1 -0
  36. package/src/assets/web-panel/assets/{Marketplace-BuIwyE4t.js → Marketplace-CigohRRW.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-D3U0o78E.js → McpTools-jcp6jVLf.js} +5 -5
  38. package/src/assets/web-panel/assets/{Memory-QYVmyb3b.js → Memory-B00NU1ph.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-BFgrj6eZ.js → MobileBridge-Cs_p3PzU.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-BFs2WljZ.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-BZQu8TcH.js → Mtc-CSEflwzw.js} +5 -5
  42. package/src/assets/web-panel/assets/{MtcAudit-BnFfEkLo.js → MtcAudit-dxSiEZ96.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-iUnNS9Cm.js → Multisig-CDAFEAyq.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-DNNz1y9Y.js → NLProgramming-HoFpZNiB.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-CZezNYRU.js → Notes-tAoQvVcE.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-DBvrplIR.js → NotificationSettings-Czo2M9F4.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-CTDtee49.js → OrderTableRenderer-CM1kNTp6.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-CCOUJX1R.js → Organization-Cudr0VOC.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-DcZghdk0.js → Overflow-BSXeDKiN.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-jmddWX9q.js → P2P-Cqv_g-F9.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-wfjMcczl.js → PdhVaultBrowser-7rjprPh8.js} +4 -4
  52. package/src/assets/web-panel/assets/{Permissions-DU1GJjca.js → Permissions-97pIDQKE.js} +3 -3
  53. package/src/assets/web-panel/assets/{PersonalDataHub-BzTMzDF9.js → PersonalDataHub-bparK9eQ.js} +3 -3
  54. package/src/assets/web-panel/assets/{Pipeline-DPmjlpVc.js → Pipeline-CJgerM09.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-B6HRLpfK.js → Privacy-JqFS3PN7.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-Cyy86AsN.js → ProjectInit-DMITd-2c.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-Bl_WQxJ5.js → ProjectSettings-1UkdJ8Ii.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-Co9yKThA.js → Projects-CYZqegqw.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-DLa4NlIs.js → Providers-ChfXz3U0.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-BvzM7q5f.js → QrScannerModal-Rzkka-mf.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-BN8yyyUj.js → QuickAsk-DEz9HaON.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-QsVN4w0i.js → Recommend-EFGP8AVu.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-tkDXw-TY.js → RemoteSession-6um0lGGy.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-DAifkGsy.js → Reputation-CA1BXTup.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-D7dZEyak.js → Row-stTnDJ9y.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-DB5sImTA.js → RssFeed-B10qCCoG.js} +2 -2
  67. package/src/assets/web-panel/assets/{Search-3py3TO9V.js → Search-DSJroZ6p.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-DhNUfT_k.js → Security-BpXkwiMW.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-DjJAA_5D.js → Services-BPQxjwK6.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-u95JjK3n.js → Skeleton-CfbKAMul.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-DPo3L0Tz.js → Skills-CttYTfX0.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-DWhkAZV1.js → Sla-ByluHich.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-PUegMtqV.js → SpeechSettings-BdZXKsfV.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-BMNYrnrU.js → SyncSettings-xCR7iDzy.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-CYm_oTJC.js → Tasks-qTko_INn.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-Dw9Er5DN.js → Templates-CkIzhgDJ.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-7P3QIBSo.js → Tenant-BIaCaaK8.js} +1 -1
  78. package/src/assets/web-panel/assets/Terminal-B2Qq38XP.js +3 -0
  79. package/src/assets/web-panel/assets/{TimelineRenderer-DbCd1bDH.js → TimelineRenderer-D5eQDnEF.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-EQofpA0t.js → Tokens-DmeJ8NMQ.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-ChNLLypd.js → Trigger-C5sgb3C7.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-dlVGUKs5.js → Trust-H2vOb3Rb.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-BXyBUhNF.js → UkeySign-NmPtHK2u.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-BE8-68ge.js → VideoEditing-DHv-Ti7K.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-7ZIJOwNg.js → Wallet-BYZ3-PRP.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-DgNd2Amr.js → WebAuthn-BaSlP7om.js} +4 -4
  87. package/src/assets/web-panel/assets/{WorkflowEditor-Z-Un-9l5.js → WorkflowEditor-CZ7IHuIi.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-CtVDEkUc.js → chat-Dd9kY8gn.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-PXtJ2f1B.js → colors-yhzo6QSV.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-oN96nuBi.js → compact-item-C1JneVdM.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-okCJXQXa.js → createContext-C_waVCg2.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-DaDlyVDY.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-pcHm--4o.js → hasIn-CkVg0Xnh.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-BeAWCGEK.js → index-1VYhIr6D.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-DC0bqrqV.js → index-BTMiVIBI.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CQYes2mk.js → index-BYOyb5X2.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-vW0lwj0Y.js → index-BjDnI7oM.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BUUJ2WfG.js → index-BlJNvg-2.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-C0rfJBU3.js → index-BlzbSUK9.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Rk0X8lVz.js → index-BrN2hAeE.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BDR2mi1p.js → index-BsSsurcn.js} +1 -1
  102. package/src/assets/web-panel/assets/index-BtsLExcc.js +1 -0
  103. package/src/assets/web-panel/assets/{index-jAY55MSx.js → index-Bwuu3Bf3.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-_SaFHNsG.js → index-Bx3A6X4J.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-T3QE8LuV.js → index-C-Sscgnv.js} +1 -1
  106. package/src/assets/web-panel/assets/index-C3qDvvIb.js +1 -0
  107. package/src/assets/web-panel/assets/{index-BUTyx-Lk.js → index-C4HNdMCt.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-nMqLdwao.js → index-CBwmwTNa.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BuME9k4Y.js → index-CErewFeT.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BHJIfdCV.js → index-CGtoL5mR.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-D0bfypsF.js → index-CUDjGt_V.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-Bl6N7ZyX.js → index-C_qqYZT7.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CXhezK4d.js → index-C_riAa6d.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BnhHxZul.js → index-CfEMjbhu.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Cs1fGk1Z.js → index-ClQhh6v3.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BjVIZfdA.js → index-CmiLmG0i.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CWUJlCvK.js → index-D1xIbAxQ.js} +3 -3
  118. package/src/assets/web-panel/assets/{index-DfL10EcN.js → index-D6UXDH2D.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-C4ravjdv.js → index-D7kQ_4uL.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DtsyPnF5.js → index-DPkqQ2Pr.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CQ1ZzJjN.js → index-DUA96wEj.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-5aO2V0V4.js → index-DXHk9Vof.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CopGaCI_.js → index-DapftLUr.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-D1efSh1X.js → index-Dez7TUgG.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DBcKBb1E.js → index-DhZgULsY.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-Bht9n8Sm.js → index-Diquc7-l.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CAQktO-6.js → index-DuGtLiTW.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CK0H3pQW.js → index-U_nLXGIx.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BZKk4Lnm.js → index-_M8El5iz.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-Cc_MkksW.js → index-_VmzCYFn.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-mB1UV7wA.js → index-_rZxgFtc.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-BEtzqJw3.js → index-ewb0-LoH.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-CG1jyiI7.js → initDefaultProps-DNN6qtGa.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-C8BYa98D.js → motion-BcM9exeo.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-D1hqwitW.js → move-DsKJ-YvA.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-C2lol8rp.js → mtc-parser-glgGkxaK.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-Ct6Zghfi.js → omit-BwHCU50S.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-CSYbYfdc.js → pickAttrs-BVp-bLt7.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-WCk8di0z.js → placementArrow-CZNe0r-w.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-CEC8CZ-3.js → responsiveObserve-DUafj9pU.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-BsXyd7Lq.js → slide-H4NqXQxu.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-tDKldiUT.js → statusUtils-UjMzzGZ5.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-BIS2unhx.js → styleChecker-qO5aCjyk.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-yFTVMDUv.js → useFlexGapSupport-ChecYwC8.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-CZYbJwkO.js → useFs-BR0QvwO3.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-sbEHRLAM.js → usePersonalDataHub-DitK2Qgz.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-BVjeSEQK.js → vnode-dxSb40dY.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-BZKVvEZ6.js → zoom-ekJk8YB2.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agent.js +75 -0
  151. package/src/commands/session.js +132 -0
  152. package/src/data/changelog.json +8 -2
  153. package/src/harness/jsonl-session-store.js +9 -0
  154. package/src/harness/session-mirror.js +212 -6
  155. package/src/harness/worktree-isolator.js +10 -2
  156. package/src/lib/agent-core.js +1 -0
  157. package/src/lib/async-hook-queue.cjs +288 -0
  158. package/src/lib/async-hook-supervisor.cjs +52 -0
  159. package/src/lib/background-phase-reporter.js +116 -0
  160. package/src/lib/doctor-checkup.js +184 -4
  161. package/src/lib/hook-runner.cjs +248 -36
  162. package/src/lib/install-command-policy.js +343 -0
  163. package/src/lib/jsonl-session-store.js +1 -0
  164. package/src/lib/lsp/__tests__/lsp-server-registry.test.js +27 -0
  165. package/src/lib/lsp/code-intelligence.js +5 -1
  166. package/src/lib/lsp/lsp-server-registry.js +14 -0
  167. package/src/lib/lsp/workspace-roots.js +115 -0
  168. package/src/lib/runtime-checkup.js +33 -0
  169. package/src/lib/settings-hook-events.cjs +173 -3
  170. package/src/lib/settings-hooks.cjs +4 -0
  171. package/src/lib/shell-approval.js +47 -1
  172. package/src/lib/sub-agent-context.js +13 -0
  173. package/src/repl/add-dir.js +59 -1
  174. package/src/repl/agent-repl.js +85 -3
  175. package/src/runtime/agent-core.js +411 -82
  176. package/src/runtime/headless-runner.js +111 -9
  177. package/src/runtime/headless-stream.js +39 -0
  178. package/src/runtime/system-prompt.js +39 -12
  179. package/src/workers/background-agent-worker.js +8 -0
  180. package/src/assets/web-panel/assets/MarkdownRenderer-BL_WsWZM.js +0 -1
  181. package/src/assets/web-panel/assets/MobileProjects-Bp5QB-46.js +0 -1
  182. package/src/assets/web-panel/assets/Terminal-ClPrfWW7.js +0 -3
  183. package/src/assets/web-panel/assets/devWarning-BzV6dk1B.js +0 -1
  184. package/src/assets/web-panel/assets/index-BFrAwXHp.js +0 -1
  185. package/src/assets/web-panel/assets/index-DJQFP47u.js +0 -1
@@ -506,6 +506,11 @@ export function registerAgentCommand(program) {
506
506
  let _worktree = null;
507
507
  let _worktreeFinished = false;
508
508
  let _finishAgentWorktreeFn = null;
509
+ // Settings hooks loaded once at worktree setup, reused by both the
510
+ // WorktreeCreate producer and the WorktreeRemove producer in
511
+ // _finishWorktree(). Null unless --worktree is active + hooks loaded.
512
+ let _worktreeSettingsHooks = null;
513
+ let _worktreeAsyncSupervisor = null;
509
514
  if (options.worktree) {
510
515
  try {
511
516
  const { setupAgentWorktree, finishAgentWorktree } =
@@ -516,6 +521,43 @@ export function registerAgentCommand(program) {
516
521
  process.stderr.write(
517
522
  `worktree: ${_worktree.path} (branch ${_worktree.branch})\n`,
518
523
  );
524
+ // WorktreeCreate lifecycle hook (observe-only, best-effort): a fresh
525
+ // isolated worktree just appeared — let automation react (register it
526
+ // with a tracker, seed per-branch tooling). Loaded from the REPO ROOT
527
+ // (its committed/user-level .claude hooks apply). No-op & byte-unchanged
528
+ // without a registered WorktreeCreate hook; never blocks the session.
529
+ try {
530
+ const { loadHooks } = await import("../lib/settings-hooks.cjs");
531
+ _worktreeSettingsHooks =
532
+ loadHooks({ cwd: _worktree.repoRoot }).hooks || null;
533
+ const { runWorktreeCreateHooks, dispatchAsyncHooks } =
534
+ await import("../lib/settings-hook-events.cjs");
535
+ runWorktreeCreateHooks(_worktreeSettingsHooks, {
536
+ worktreePath: _worktree.path,
537
+ branch: _worktree.branch,
538
+ baseSha: _worktree.baseSha,
539
+ cwd: _worktree.path,
540
+ });
541
+ if (!_worktreeAsyncSupervisor) {
542
+ const { AsyncHookSupervisor } =
543
+ await import("../lib/async-hook-supervisor.cjs");
544
+ _worktreeAsyncSupervisor = new AsyncHookSupervisor({
545
+ persistStats: true,
546
+ });
547
+ }
548
+ dispatchAsyncHooks(
549
+ _worktreeSettingsHooks,
550
+ "WorktreeCreate",
551
+ {
552
+ worktree_path: _worktree.path,
553
+ branch: _worktree.branch,
554
+ base_sha: _worktree.baseSha,
555
+ },
556
+ { cwd: _worktree.path, supervisor: _worktreeAsyncSupervisor },
557
+ );
558
+ } catch {
559
+ /* worktree lifecycle hooks are best-effort — never block */
560
+ }
519
561
  // The worktree is created BEFORE flag validation, and several
520
562
  // `process.exit(1)` guards below (plus any future ones) would skip the
521
563
  // normal cleanup and orphan an empty worktree + branch on disk. A sync
@@ -548,6 +590,39 @@ export function registerAgentCommand(program) {
548
590
  ? `worktree removed (${fin.reason}).\n`
549
591
  : `worktree kept (${fin.reason}): ${_worktree.path}\n${fin.mergeHint ? ` merge: ${fin.mergeHint}\n` : ""}`,
550
592
  );
593
+ // WorktreeRemove lifecycle hook (observe-only, best-effort): teardown
594
+ // just resolved — fire with `removed` distinguishing an auto-removed
595
+ // empty worktree from one kept for its changes. Reuses the hooks loaded
596
+ // at setup; no-op & byte-unchanged without a registered hook.
597
+ if (_worktreeSettingsHooks) {
598
+ try {
599
+ const { runWorktreeRemoveHooks, dispatchAsyncHooks } =
600
+ await import("../lib/settings-hook-events.cjs");
601
+ runWorktreeRemoveHooks(_worktreeSettingsHooks, {
602
+ worktreePath: _worktree.path,
603
+ branch: _worktree.branch,
604
+ removed: fin.removed === true,
605
+ reason: fin.reason,
606
+ cwd: _worktree.repoRoot,
607
+ });
608
+ dispatchAsyncHooks(
609
+ _worktreeSettingsHooks,
610
+ "WorktreeRemove",
611
+ {
612
+ worktree_path: _worktree.path,
613
+ branch: _worktree.branch,
614
+ removed: fin.removed === true,
615
+ reason: fin.reason,
616
+ },
617
+ {
618
+ cwd: _worktree.repoRoot,
619
+ supervisor: _worktreeAsyncSupervisor,
620
+ },
621
+ );
622
+ } catch {
623
+ /* WorktreeRemove firing is best-effort */
624
+ }
625
+ }
551
626
  } catch {
552
627
  /* keep silently — never block exit */
553
628
  }
@@ -967,6 +967,18 @@ export function registerSessionCommand(program) {
967
967
  .option("--rebuild", "Force a full re-index (ignore mtime cache)")
968
968
  .option("--stats", "Show index-wide counters")
969
969
  .option("--mirror <ids...>", "Push session id(s) to the configured mirror")
970
+ .option(
971
+ "--mirror-delete <ids...>",
972
+ "Delete session id(s) from the configured mirror",
973
+ )
974
+ .option(
975
+ "--mirror-prune",
976
+ "Delete mirror entries whose session no longer exists locally (retention)",
977
+ )
978
+ .option(
979
+ "--mirror-rotate",
980
+ "Re-encrypt all mirrored sessions from session.mirror.encryptionPrevious to session.mirror.encryption (key rotation)",
981
+ )
970
982
  .option(
971
983
  "--limit <n>",
972
984
  "Max rows for search/list",
@@ -996,6 +1008,48 @@ export function registerSessionCommand(program) {
996
1008
  return;
997
1009
  }
998
1010
 
1011
+ if (options.mirrorDelete && options.mirrorDelete.length) {
1012
+ const result = await mirrorDeleteSessions(options.mirrorDelete);
1013
+ if (options.json) return console.log(JSON.stringify(result, null, 2));
1014
+ if (!result.target)
1015
+ return logger.log(chalk.yellow("No mirror configured."));
1016
+ for (const id of result.deleted)
1017
+ logger.log(chalk.green(` deleted ${id} from mirror`));
1018
+ for (const r of result.errors) logger.error(` ${r.id}: ${r.error}`);
1019
+ return;
1020
+ }
1021
+
1022
+ if (options.mirrorPrune) {
1023
+ const result = await mirrorPruneSessions();
1024
+ if (options.json) return console.log(JSON.stringify(result, null, 2));
1025
+ if (!result.target)
1026
+ return logger.log(chalk.yellow("No mirror configured."));
1027
+ logger.log(
1028
+ chalk.green(
1029
+ ` pruned ${result.deleted.length} orphaned mirror entr${
1030
+ result.deleted.length === 1 ? "y" : "ies"
1031
+ } (${result.kept.length} kept)`,
1032
+ ),
1033
+ );
1034
+ return;
1035
+ }
1036
+
1037
+ if (options.mirrorRotate) {
1038
+ const result = await mirrorRotateKey();
1039
+ if (options.json) return console.log(JSON.stringify(result, null, 2));
1040
+ if (!result.target)
1041
+ return logger.log(chalk.yellow("No mirror configured."));
1042
+ if (result.error) return logger.error(` ${result.error}`);
1043
+ logger.log(
1044
+ chalk.green(
1045
+ ` rotated ${result.rotated.length} session(s) to key "${result.keyId}"` +
1046
+ ` (${result.skipped.length} already current)`,
1047
+ ),
1048
+ );
1049
+ for (const r of result.failed) logger.error(` ${r.id}: ${r.error}`);
1050
+ return;
1051
+ }
1052
+
999
1053
  if (options.stats) {
1000
1054
  const st = idx.indexStats(db);
1001
1055
  if (options.json) return console.log(JSON.stringify(st, null, 2));
@@ -1781,6 +1835,84 @@ async function mirrorSessions(ids) {
1781
1835
  return { target: mirror.target, pushed, errors };
1782
1836
  }
1783
1837
 
1838
+ /** Load the configured mirror (or null). Shared by every mirror subcommand. */
1839
+ async function loadConfiguredMirror() {
1840
+ const { createMirror } = await import("../harness/session-mirror.js");
1841
+ let cfg = {};
1842
+ try {
1843
+ cfg =
1844
+ (await import("../lib/config-manager.js")).loadConfig()?.session
1845
+ ?.mirror || {};
1846
+ } catch {
1847
+ cfg = {};
1848
+ }
1849
+ return { mirror: createMirror(cfg), cfg };
1850
+ }
1851
+
1852
+ /** Delete session id(s) from the configured off-box mirror. */
1853
+ async function mirrorDeleteSessions(ids) {
1854
+ const store = await import("../harness/jsonl-session-store.js");
1855
+ const { mirror } = await loadConfiguredMirror();
1856
+ if (!mirror) return { target: null, deleted: [], errors: [] };
1857
+ const deleted = [];
1858
+ const errors = [];
1859
+ for (const id of ids) {
1860
+ try {
1861
+ if (store.isUnsafeSessionId(id)) throw new Error("unsafe session id");
1862
+ await mirror.delete(id);
1863
+ deleted.push(id);
1864
+ } catch (err) {
1865
+ errors.push({ id, error: err.message });
1866
+ }
1867
+ }
1868
+ return { target: mirror.target, deleted, errors };
1869
+ }
1870
+
1871
+ /**
1872
+ * Retention prune: delete mirror entries whose session no longer exists in the
1873
+ * local store (the mirror is one-way derived, so local is the source of truth).
1874
+ */
1875
+ async function mirrorPruneSessions() {
1876
+ const store = await import("../harness/jsonl-session-store.js");
1877
+ const { pruneMirror } = await import("../harness/session-mirror.js");
1878
+ const { mirror } = await loadConfiguredMirror();
1879
+ if (!mirror) return { target: null, deleted: [], kept: [] };
1880
+ const keep = store.listSessionIds();
1881
+ const res = await pruneMirror(mirror, { keep });
1882
+ return { target: mirror.target, ...res };
1883
+ }
1884
+
1885
+ /**
1886
+ * Key rotation: re-encrypt every mirrored session from the PREVIOUS key
1887
+ * (`session.mirror.encryptionPrevious`) to the CURRENT key
1888
+ * (`session.mirror.encryption`). Requires both to be configured.
1889
+ */
1890
+ async function mirrorRotateKey() {
1891
+ const { createMirror, createMirrorCipher, rotateMirrorKey } =
1892
+ await import("../harness/session-mirror.js");
1893
+ const { cfg } = await loadConfiguredMirror();
1894
+ if (!cfg || !cfg.kind || cfg.kind === "none") {
1895
+ return { target: null };
1896
+ }
1897
+ const prev = cfg.encryptionPrevious || cfg.encryptionOld;
1898
+ const next = cfg.encryption;
1899
+ if (!prev?.passphrase || !next?.passphrase) {
1900
+ return {
1901
+ target: cfg.dir || cfg.baseUrl || cfg.url || null,
1902
+ error:
1903
+ "rotation needs both session.mirror.encryptionPrevious and session.mirror.encryption passphrases",
1904
+ };
1905
+ }
1906
+ // Drive the RAW driver (createMirror without encryption) so rotation manages
1907
+ // the ciphertext itself with the two explicit ciphers.
1908
+ const raw = createMirror({ ...cfg, encryption: undefined });
1909
+ const res = await rotateMirrorKey(raw, {
1910
+ from: createMirrorCipher(prev),
1911
+ to: createMirrorCipher(next),
1912
+ });
1913
+ return { target: raw.target, ...res };
1914
+ }
1915
+
1784
1916
  // === Iter21 V2 governance overlay ===
1785
1917
  export function registerSesgovV2Commands(program) {
1786
1918
  const parent = program.commands.find((c) => c.name() === "session");
@@ -3,7 +3,7 @@
3
3
  "releases": [
4
4
  {
5
5
  "productVersion": "Unreleased",
6
- "cliVersion": "0.162.166",
6
+ "cliVersion": "0.162.168",
7
7
  "cliVersions": [
8
8
  "0.4.46",
9
9
  "0.4.51",
@@ -24,11 +24,17 @@
24
24
  "0.162.163",
25
25
  "0.162.164",
26
26
  "0.162.165",
27
- "0.162.166"
27
+ "0.162.166",
28
+ "0.162.167",
29
+ "0.162.168"
28
30
  ],
29
31
  "date": null,
30
32
  "title": "",
31
33
  "sections": [
34
+ {
35
+ "heading": "Added — cc CLI 0.162.168:增量 gap-analysis 第二批接线——后台 Agent「等待审批」真实状态 + hook 事件溯源(trace/parent)+ LSP 多根与重启退避默认开 + 严格 hook 合并真并行 + 大仓 subagent worktree/子树指令 + run_code install 审计(CLI-only npm 发版)",
36
+ "body": "> `chainlesschain` 0.162.167 → **0.162.168** 发 npm `latest`(经 `npm-publish.yml`,`--provenance --access public`)。纯 `packages/cli/src` 增量(10 个 feat commit + 测试 + gap-analysis 文档状态);未触 `pdh/lib` → 无 Android cc bundle rollover / 无 USR_VERSION 改动。**无新增顶层命令,命令数 175 不变**。绝大多数改动 opt-in / 默认路径字节不变;**两处默认行为变化**(均可 env 关):LSP 重启退避默认开(`CC_LSP_RESTART_BACKOFF_MS=0` 恢复旧行为)与子树指令懒注入默认开(`CC_SUBTREE_INSTRUCTIONS=0` 关)。\n\n- **P0 后台 Agent:`waiting_permission` 真实生产者**:后台 agent 卡在人工审批(`--permission-prompt-tool` / `--remote-control`)时不再永远显示 \"Working\"——agent 子进程(经 worker 注入的 `CC_BACKGROUND_AGENT_ID`)在审批 pending 期间把 `phase:\"waiting_permission\"` + 实时 `pendingApprovals` 计数写进共享 state,settle 即恢复;Dashboard \"Needs input\" 分组从此有真数据。前台运行返回同一 confirmer 函数对象、零 IO 字节不变;worker 在每回合开始与 idle 重置计数,被 SIGKILL 于审批中途的子进程不会把会话永久钉在 \"Needs input\"。\n- **P0 沙箱:统一 install 审计扩到 `run_code`**:分类器新识别模块调用 `python`/`python3`/`py` `-m pip install …`(含 `sudo` 包裹;`-m http.server`/`-m venv`/`setup.py install` 不误报);`run_code` 的 pip 自动安装 installed/failed 尝试在同一 opt-in 策略(`CC_INSTALL_AUDIT` / settings `installPolicy`)下落统一 `install-commands.jsonl`(`source:\"run_code_auto_install\"` + pip 分类 + global 判定)——一份审计覆盖所有「下载并运行第三方代码」入口。\n- **P2 Hooks:strictest-merge 决策合并(opt-in `CC_HOOK_STRICT_MERGE=1`)+ PreToolUse 真并行**:修掉真实安全缺口——原按序短路下**较早**的一个 `ask` 会掩盖**较晚**一个 hook 的 `block`(blocker 根本没跑)。开启后跑完全部匹配 hook 取最严(block>ask>allow>continue);PreToolUse 路径经异步 spawn **真并行**(wall-clock=最慢单 hook 而非求和,硬超时 SIGKILL 挂死 hook)。默认关=逐字节原短路行为。\n- **P2 Hooks:trace_id/parent_id 事件溯源**:PreToolUse / PostToolUse / PermissionRequest / SubagentStop 四个 payload 构造点现带 `event_id` + `trace_id`(本 run)+ `parent_id`(spawn 方 run);`spawn_sub_agent` 与隔离 `run_skill` 把父 trace 线程进子 loop,子 agent 的 hook 事件跨深度关联回父 trace。未线程时字段整个省略——legacy payload 字节不变;`CC_HOOK_EVENT_LOG` 开启时这些决策事件顺带可被 `cc hook replay` 寻址。\n- **P2 LSP:重启退避默认开 + 多根 workspace**:池化语言服务器重启退避默认 1s→2s→4s→8s 封顶(启动即崩的 server 不再毫秒级连爆 `maxRestarts` 次;`CC_LSP_RESTART_BACKOFF_MS` 覆盖、`=0` 恢复立即重生)。`code_intelligence` 现按**包含该文件的根**(containment 最深根胜)key 共享 server 池——`--add-dir` 根内的文件拿到它自己项目的语言服务器而非 cwd 的;`workspace_symbols` 跨全部根扇出合并、逐 symbol 标注 `root`。单根会话字节不变。\n- **P1 大仓:子树指令懒注入(tool-time)**:工具**首次访问**某子树(read_file / list_dir / write_file / edit_file / edit_file_hashed)时把该子树自带的 `cc.md`/`CLAUDE.md`/`AGENTS.md` 附到工具结果的 `subtreeInstructions` 字段(每子树每进程恰一次,honor `instructionExcludes`,fail-open)——monorepo 里 per-package 规则不再要求启动时全量注入。子树无指令文件(常态)零成本;`CC_SUBTREE_INSTRUCTIONS=0` 整体关闭。同批:post-edit 诊断从未排序裸 cap 20 改为 **severity 优先 + token 预算**封顶(error 先于 warning,`CC_EDIT_DIAGNOSTICS_TOKENS` 默认 2000)。\n- **P1 subagent/大仓三项收尾**:①subagent worktree spawn 透传 `sparsePaths`/`symlinkDirectories`(spawn-arg 胜 agent-file;隔离 worktree 只 checkout 任务需要的包 + junction 复用依赖目录);②agent-file frontmatter `background: true` 现真生效(此前只认调用方显式传参);③headless `--add-dir` 启动根 seed——`cc agent -p --add-dir` 现把完整根列表告知已连 MCP 服务器(`roots/list_changed`),此前它们的 `roots/list` 永远只见 cwd。\n\n> `chainlesschain` 0.162.166 → **0.162.167** 发 npm `latest`(经 `npm-publish.yml`,`--provenance --access public`)。纯 `packages/cli/src` 增量(+ 测试 + gap-analysis 文档状态更新);未触 `pdh/lib` → 无 Android cc bundle rollover / 无 USR_VERSION 改动。**无新增顶层命令,命令数 175 不变**;所有改动 opt-in / 默认路径字节不变。本版把 `docs/CLAUDE_CODE_CLI_INCREMENTAL_GAP_ANALYSIS_2026-07-12.md` 的多个可 Windows 落地「仍欠」项整批接线。\n\n- **P0 沙箱:run_shell 远程脚本执行检测**:新增统一「代码获取」分类器——把 20+ 包管理器安装命令(npm/pip/cargo/gem/go/apt/…)与 `curl … | sh` / `wget -O- | bash` 一类**下载即执行**的远程脚本归为一类,在 shell 审批时抬高风险地板并落审计(记录带 `remoteExec` 标记);`cat script.sh | sh`(本地文件)与 `curl -O file`(仅下载不执行)不误报。默认路径字节不变。\n- **P2 生命周期事件钩子——4 个命令层生产者**:为新的 settings.json 生命周期事件类型接上真实触发点(此前它们只在事件总线注册、无生产者、loader 不认、`cc doctor` 还会当 unknown-event 告警用户配的钩子):`CwdChanged`(REPL `/cd` 改工作目录后)、`WorktreeCreate`/`WorktreeRemove`(`cc agent --worktree` 会话 create/finish 一对,payload 含 branch/base_sha,remove 带 `removed`/`reason` 区分自动删与保留)、`InstructionsLoaded`(会话启动组装完项目指令块后,携本会话权威指令文件清单 `{path,scope,truncated}`,**绝不含文件内容**)。全部 observe-only、无注册 hook 时字节不变;`InstructionsLoaded` 经 `composeSystemPrompt` 可选回调拿**真正注入**的那份文件集(零漂移,非另行 re-discover)。三处会话入口(headless / stream / REPL)与 SessionStart 同点触发。\n- **P2 Doctor:孤儿 agent 子进程检测**:`cc doctor` 现从持久化的后台 agent 记录探活,标出「worker 已死但子进程仍在」或「会话已消失」的泄漏进程为 error 并给对应 `taskkill`/`kill` 修复命令(进程起始时间锚定,防 PID 复用误判;仅喂持久记录不做启发式全机扫描 → 零误报)。\n- **4 项杂项 gap 接线**:①async-hook 持久重唤队列 + 崩溃恢复(失败的 async-hook 重唤按会话落盘,`--resume` 时作「Recovery notice」系统消息恢复);②monorepo 附加根变更时向连接的 MCP 服务器广播 roots/list_changed(`/add-dir` + 启动 `--add-dir`);③`cc doctor` LSP 就绪安装缺口检查(源文件在但对应语言服务器未装 → 代码智能静默降级为文本搜索时告警);④Session Mirror at-rest 加密/删除/保留/密钥轮换(scrypt+AES-256-GCM,`cc session index --mirror-delete` / `--mirror-prune` / `--mirror-rotate`)。"
37
+ },
32
38
  {
33
39
  "heading": "Added — cc CLI 0.162.166:P1-9「Capability Manifest 与脱敏诊断包」收官——协议文档 CI byte-diff / 离线协议回放 / 治理覆盖率指标(CLI-only npm 发版)",
34
40
  "body": "> `chainlesschain` 0.162.165 → **0.162.166** 发 npm `latest`(经 `npm-publish.yml`,`--provenance --access public`)。纯 `packages/cli/src` 增量(新增 3 个 `src/lib` 纯逻辑模块 + 3 个 `scripts/` 离线工具 + 测试);未触 `pdh/lib` → 无 Android cc bundle rollover / 无 USR_VERSION 改动。**只加 npm 脚本 + 离线工具,无新增顶层命令,命令数 175 不变**;默认路径字节不变。本版把 `docs/CLAUDE_CODE_IDE_INCREMENTAL_GAP_ANALYSIS_2026-07-13.md` 的 P1-9 三个剩项全部收口。\n\n- **协议能力文档 CI byte-diff 单源生成**:新增生成器 `scripts/gen-protocol-doc.mjs`(`--out`/`--check`)从唯一源 `capability-manifest.js` `renderProtocolDoc()` 投影出 `docs/cli/PROTOCOL_CAPABILITY_MANIFEST.generated.md`(协议版本 + 可协商 wire 特性表 + 权限模式);npm 脚本 `docs:protocol` / `docs:protocol:check`;`capability-manifest.test.js` 新增 byte-diff 断言(读签入副本 = `renderProtocolDoc()`),manifest 加 wire 字段但未重生成即测试红——drift 不可能 merge 绿。\n- **离线协议回放 + 兼容审计**:新增 `protocol-replay.js`——拿一段录制的 stream-json 会话 + 协商上下文(server×client offer),**无需活体对端**离线①回放到该协商下的线上形态(未通过协商的字段按 field-gate 剥离)②审计录制是否遵守协商(任何帧携带已 gate-OFF 的可协商 wire 字段 = 前向兼容违规,fail-closed)。可 gate 字段集从唯一源 `toFieldGate()` 投影不会漂移;`sessionDigest` 键序无关可 pin golden 录制。离线工具 `scripts/replay-protocol.mjs`(`--replay`/`--audit`,违规 exit 1)+ golden fixture。\n- **治理覆盖率指标(§11.3 验收)**:新增 `governance-coverage.js`——①高风险 Tool Call Ledger/Trace 覆盖率(每个不可逆副作用调用须既有 side-effect-ledger 记录又有 trace span,「高风险」复用唯一源 `classifyToolSideEffect`)②Plugin/MCP/Skill/Hook 溯源可追溯率(外部来源调用须带 source+version+scope+权限决策);`ok` 仅双 100% 才真(fail-closed),给逐条未覆盖项。离线工具 `scripts/coverage-report.mjs`(读 run-summary JSON,任一 gap exit 1,可对录制 run 工件 CI 强制 100%)。"
@@ -118,6 +118,15 @@ export function verifyAllSessions(options = {}) {
118
118
  .map((file) => verifySession(basename(file, ".jsonl")));
119
119
  }
120
120
 
121
+ /** All locally-stored session ids (the source of truth a mirror derives from). */
122
+ export function listSessionIds() {
123
+ const dir = getSessionsDir();
124
+ if (!existsSync(dir)) return [];
125
+ return readdirSync(dir)
126
+ .filter((file) => file.endsWith(".jsonl"))
127
+ .map((file) => basename(file, ".jsonl"));
128
+ }
129
+
121
130
  export { TRANSCRIPT_CHAIN_STATUS };
122
131
 
123
132
  export function startSession(sessionId, meta = {}) {
@@ -26,10 +26,20 @@ import {
26
26
  readFileSync,
27
27
  writeFileSync,
28
28
  readdirSync,
29
+ unlinkSync,
29
30
  } from "node:fs";
31
+ import {
32
+ scryptSync,
33
+ randomBytes,
34
+ createCipheriv,
35
+ createDecipheriv,
36
+ } from "node:crypto";
30
37
  import { join, basename } from "node:path";
31
38
  import { isUnsafeSessionId } from "./jsonl-session-store.js";
32
39
 
40
+ const CIPHER_VERSION = 1;
41
+ const CIPHER_ALGO = "aes-256-gcm";
42
+
33
43
  /**
34
44
  * Build a mirror driver from config. `config.kind` selects the driver:
35
45
  * { kind: "fs", dir: "/mnt/backup/sessions" }
@@ -39,15 +49,193 @@ import { isUnsafeSessionId } from "./jsonl-session-store.js";
39
49
  export function createMirror(config = {}, deps = {}) {
40
50
  const kind = String(config.kind || config.type || "none").toLowerCase();
41
51
  if (!kind || kind === "none") return null;
52
+ let driver;
42
53
  if (kind === "fs" || kind === "dir" || kind === "directory") {
43
- return createFsMirror(config, deps);
54
+ driver = createFsMirror(config, deps);
55
+ } else if (kind === "http" || kind === "api") {
56
+ driver = createHttpMirror(config, deps);
57
+ } else {
58
+ throw new Error(
59
+ `unknown session mirror kind "${kind}" (use fs | http, or wire an s3/webdav/redis driver)`,
60
+ );
44
61
  }
45
- if (kind === "http" || kind === "api") {
46
- return createHttpMirror(config, deps);
62
+ // When `encryption` (a passphrase) is configured, wrap the driver so bytes are
63
+ // AES-256-GCM encrypted at rest — the mirror target only ever holds ciphertext,
64
+ // and only the run-time passphrase can decrypt it. Absent → raw bytes (the
65
+ // prior behaviour, byte-for-byte).
66
+ const enc = config.encryption || config.encrypt;
67
+ if (enc && (enc.passphrase || enc.secret)) {
68
+ return wrapEncryptedMirror(driver, createMirrorCipher(enc, deps));
47
69
  }
48
- throw new Error(
49
- `unknown session mirror kind "${kind}" (use fs | http, or wire an s3/webdav/redis driver)`,
50
- );
70
+ return driver;
71
+ }
72
+
73
+ /**
74
+ * Passphrase-based AES-256-GCM cipher for mirrored session bytes. scrypt derives
75
+ * a 256-bit key from the passphrase + a per-record random salt; a random 12-byte
76
+ * nonce and the GCM tag give confidentiality + integrity (a tampered ciphertext
77
+ * fails to decrypt). Each envelope is self-describing JSON — `{ v, alg, keyId,
78
+ * salt, nonce, tag, ct }`, all base64url — carrying `keyId` so key ROTATION can
79
+ * tell old ciphertext from new. No secret is ever stored; only the passphrase
80
+ * supplied at run time can read it. `randomBytes`/`scryptSync` are injectable so
81
+ * round-trips are unit-testable deterministically.
82
+ */
83
+ export function createMirrorCipher(config = {}, deps = {}) {
84
+ const passphrase = config.passphrase || config.secret;
85
+ if (!passphrase) throw new Error("mirror encryption requires a `passphrase`");
86
+ const keyId = String(config.keyId || "default");
87
+ const _randomBytes = deps.randomBytes || randomBytes;
88
+ const _scrypt = deps.scryptSync || scryptSync;
89
+ const deriveKey = (salt) => _scrypt(String(passphrase), salt, 32);
90
+
91
+ return {
92
+ keyId,
93
+ encrypt(bytes) {
94
+ const salt = Buffer.from(_randomBytes(16));
95
+ const nonce = Buffer.from(_randomBytes(12));
96
+ const cipher = createCipheriv(CIPHER_ALGO, deriveKey(salt), nonce);
97
+ const ct = Buffer.concat([
98
+ cipher.update(Buffer.from(bytes, "utf-8")),
99
+ cipher.final(),
100
+ ]);
101
+ return JSON.stringify({
102
+ v: CIPHER_VERSION,
103
+ alg: CIPHER_ALGO,
104
+ keyId,
105
+ salt: salt.toString("base64url"),
106
+ nonce: nonce.toString("base64url"),
107
+ tag: cipher.getAuthTag().toString("base64url"),
108
+ ct: ct.toString("base64url"),
109
+ });
110
+ },
111
+ decrypt(envelope) {
112
+ let e;
113
+ try {
114
+ e = typeof envelope === "string" ? JSON.parse(envelope) : envelope;
115
+ } catch {
116
+ throw new Error("mirror ciphertext is not a valid envelope");
117
+ }
118
+ if (!e || e.v !== CIPHER_VERSION || e.alg !== CIPHER_ALGO) {
119
+ throw new Error("unsupported mirror ciphertext envelope");
120
+ }
121
+ const key = deriveKey(Buffer.from(e.salt, "base64url"));
122
+ const decipher = createDecipheriv(
123
+ CIPHER_ALGO,
124
+ key,
125
+ Buffer.from(e.nonce, "base64url"),
126
+ );
127
+ decipher.setAuthTag(Buffer.from(e.tag, "base64url"));
128
+ const pt = Buffer.concat([
129
+ decipher.update(Buffer.from(e.ct, "base64url")),
130
+ decipher.final(),
131
+ ]);
132
+ return pt.toString("utf-8");
133
+ },
134
+ /** Read an envelope's keyId without decrypting (rotation bookkeeping). */
135
+ envelopeKeyId(envelope) {
136
+ try {
137
+ const e =
138
+ typeof envelope === "string" ? JSON.parse(envelope) : envelope;
139
+ return e && e.keyId != null ? String(e.keyId) : null;
140
+ } catch {
141
+ return null;
142
+ }
143
+ },
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Wrap a driver so push encrypts and pull decrypts transparently, keeping the
149
+ * raw driver reachable as `.raw` (key rotation drives the raw driver with two
150
+ * ciphers). list/delete pass through — ids stay plaintext in the target (they
151
+ * are random handles, not content).
152
+ */
153
+ function wrapEncryptedMirror(driver, cipher) {
154
+ return {
155
+ ...driver,
156
+ encrypted: true,
157
+ keyId: cipher.keyId,
158
+ raw: driver,
159
+ cipher,
160
+ async push(id, bytes) {
161
+ const enc = cipher.encrypt(bytes);
162
+ const r = await driver.push(id, enc);
163
+ return {
164
+ ...r,
165
+ bytes: Buffer.byteLength(bytes),
166
+ storedBytes: Buffer.byteLength(enc),
167
+ encrypted: true,
168
+ };
169
+ },
170
+ async pull(id) {
171
+ const raw = await driver.pull(id);
172
+ return raw == null ? null : cipher.decrypt(raw);
173
+ },
174
+ };
175
+ }
176
+
177
+ /**
178
+ * RETENTION: delete every mirror entry whose id is NOT in `keep` — the mirror is
179
+ * one-way derived from the local store, so retention = "hold only what the local
180
+ * source still keeps" (e.g. the ids `cc session list` still returns). Best-effort
181
+ * per id; returns what was deleted vs kept.
182
+ */
183
+ export async function pruneMirror(mirror, { keep = [] } = {}) {
184
+ if (!mirror || typeof mirror.list !== "function") {
185
+ return { deleted: [], kept: [] };
186
+ }
187
+ const keepSet = new Set((keep || []).map(String));
188
+ const ids = (await mirror.list()) || [];
189
+ const deleted = [];
190
+ for (const id of ids) {
191
+ if (keepSet.has(String(id))) continue;
192
+ try {
193
+ await mirror.delete(id);
194
+ deleted.push(id);
195
+ } catch {
196
+ /* best-effort — a failed delete must not abort the prune */
197
+ }
198
+ }
199
+ return { deleted, kept: ids.filter((i) => keepSet.has(String(i))) };
200
+ }
201
+
202
+ /**
203
+ * KEY ROTATION: re-encrypt every mirrored session from the `from` cipher to the
204
+ * `to` cipher, in place. Drives the RAW driver (pass `mirror.raw` for an
205
+ * encrypted mirror) so it reads/writes ciphertext directly. Entries already at
206
+ * `to.keyId` are skipped (idempotent re-runs); an entry that fails to decrypt
207
+ * with `from` is reported, not silently dropped.
208
+ */
209
+ export async function rotateMirrorKey(driver, { from, to } = {}) {
210
+ if (!driver || !from || !to) {
211
+ throw new Error("rotateMirrorKey requires (driver, { from, to })");
212
+ }
213
+ const ids = (await driver.list()) || [];
214
+ const rotated = [];
215
+ const skipped = [];
216
+ const failed = [];
217
+ for (const id of ids) {
218
+ let raw;
219
+ try {
220
+ raw = await driver.pull(id);
221
+ } catch (e) {
222
+ failed.push({ id, error: e.message });
223
+ continue;
224
+ }
225
+ if (raw == null) continue;
226
+ if (from.envelopeKeyId(raw) === to.keyId) {
227
+ skipped.push(id);
228
+ continue;
229
+ }
230
+ try {
231
+ const plain = from.decrypt(raw);
232
+ await driver.push(id, to.encrypt(plain));
233
+ rotated.push(id);
234
+ } catch (e) {
235
+ failed.push({ id, error: e.message });
236
+ }
237
+ }
238
+ return { rotated, skipped, failed, keyId: to.keyId };
51
239
  }
52
240
 
53
241
  /** Directory mirror — the reference driver. */
@@ -59,6 +247,7 @@ export function createFsMirror(config = {}, deps = {}) {
59
247
  const _readFileSync = deps.readFileSync || readFileSync;
60
248
  const _writeFileSync = deps.writeFileSync || writeFileSync;
61
249
  const _readdirSync = deps.readdirSync || readdirSync;
250
+ const _unlinkSync = deps.unlinkSync || unlinkSync;
62
251
 
63
252
  const ensure = () => {
64
253
  if (!_existsSync(dir)) _mkdirSync(dir, { recursive: true });
@@ -88,6 +277,12 @@ export function createFsMirror(config = {}, deps = {}) {
88
277
  .filter((f) => f.endsWith(".jsonl"))
89
278
  .map((f) => basename(f, ".jsonl"));
90
279
  },
280
+ async delete(id) {
281
+ const f = fileFor(id);
282
+ if (!_existsSync(f)) return { id, deleted: false };
283
+ _unlinkSync(f);
284
+ return { id, deleted: true };
285
+ },
91
286
  };
92
287
  }
93
288
 
@@ -138,5 +333,16 @@ export function createHttpMirror(config = {}, deps = {}) {
138
333
  .map((x) => (typeof x === "string" ? x : x?.id))
139
334
  .filter(Boolean);
140
335
  },
336
+ async delete(id) {
337
+ const res = await doFetch(`${baseUrl}/sessions/${guard(id)}`, {
338
+ method: "DELETE",
339
+ headers: headers(),
340
+ });
341
+ // 404 = already gone → idempotent success, not an error.
342
+ if (!res.ok && res.status !== 404) {
343
+ throw new Error(`mirror DELETE ${id} → HTTP ${res.status}`);
344
+ }
345
+ return { id, deleted: res.ok };
346
+ },
141
347
  };
142
348
  }
@@ -163,9 +163,17 @@ export function pruneWorktrees(repoDir) {
163
163
  return before - after;
164
164
  }
165
165
 
166
- export async function isolateTask(repoDir, taskId, fn) {
166
+ export async function isolateTask(repoDir, taskId, fn, options = {}) {
167
167
  const branchName = `agent/${taskId}`;
168
- const { path: worktreePath } = createWorktree(repoDir, branchName);
168
+ // options (large-monorepo): { sparsePaths, symlinkDirectories } forwarded to
169
+ // createWorktree so an isolated task only materializes the packages it needs
170
+ // and reuses approved dep dirs. Empty/absent → full checkout (byte-identical).
171
+ const { path: worktreePath } = createWorktree(
172
+ repoDir,
173
+ branchName,
174
+ undefined,
175
+ options || {},
176
+ );
169
177
 
170
178
  try {
171
179
  const result = await fn(worktreePath);
@@ -46,4 +46,5 @@ export {
46
46
  _resetFileFreshness,
47
47
  _getSharedCodeIntel,
48
48
  disposeSharedCodeIntel,
49
+ _resetSubtreeInstructionLoaders,
49
50
  } from "../runtime/agent-core.js";