chainlesschain 0.162.162 → 0.162.164

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 (237) hide show
  1. package/package.json +3 -1
  2. package/src/assets/web-panel/assets/{AIOps-ClI2ALgv.js → AIOps-BqRkaHGb.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BDjRcJaK.js → ActionButton-DWO7OZLC.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-DikSRsaT.js → Analytics-C5oaVezM.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-9mLhMFmz.js → AppLayout-CsUESDoZ.js} +3 -3
  6. package/src/assets/web-panel/assets/{Artifacts-BSr88BrH.js → Artifacts-Dragqu5k.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-CdvyimOF.js → Audit-CQsaHctY.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-D3_hCedh.js → BackgroundAgents-DdG1jNSM.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-Cyvv9RHf.js → Backup-DN6sjK4z.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BZM14kt8.js → BaseInput-Cxm_ZTCl.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DrQSokVz.js → Chat-DVXGmA56.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-HwJR-C6B.js → ChatBubbleRenderer-BdaCL_iE.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-bFLYeWMV.js → Checkbox-DEc4vD_A.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-7mN7xJXT.js → Codegen-CKiTcv36.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-DAB5ZWm7.js → Col-Br9UUNN0.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-B_1TymaK.js → Community-NeUH_70T.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-Bq1uSWLJ.js → Compact-BqR-z2Df.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-CyiE1jEe.js → Compliance-DcWB_SgZ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Cpk0D7vm.js → Cowork-BfhgcudF.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-d_Lv3rdB.js → Cron-LaU29LgD.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DMqhHjMR.js → Crosschain-B2ZB9uwW.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-Br-oPuBw.js → DID-BuHl38A2.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-CfLW_RaP.js → Dashboard-m627yqvZ.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-DZnTOExV.js → Dropdown-C7bxF8F5.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DHIUHDv1.js → EmailListRenderer-DPLPQzLp.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bw3t3myu.js → FamilyGuardDashboard-cOiH6ike.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-D9cpQPAY.js → Federation-Bvkzg88z.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Jdczr9Mr.js → FormItemContext-BTsiA7h_.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-BT8ZMpC5.js → GenericCardRenderer-BGnVE5qu.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DjARip2b.js → Git-DS_j6r25.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-BgWbSRcj.js → Governance-CKRG3Rsl.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CfouJciM.js → Inference-DW8INoNQ.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-50l7mrAr.js → KnowledgeGraph-BhEYeFhC.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-rq50H84B.js → Logs-wqi0uxuR.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-DUPIEdQH.js → MarkdownRenderer-DS8dE8ty.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-BYgPDmHU.js → Marketplace-CMlTuPLT.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-5DVgcFdx.js → McpTools-CSx4m5JJ.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-B70LPk7f.js → Memory-vnQCUy-_.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-De8ssA5Y.js → MobileBridge-BV2NA742.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-Bw0k8e1B.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-RJsOTsIA.js → Mtc-CzK_FPB1.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-Crh-6BEK.js → MtcAudit-Cb7G9oL3.js} +5 -5
  43. package/src/assets/web-panel/assets/{Multisig-wuoR5l3x.js → Multisig-qJxlUyzr.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-U2Mji4fZ.js → NLProgramming-DqaBJ7Te.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-CdTZAyDH.js → Notes-CwhTJkRi.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-C-FucWE7.js → NotificationSettings-YDs4Fal4.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-DCYFTeaI.js → OrderTableRenderer-CvEALpq7.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-an3OF85L.js → Organization-CEmQm9n8.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-C-1vmYFe.js → Overflow-zby_P_LA.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-Cwh47xKg.js → P2P-DsmDNFNT.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-Dm38GT0k.js → PdhVaultBrowser-FWwFlZGF.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-DS2Y8awD.js → Permissions-DuzQ6me5.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-BQNziA0I.js → PersonalDataHub-DZbeN613.js} +3 -3
  54. package/src/assets/web-panel/assets/{Pipeline-DF85L1ue.js → Pipeline-CoNSVAUS.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-BuLornF-.js → Privacy-OYJEAaMC.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-C3Y31IwX.js → ProjectInit-CWIapV--.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-BCPiI8aD.js → ProjectSettings-CX8xMxZn.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-BZ6s4k6g.js → Projects-BLCN8YyY.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-D3Wp022X.js → Providers-BV_sxp75.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-CUJfQkH4.js → QrScannerModal-BTnRrHRp.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-UAc0EO8l.js → QuickAsk-D1KZg018.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-BaxKupcs.js → Recommend-COm5cuNg.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-C75NNnXZ.js → RemoteSession-0saWEkYI.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-BVXynZAk.js → Reputation-Dr7iKLS2.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-C5MRZ0bi.js → Row-C1vpPBcr.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-DAKkEYka.js → RssFeed-CxlWrtNj.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-DvWv9YOA.js → Search-BT2V8dPN.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-B5peafW1.js → Security-bm9V3c5k.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-UQmvPEaG.js → Services-CocH1yyX.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-C3bhWpuF.js → Skeleton-DHItlMdd.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-DfkKf9_H.js → Skills-C59sLc4w.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-DqMSjlBM.js → Sla-Crj4BMc8.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-C-WbxWUX.js → SpeechSettings-DX4KFceY.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-CYuHZ4N2.js → SyncSettings-BD_iwQLr.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-CyeAyB9d.js → Tasks-BmCTzYsH.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-BTCeQnpq.js → Templates-BGUaxZYl.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-BifQuA9Y.js → Tenant-BS0tJx2O.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-Bx_9Bocf.js → Terminal--tHSAsld.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-BX0RFCOm.js → TimelineRenderer-D9-MFxS4.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-CGHrCmUS.js → Tokens-CiCB485f.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DlqaY4SO.js → Trigger-SmCFuW_l.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-DfOmj-ay.js → Trust-DkK3Rqe0.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-B72jH5fE.js → UkeySign-CDM4LGdB.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-BT-63fqv.js → VideoEditing-IDB7DJXt.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-Dsul3sHr.js → Wallet-Bikj5cmt.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-Izkwwdgy.js → WebAuthn-C9abWlAw.js} +2 -2
  87. package/src/assets/web-panel/assets/{WorkflowEditor-D3FHSiH1.js → WorkflowEditor-DrbRKVky.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-CmVz5z3U.js → chat-CccCSDQf.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-CuLgyJBp.js → colors-CXObXLhb.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-Ml1YkXpw.js → compact-item-B5kfYAxC.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-BvjJ48bs.js → createContext-DmiPBfip.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-CvKrADNa.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DVC0157C.js → hasIn-Q6lYteAc.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-C229ABfy.js → index-25Wlqps4.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-Ck_52dz_.js → index-33U29UNV.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-QXzlT9KD.js → index-4oXBDczR.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-4SBta3d7.js → index-BGYXvMc5.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-B0S8NzO6.js → index-BJCCa0s_.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-DJyN28Et.js → index-BJJ5DhKI.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DC_OI9H2.js → index-BLOzViJ6.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-C_XuRgGn.js → index-BRrbUMoG.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-wvb_FtaV.js → index-BUFpT17Q.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-KMqhcF7l.js → index-BkRDiqGc.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-vG4XSJ1d.js → index-BsfvW6my.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DYXHFr4r.js → index-By-kfPED.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DuGszaWY.js → index-BzFTcQsc.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-CXkJH-lK.js → index-C2FXdboq.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-Dt5KMjwJ.js → index-C5XOlTHN.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-4wcgvPoF.js → index-CLhBDS2w.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-C_H_S-DY.js → index-CMq7UeoT.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BydjdZnl.js → index-CVMTWNKl.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BjHBpRYZ.js → index-Caghi-yL.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-Bzr0rB5T.js → index-CdB6FuSH.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BbPtzB8M.js → index-CoK8-kWQ.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BUt8kTIB.js → index-CsO3GUAY.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-C_vRgTl9.js → index-CwZBa20Z.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-DukDVXCv.js → index-D-kASucw.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BbepULSc.js → index-D8eBw7vW.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BfuKP4qe.js → index-D9wgifk4.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BiC6dQZG.js → index-DAYCTc07.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CmFNvNLZ.js → index-DF6Q8l2h.js} +1 -1
  122. package/src/assets/web-panel/assets/index-DTdFUWc_.js +1 -0
  123. package/src/assets/web-panel/assets/{index-KjgKdZe-.js → index-DgTLApS9.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-BpFJHiTv.js → index-DgxVJ8pg.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-Cw0Rjy87.js → index-DlJvIqoR.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CFYVQMvu.js → index-Dw4eHfsG.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CudmwNBV.js → index-Rh-ytwow.js} +3 -3
  128. package/src/assets/web-panel/assets/{index-DDGaOWBV.js → index-StAEt2pq.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-D3lD5qap.js → index-bzfw951k.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BzG-x9p4.js → index-fSUMbMvv.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-B_EvG_GW.js → index-tFHy5Wol.js} +1 -1
  132. package/src/assets/web-panel/assets/index-zQ0M_kz-.js +1 -0
  133. package/src/assets/web-panel/assets/{initDefaultProps-BDgd2YZ8.js → initDefaultProps-BNq7X72N.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-ErtrXdYe.js → motion-DOf9viTc.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-_GPPNCgs.js → move-CRHTcW4x.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-DRLL-8V2.js → mtc-parser-Cyoozgm2.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-p86sl023.js → omit-6-TjFNru.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-BTu7cMLH.js → pickAttrs-DlUmO1zi.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-BEKz6Isr.js → placementArrow-CFOL8mIR.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-Ck42c9MP.js → responsiveObserve-BtaFK5zd.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-1CEvuPsB.js → slide-BIbc2bur.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BDfdx1GJ.js → statusUtils-BXX9d6-w.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-B4K46m5w.js → styleChecker-BGv10Bpn.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DoIVpJNq.js → useFlexGapSupport-bKnKT8tw.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-DdAWi-ln.js → useFs-FY4IL4jn.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-iUu6k8dx.js → usePersonalDataHub-Bwl9TEgu.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-DZz4R6Tw.js → vnode-BM3OHxAT.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DXlf9sA3.js → zoom-D1fbFiHB.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +320 -32
  151. package/src/commands/agent.js +105 -11
  152. package/src/commands/context.js +125 -14
  153. package/src/commands/doctor.js +85 -1
  154. package/src/commands/hook.js +181 -7
  155. package/src/commands/plugin.js +456 -1
  156. package/src/commands/review.js +363 -2
  157. package/src/commands/session.js +191 -0
  158. package/src/commands/team.js +19 -1
  159. package/src/data/changelog.json +10 -4
  160. package/src/gateways/ws/remote-session-protocol.js +39 -0
  161. package/src/gateways/ws/session-protocol.js +55 -2
  162. package/src/gateways/ws/ws-agent-handler.js +50 -0
  163. package/src/harness/jsonl-session-store.js +60 -0
  164. package/src/harness/worktree-isolator.js +140 -5
  165. package/src/lib/agent-schedule-store.js +318 -10
  166. package/src/lib/agent-team/team-worktree.js +37 -2
  167. package/src/lib/agent-tool-admission.js +125 -0
  168. package/src/lib/app-preview.js +360 -0
  169. package/src/lib/async-hook-supervisor.cjs +54 -0
  170. package/src/lib/capability-manifest.js +319 -0
  171. package/src/lib/capability-negotiation.js +23 -12
  172. package/src/lib/channels/channel-manager.js +54 -2
  173. package/src/lib/cli-context-engineering.js +9 -2
  174. package/src/lib/context-breakdown.js +116 -0
  175. package/src/lib/diagnostic-bundle.js +230 -0
  176. package/src/lib/doctor-checkup.js +275 -4
  177. package/src/lib/execution-location.js +286 -0
  178. package/src/lib/headless-manifest.js +13 -40
  179. package/src/lib/hook-event-log.cjs +193 -0
  180. package/src/lib/hook-stats-store.cjs +197 -0
  181. package/src/lib/idempotency.js +109 -0
  182. package/src/lib/interaction-adapter.js +34 -4
  183. package/src/lib/json-schema-output.js +46 -12
  184. package/src/lib/json-schema-validate.js +143 -3
  185. package/src/lib/jsonl-session-store.js +1 -0
  186. package/src/lib/lsp/__tests__/lsp-manager.test.js +58 -0
  187. package/src/lib/lsp/lsp-manager.js +56 -0
  188. package/src/lib/monitor-event.js +290 -0
  189. package/src/lib/operation-fingerprint.js +217 -0
  190. package/src/lib/plugin-runtime/bin.js +37 -0
  191. package/src/lib/plugin-runtime/capabilities.js +19 -0
  192. package/src/lib/plugin-runtime/capability-consent.js +203 -0
  193. package/src/lib/plugin-runtime/hooks.js +37 -0
  194. package/src/lib/plugin-runtime/lsp.js +38 -0
  195. package/src/lib/plugin-runtime/mcp.js +40 -0
  196. package/src/lib/plugin-runtime/monitors.js +37 -0
  197. package/src/lib/plugin-runtime/plugin-options.js +151 -0
  198. package/src/lib/plugin-runtime/policy.js +62 -0
  199. package/src/lib/plugin-runtime/scopes.js +20 -4
  200. package/src/lib/pr-automation-policy.js +224 -0
  201. package/src/lib/project-instructions.js +128 -0
  202. package/src/lib/remote-approval-bridge.js +66 -13
  203. package/src/lib/repl-goal.js +132 -0
  204. package/src/lib/repl-rewind.js +153 -0
  205. package/src/lib/review-comment-anchor.js +241 -0
  206. package/src/lib/review-pipeline.js +46 -7
  207. package/src/lib/runtime-checkup.js +221 -0
  208. package/src/lib/schedule-planner.js +17 -1
  209. package/src/lib/secret-scan.js +409 -0
  210. package/src/lib/session-branch.js +0 -0
  211. package/src/lib/settings-hook-events.cjs +13 -0
  212. package/src/lib/settings-loader.cjs +42 -0
  213. package/src/lib/side-effect-ledger-store.js +88 -0
  214. package/src/lib/side-effect-ledger.js +401 -0
  215. package/src/lib/skill-loader.js +32 -0
  216. package/src/lib/skill-path-scope.js +63 -0
  217. package/src/lib/sub-agent-context.js +110 -9
  218. package/src/lib/sub-agent-registry.js +19 -0
  219. package/src/lib/subagent-inheritance.js +112 -0
  220. package/src/lib/telemetry/span-recorder.js +17 -4
  221. package/src/lib/terminal-context.js +195 -0
  222. package/src/lib/turn-binding-store.js +119 -0
  223. package/src/lib/unattended-action-policy.js +182 -0
  224. package/src/lib/worktree-cleanup-safety.js +138 -0
  225. package/src/lib/worktree-isolator.js +8 -0
  226. package/src/lib/worktree-sparse.js +131 -0
  227. package/src/repl/agent-repl.js +280 -30
  228. package/src/repl/permission-tier.js +28 -0
  229. package/src/runtime/agent-core.js +565 -21
  230. package/src/runtime/coding-agent-contract-shared.cjs +60 -1
  231. package/src/runtime/diagnostics.js +60 -0
  232. package/src/runtime/headless-runner.js +393 -1
  233. package/src/runtime/headless-stream.js +60 -1
  234. package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +0 -1
  235. package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +0 -1
  236. package/src/assets/web-panel/assets/index-B7a1TrU6.js +0 -1
  237. package/src/assets/web-panel/assets/index-C2B1KRja.js +0 -1
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Persist the explicit turn→checkpoint binding table ([[turn-binding.js]]) as a
3
+ * tamper-evident chained event in the session transcript — the first half of the
4
+ * P1 gap "把绑定作为新链式事件落 jsonl-session-store.js(继承哈希链防篡改,替代
5
+ * 易失的 `_checkpointMarks`)".
6
+ *
7
+ * The REPL's in-process `_checkpointMarks` array is lost the moment the session
8
+ * ends or the process exits, so a resumed session can't offer a coverage-aware
9
+ * rewind for anything from before the restart. Snapshotting the whole
10
+ * `TurnBindingLog` into an append-only `turn_checkpoint_binding` event makes the
11
+ * binding survive resume AND inherit the transcript's hash chain (tamper
12
+ * evidence), exactly like every other session event.
13
+ *
14
+ * All I/O is delegated to the canonical session store via `_deps` so this module
15
+ * stays pure-testable (inject an in-memory append/read pair — no fs, no home dir).
16
+ */
17
+
18
+ import {
19
+ appendEvent as storeAppendEvent,
20
+ readEvents as storeReadEvents,
21
+ } from "../harness/jsonl-session-store.js";
22
+ import {
23
+ TurnBindingLog,
24
+ resolveRestorePlan,
25
+ selectTurnRange,
26
+ RESTORE_SCOPE,
27
+ } from "./turn-binding.js";
28
+
29
+ /** Event type carrying a full TurnBindingLog snapshot. */
30
+ export const TURN_BINDING_EVENT = "turn_checkpoint_binding";
31
+
32
+ // Injected so tests can drive persistence without touching the real sessions dir.
33
+ export const _deps = {
34
+ appendEvent: storeAppendEvent,
35
+ readEvents: storeReadEvents,
36
+ };
37
+
38
+ /**
39
+ * Append the current binding table as a new chained event. Each call writes the
40
+ * FULL table (append-only snapshot), so the newest event is authoritative — a
41
+ * later `loadTurnBindingLog` never has to merge partial deltas across a chain
42
+ * that a compaction could have truncated.
43
+ *
44
+ * Best-effort: a persistence failure must not crash the turn it is recording, so
45
+ * the caller gets `false` instead of a throw.
46
+ *
47
+ * @param {string} sessionId
48
+ * @param {TurnBindingLog|object} log a TurnBindingLog or its toJSON() shape
49
+ * @returns {boolean} true when the snapshot was written
50
+ */
51
+ export function persistTurnBinding(sessionId, log) {
52
+ if (!sessionId || !log) return false;
53
+ const snapshot = log instanceof TurnBindingLog ? log.toJSON() : log;
54
+ if (!snapshot || !Array.isArray(snapshot.turns)) return false;
55
+ try {
56
+ _deps.appendEvent(sessionId, TURN_BINDING_EVENT, snapshot);
57
+ return true;
58
+ } catch {
59
+ // Persistence is advisory — never let it break the agent turn.
60
+ return false;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Rebuild the TurnBindingLog from the LATEST persisted snapshot in a session
66
+ * transcript. Returns an empty log when the session has no binding event (or on
67
+ * any read error) so callers can treat "never persisted" and "unreadable" alike.
68
+ *
69
+ * @param {string} sessionId
70
+ * @returns {TurnBindingLog}
71
+ */
72
+ export function loadTurnBindingLog(sessionId) {
73
+ let events = [];
74
+ try {
75
+ events = _deps.readEvents(sessionId) || [];
76
+ } catch {
77
+ return new TurnBindingLog();
78
+ }
79
+ // Newest snapshot wins — scan backwards for the first well-formed one.
80
+ for (let i = events.length - 1; i >= 0; i--) {
81
+ const e = events[i];
82
+ if (
83
+ e &&
84
+ e.type === TURN_BINDING_EVENT &&
85
+ e.data &&
86
+ Array.isArray(e.data.turns)
87
+ ) {
88
+ return TurnBindingLog.fromJSON(e.data);
89
+ }
90
+ }
91
+ return new TurnBindingLog();
92
+ }
93
+
94
+ /**
95
+ * Compose load + plan: resolve the honest restore plan for one turn of a
96
+ * PERSISTED session, so a resumed `/rewind` can show coverage warnings for a
97
+ * turn taken before the restart. Delegates the over-promise guards to
98
+ * `resolveRestorePlan` (unknown turn → a "nothing to restore" plan).
99
+ *
100
+ * @param {string} sessionId
101
+ * @param {string} turnId
102
+ * @param {string} [scope] one of RESTORE_SCOPE
103
+ */
104
+ export function resolveRestorePlanFromSession(
105
+ sessionId,
106
+ turnId,
107
+ scope = RESTORE_SCOPE.BOTH,
108
+ ) {
109
+ const log = loadTurnBindingLog(sessionId);
110
+ return resolveRestorePlan(log.get(turnId), scope);
111
+ }
112
+
113
+ /**
114
+ * Compose load + range select for "Summarize from here / up to here" over a
115
+ * persisted session's turns.
116
+ */
117
+ export function selectTurnRangeFromSession(sessionId, range = {}) {
118
+ return selectTurnRange(loadTurnBindingLog(sessionId), range);
119
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Unattended-action policy — the fail-closed guard behind P1-8's
3
+ * "默认禁止无人值守发布、合并或修改共享基础设施" of
4
+ * CLAUDE_CODE_IDE_INCREMENTAL_GAP_ANALYSIS_2026-07-13.md.
5
+ *
6
+ * A scheduled or event-driven session runs with NO human watching, and P1-8 is
7
+ * explicit that external trigger content is untrusted by default. [[cost-budget.js]]
8
+ * already caps USD spend and [[schedule-planner.js]] decides WHEN a task fires,
9
+ * but nothing decides WHAT a fired task may irreversibly do. Publishing a
10
+ * package, merging a PR, deploying, or mutating shared infrastructure while
11
+ * unattended is exactly the class of action that must never happen ambiently.
12
+ *
13
+ * This module is that pure decision. Given an action's risk class and the run's
14
+ * context (attended? trigger trusted? explicitly allowlisted? within budget?) it
15
+ * returns allow/deny with a named reason. Everything defaults to DENY: an
16
+ * unrecognized action on an unattended run is refused, not assumed safe.
17
+ *
18
+ * PURE: no fs / clock / RNG / process.
19
+ */
20
+
21
+ /** Canonical action classes, ordered low → high consequence. */
22
+ export const ACTION_CLASS = Object.freeze({
23
+ READ: "read",
24
+ LOCAL_WRITE: "local_write", // edit files in the worktree
25
+ COMMIT: "commit", // local commit (reversible, not shared)
26
+ PUSH: "push", // push a branch to a remote
27
+ PUBLISH: "publish", // npm publish / release — irreversible & public
28
+ MERGE: "merge", // merge a PR into a shared branch
29
+ DEPLOY: "deploy", // deploy / roll out
30
+ INFRA_MUTATION: "infra_mutation", // modify shared infrastructure
31
+ EXTERNAL_MESSAGE: "external_message", // send mail / chat / webhook outward
32
+ });
33
+
34
+ const ACTION_ALIASES = new Map([
35
+ ["read", ACTION_CLASS.READ],
36
+ ["local_write", ACTION_CLASS.LOCAL_WRITE],
37
+ ["write", ACTION_CLASS.LOCAL_WRITE],
38
+ ["edit", ACTION_CLASS.LOCAL_WRITE],
39
+ ["commit", ACTION_CLASS.COMMIT],
40
+ ["push", ACTION_CLASS.PUSH],
41
+ ["publish", ACTION_CLASS.PUBLISH],
42
+ ["release", ACTION_CLASS.PUBLISH],
43
+ ["merge", ACTION_CLASS.MERGE],
44
+ ["deploy", ACTION_CLASS.DEPLOY],
45
+ ["rollout", ACTION_CLASS.DEPLOY],
46
+ ["infra_mutation", ACTION_CLASS.INFRA_MUTATION],
47
+ ["infra", ACTION_CLASS.INFRA_MUTATION],
48
+ ["external_message", ACTION_CLASS.EXTERNAL_MESSAGE],
49
+ ["message", ACTION_CLASS.EXTERNAL_MESSAGE],
50
+ ["notify", ACTION_CLASS.EXTERNAL_MESSAGE],
51
+ ]);
52
+
53
+ /** Actions that are safe to run unattended (local, reversible, not shared). */
54
+ const LOW_RISK = new Set([
55
+ ACTION_CLASS.READ,
56
+ ACTION_CLASS.LOCAL_WRITE,
57
+ ACTION_CLASS.COMMIT,
58
+ ]);
59
+
60
+ /**
61
+ * Actions that touch a shared / public / irreversible surface and therefore
62
+ * require attendance (or an explicit allowlist entry) when unattended.
63
+ */
64
+ const HIGH_RISK = new Set([
65
+ ACTION_CLASS.PUBLISH,
66
+ ACTION_CLASS.MERGE,
67
+ ACTION_CLASS.DEPLOY,
68
+ ACTION_CLASS.INFRA_MUTATION,
69
+ ACTION_CLASS.EXTERNAL_MESSAGE,
70
+ ]);
71
+
72
+ /** Normalize an action label to a canonical class, or null when unrecognized. */
73
+ export function normalizeActionClass(value) {
74
+ if (typeof value !== "string") return null;
75
+ return ACTION_ALIASES.get(value.trim().toLowerCase()) || null;
76
+ }
77
+
78
+ /**
79
+ * Agent tools whose effect maps to a high-risk action class that can be cleanly
80
+ * blocked at the tool layer. `git push` / npm-publish-via-shell are deliberately
81
+ * NOT here — they ride the `git` / `run_shell` tools (which also do read-only
82
+ * work) and stay governed by the shell policy, not a blanket tool removal.
83
+ */
84
+ export const TOOL_ACTION_CLASS = Object.freeze({
85
+ publish_artifact: ACTION_CLASS.PUBLISH,
86
+ notify: ACTION_CLASS.EXTERNAL_MESSAGE,
87
+ });
88
+
89
+ /**
90
+ * The agent tools an UNATTENDED run must be denied — the enforcement projection
91
+ * of `evaluateUnattendedAction` onto the tool layer (P1-8). A scheduled
92
+ * `cc agent` run passes the result as `--disallowed-tools`, so the model can
93
+ * never even call a denied high-risk tool. An attended run — or one whose class
94
+ * is on the allowlist — yields no restriction. PURE; result is sorted so the
95
+ * spawned argv is stable.
96
+ *
97
+ * @param {{attended?:boolean, allowlist?:string[], trigger?:object, budgetExhausted?:boolean}} p
98
+ * @returns {string[]} tool names to disallow
99
+ */
100
+ export function unattendedDisallowedTools(p = {}) {
101
+ const out = [];
102
+ for (const [tool, actionClass] of Object.entries(TOOL_ACTION_CLASS)) {
103
+ const verdict = evaluateUnattendedAction({
104
+ actionClass,
105
+ attended: p.attended === true,
106
+ allowlist: p.allowlist,
107
+ trigger: p.trigger,
108
+ budgetExhausted: p.budgetExhausted,
109
+ });
110
+ if (!verdict.allow) out.push(tool);
111
+ }
112
+ return out.sort();
113
+ }
114
+
115
+ /** Coarse risk tier for an action: "low" | "high" | "conditional" | "unknown". */
116
+ export function classifyActionRisk(actionClass) {
117
+ const a = normalizeActionClass(actionClass);
118
+ if (a == null) return "unknown";
119
+ if (LOW_RISK.has(a)) return "low";
120
+ if (HIGH_RISK.has(a)) return "high";
121
+ return "conditional"; // push — depends on branch protection
122
+ }
123
+
124
+ /**
125
+ * Decide whether a fired task may perform one action. Fail-closed.
126
+ *
127
+ * @param {object} params
128
+ * @param {string} params.actionClass action label (normalized)
129
+ * @param {boolean} params.attended a human is watching this run
130
+ * @param {object} [params.trigger] {trusted?:boolean} — event source trust
131
+ * @param {string[]} [params.allowlist] action classes pre-approved for unattended
132
+ * @param {boolean} [params.protectedBranch] the push/merge targets a protected branch
133
+ * @param {boolean} [params.budgetExhausted] the run's budget is spent
134
+ * @returns {{allow:boolean, reason:string}}
135
+ */
136
+ export function evaluateUnattendedAction(params = {}) {
137
+ const action = normalizeActionClass(params.actionClass);
138
+
139
+ // A hard budget cap blocks everything, attended or not.
140
+ if (params.budgetExhausted === true) {
141
+ return { allow: false, reason: "budget-exhausted" };
142
+ }
143
+
144
+ // Unrecognized action: a human may vet it, but an unattended run must not.
145
+ if (action == null) {
146
+ return params.attended === true
147
+ ? { allow: true, reason: "attended" }
148
+ : { allow: false, reason: "unknown-action-unattended" };
149
+ }
150
+
151
+ // A watching human can authorize anything within budget.
152
+ if (params.attended === true) return { allow: true, reason: "attended" };
153
+
154
+ // ── Unattended from here on ──
155
+ const allowlist = new Set(
156
+ (Array.isArray(params.allowlist) ? params.allowlist : [])
157
+ .map((a) => normalizeActionClass(a))
158
+ .filter(Boolean),
159
+ );
160
+
161
+ // A push to a PROTECTED branch is effectively a merge into shared state.
162
+ const effective =
163
+ action === ACTION_CLASS.PUSH && params.protectedBranch === true
164
+ ? ACTION_CLASS.MERGE
165
+ : action;
166
+
167
+ if (LOW_RISK.has(effective)) return { allow: true, reason: "low-risk" };
168
+
169
+ // A plain push to an unprotected branch is allowed unattended.
170
+ if (effective === ACTION_CLASS.PUSH)
171
+ return { allow: true, reason: "push-unprotected" };
172
+
173
+ // High-risk unattended: external trigger content is untrusted by default, and
174
+ // even a trusted trigger must be on the explicit allowlist.
175
+ if (params.trigger && params.trigger.trusted === false) {
176
+ return { allow: false, reason: "untrusted-trigger" };
177
+ }
178
+ if (!allowlist.has(effective)) {
179
+ return { allow: false, reason: "requires-attendance" };
180
+ }
181
+ return { allow: true, reason: "allowlisted" };
182
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Worktree cleanup-safety gate — the fail-closed pre-removal check from P1-5
3
+ * (统一后台 Agent / Worktree 工作台) of
4
+ * CLAUDE_CODE_IDE_INCREMENTAL_GAP_ANALYSIS_2026-07-13.md:
5
+ *
6
+ * "清理前检查未提交修改、未追踪文件、未 Push Commit 和关联 PR。"
7
+ *
8
+ * [[agent-worktree.js]] `finishAgentWorktree()` already refuses to remove a
9
+ * worktree with uncommitted changes or new commits, but it collapses "dirty"
10
+ * into one flag and does NOT distinguish untracked files, does NOT know whether
11
+ * commits were pushed, and has no notion of a linked PR — so a background agent
12
+ * that committed + pushed but left an OPEN PR, or left brand-new untracked
13
+ * scratch files, could still be reaped by a broader sweep.
14
+ *
15
+ * This module is the PURE decision behind any worktree reaper: given a plain
16
+ * state descriptor (whatever the caller gathered from `git status --porcelain`,
17
+ * `git rev-list @{u}..HEAD`, the PR store, …) it returns whether the worktree is
18
+ * safe to remove, and — like [[pr-automation-policy.js]] — it EXHAUSTIVELY
19
+ * collects every blocker so the caller can show the user all of them at once.
20
+ *
21
+ * Everything defaults to KEEP: an unreadable or under-described worktree is
22
+ * never removed, because we must never destroy work we cannot verify is safe to
23
+ * lose (the same invariant `finishAgentWorktree` guards with its try/catch).
24
+ */
25
+
26
+ /** Named blockers — one per reason a worktree must be kept, not reaped. */
27
+ export const CLEANUP_BLOCKER = Object.freeze({
28
+ UNVERIFIABLE: "unverifiable", // git state could not be read → fail-closed keep
29
+ UNCOMMITTED: "uncommitted-changes", // tracked files modified but not committed
30
+ UNTRACKED: "untracked-files", // new files git isn't tracking yet
31
+ UNPUSHED: "unpushed-commits", // commits beyond base not on any remote
32
+ LINKED_PR: "linked-pr", // an open PR references this branch
33
+ });
34
+
35
+ const BLOCKER_REASON = Object.freeze({
36
+ [CLEANUP_BLOCKER.UNVERIFIABLE]:
37
+ "git state could not be read — refusing to remove work that cannot be verified safe to lose",
38
+ [CLEANUP_BLOCKER.UNCOMMITTED]: "tracked files have uncommitted modifications",
39
+ [CLEANUP_BLOCKER.UNTRACKED]: "untracked files would be lost on removal",
40
+ [CLEANUP_BLOCKER.UNPUSHED]:
41
+ "commits beyond the base are not pushed to any remote",
42
+ [CLEANUP_BLOCKER.LINKED_PR]:
43
+ "an open pull request still references this branch",
44
+ });
45
+
46
+ function toCount(v) {
47
+ const n = Number(v);
48
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
49
+ }
50
+
51
+ function prList(v) {
52
+ if (!Array.isArray(v)) return [];
53
+ return v.filter((p) => p != null);
54
+ }
55
+
56
+ /**
57
+ * Decide whether commits on this worktree are unpushed. A caller that already
58
+ * computed push-state passes an explicit `unpushed` boolean, which wins. Else it
59
+ * is derived: a worktree whose HEAD equals its base has no new commits;
60
+ * otherwise, with a known upstream, unpushed iff `aheadCount > 0`; WITHOUT a
61
+ * known upstream, any divergence from base is treated as unpushed (there is
62
+ * nowhere it could have been pushed to) — the fail-closed reading.
63
+ */
64
+ function commitsAreUnpushed(state) {
65
+ if (state.unpushed === true) return true;
66
+ if (state.unpushed === false) return false;
67
+ const { baseSha, headSha, hasUpstream, aheadCount } = state;
68
+ const hasNewCommits =
69
+ headSha != null && baseSha != null && String(headSha) !== String(baseSha);
70
+ if (!hasNewCommits) {
71
+ // Base/head unknown but the caller told us we're ahead → still unpushed.
72
+ return toCount(aheadCount) > 0;
73
+ }
74
+ if (hasUpstream === true) return toCount(aheadCount) > 0;
75
+ return true; // new commits + no upstream → nowhere pushed
76
+ }
77
+
78
+ /**
79
+ * Evaluate a worktree's cleanup safety. EXHAUSTIVE + fail-closed.
80
+ *
81
+ * @param {object} state
82
+ * @param {boolean} [state.readable] false / omitted when git status could not be read
83
+ * @param {boolean} [state.uncommitted] tracked modifications present
84
+ * @param {boolean} [state.untracked] untracked files present
85
+ * @param {string} [state.baseSha] worktree base commit
86
+ * @param {string} [state.headSha] worktree HEAD commit
87
+ * @param {boolean} [state.hasUpstream] branch has a configured upstream
88
+ * @param {number} [state.aheadCount] commits ahead of upstream
89
+ * @param {boolean} [state.unpushed] explicit push-state (wins over derivation)
90
+ * @param {Array} [state.linkedPrs] open PRs referencing this branch
91
+ * @returns {{safeToRemove:boolean, blockers:string[], reasons:object}}
92
+ */
93
+ export function evaluateWorktreeCleanup(state = {}) {
94
+ const blockers = [];
95
+
96
+ // Unverifiable state is the hard fail-closed gate: if we could not read the
97
+ // worktree, we cannot prove anything else, so keep and report only this.
98
+ if (state.readable === false) {
99
+ blockers.push(CLEANUP_BLOCKER.UNVERIFIABLE);
100
+ return finalize(blockers);
101
+ }
102
+
103
+ if (state.uncommitted === true) blockers.push(CLEANUP_BLOCKER.UNCOMMITTED);
104
+ if (state.untracked === true) blockers.push(CLEANUP_BLOCKER.UNTRACKED);
105
+ if (commitsAreUnpushed(state)) blockers.push(CLEANUP_BLOCKER.UNPUSHED);
106
+ if (prList(state.linkedPrs).length > 0)
107
+ blockers.push(CLEANUP_BLOCKER.LINKED_PR);
108
+
109
+ return finalize(blockers);
110
+ }
111
+
112
+ function finalize(blockers) {
113
+ const reasons = {};
114
+ for (const b of blockers) reasons[b] = BLOCKER_REASON[b];
115
+ return {
116
+ safeToRemove: blockers.length === 0,
117
+ blockers,
118
+ reasons,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * A human-readable one-line summary of a cleanup decision — never echoes file
124
+ * contents or PR bodies, only counts and named blockers.
125
+ */
126
+ export function summarizeWorktreeCleanup(state = {}) {
127
+ const { safeToRemove, blockers } = evaluateWorktreeCleanup(state);
128
+ if (safeToRemove)
129
+ return "safe to remove — no uncommitted, untracked, unpushed or PR-linked work";
130
+ const parts = blockers.map((b) => {
131
+ if (b === CLEANUP_BLOCKER.LINKED_PR) {
132
+ const n = prList(state.linkedPrs).length;
133
+ return `${n} open PR${n === 1 ? "" : "s"}`;
134
+ }
135
+ return b;
136
+ });
137
+ return `keep — ${parts.join(", ")}`;
138
+ }
@@ -5,9 +5,17 @@ export {
5
5
  pruneWorktrees,
6
6
  isolateTask,
7
7
  cleanupAgentWorktrees,
8
+ assessAgentWorktreeCleanup,
8
9
  diffWorktree,
9
10
  previewWorktreeMerge,
10
11
  applyWorktreeAutomationCandidate,
11
12
  mergeWorktree,
12
13
  worktreeLog,
13
14
  } from "../harness/worktree-isolator.js";
15
+
16
+ export {
17
+ normalizeSparsePaths,
18
+ planSymlinkDirectories,
19
+ isContainedPath,
20
+ isSafeRelPath,
21
+ } from "./worktree-sparse.js";
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Worktree sparse-checkout + dependency-symlink planning (pure core).
3
+ *
4
+ * P1 "large monorepo" pillar: only check out the packages a task needs
5
+ * (`worktree.sparsePaths`) and reuse explicitly-approved dependency
6
+ * directories from the main checkout via junction/symlink
7
+ * (`worktree.symlinkDirectories`), with junction/symlink escape defense.
8
+ *
9
+ * Pure + deterministic: no fs, no git, no clock. The isolator does the
10
+ * actual sparse-checkout and link creation from the plans produced here.
11
+ */
12
+
13
+ import { resolve, join } from "node:path";
14
+
15
+ /**
16
+ * Safe repo-relative path predicate — mirrors the reject rules of
17
+ * `git-integration.assertSafeGitPath` but as a boolean, so callers can
18
+ * filter/normalize instead of throwing. Rejects absolute paths, `..`
19
+ * traversal, flag injection, empty strings, and shell metacharacters.
20
+ */
21
+ export function isSafeRelPath(p) {
22
+ if (typeof p !== "string") return false;
23
+ const s = p.trim();
24
+ if (s.length === 0) return false;
25
+ return (
26
+ /^[A-Za-z0-9._/+@=, -]+$/.test(s) && // safe chars only
27
+ !s.startsWith("-") && // no flag injection
28
+ !s.startsWith("/") && // no absolute (posix)
29
+ !/^[A-Za-z]:/.test(s) && // no absolute (windows drive)
30
+ !s.includes("..") // no traversal
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Normalize a raw relative path to git's forward-slash form. Converts
36
+ * backslash separators (Windows-style `packages\cli`) and trims trailing
37
+ * slashes, but keeps any LEADING slash so absolute paths are still rejected
38
+ * by the safety check (never silently reinterpreted as repo-relative).
39
+ */
40
+ function toGitRel(p) {
41
+ return String(p).trim().replace(/\\/g, "/").replace(/\/+$/, "");
42
+ }
43
+
44
+ /**
45
+ * Normalize a raw path (backslash → slash, trim slashes) THEN validate — so
46
+ * Windows-style config (`packages\cli`) is accepted, not silently dropped.
47
+ * Returns the safe git-relative form, or null when unsafe/empty.
48
+ */
49
+ function safeGitRel(raw) {
50
+ if (typeof raw !== "string") return null;
51
+ const rel = toGitRel(raw);
52
+ if (rel.length === 0) return null;
53
+ return isSafeRelPath(rel) ? rel : null;
54
+ }
55
+
56
+ /**
57
+ * Validate + normalize sparse-checkout include paths.
58
+ *
59
+ * @param {string[]|string|null|undefined} paths
60
+ * @returns {string[]|null} sorted, de-duped, forward-slash repo-relative
61
+ * directories, or null when nothing valid is requested (caller keeps a
62
+ * full checkout — byte-identical to the no-sparse path).
63
+ */
64
+ export function normalizeSparsePaths(paths) {
65
+ if (paths == null) return null;
66
+ const list = Array.isArray(paths) ? paths : [paths];
67
+ const seen = new Set();
68
+ for (const raw of list) {
69
+ const rel = safeGitRel(raw);
70
+ if (rel) seen.add(rel);
71
+ }
72
+ if (seen.size === 0) return null;
73
+ return [...seen].sort();
74
+ }
75
+
76
+ /** True when `child` resolves to `parent` itself or a descendant of it. */
77
+ export function isContainedPath(child, parent) {
78
+ const p = resolve(parent);
79
+ const c = resolve(child);
80
+ if (c === p) return true;
81
+ // Append a separator so `/repo-evil` is not treated as inside `/repo`.
82
+ const sep = p.endsWith("/") || p.endsWith("\\") ? "" : "/";
83
+ const cNorm = c.replace(/\\/g, "/");
84
+ const pNorm = (p + sep).replace(/\\/g, "/");
85
+ return cNorm.startsWith(pNorm);
86
+ }
87
+
88
+ /**
89
+ * Plan dependency-directory links from the main checkout into a worktree.
90
+ *
91
+ * Each entry must be an explicitly-approved, safe repo-relative directory
92
+ * (e.g. `node_modules`, `packages/cli/node_modules`). The plan asserts both
93
+ * lexical safety (no `..`, no absolute, no flag) AND resolved containment:
94
+ * the source stays inside `repoDir` and the destination stays inside
95
+ * `worktreePath`. This defeats junction/symlink escape where a crafted name
96
+ * would place the link (or its target) outside the intended roots.
97
+ *
98
+ * @throws {Error} on any unsafe/escaping entry — fail-closed, never a
99
+ * silent skip, so a bad config cannot quietly link outside the sandbox.
100
+ * @returns {{name:string, source:string, dest:string}[]}
101
+ */
102
+ export function planSymlinkDirectories(dirs, { repoDir, worktreePath } = {}) {
103
+ if (dirs == null) return [];
104
+ if (!repoDir || !worktreePath) {
105
+ throw new Error("planSymlinkDirectories requires repoDir and worktreePath");
106
+ }
107
+ const list = Array.isArray(dirs) ? dirs : [dirs];
108
+ const plan = [];
109
+ const seen = new Set();
110
+ for (const raw of list) {
111
+ const rel = safeGitRel(raw);
112
+ if (!rel) {
113
+ throw new Error(`Unsafe symlink directory: ${JSON.stringify(raw)}`);
114
+ }
115
+ if (seen.has(rel)) continue;
116
+ seen.add(rel);
117
+
118
+ const source = resolve(join(repoDir, rel));
119
+ const dest = resolve(join(worktreePath, rel));
120
+ if (!isContainedPath(source, repoDir)) {
121
+ throw new Error(`Symlink source escapes repo: ${JSON.stringify(raw)}`);
122
+ }
123
+ if (!isContainedPath(dest, worktreePath)) {
124
+ throw new Error(
125
+ `Symlink target escapes worktree: ${JSON.stringify(raw)}`,
126
+ );
127
+ }
128
+ plan.push({ name: rel, source, dest });
129
+ }
130
+ return plan;
131
+ }