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
@@ -11,6 +11,7 @@ import {
11
11
  resolveServer,
12
12
  resolveBin,
13
13
  probeServers,
14
+ describeServer,
14
15
  registerLanguageServer,
15
16
  _resetPluginServers,
16
17
  _deps,
@@ -126,3 +127,29 @@ describe("probeServers", () => {
126
127
  expect(ids).toContain("typescript-language-server");
127
128
  });
128
129
  });
130
+
131
+ describe("describeServer", () => {
132
+ it("names the builtin server for a language even when not installed", () => {
133
+ existsFor([]); // nothing on PATH
134
+ const ts = describeServer("typescript");
135
+ expect(ts.id).toBe("typescript-language-server");
136
+ expect(ts.bins).toContain("typescript-language-server");
137
+ // Independent of install state — it describes the EXPECTED server.
138
+ expect(describeServer("python").id).toBe("pyright");
139
+ expect(describeServer("go").id).toBe("gopls");
140
+ });
141
+
142
+ it("returns null for an unsupported language", () => {
143
+ expect(describeServer("cobol")).toBe(null);
144
+ expect(describeServer(undefined)).toBe(null);
145
+ });
146
+
147
+ it("prefers a registered plugin server over the builtin", () => {
148
+ registerLanguageServer({
149
+ languageId: "typescript",
150
+ command: "my-ts-lsp",
151
+ id: "plugin-ts",
152
+ });
153
+ expect(describeServer("typescript").id).toBe("plugin-ts");
154
+ });
155
+ });
@@ -64,7 +64,11 @@ export class CodeIntelligence {
64
64
  /* plugin LSP contributions are best-effort */
65
65
  }
66
66
  this.manager =
67
- opts.manager || new LSPManager({ projectRoot: opts.projectRoot });
67
+ opts.manager ||
68
+ new LSPManager({
69
+ projectRoot: opts.projectRoot,
70
+ ...(opts.lspOptions || {}),
71
+ });
68
72
  this._ownsManager = !opts.manager;
69
73
  // One-shot callers (the `cc code-intel` CLI) spawn a cold server per run and
70
74
  // must wait for the project to load before querying; the long-lived agent
