chainlesschain 0.162.150 → 0.162.152

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 (186) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -2
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-CBLvrjeG.js → AIOps-CVLVJS-Q.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-DYv2GtE0.js → ActionButton-CJ2_GJsI.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-CL6PZ-Ww.js → Analytics-CZsmlTF8.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-xuyHt4uA.js → AppLayout-DREvMZ62.js} +5 -5
  8. package/src/assets/web-panel/assets/{Audit-DOvSVycq.js → Audit-B4NR42w_.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-DMhfTswr.js → Backup-CJFmf3In.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CQxDdm63.js → BaseInput-Bh8_iUZY.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-BEeNhK2a.js → Chat-DEwKjIxV.js} +5 -5
  12. package/src/assets/web-panel/assets/ChatBubbleRenderer-BOqHkxK_.js +1 -0
  13. package/src/assets/web-panel/assets/{Checkbox-CL04O-ER.js → Checkbox-DYWCqYI9.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-C50o-n7x.js → Codegen-BlXrfZAJ.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-CH-bDkzs.js → Col-DLPM22eX.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-DqAIMvSe.js → Community-PFyzqijs.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-C43fo_my.js → Compact-7DIfY5Wp.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-BERnMrdP.js → Compliance-DF4CinPU.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Baw3w1gp.js → Cowork-B1J80Vcj.js} +4 -4
  20. package/src/assets/web-panel/assets/{Cron-BEqAHF1-.js → Cron-B7sx2OXX.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-CvnmKwK4.js → Crosschain-omLkxmu-.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-iMPxgVdt.js → DID-B_7r7b5F.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard--4SJX3YR.js → Dashboard-Cp7k3GZg.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-BgLV6xgz.js → Dropdown-BCKO89Y8.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-Bvb8oUD8.js → EmailListRenderer-CNgTSsFZ.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DpXPjJB5.js → FamilyGuardDashboard-DobM83JP.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-BbFfJ1Xi.js → Federation-XVAYQIeu.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-DeGq1B-q.js → FormItemContext-DV5ka1KQ.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-CNW7s9zM.js → GenericCardRenderer-Dg5q-KpC.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-L1PjW-lT.js → Git-Dsn3Xhxf.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-PK-X58to.js → Governance-D06S4Qow.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-BePWEH-d.js → Inference-nAvwxUXy.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-LF_sBvdL.js → KnowledgeGraph-CtqRP_rF.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-BKkfk9hy.js → Logs-J5aGAyfY.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-BNMqUDla.js → Marketplace-CaeY23Kj.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-w4WPiyz2.js → McpTools-C2HHnwnE.js} +5 -5
  37. package/src/assets/web-panel/assets/{Memory-DUKMVGNi.js → Memory-CWBrdKlR.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-BWhiEyTJ.js → MobileBridge-CBLQ8uCY.js} +1 -1
  39. package/src/assets/web-panel/assets/{MobileProjects-Ocf5JEkX.js → MobileProjects-Q_Cy_W4_.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc-D2B6MMhU.js → Mtc-BXur-euo.js} +4 -4
  41. package/src/assets/web-panel/assets/{MtcAudit-DdYjCq1O.js → MtcAudit-CpQSlOQC.js} +5 -5
  42. package/src/assets/web-panel/assets/{Multisig-Cb1hfuUZ.js → Multisig-DlFwdEOA.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-CTeImGp7.js → NLProgramming-DSh7fevc.js} +1 -1
  44. package/src/assets/web-panel/assets/{Notes-BjYNc7eQ.js → Notes-Bv4NOVxg.js} +4 -4
  45. package/src/assets/web-panel/assets/{NotificationSettings-Dn_NtRXQ.js → NotificationSettings-CzHNnIfa.js} +1 -1
  46. package/src/assets/web-panel/assets/{OrderTableRenderer-DVYYIizh.js → OrderTableRenderer-D7pHfKK4.js} +1 -1
  47. package/src/assets/web-panel/assets/{Organization-Be2Kmr5j.js → Organization-COzV01ed.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-Cu_-qRlR.js → Overflow-CYRZJb2_.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-DD2HoGzE.js → P2P-DFm49FE0.js} +2 -2
  50. package/src/assets/web-panel/assets/{PdhVaultBrowser-BRI7DVth.js → PdhVaultBrowser-Ctr-6k5B.js} +3 -3
  51. package/src/assets/web-panel/assets/{Permissions-C9Srcs2M.js → Permissions-DXVrhFyv.js} +3 -3
  52. package/src/assets/web-panel/assets/{PersonalDataHub-CUqcMgo1.js → PersonalDataHub-BO5utpLK.js} +3 -3
  53. package/src/assets/web-panel/assets/{Pipeline-HBrQGLXt.js → Pipeline-C9irYCg7.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-D-B0vjwh.js → Privacy-r13hrvoF.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-eTeLIu9e.js → ProjectInit-PRKNPZZC.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-DrRqCsC6.js → ProjectSettings-QvxM1ZDO.js} +2 -2
  57. package/src/assets/web-panel/assets/{Projects-1G9DNOhI.js → Projects-DfgmsN3j.js} +1 -1
  58. package/src/assets/web-panel/assets/{Providers-RkTZzpxP.js → Providers-DxbrccZ7.js} +1 -1
  59. package/src/assets/web-panel/assets/{QrScannerModal-BSCUgsgC.js → QrScannerModal-BMHVRxBX.js} +1 -1
  60. package/src/assets/web-panel/assets/{QuickAsk-CHgyM3Bz.js → QuickAsk-Co3_ndAH.js} +1 -1
  61. package/src/assets/web-panel/assets/{Recommend-0lry4OZq.js → Recommend-Cv3UWR6s.js} +1 -1
  62. package/src/assets/web-panel/assets/{RemoteSession-Cn45UroZ.js → RemoteSession-CSDsO2zA.js} +2 -2
  63. package/src/assets/web-panel/assets/{Reputation-Q-Zjb707.js → Reputation-Bdj59vzM.js} +1 -1
  64. package/src/assets/web-panel/assets/{Row-BdM70oda.js → Row-CejfUjmo.js} +1 -1
  65. package/src/assets/web-panel/assets/{RssFeed-DmOAKKap.js → RssFeed-C83yKkvU.js} +2 -2
  66. package/src/assets/web-panel/assets/{Search-Dwavbm07.js → Search-Ck4l_VI3.js} +1 -1
  67. package/src/assets/web-panel/assets/{Security-DukXFCnk.js → Security-DGIIEx6K.js} +3 -3
  68. package/src/assets/web-panel/assets/{Services-BIlRnITu.js → Services-Cj2aNAy3.js} +2 -2
  69. package/src/assets/web-panel/assets/{Skeleton-YYVQdhhQ.js → Skeleton-Dlyy8akb.js} +1 -1
  70. package/src/assets/web-panel/assets/{Skills-B8_iYHz2.js → Skills-C4aDa9Zh.js} +1 -1
  71. package/src/assets/web-panel/assets/{Sla-BdVrhT31.js → Sla-tHd_Bauo.js} +1 -1
  72. package/src/assets/web-panel/assets/{SpeechSettings-CD3ggRx7.js → SpeechSettings-DI7R0Qgd.js} +1 -1
  73. package/src/assets/web-panel/assets/{SyncSettings-Bp02VZFH.js → SyncSettings-OOxiPdsE.js} +2 -2
  74. package/src/assets/web-panel/assets/{Tasks-DEVmCEsr.js → Tasks-Nro_Ltg5.js} +1 -1
  75. package/src/assets/web-panel/assets/{Templates-B6czWc4N.js → Templates-Bm7eSMWH.js} +1 -1
  76. package/src/assets/web-panel/assets/{Tenant-BCJLv17r.js → Tenant-B8F8eFvD.js} +1 -1
  77. package/src/assets/web-panel/assets/{Terminal-CD132d2Y.js → Terminal-2oWhR7QF.js} +2 -2
  78. package/src/assets/web-panel/assets/{TimelineRenderer-ZB-CvkZl.js → TimelineRenderer-GLnecmpd.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tokens-BnSY0S-s.js → Tokens-CtpFyati.js} +1 -1
  80. package/src/assets/web-panel/assets/{Trigger-VZw9Oy2w.js → Trigger-DGkXZzbN.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trust-pZq7Hjd2.js → Trust-ClYZMkXN.js} +1 -1
  82. package/src/assets/web-panel/assets/{UkeySign-DnQaqk4q.js → UkeySign-C1AuEW5-.js} +1 -1
  83. package/src/assets/web-panel/assets/{VideoEditing-BEUfmvJV.js → VideoEditing-DHa9ekSK.js} +1 -1
  84. package/src/assets/web-panel/assets/{Wallet-CoZKMXJ2.js → Wallet-CyQv8ZAN.js} +4 -4
  85. package/src/assets/web-panel/assets/{WebAuthn-CBOGVx1I.js → WebAuthn-DPxEEb5Y.js} +5 -5
  86. package/src/assets/web-panel/assets/{WorkflowEditor-huSKn7TT.js → WorkflowEditor-C-fCM8Si.js} +1 -1
  87. package/src/assets/web-panel/assets/{chat-A_3w6FBO.js → chat-BMu1Pk8B.js} +1 -1
  88. package/src/assets/web-panel/assets/{colors-D1Bem9Oy.js → colors-DYPOXyWQ.js} +1 -1
  89. package/src/assets/web-panel/assets/{compact-item-CJYJj0oO.js → compact-item-C5r_plPk.js} +1 -1
  90. package/src/assets/web-panel/assets/{createContext-BkbFiKT4.js → createContext-9fWrWmuJ.js} +1 -1
  91. package/src/assets/web-panel/assets/devWarning-DmRz5Pet.js +1 -0
  92. package/src/assets/web-panel/assets/{hasIn-CKvUafVY.js → hasIn-CjZM2-qE.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-BHdailyo.js → index-0DVMoDzF.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-C-WIY-FQ.js → index-B1iUe3Ba.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BOoAQjJz.js → index-B56CI20h.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-AjrSPnvv.js → index-BBVCXmJV.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-CD62EEGo.js → index-BPucc_ES.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-tgO8iza6.js → index-BVFtY8Y4.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-F--HuPG1.js → index-BW-x3mUK.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DTs_D1OL.js → index-B_bTdeou.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BiDWxzbn.js → index-Bglw_kmu.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CBaosWRO.js → index-BsHmE__1.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-B8i0lViZ.js → index-Bti4lpw5.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BJwlEnYo.js → index-ByVDaARk.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CVS8Ymuq.js → index-C3E7xVl1.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CBlBVJ_m.js → index-CDaA4A0b.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DfgCdFrN.js → index-CGqrVLWe.js} +27 -27
  108. package/src/assets/web-panel/assets/{index-Crp8CbRg.js → index-CJJx0kbR.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-JEAGMii9.js → index-CTnaRErh.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Co3OFL0t.js → index-CYoS2cwK.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-b-sbVdjQ.js → index-CcDGi9U1.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-DjYrecNb.js → index-Cd9_KCPs.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-PF-mpv8K.js → index-Co9kBFxY.js} +1 -1
  114. package/src/assets/web-panel/assets/index-CrsL7YBW.js +1 -0
  115. package/src/assets/web-panel/assets/{index-Fuk6t_3K.js → index-CvUR6l7C.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BHdHSX06.js → index-CyUtBxnC.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CUj-l7GM.js → index-D0rFX-xN.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-C9bWmTcO.js → index-D6zyUpD6.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-ChkXUj3f.js → index-DJk9cPrs.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-pyvMVX8r.js → index-DLCoKst9.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-ttDQVhZy.js → index-DNxR1CHs.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-B9Svn0zc.js → index-DWwI0Jjd.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BdutMsne.js → index-DjTB4wPn.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-By5a0T_W.js → index-Du-w2i6G.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-D22zNXiS.js → index-HJdfPSMz.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BSgWxAOG.js → index-I3Qdrfcf.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-qNTtOVi_.js → index-Mipa7XgU.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-DpW9cf3e.js → index-jO4lIpFE.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-C37f6iey.js → index-oerotzoq.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-Cj0OZ-37.js → index-phHPSXlV.js} +1 -1
  131. package/src/assets/web-panel/assets/index-zW69WSDA.js +1 -0
  132. package/src/assets/web-panel/assets/{initDefaultProps-DtrnfsZH.js → initDefaultProps-B7tpABc_.js} +1 -1
  133. package/src/assets/web-panel/assets/{motion-D4zqSaX3.js → motion-DmvaRoVv.js} +1 -1
  134. package/src/assets/web-panel/assets/{move-CGFGOUQj.js → move-DF4X29Wf.js} +1 -1
  135. package/src/assets/web-panel/assets/{mtc-parser-DH2HvkJl.js → mtc-parser-C0cbg4PO.js} +1 -1
  136. package/src/assets/web-panel/assets/{omit-jJ2at5HX.js → omit-DZzcYy0l.js} +1 -1
  137. package/src/assets/web-panel/assets/{pickAttrs-CRyMT1Fv.js → pickAttrs-DaZWfzuH.js} +1 -1
  138. package/src/assets/web-panel/assets/{placementArrow-DZ_CX0Pt.js → placementArrow-CBxOEyvn.js} +1 -1
  139. package/src/assets/web-panel/assets/{responsiveObserve-wruHk-h5.js → responsiveObserve-CGl8NIlO.js} +1 -1
  140. package/src/assets/web-panel/assets/{slide-BGgx8q67.js → slide-Bv330MSZ.js} +1 -1
  141. package/src/assets/web-panel/assets/{statusUtils-Do5M7gKm.js → statusUtils-DJVy5oEM.js} +1 -1
  142. package/src/assets/web-panel/assets/{styleChecker-CRa-GZpX.js → styleChecker-BiY1H5ST.js} +1 -1
  143. package/src/assets/web-panel/assets/{useFlexGapSupport-BxPcq6pj.js → useFlexGapSupport-D1sN2OlQ.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFs-v5O2ZcJm.js → useFs-DxxHawzW.js} +1 -1
  145. package/src/assets/web-panel/assets/{usePersonalDataHub--z2FU0Px.js → usePersonalDataHub-C1HblLrD.js} +1 -1
  146. package/src/assets/web-panel/assets/{vnode-1emidVKd.js → vnode-Dex0jy4T.js} +1 -1
  147. package/src/assets/web-panel/assets/{zoom-CcJaRyHs.js → zoom-Bk1MScAD.js} +1 -1
  148. package/src/assets/web-panel/index.html +1 -1
  149. package/src/command-manifest.json +8 -1
  150. package/src/commands/agent.js +8 -1
  151. package/src/commands/changelog.js +148 -0
  152. package/src/commands/eval.js +88 -23
  153. package/src/commands/plugin.js +10 -0
  154. package/src/commands/team.js +26 -3
  155. package/src/data/changelog.json +199 -0
  156. package/src/gateways/ws/remote-session-protocol.js +9 -2
  157. package/src/harness/remote-command-ledger.js +60 -10
  158. package/src/index.js +2 -0
  159. package/src/lib/agent-sandbox.js +11 -5
  160. package/src/lib/agent-team/task-lease.js +28 -0
  161. package/src/lib/agent-team/team-runner.js +25 -0
  162. package/src/lib/changelog.js +252 -0
  163. package/src/lib/eval/tasks.js +62 -41
  164. package/src/lib/eval/trend.js +7 -1
  165. package/src/lib/lsp/__tests__/lsp-client.test.js +12 -6
  166. package/src/lib/lsp/code-intelligence.js +55 -17
  167. package/src/lib/lsp/lsp-client.js +7 -1
  168. package/src/lib/lsp/lsp-server-registry.js +15 -4
  169. package/src/lib/plugin-runtime/hooks.js +20 -4
  170. package/src/lib/plugin-runtime/install.js +20 -1
  171. package/src/lib/plugin-runtime/monitors.js +20 -4
  172. package/src/lib/plugin-runtime/policy.js +13 -0
  173. package/src/lib/plugin-runtime/remote-source.js +34 -0
  174. package/src/lib/plugin-runtime/signature.js +95 -22
  175. package/src/lib/plugin-security.js +13 -14
  176. package/src/lib/sandbox-egress-proxy.js +38 -2
  177. package/src/lib/sandbox-network-policy.js +26 -4
  178. package/src/lib/telemetry/span-recorder.js +26 -4
  179. package/src/repl/agent-repl.js +12 -8
  180. package/src/runtime/__tests__/auto-pin.test.js +27 -4
  181. package/src/runtime/auto-pin.js +19 -0
  182. package/src/runtime/headless-runner.js +23 -5
  183. package/src/assets/web-panel/assets/ChatBubbleRenderer-8nul8eTq.js +0 -1
  184. package/src/assets/web-panel/assets/devWarning-WDr3_M_N.js +0 -1
  185. package/src/assets/web-panel/assets/index-Dwix3p4g.js +0 -1
  186. package/src/assets/web-panel/assets/index-SHaoZ0CA.js +0 -1