@@ -232,3 +232,17 @@ export function probeServers(projectRoot) {
232
232
  export function supportedExtensions() {
233
233
  return Object.keys(EXTENSION_LANGUAGE).sort();
234
234
  }
235
+
236
+ /**
237
+ * Describe the server that WOULD serve a languageId, whether or not its binary
238
+ * is installed — the first plugin candidate, else the builtin. Unlike
239
+ * `resolveServer` (which returns null when nothing is on PATH), this names the
240
+ * expected server so a doctor / status view can say *what to install*. Returns
241
+ * `{ id, bins }` or null for an unsupported language.
242
+ */
243
+ export function describeServer(languageId) {
244
+ const candidates = [...(pluginServers.get(languageId) || [])];
245
+ if (BUILTIN_SERVERS[languageId]) candidates.push(BUILTIN_SERVERS[languageId]);
246
+ const def = candidates[0];
247
+ return def ? { id: def.id, bins: [...def.bins] } : null;
248
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * workspace-roots — pure helpers for multi-root LSP workspaces (P2 LSP slice,
3
+ * CLAUDE_CODE_CLI_INCREMENTAL_GAP_ANALYSIS_2026-07-12 §"LSP 与 Code Review").
4
+ *
5
+ * A session started with `--add-dir` works across several project roots, but
6
+ * `code_intelligence` always keyed its shared language-server pool on the cwd
7
+ * alone — a query for a file living in an additional root spawned (or reused)
8
+ * the WRONG project's server, and `workspace_symbols` could only ever see the
9
+ * cwd project. These helpers make root selection explicit and testable:
10
+ *
11
+ * - `workspaceRootsFor(cwd, additionalDirectories)` — the deduped, resolved
12
+ * root list (cwd first, mirroring the roots the system prompt advertises).
13
+ * - `pickRootForFile(file, roots)` — the root that CONTAINS the file
14
+ * (deepest match wins so nested roots resolve to the more specific
15
+ * project); falls back to the first root (cwd) for out-of-root files,
16
+ * which is the pre-multi-root behavior.
17
+ * - `mergeWorkspaceSymbolResults(results, roots)` — fold per-root
18
+ * `workspaceSymbols` outcomes into one: available when ANY root answered,
19
+ * symbols concatenated in root order and stamped with their `root` so
20
+ * same-named symbols from different projects stay unambiguous.
21
+ *
22
+ * Pure + IO-free (path math only) so agent-core can consume them without any
23
+ * new export surface of its own.
24
+ */
25
+
26
+ import path from "node:path";
27
+
28
+ /** Case-fold for prefix comparison on case-insensitive filesystems. */
29
+ function fold(p) {
30
+ return process.platform === "win32" ? p.toLowerCase() : p;
31
+ }
32
+
33
+ /**
34
+ * Resolved, deduped workspace roots — cwd first, then each additional
35
+ * directory. Non-string/empty entries are dropped; never returns empty (falls
36
+ * back to process.cwd()).
37
+ */
38
+ export function workspaceRootsFor(cwd, additionalDirectories) {
39
+ const out = [];
40
+ const seen = new Set();
41
+ const push = (dir) => {
42
+ if (typeof dir !== "string" || dir.trim() === "") return;
43
+ const resolved = path.resolve(dir);
44
+ const key = fold(resolved);
45
+ if (seen.has(key)) return;
46
+ seen.add(key);
47
+ out.push(resolved);
48
+ };
49
+ push(cwd || process.cwd());
50
+ for (const d of Array.isArray(additionalDirectories)
51
+ ? additionalDirectories
52
+ : []) {
53
+ push(d);
54
+ }
55
+ if (out.length === 0) out.push(path.resolve(process.cwd()));
56
+ return out;
57
+ }
58
+
59
+ /**
60
+ * The workspace root that contains `file` — deepest containing root wins
61
+ * (nested roots resolve to the more specific project). Files outside every
62
+ * root fall back to the FIRST root (cwd), preserving the single-root
63
+ * behavior byte-for-byte.
64
+ */
65
+ export function pickRootForFile(file, roots) {
66
+ const list = Array.isArray(roots) && roots.length > 0 ? roots : null;
67
+ if (!list) return path.resolve(process.cwd());
68
+ if (typeof file !== "string" || file.trim() === "") return list[0];
69
+ const f = fold(path.resolve(file));
70
+ let best = null;
71
+ for (const root of list) {
72
+ const r = fold(path.resolve(root));
73
+ if (f === r || f.startsWith(r + path.sep)) {
74
+ if (best === null || r.length > fold(best).length) {
75
+ best = path.resolve(root);
76
+ }
77
+ }
78
+ }
79
+ return best || list[0];
80
+ }
81
+
82
+ /**
83
+ * Merge per-root `workspaceSymbols` results (same order as `roots`).
84
+ * Any available root makes the merged result available; each symbol is
85
+ * stamped with its `root`. All-unavailable folds the reasons together so the
86
+ * caller still sees WHY (first reason wins the summary, all are listed).
87
+ */
88
+ export function mergeWorkspaceSymbolResults(results, roots = []) {
89
+ const list = Array.isArray(results) ? results : [];
90
+ const symbols = [];
91
+ const reasons = [];
92
+ let anyAvailable = false;
93
+ for (let i = 0; i < list.length; i++) {
94
+ const r = list[i];
95
+ if (r && r.available) {
96
+ anyAvailable = true;
97
+ for (const s of Array.isArray(r.symbols) ? r.symbols : []) {
98
+ symbols.push(roots[i] ? { ...s, root: roots[i] } : s);
99
+ }
100
+ } else if (r) {
101
+ reasons.push(
102
+ roots[i]
103
+ ? `${roots[i]}: ${r.reason || "unavailable"}`
104
+ : r.reason || "unavailable",
105
+ );
106
+ }
107
+ }
108
+ if (!anyAvailable) {
109
+ return {
110
+ available: false,
111
+ reason: reasons.join("; ") || "language server unavailable",
112
+ };
113
+ }
114
+ return { available: true, symbols };
115
+ }
@@ -437,6 +437,39 @@ export function checkDuplicateSkills(entries = []) {
437
437
  return out;
438
438
  }
439
439
 
440
+ /**
441
+ * LSP READINESS: languages the project actually contains but for which no
442
+ * language server is installed — the silent reason `code_intelligence` /
443
+ * `cc code-intel` quietly degrades to plain text search. Unlike
444
+ * `checkPluginsAndLsp` (live server health) this is a STATIC install-gap check a
445
+ * separate `cc doctor` process can compute without any server running: it
446
+ * correlates source files present with `resolveServer` availability. Only
447
+ * present-but-missing languages warn; installed languages produce nothing (no
448
+ * noise). Each `languages` entry: `{ languageId, fileCount, available, serverId,
449
+ * exampleBin }`.
450
+ */
451
+ export function checkLspReadiness(languages = []) {
452
+ const out = [];
453
+ for (const l of languages) {
454
+ if (!l || !l.languageId) continue;
455
+ const fileCount = Number(l.fileCount) || 0;
456
+ if (fileCount <= 0 || l.available) continue;
457
+ const server = l.serverId || `a ${l.languageId} language server`;
458
+ const install = l.exampleBin ? ` (e.g. install ${l.exampleBin})` : "";
459
+ out.push(
460
+ finding(
461
+ "lsp-server-missing",
462
+ "lsp",
463
+ "warn",
464
+ `${fileCount} ${l.languageId} file(s) present but no language server installed${install}`,
465
+ `install ${server} for real code-intelligence, or ignore to keep text-search fallback`,
466
+ { ref: l.languageId, fileCount, serverId: l.serverId || null },
467
+ ),
468
+ );
469
+ }
470
+ return out;
471
+ }
472
+
440
473
  /** Plugins / LSP servers reported unhealthy or dead. */
441
474
  export function checkPluginsAndLsp(plugins = []) {
442
475
  const out = [];
@@ -47,7 +47,15 @@ function withDeliveryId(event, payload, { sessionId, traceId, parentId } = {}) {
47
47
  /* best-effort — recording must not break the turn */
48
48
  }
49
49
  }
50
- return { ...payload, event_id: env.event_id };
50
+ // trace_id/parent_id are stamped onto the DELIVERED payload only when the
51
+ // caller actually threaded them (agent-core context) — absent they are
52
+ // omitted entirely, keeping every legacy payload byte-identical.
53
+ return {
54
+ ...payload,
55
+ event_id: env.event_id,
56
+ ...(env.trace_id ? { trace_id: env.trace_id } : {}),
57
+ ...(env.parent_id ? { parent_id: env.parent_id } : {}),
58
+ };
51
59
  }
52
60
 
53
61
  /**
@@ -178,6 +186,159 @@ function runSessionStartHooks(settingsHooks, { source, cwd, sessionId } = {}) {
178
186
  return { additionalContext: aggregateContext(outcome.results) };
179
187
  }
180
188
 
189
+ /**
190
+ * CwdChanged settings hooks (observe + context injection). Fired when the
191
+ * working directory changes mid-session (`/cd`) — a distinct lifecycle event
192
+ * (Claude-Code-parity extension) automation can react to (re-audit trust of the
193
+ * new dir, reload a per-dir tool config, …). Observe-only: a cwd change never
194
+ * gates flow, so any `block` decision is ignored and only emitted context is
195
+ * returned. No-op (byte-unchanged) without a registered CwdChanged hook.
196
+ * @returns {{ additionalContext:string|null }}
197
+ */
198
+ function runCwdChangedHooks(
199
+ settingsHooks,
200
+ { oldCwd, newCwd, cwd, sessionId } = {},
201
+ ) {
202
+ if (!settingsHooks) return { additionalContext: null };
203
+ const matched = collectHooks(settingsHooks, "CwdChanged", newCwd || "");
204
+ const { sync } = partitionAsyncHooks(matched);
205
+ if (sync.length === 0) return { additionalContext: null };
206
+ const payload = withDeliveryId(
207
+ "CwdChanged",
208
+ {
209
+ hook_event_name: "CwdChanged",
210
+ old_cwd: oldCwd || null,
211
+ cwd: newCwd || cwd || null,
212
+ session_id: sessionId || null,
213
+ },
214
+ { sessionId },
215
+ );
216
+ const outcome = runHooks(sync, payload, {
217
+ cwd: newCwd || cwd,
218
+ event: "CwdChanged",
219
+ });
220
+ return { additionalContext: aggregateContext(outcome.results) };
221
+ }
222
+
223
+ /**
224
+ * WorktreeCreate settings hooks (observe + context injection). Fired right after
225
+ * `cc agent --worktree` carves a fresh isolated git worktree — a hook can react
226
+ * (register the worktree with an external tracker, seed per-branch tooling,
227
+ * warm a build cache). The matcher target is the branch name, so a regex matcher
228
+ * can scope to a branch pattern. Observe-only: worktree setup already succeeded,
229
+ * so a `block` decision is ignored and only emitted context is returned. No-op
230
+ * (byte-unchanged) without a registered WorktreeCreate hook.
231
+ * @returns {{ additionalContext:string|null }}
232
+ */
233
+ function runWorktreeCreateHooks(
234
+ settingsHooks,
235
+ { worktreePath, branch, baseSha, cwd, sessionId } = {},
236
+ ) {
237
+ if (!settingsHooks) return { additionalContext: null };
238
+ const matched = collectHooks(settingsHooks, "WorktreeCreate", branch || "");
239
+ const { sync } = partitionAsyncHooks(matched);
240
+ if (sync.length === 0) return { additionalContext: null };
241
+ const payload = withDeliveryId(
242
+ "WorktreeCreate",
243
+ {
244
+ hook_event_name: "WorktreeCreate",
245
+ worktree_path: worktreePath || null,
246
+ branch: branch || null,
247
+ base_sha: baseSha || null,
248
+ cwd: cwd || worktreePath || null,
249
+ session_id: sessionId || null,
250
+ },
251
+ { sessionId },
252
+ );
253
+ const outcome = runHooks(sync, payload, {
254
+ cwd: cwd || worktreePath,
255
+ event: "WorktreeCreate",
256
+ });
257
+ return { additionalContext: aggregateContext(outcome.results) };
258
+ }
259
+
260
+ /**
261
+ * WorktreeRemove settings hooks (observe + context injection). Fired when a
262
+ * `cc agent --worktree` session finishes and its worktree is torn down (or when
263
+ * a `keep` decision leaves it in place — `removed` distinguishes the two) — a
264
+ * hook can react (unregister the worktree, prune a per-branch cache, notify CI).
265
+ * Matcher target is the branch name. Observe-only: teardown already happened, so
266
+ * a `block` decision is ignored and only emitted context is returned. No-op
267
+ * (byte-unchanged) without a registered WorktreeRemove hook.
268
+ * @returns {{ additionalContext:string|null }}
269
+ */
270
+ function runWorktreeRemoveHooks(
271
+ settingsHooks,
272
+ { worktreePath, branch, removed, reason, cwd, sessionId } = {},
273
+ ) {
274
+ if (!settingsHooks) return { additionalContext: null };
275
+ const matched = collectHooks(settingsHooks, "WorktreeRemove", branch || "");
276
+ const { sync } = partitionAsyncHooks(matched);
277
+ if (sync.length === 0) return { additionalContext: null };
278
+ const payload = withDeliveryId(
279
+ "WorktreeRemove",
280
+ {
281
+ hook_event_name: "WorktreeRemove",
282
+ worktree_path: worktreePath || null,
283
+ branch: branch || null,
284
+ removed: removed === true,
285
+ reason: reason || null,
286
+ cwd: cwd || worktreePath || null,
287
+ session_id: sessionId || null,
288
+ },
289
+ { sessionId },
290
+ );
291
+ const outcome = runHooks(sync, payload, {
292
+ cwd: cwd || worktreePath,
293
+ event: "WorktreeRemove",
294
+ });
295
+ return { additionalContext: aggregateContext(outcome.results) };
296
+ }
297
+
298
+ /**
299
+ * InstructionsLoaded settings hooks (observe + context injection). Fired once at
300
+ * session start after the project-instruction block (cc.md / CLAUDE.md / AGENTS.md
301
+ * hierarchy + `.claude/rules/*`) is composed into the system prompt — a hook can
302
+ * react to WHICH instruction files are authoritative this session (audit them,
303
+ * warn on an unexpected local override, load a companion tool config). The
304
+ * caller passes the EXACT loaded set; this producer trims each entry to
305
+ * `{path, scope, truncated}` (never the file CONTENT — a hook that wants content
306
+ * reads the path). Observe-only: instructions are already loaded, so a `block`
307
+ * decision is ignored and only emitted context is returned. No-op (byte-unchanged)
308
+ * without a registered InstructionsLoaded hook.
309
+ * @returns {{ additionalContext:string|null }}
310
+ */
311
+ function runInstructionsLoadedHooks(
312
+ settingsHooks,
313
+ { files, cwd, sessionId } = {},
314
+ ) {
315
+ if (!settingsHooks) return { additionalContext: null };
316
+ const matched = collectHooks(settingsHooks, "InstructionsLoaded", "");
317
+ const { sync } = partitionAsyncHooks(matched);
318
+ if (sync.length === 0) return { additionalContext: null };
319
+ const list = Array.isArray(files) ? files : [];
320
+ const payload = withDeliveryId(
321
+ "InstructionsLoaded",
322
+ {
323
+ hook_event_name: "InstructionsLoaded",
324
+ files: list.map((f) => ({
325
+ path: f?.path || null,
326
+ scope: f?.scope || null,
327
+ truncated: f?.truncated === true,
328
+ })),
329
+ count: list.length,
330
+ cwd: cwd || null,
331
+ session_id: sessionId || null,
332
+ },
333
+ { sessionId },
334
+ );
335
+ const outcome = runHooks(sync, payload, {
336
+ cwd,
337
+ event: "InstructionsLoaded",
338
+ });
339
+ return { additionalContext: aggregateContext(outcome.results) };
340
+ }
341
+
181
342
  /**
182
343
  * Generic observe-only fire (SessionEnd / Stop / PreCompact). Returns the raw
183
344
  * runHooks outcome so callers can read a block reason; never gates flow here.
@@ -190,7 +351,7 @@ function runObserveHooks(
190
351
  settingsHooks,
191
352
  event,
192
353
  payload = {},
193
- { cwd, matchTarget } = {},
354
+ { cwd, matchTarget, traceId, parentId } = {},
194
355
  ) {
195
356
  if (!settingsHooks) return { decision: "continue", results: [] };
196
357
  const matched = collectHooks(settingsHooks, event, matchTarget || "");
@@ -198,14 +359,23 @@ function runObserveHooks(
198
359
  if (sync.length === 0) return { decision: "continue", results: [] };
199
360
  return runHooks(
200
361
  sync,
201
- withDeliveryId(event, { hook_event_name: event, cwd, ...payload }),
362
+ withDeliveryId(
363
+ event,
364
+ { hook_event_name: event, cwd, ...payload },
365
+ { traceId, parentId },
366
+ ),
202
367
  { cwd, event },
203
368
  );
204
369
  }
205
370
 
206
371
  module.exports = {
372
+ withDeliveryId,
207
373
  runUserPromptSubmitHooks,
208
374
  runSessionStartHooks,
375
+ runCwdChangedHooks,
376
+ runWorktreeCreateHooks,
377
+ runWorktreeRemoveHooks,
378
+ runInstructionsLoadedHooks,
209
379
  runObserveHooks,
210
380
  aggregateContext,
211
381
  partitionAsyncHooks,
@@ -49,6 +49,10 @@ const HOOK_EVENTS = Object.freeze([
49
49
  "PreCompact",
50
50
  "Notification",
51
51
  "PermissionRequest",
52
+ "CwdChanged",
53
+ "WorktreeCreate",
54
+ "WorktreeRemove",
55
+ "InstructionsLoaded",
52
56
  ]);
53
57
 
54
58
  /**
@@ -24,6 +24,13 @@ import {
24
24
  APPROVAL_RISK as RISK,
25
25
  APPROVAL_DECISION as DECISION,
26
26
  } from "@chainlesschain/session-core";
27
+ import {
28
+ classifyCodeAcquisition,
29
+ hasGlobalInstall,
30
+ applyRiskFloor,
31
+ recordInstallCommandAudit,
32
+ resolveInstallPolicy,
33
+ } from "./install-command-policy.js";
27
34
 
28
35
  export const SHELL_TO_RISK = {
29
36
  allow: RISK.LOW,
@@ -37,13 +44,49 @@ export async function evaluateShellCommandWithApproval({
37
44
  sessionId = null,
38
45
  approvalGate = null,
39
46
  shellPolicyOptions = {},
47
+ installPolicy = null,
40
48
  } = {}) {
41
49
  const shellPolicy = sharedShellPolicy.evaluateShellCommandPolicy(
42
50
  command,
43
51
  shellPolicyOptions,
44
52
  );
45
53
 
46
- const riskLevel = SHELL_TO_RISK[shellPolicy.decision] || RISK.MEDIUM;
54
+ let riskLevel = SHELL_TO_RISK[shellPolicy.decision] || RISK.MEDIUM;
55
+
56
+ // Unified install-command classification (OPT-IN via installPolicy). A package
57
+ // install (npm/pip/winget/brew/…) fetches and runs third-party code, so it is
58
+ // a distinct, auditable permission type regardless of which tool does it. When
59
+ // a riskFloor is configured it RAISES (never lowers) the risk before gating;
60
+ // audit records the attempt. Absent/disabled → nothing runs, byte-unchanged.
61
+ // Explicit policy wins; otherwise self-activate from env (CC_INSTALL_AUDIT /
62
+ // CC_INSTALL_RISK_FLOOR) so the real run_shell path picks it up with no extra
63
+ // wiring. Both unset → disabled → the block below is skipped, byte-unchanged.
64
+ const effectiveInstallPolicy =
65
+ installPolicy || resolveInstallPolicy({ env: process.env });
66
+ let install = null;
67
+ if (effectiveInstallPolicy && effectiveInstallPolicy.enabled) {
68
+ const cls = classifyCodeAcquisition(command);
69
+ if (cls.flagged) {
70
+ install = cls;
71
+ if (effectiveInstallPolicy.riskFloor) {
72
+ riskLevel = applyRiskFloor(riskLevel, effectiveInstallPolicy.riskFloor);
73
+ }
74
+ if (effectiveInstallPolicy.audit) {
75
+ recordInstallCommandAudit(
76
+ {
77
+ command,
78
+ shellDecision: shellPolicy.decision,
79
+ riskLevel,
80
+ installs: cls.installs,
81
+ remoteExec: cls.remoteExec,
82
+ global: hasGlobalInstall(cls),
83
+ sessionId,
84
+ },
85
+ effectiveInstallPolicy.auditOpts || {},
86
+ );
87
+ }
88
+ }
89
+ }
47
90
 
48
91
  // Hard-blocked rules bypass the gate entirely — the gate tier cannot
49
92
  // up-authorize them.
@@ -56,6 +99,7 @@ export async function evaluateShellCommandWithApproval({
56
99
  shellPolicy,
57
100
  riskLevel,
58
101
  policy: null,
102
+ install,
59
103
  };
60
104
  }
61
105
 
@@ -69,6 +113,7 @@ export async function evaluateShellCommandWithApproval({
69
113
  shellPolicy,
70
114
  riskLevel,
71
115
  policy: null,
116
+ install,
72
117
  };
73
118
  }
74
119
 
@@ -87,6 +132,7 @@ export async function evaluateShellCommandWithApproval({
87
132
  shellPolicy,
88
133
  riskLevel,
89
134
  policy: gateResult.policy,
135
+ install,
90
136
  // Explainability passthrough (autoMode.decisions wrapper attaches these):
91
137
  // which gate rule fired and why — surfaced in the permission chain.
92
138
  gateReason: gateResult.reason ?? null,
@@ -87,6 +87,10 @@ export class SubAgentContext {
87
87
  // restricts run_skill/list_skills in this context's loop to those skills.
88
88
  this.skillAllowlist =
89
89
  options.skillAllowlist != null ? options.skillAllowlist : null;
90
+ // Hook-envelope tracing: the spawning run's id — this child loop stamps it
91
+ // as parent_id on every settings-hook payload it fires (its own runId
92
+ // becomes the trace_id). null → top-level semantics (no parent).
93
+ this._hookParentTraceId = options.hookParentTraceId || null;
90
94
  this.cwd = options.cwd || process.cwd();
91
95
  this.status = "active";
92
96
  this.result = null;
@@ -98,6 +102,10 @@ export class SubAgentContext {
98
102
  this._worktreePath = null;
99
103
  this._worktreeBranch = null;
100
104
  this._repoDir = this.cwd;
105
+ // Optional worktree creation options (large-monorepo): { sparsePaths,
106
+ // symlinkDirectories } — only materialize the needed packages and reuse
107
+ // approved dep dirs. null → full checkout (byte-identical default).
108
+ this._worktreeOptions = options.worktreeOptions || null;
101
109
 
102
110
  // ── Isolated state ──────────────────────────────────────────────
103
111
  // Independent message history — never shared with parent
@@ -254,6 +262,7 @@ export class SubAgentContext {
254
262
  this.cwd = wtPath;
255
263
  return this._runCore(userPrompt, loopOptions);
256
264
  },
265
+ this._worktreeOptions || {},
257
266
  );
258
267
 
259
268
  // Annotate result with worktree info + diff preview
@@ -328,6 +337,10 @@ export class SubAgentContext {
328
337
  subAgentContract: this.subAgentContract,
329
338
  // Skill allow-list restricting run_skill/list_skills in this loop.
330
339
  skillAllowlist: this.skillAllowlist,
340
+ // Parent trace for hook envelopes (absent → key omitted, no parent_id).
341
+ ...(this._hookParentTraceId
342
+ ? { hookParentTraceId: this._hookParentTraceId }
343
+ : {}),
331
344
  ...loopOptions,
332
345
  };
333
346
  if (this.iterationBudget) {
@@ -61,10 +61,68 @@ export function resolveAddDir(input, { cwd, existing = [], deps } = {}) {
61
61
  */
62
62
  export function formatAddDirRoots(cwd, extraRoots) {
63
63
  const extras = Array.isArray(extraRoots) ? extraRoots : [];
64
- const lines = [`Working roots (${1 + extras.length}):`, ` ${cwd} (primary)`];
64
+ const lines = [
65
+ `Working roots (${1 + extras.length}):`,
66
+ ` ${cwd} (primary)`,
67
+ ];
65
68
  for (const r of extras) lines.push(` ${r}`);
66
69
  if (extras.length === 0) {
67
70
  lines.push(" (no extra roots — add one with /add-dir <dir>)");
68
71
  }
69
72
  return lines.join("\n");
70
73
  }
74
+
75
+ /**
76
+ * The full workspace root list a session advertises to MCP servers (`roots/list`):
77
+ * the primary cwd first, then each extra `--add-dir` / `/add-dir` root, all
78
+ * resolved absolute and de-duplicated (the primary is dropped from the extras if
79
+ * a caller re-added it). Pure. Mirrors what the system prompt advertises so an
80
+ * MCP server sees the same roots the model does.
81
+ *
82
+ * @param {string} cwd primary working directory
83
+ * @param {string[]} extraRoots additional roots
84
+ * @returns {string[]} deduped absolute dirs, primary first
85
+ */
86
+ export function workspaceRootDirs(cwd, extraRoots) {
87
+ const primary = nodePath.resolve(cwd);
88
+ const out = [primary];
89
+ const seen = new Set([primary]);
90
+ for (const r of Array.isArray(extraRoots) ? extraRoots : []) {
91
+ if (!r) continue;
92
+ const abs = nodePath.resolve(cwd, String(r));
93
+ if (seen.has(abs)) continue;
94
+ seen.add(abs);
95
+ out.push(abs);
96
+ }
97
+ return out;
98
+ }
99
+
100
+ /**
101
+ * Best-effort broadcast of a changed workspace-root list to every connected MCP
102
+ * client (via `client.setRoots(dirs)`, which advertises the new list AND fires
103
+ * `notifications/roots/list_changed` only when it actually changed). Skips
104
+ * nullish/duplicate clients and any without `setRoots`; a failing client never
105
+ * disturbs the others or the caller. Returns the number of clients notified.
106
+ * Claude-Code 2.1.203 roots-change parity for the `/add-dir` (and startup
107
+ * `--add-dir`) vector, mirroring what `/cd` already does via
108
+ * `notifyRootsListChanged`.
109
+ *
110
+ * @param {Array<object|null|undefined>} clients MCP clients (may repeat/null)
111
+ * @param {string[]} dirs absolute root dirs
112
+ * @returns {number}
113
+ */
114
+ export function notifyMcpRootsChanged(clients, dirs) {
115
+ let notified = 0;
116
+ const seen = new Set();
117
+ for (const c of Array.isArray(clients) ? clients : []) {
118
+ if (!c || seen.has(c) || typeof c.setRoots !== "function") continue;
119
+ seen.add(c);
120
+ try {
121
+ c.setRoots(dirs);
122
+ notified += 1;
123
+ } catch {
124
+ /* best-effort — one server's failure must not block the rest */
125
+ }
126
+ }
127
+ return notified;
128
+ }