@@ -0,0 +1,252 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ /**
8
+ * The `cc changelog` data source.
9
+ *
10
+ * The canonical update record is the repository-root `CHANGELOG.md`, which is
11
+ * organized by PRODUCT version (`## [v5.0.3.x]`). Each block references the CLI
12
+ * npm version (`chainlesschain 0.162.x`) it shipped. This parser distills that
13
+ * into a CLI-focused view: it keeps only blocks that actually reference a CLI
14
+ * npm version, and within each block keeps only the CLI-relevant sub-sections.
15
+ *
16
+ * At publish time `scripts/build-changelog.mjs` runs `parseChangelog` over the
17
+ * root CHANGELOG and writes the result to `src/data/changelog.json`, which is
18
+ * bundled with the npm package. `loadChangelog()` reads that file (with a
19
+ * fallback to parsing a raw CHANGELOG.md for in-repo dev use).
20
+ */
21
+
22
+ // Signals that a `### ` sub-section HEADING is about the CLI npm package rather
23
+ // than the desktop app / Android / iOS / PDH portions of the same product
24
+ // release. CASE-SENSITIVE on purpose: real CLI notes write the acronym "CLI"
25
+ // (e.g. "cc CLI 0.162.150", "CLI 安全"), whereas PDH/Android bundle-roll notes
26
+ // write the version tag lowercase ("(pdh 0.4.6 / cli 0.162.48)"). Matching only
27
+ // uppercase "CLI" keeps collector releases that merely bundle a cli bump out of
28
+ // the CLI view. Heading-only (not body) for the same reason.
29
+ const CLI_HEADING_SIGNAL = /\bCLI\b|命令行/;
30
+
31
+ // A block with no `### ` sub-sections is a CLI release only if its header or
32
+ // preamble uses CLI-publish language. Same case-sensitivity: a lowercase
33
+ // bundle-roll "cli 0.162.x" mention is not enough.
34
+ const CLI_PUBLISH_SIGNAL = /\bCLI\b|命令行|catch-up/;
35
+
36
+ // CLI npm versions are 3-segment (0.162.x). The `(?<![\d.])` / `(?!\.\d)` guards
37
+ // stop a 4-segment PRODUCT version (v5.0.3.134) from matching as "5.0.3".
38
+ const V = "(?<![\\d.])(\\d+\\.\\d+\\.\\d+)(?!\\.\\d)";
39
+
40
+ /**
41
+ * Extract every CLI npm version token referenced in a block of text and return
42
+ * the "primary" one (the highest semver — the released target in a
43
+ * `0.162.149 → 0.162.150` style note).
44
+ *
45
+ * @param {string} text
46
+ * @returns {{ primary: string|null, all: string[] }}
47
+ */
48
+ export function extractCliVersions(text) {
49
+ const found = new Set();
50
+ const patterns = [
51
+ // "cc CLI 0.162.150", "CLI 0.162.150"
52
+ new RegExp(`(?:cc\\s+)?cli\\s+${V}`, "gi"),
53
+ // "chainlesschain` 0.162.149 → **0.162.150**" (capture the arrow target)
54
+ new RegExp(
55
+ `chainlesschain[\`\\s]+(?:\\d+\\.\\d+\\.\\d+\\s*(?:→|->)\\s*)?\\*{0,2}${V}`,
56
+ "gi",
57
+ ),
58
+ // bare "cli 0.162.71" inside "(pdh 0.4.25 / cli 0.162.71)"
59
+ new RegExp(`\\bcli\\s+${V}`, "gi"),
60
+ // "→ 0.162.150" / "→ **0.162.150**"
61
+ new RegExp(`(?:→|->)\\s*\\*{0,2}${V}`, "gi"),
62
+ ];
63
+ for (const re of patterns) {
64
+ let m;
65
+ while ((m = re.exec(text)) !== null) {
66
+ found.add(m[1]);
67
+ }
68
+ }
69
+ // The CLI npm package (`chainlesschain`) is on the 0.x alpha track; the
70
+ // product line is 5.x. Keeping only major-0 tokens discards any product
71
+ // version (v5.0.3.x) or desktop-app-vue (5.0.3-alpha.x) tokens that slip past
72
+ // the context regexes, so the "primary" is always a real CLI npm version.
73
+ const all = [...found].filter((v) => v.startsWith("0."));
74
+ const primary =
75
+ all.length === 0
76
+ ? null
77
+ : all.reduce(
78
+ (best, v) => (compareSemver(v, best) > 0 ? v : best),
79
+ all[0],
80
+ );
81
+ return { primary, all };
82
+ }
83
+
84
+ /** Numeric semver compare for plain X.Y.Z (no prerelease). */
85
+ export function compareSemver(a, b) {
86
+ const pa = a.split(".").map(Number);
87
+ const pb = b.split(".").map(Number);
88
+ for (let i = 0; i < 3; i++) {
89
+ if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
90
+ }
91
+ return 0;
92
+ }
93
+
94
+ /**
95
+ * Parse a raw CHANGELOG.md string into CLI-focused release entries.
96
+ *
97
+ * @param {string} md
98
+ * @returns {Array<{
99
+ * productVersion: string,
100
+ * cliVersion: string|null,
101
+ * cliVersions: string[],
102
+ * date: string|null,
103
+ * title: string,
104
+ * sections: Array<{ heading: string, body: string }>,
105
+ * }>}
106
+ */
107
+ export function parseChangelog(md) {
108
+ const lines = md.split(/\r?\n/);
109
+ const blocks = [];
110
+ let current = null;
111
+
112
+ for (const line of lines) {
113
+ const h2 = line.match(/^##\s+(.+)$/);
114
+ if (h2 && !line.startsWith("###")) {
115
+ if (current) blocks.push(current);
116
+ current = { header: h2[1].trim(), lines: [] };
117
+ continue;
118
+ }
119
+ if (current) current.lines.push(line);
120
+ }
121
+ if (current) blocks.push(current);
122
+
123
+ const releases = [];
124
+ for (const block of blocks) {
125
+ const parsed = parseBlockHeader(block.header);
126
+ const bodyText = block.lines.join("\n");
127
+ const fullText = block.header + "\n" + bodyText;
128
+
129
+ const { primary, all } = extractCliVersions(fullText);
130
+ // A release must reference a CLI npm version at all.
131
+ if (!primary) continue;
132
+
133
+ // CLI-relevance gate is decided by section HEADINGS (or, for a section-less
134
+ // block, by CLI-publish language) — not by a bare bundled-cli mention.
135
+ const sections = extractCliSections(block.lines, block.header);
136
+ if (!sections) continue;
137
+
138
+ releases.push({
139
+ productVersion: parsed.version,
140
+ cliVersion: primary,
141
+ cliVersions: all.sort(compareSemver),
142
+ date: parsed.date,
143
+ title: parsed.title,
144
+ sections,
145
+ });
146
+ }
147
+ return releases;
148
+ }
149
+
150
+ /** Parse "## [v5.0.3.134] - 2026-07-03 — title" into parts. */
151
+ function parseBlockHeader(header) {
152
+ const tag = header.match(/\[([^\]]+)\]/);
153
+ const version = tag ? tag[1] : header;
154
+ const rest = header.replace(/\[[^\]]+\]\s*/, "");
155
+ const dateMatch = rest.match(/(\d{4}-\d{2}-\d{2})/);
156
+ const date = dateMatch ? dateMatch[1] : null;
157
+ // Title = text after the em dash separator, else whatever remains.
158
+ let title = "";
159
+ const emDash = rest.split(/\s+—\s+/);
160
+ if (emDash.length > 1) {
161
+ title = emDash.slice(1).join(" — ").trim();
162
+ } else {
163
+ title = rest
164
+ .replace(/^-\s*/, "")
165
+ .replace(/\d{4}-\d{2}-\d{2}/, "")
166
+ .trim();
167
+ }
168
+ return { version, date, title };
169
+ }
170
+
171
+ /**
172
+ * Split a block body into `### ` sub-sections and keep only CLI-relevant ones.
173
+ *
174
+ * Returns:
175
+ * - an array of CLI sections, when the block is a CLI release; or
176
+ * - `null`, when the block is NOT a CLI release (all-non-CLI sub-sections, e.g.
177
+ * a PDH/Android collector release that merely bundles a new cli version).
178
+ *
179
+ * @param {string[]} bodyLines
180
+ * @param {string} header
181
+ * @returns {Array<{heading:string, body:string}>|null}
182
+ */
183
+ function extractCliSections(bodyLines, header) {
184
+ const sections = [];
185
+ const preamble = [];
186
+ let cur = null;
187
+ for (const line of bodyLines) {
188
+ const h3 = line.match(/^###\s+(.+)$/);
189
+ if (h3) {
190
+ if (cur) sections.push(cur);
191
+ cur = { heading: h3[1].trim(), lines: [] };
192
+ continue;
193
+ }
194
+ if (cur) cur.lines.push(line);
195
+ else preamble.push(line); // before the first ### (blockquote summary etc.)
196
+ }
197
+ if (cur) sections.push(cur);
198
+
199
+ if (sections.length === 0) {
200
+ // Section-less block: CLI release only if the `##` TITLE names the CLI.
201
+ // The preamble is NOT consulted — every block carries a boilerplate
202
+ // "Version surfaces: CLI 0.162.x → ..." line, so a body match would let
203
+ // every PDH/Android release through.
204
+ if (!CLI_PUBLISH_SIGNAL.test(header)) return null;
205
+ const body = preamble.join("\n").trim();
206
+ return body ? [{ heading: "", body }] : [];
207
+ }
208
+
209
+ const cliSections = sections
210
+ .filter((s) => CLI_HEADING_SIGNAL.test(s.heading))
211
+ .map((s) => ({ heading: s.heading, body: s.lines.join("\n").trim() }));
212
+
213
+ // Has `### ` sections but none are CLI-headed → not a CLI release.
214
+ return cliSections.length > 0 ? cliSections : null;
215
+ }
216
+
217
+ /**
218
+ * Resolve and load the CLI changelog data.
219
+ *
220
+ * Order is CHANGELOG-FIRST so the data can never go stale: when the repo-root
221
+ * CHANGELOG.md is reachable (source checkout / dev / this monorepo) it is parsed
222
+ * live, always reflecting the latest edits. The bundled `src/data/changelog.json`
223
+ * (a build artifact, refreshed at `prepublishOnly`) is only the fallback for an
224
+ * installed npm package, where CHANGELOG.md is not shipped.
225
+ *
226
+ * @returns {{ source: string, releases: Array }}
227
+ */
228
+ export function loadChangelog() {
229
+ const changelogCandidates = [
230
+ path.join(__dirname, "..", "..", "CHANGELOG.md"), // packages/cli/CHANGELOG.md
231
+ path.join(__dirname, "..", "..", "..", "..", "CHANGELOG.md"), // repo root
232
+ ];
233
+ for (const p of changelogCandidates) {
234
+ if (fs.existsSync(p)) {
235
+ return {
236
+ source: "CHANGELOG.md",
237
+ releases: parseChangelog(fs.readFileSync(p, "utf-8")),
238
+ };
239
+ }
240
+ }
241
+ // Installed npm package: CHANGELOG.md isn't shipped, use the bundled artifact.
242
+ const jsonPath = path.join(__dirname, "..", "data", "changelog.json");
243
+ if (fs.existsSync(jsonPath)) {
244
+ try {
245
+ const data = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
246
+ if (Array.isArray(data.releases)) return data;
247
+ } catch {
248
+ /* fall through to empty */
249
+ }
250
+ }
251
+ return { source: null, releases: [] };
252
+ }
@@ -30,6 +30,39 @@ const STRINGUTIL_V2 =
30
30
  " return str.charAt(0).toUpperCase() + str.slice(1);\n" +
31
31
  "}\n";
32
32
 
33
+ // Harness files the agent is told NOT to edit, shared by setup and check so the
34
+ // check can byte-compare them (same anti-cheat as STRINGUTIL_V2 above). A mere
35
+ // presence-regex is neuterable: prepending `console.log('ALL OK');
36
+ // process.exit(0)` keeps the assertion text present while never running it.
37
+ const RUN_CHECKS_MJS = [
38
+ "import { sum } from './calc.js';",
39
+ "const cases = [[1,2,3],[10,5,15],[0,0,0]];",
40
+ "for (const [a,b,want] of cases) {",
41
+ " if (sum(a,b) !== want) {",
42
+ " console.error(`FAIL sum(${a},${b})=${sum(a,b)} want ${want}`);",
43
+ " process.exit(1);",
44
+ " }",
45
+ "}",
46
+ "console.log('ALL OK');",
47
+ "",
48
+ ].join("\n");
49
+
50
+ const BUILD_MJS =
51
+ 'import { banner } from "./app.mjs";\n' +
52
+ 'if (banner() !== "v1.0.0") {\n' +
53
+ ' console.error("BUILD FAIL: " + banner());\n' +
54
+ " process.exit(1);\n" +
55
+ "}\n" +
56
+ 'console.log("BUILD OK");\n';
57
+
58
+ const RUN_MJS =
59
+ 'import { label } from "./app.mjs";\n' +
60
+ 'if (label("hello") !== "Hello") {\n' +
61
+ ' console.error("FAIL: " + label("hello"));\n' +
62
+ " process.exit(1);\n" +
63
+ "}\n" +
64
+ 'console.log("OK");\n';
65
+
33
66
  export const BUILTIN_TASKS = [
34
67
  {
35
68
  id: "create-file",
@@ -134,32 +167,22 @@ export const BUILTIN_TASKS = [
134
167
  );
135
168
  fs.writeFileSync(
136
169
  path.join(dir, "run-checks.mjs"),
137
- [
138
- "import { sum } from './calc.js';",
139
- "const cases = [[1,2,3],[10,5,15],[0,0,0]];",
140
- "for (const [a,b,want] of cases) {",
141
- " if (sum(a,b) !== want) {",
142
- " console.error(`FAIL sum(${a},${b})=${sum(a,b)} want ${want}`);",
143
- " process.exit(1);",
144
- " }",
145
- "}",
146
- "console.log('ALL OK');",
147
- "",
148
- ].join("\n"),
170
+ RUN_CHECKS_MJS,
149
171
  "utf8",
150
172
  );
151
173
  },
152
174
  check: (dir) => {
153
- // Objective: the test harness must actually pass. Guard against the agent
154
- // "fixing" it by neutering run-checks.mjs require the assertions to
155
- // still be present.
175
+ // Objective: the test harness must actually pass. Byte-compare the
176
+ // harness a presence-regex alone is neuterable (prepend an early
177
+ // `console.log('ALL OK'); process.exit(0)` and the assertion text is
178
+ // still there but never runs).
156
179
  const checks = read(dir, "run-checks.mjs");
157
180
  if (checks == null)
158
181
  return { pass: false, detail: "run-checks.mjs missing" };
159
- if (!/sum\(a,\s*b\)\s*!==\s*want/.test(checks)) {
182
+ if (checks !== RUN_CHECKS_MJS) {
160
183
  return {
161
184
  pass: false,
162
- detail: "run-checks.mjs assertions were altered",
185
+ detail: "run-checks.mjs was edited (fix calc.js instead)",
163
186
  };
164
187
  }
165
188
  try {
@@ -487,23 +510,19 @@ export const BUILTIN_TASKS = [
487
510
  'export function banner() {\n return "v" + VERSION;\n}\n',
488
511
  "utf8",
489
512
  );
490
- fs.writeFileSync(
491
- path.join(dir, "build.mjs"),
492
- 'import { banner } from "./app.mjs";\n' +
493
- 'if (banner() !== "v1.0.0") {\n' +
494
- ' console.error("BUILD FAIL: " + banner());\n' +
495
- " process.exit(1);\n" +
496
- "}\n" +
497
- 'console.log("BUILD OK");\n',
498
- "utf8",
499
- );
513
+ fs.writeFileSync(path.join(dir, "build.mjs"), BUILD_MJS, "utf8");
500
514
  },
501
515
  check: (dir) => {
502
516
  const build = read(dir, "build.mjs");
503
517
  if (build == null) return { pass: false, detail: "build.mjs missing" };
504
- // Guard: fix the modules, don't neuter the build harness.
505
- if (!/banner\(\)\s*!==\s*"v1\.0\.0"/.test(build)) {
506
- return { pass: false, detail: "build.mjs assertion was altered" };
518
+ // Guard: fix the modules, don't neuter the build harness. Byte-compare —
519
+ // a presence-regex is satisfiable by a rewritten build.mjs that defines
520
+ // its own banner() and never imports app.mjs.
521
+ if (build !== BUILD_MJS) {
522
+ return {
523
+ pass: false,
524
+ detail: "build.mjs was edited (reconcile config.mjs/app.mjs instead)",
525
+ };
507
526
  }
508
527
  try {
509
528
  const out = execFileSync(process.execPath, ["build.mjs"], {
@@ -534,7 +553,7 @@ export const BUILTIN_TASKS = [
534
553
  "`capitalize` export was renamed to `toTitle` (same behavior). Do all of: " +
535
554
  "(1) bump stringutil to ^2.0.0 in package.json; (2) update app.mjs to " +
536
555
  "import and use `toTitle` instead of `capitalize`; (3) do NOT edit " +
537
- "stringutil.mjs. Afterwards `node run.mjs` must print OK.",
556
+ "stringutil.mjs or run.mjs. Afterwards `node run.mjs` must print OK.",
538
557
  setup: (dir) => {
539
558
  fs.writeFileSync(
540
559
  path.join(dir, "package.json"),
@@ -558,16 +577,7 @@ export const BUILTIN_TASKS = [
558
577
  "export function label(s) {\n return capitalize(s);\n}\n",
559
578
  "utf8",
560
579
  );
561
- fs.writeFileSync(
562
- path.join(dir, "run.mjs"),
563
- 'import { label } from "./app.mjs";\n' +
564
- 'if (label("hello") !== "Hello") {\n' +
565
- ' console.error("FAIL: " + label("hello"));\n' +
566
- " process.exit(1);\n" +
567
- "}\n" +
568
- 'console.log("OK");\n',
569
- "utf8",
570
- );
580
+ fs.writeFileSync(path.join(dir, "run.mjs"), RUN_MJS, "utf8");
571
581
  },
572
582
  check: (dir) => {
573
583
  // The dependency is vendored — the agent must adapt the caller, NOT edit
@@ -580,6 +590,17 @@ export const BUILTIN_TASKS = [
580
590
  detail: "the stringutil dependency was edited (adapt the caller)",
581
591
  };
582
592
  }
593
+ // The run harness is equally off-limits: without this guard an agent
594
+ // could overwrite run.mjs with `console.log("OK")` and pass the textual
595
+ // checks below without app.mjs ever being executed.
596
+ const runner = read(dir, "run.mjs");
597
+ if (runner == null) return { pass: false, detail: "run.mjs missing" };
598
+ if (runner !== RUN_MJS) {
599
+ return {
600
+ pass: false,
601
+ detail: "run.mjs was edited (adapt app.mjs/package.json instead)",
602
+ };
603
+ }
583
604
  const pkg = read(dir, "package.json");
584
605
  if (pkg == null || !/"stringutil"\s*:\s*"[\^~]?2\./.test(pkg)) {
585
606
  return {
@@ -84,7 +84,13 @@ function ordered(records) {
84
84
  * }}
85
85
  */
86
86
  export function computeTrend(runs, { regressionThreshold = 0 } = {}) {
87
- const norm = ordered((runs || []).map((r) => summarizeRun(r)));
87
+ // A dry-run record is always 0% (the no-op agent creates nothing) folding
88
+ // it in would flag every previously-passing task as a regression (or, in the
89
+ // other direction, fake an "all fixed" jump). Smoke-testing with
90
+ // `--dry-run --history` must not poison the release gate.
91
+ const norm = ordered(
92
+ (runs || []).filter((r) => r?.dryRun !== true).map((r) => summarizeRun(r)),
93
+ );
88
94
  const history = norm.map((r) => r.passRate);
89
95
  if (norm.length === 0) {
90
96
  return {
@@ -225,14 +225,20 @@ describe("uri <-> path", () => {
225
225
  describe("normalizeUri", () => {
226
226
  it("lowercases the windows drive letter so client/server keys match", () => {
227
227
  // path.resolve yields C:, servers echo c: — must collapse to one key
228
- expect(normalizeUri("file:///C%3A/proj/a.ts")).toBe(
229
- "file:///c%3A/proj/a.ts",
230
- );
231
- expect(normalizeUri("file:///c%3A/proj/a.ts")).toBe(
232
- "file:///c%3A/proj/a.ts",
233
- );
228
+ expect(normalizeUri("file:///C%3A/proj/a.ts")).toBe("file:///c:/proj/a.ts");
229
+ expect(normalizeUri("file:///c%3A/proj/a.ts")).toBe("file:///c:/proj/a.ts");
234
230
  expect(normalizeUri("file:///C:/proj/a.ts")).toBe("file:///c:/proj/a.ts");
235
231
  });
232
+ it("collapses the %3A and bare-colon drive forms to ONE key (gopls)", () => {
233
+ // The client sends percent-encoded didOpen URIs (file:///C%3A/…);
234
+ // tsserver/pyright echo that form back, but gopls REWRITES to a bare
235
+ // colon (file:///C:/…). Before this canonicalization the two forms were
236
+ // distinct diagnostics-Map keys, so every gopls publish missed the
237
+ // didOpen key and `cc code-intel diag` on Go was permanently empty.
238
+ const ours = normalizeUri("file:///C%3A/Users/u/goproj/broken.go");
239
+ const goplsPublished = normalizeUri("file:///C:/Users/u/goproj/broken.go");
240
+ expect(ours).toBe(goplsPublished);
241
+ });
236
242
  it("leaves POSIX file URIs unchanged", () => {
237
243
  expect(normalizeUri("file:///home/u/a.ts")).toBe("file:///home/u/a.ts");
238
244
  });
@@ -85,11 +85,15 @@ export class CodeIntelligence {
85
85
  async references(filePath, line, col, { includeDeclaration = true } = {}) {
86
86
  const ready = await this._ensure(filePath);
87
87
  if (ready.unavailable) return unavailable(ready.reason);
88
- const result = await ready.client.request("textDocument/references", {
89
- textDocument: { uri: ready.uri },
90
- position: toLspPosition({ line, col }),
91
- context: { includeDeclaration },
92
- });
88
+ const result = await this._readRequest(
89
+ ready.client,
90
+ "textDocument/references",
91
+ {
92
+ textDocument: { uri: ready.uri },
93
+ position: toLspPosition({ line, col }),
94
+ context: { includeDeclaration },
95
+ },
96
+ );
93
97
  return { available: true, locations: this._normalizeLocations(result) };
94
98
  }
95
99
 
@@ -97,7 +101,7 @@ export class CodeIntelligence {
97
101
  async hover(filePath, line, col) {
98
102
  const ready = await this._ensure(filePath);
99
103
  if (ready.unavailable) return unavailable(ready.reason);
100
- const result = await ready.client.request("textDocument/hover", {
104
+ const result = await this._readRequest(ready.client, "textDocument/hover", {
101
105
  textDocument: { uri: ready.uri },
102
106
  position: toLspPosition({ line, col }),
103
107
  });
@@ -108,9 +112,13 @@ export class CodeIntelligence {
108
112
  async documentSymbols(filePath) {
109
113
  const ready = await this._ensure(filePath);
110
114
  if (ready.unavailable) return unavailable(ready.reason);
111
- const result = await ready.client.request("textDocument/documentSymbol", {
112
- textDocument: { uri: ready.uri },
113
- });
115
+ const result = await this._readRequest(
116
+ ready.client,
117
+ "textDocument/documentSymbol",
118
+ {
119
+ textDocument: { uri: ready.uri },
120
+ },
121
+ );
114
122
  return {
115
123
  available: true,
116
124
  symbols: this._normalizeSymbols(result, ready.uri),
@@ -124,7 +132,9 @@ export class CodeIntelligence {
124
132
  if (!seed) return unavailable("no indexable file found to start a server");
125
133
  const ready = await this.manager.ensureFor(seed, this._ensureOpts);
126
134
  if (ready.unavailable) return unavailable(ready.reason);
127
- const result = await ready.client.request("workspace/symbol", { query });
135
+ const result = await this._readRequest(ready.client, "workspace/symbol", {
136
+ query,
137
+ });
128
138
  return { available: true, symbols: this._normalizeSymbols(result) };
129
139
  }
130
140
 
@@ -160,7 +170,8 @@ export class CodeIntelligence {
160
170
  const caps = ready.client.serverCapabilities || {};
161
171
  if (caps.diagnosticProvider) {
162
172
  try {
163
- const report = await ready.client.request(
173
+ const report = await this._readRequest(
174
+ ready.client,
164
175
  "textDocument/diagnostic",
165
176
  { textDocument: { uri: ready.uri } },
166
177
  { timeoutMs },
@@ -182,11 +193,15 @@ export class CodeIntelligence {
182
193
  async renamePreview(filePath, line, col, newName) {
183
194
  const ready = await this._ensure(filePath);
184
195
  if (ready.unavailable) return unavailable(ready.reason);
185
- const result = await ready.client.request("textDocument/rename", {
186
- textDocument: { uri: ready.uri },
187
- position: toLspPosition({ line, col }),
188
- newName,
189
- });
196
+ const result = await this._readRequest(
197
+ ready.client,
198
+ "textDocument/rename",
199
+ {
200
+ textDocument: { uri: ready.uri },
201
+ position: toLspPosition({ line, col }),
202
+ newName,
203
+ },
204
+ );
190
205
  return { available: true, edits: this._normalizeWorkspaceEdit(result) };
191
206
  }
192
207
 
@@ -196,10 +211,33 @@ export class CodeIntelligence {
196
211
 
197
212
  // ---- internals ----
198
213
 
214
+ /**
215
+ * Issue an idempotent READ request with a bounded ContentModified retry.
216
+ * LSP -32801 (ContentModified) means "server state changed mid-request —
217
+ * the result would be stale, ask again"; rust-analyzer emits it freely
218
+ * while its index is still settling after didOpen. Read-only queries can
219
+ * always be retried; mutating requests must NOT go through here.
220
+ */
221
+ async _readRequest(client, method, params, opts) {
222
+ let lastErr;
223
+ for (let attempt = 0; attempt < 4; attempt++) {
224
+ try {
225
+ return await client.request(method, params, opts);
226
+ } catch (err) {
227
+ const contentModified =
228
+ err?.code === -32801 || /content modified/i.test(err?.message || "");
229
+ if (!contentModified) throw err;
230
+ lastErr = err;
231
+ await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
232
+ }
233
+ }
234
+ throw lastErr;
235
+ }
236
+
199
237
  async _locationQuery(method, filePath, line, col) {
200
238
  const ready = await this._ensure(filePath);
201
239
  if (ready.unavailable) return unavailable(ready.reason);
202
- const result = await ready.client.request(method, {
240
+ const result = await this._readRequest(ready.client, method, {
203
241
  textDocument: { uri: ready.uri },
204
242
  position: toLspPosition({ line, col }),
205
243
  });
@@ -297,9 +297,15 @@ export function pathToFileUri(p) {
297
297
  */
298
298
  export function normalizeUri(uri) {
299
299
  if (!uri) return uri;
300
+ // Canonicalize BOTH the drive-letter case AND the colon encoding. Servers
301
+ // differ in the form they publish back: tsserver/pyright echo the client's
302
+ // percent-encoded `c%3A/…`, but gopls rewrites to a bare `C:/…` — keeping
303
+ // the incoming colon form made the two forms distinct Map keys, so every
304
+ // gopls publishDiagnostics missed the didOpen key and Go diagnostics were
305
+ // permanently empty regardless of timeout.
300
306
  return String(uri).replace(
301
307
  /^(file:\/\/\/)([A-Za-z])(%3[Aa]|:)/,
302
- (_m, p, drive, colon) => p + drive.toLowerCase() + colon,
308
+ (_m, p, drive) => p + drive.toLowerCase() + ":",
303
309
  );
304
310
  }
305
311
 
@@ -70,6 +70,13 @@ function tsServer() {
70
70
  // Registry of plugin-contributed servers, keyed by languageId. Wins over
71
71
  // builtins if a plugin explicitly overrides.
72
72
  const pluginServers = new Map();
73
+ // Plugin-contributed extension→languageId overrides, kept SEPARATE from the
74
+ // builtin EXTENSION_LANGUAGE map so `_resetPluginServers` (used by
75
+ // `/reload-plugins`) can fully roll them back. Mutating the builtin map
76
+ // directly meant an uninstalled/untrusted plugin's `.py`→"mylang" remap
77
+ // survived a reload — languageIdForFile kept returning the dead languageId and
78
+ // the builtin server for that extension became unreachable until process exit.
79
+ const pluginExtensions = new Map();
73
80
 
74
81
  /**
75
82
  * Register an extra language server (from a plugin `.lsp.json`).
@@ -87,21 +94,25 @@ export function registerLanguageServer(def) {
87
94
  });
88
95
  if (Array.isArray(def.extensions)) {
89
96
  for (const ext of def.extensions) {
90
- EXTENSION_LANGUAGE[ext.startsWith(".") ? ext : "." + ext] =
91
- def.languageId;
97
+ pluginExtensions.set(
98
+ ext.startsWith(".") ? ext : "." + ext,
99
+ def.languageId,
100
+ );
92
101
  }
93
102
  }
94
103
  }
95
104
 
96
- /** Test/reset hook — clears plugin registrations. */
105
+ /** Test/reset hook — clears plugin registrations (servers AND extension maps). */
97
106
  export function _resetPluginServers() {
98
107
  pluginServers.clear();
108
+ pluginExtensions.clear();
99
109
  }
100
110
 
101
111
  /** Map a file path to its LSP languageId, or null if unsupported. */
102
112
  export function languageIdForFile(filePath) {
103
113
  const ext = path.extname(String(filePath || "")).toLowerCase();
104
- return EXTENSION_LANGUAGE[ext] || null;
114
+ // Plugin overrides win over builtins (same precedence as pluginServers).
115
+ return pluginExtensions.get(ext) || EXTENSION_LANGUAGE[ext] || null;
105
116
  }
106
117
 
107
118
  /**
@@ -60,11 +60,27 @@ export function collectPluginHooks(opts = {}) {
60
60
  for (const p of trusted) {
61
61
  if (!p.manifest || p.manifest.ok !== true) continue;
62
62
  const h = p.manifest.components?.hooks;
63
- if (!h || !h.absPath) continue;
63
+ if (!h) continue;
64
64
  let parsed;
65
- try {
66
- parsed = JSON.parse(_deps.readFileSync(h.absPath, "utf8"));
67
- } catch {
65
+ if (h.absPath) {
66
+ try {
67
+ parsed = JSON.parse(_deps.readFileSync(h.absPath, "utf8"));
68
+ } catch {
69
+ continue;
70
+ }
71
+ } else if (h.inline) {
72
+ // Hooks declared inline in plugin.json — the normalized component keeps
73
+ // only counts, so re-read the raw manifest for the actual entries (same
74
+ // approach as the MCP collector). Without this, inline hooks never fire.
75
+ try {
76
+ const raw = JSON.parse(
77
+ _deps.readFileSync(p.manifest.manifestPath, "utf8"),
78
+ );
79
+ parsed = raw && typeof raw === "object" ? raw.hooks : null;
80
+ } catch {
81
+ continue;
82
+ }
83
+ } else {
68
84
  continue;
69
85
  }
70
86
  const map = normalizeHookMap(parsed);