@herbertgao/pi-extensions 2026.9.0 → 2026.9.1

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 (293) hide show
  1. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +13 -1
  2. package/node_modules/@czottmann/pi-automode/README.md +20 -0
  3. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +1 -1
  4. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +2 -1
  5. package/node_modules/@czottmann/pi-automode/docs/defaults.md +3 -2
  6. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +38 -2
  7. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +5 -3
  8. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +11 -3
  9. package/node_modules/@czottmann/pi-automode/package.json +1 -1
  10. package/node_modules/@pi-plugins/fast-mode/README.md +5 -3
  11. package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
  12. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20 -55
  13. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  14. package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
  15. package/node_modules/pi-lens/CHANGELOG.md +988 -0
  16. package/node_modules/pi-lens/README.md +3 -0
  17. package/node_modules/pi-lens/config/biome/core.jsonc +11 -2
  18. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +13 -1
  19. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +2 -2
  20. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +927 -80
  21. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +13 -4
  22. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +123 -3
  23. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +60 -5
  24. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +2 -2
  25. package/node_modules/pi-lens/dist/clients/bash-file-access.js +1 -3
  26. package/node_modules/pi-lens/dist/clients/biome-client.js +9 -2
  27. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +14 -0
  28. package/node_modules/pi-lens/dist/clients/bootstrap.js +509 -73
  29. package/node_modules/pi-lens/dist/clients/bounded-cache.js +152 -12
  30. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +59 -4
  31. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +113 -0
  32. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +2 -2
  33. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +31 -4
  34. package/node_modules/pi-lens/dist/clients/cache-manager.js +105 -6
  35. package/node_modules/pi-lens/dist/clients/cache-observability.js +44 -18
  36. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +422 -0
  37. package/node_modules/pi-lens/dist/clients/cascade-logger.js +2 -2
  38. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +13 -3
  39. package/node_modules/pi-lens/dist/clients/complexity-client.js +16 -5
  40. package/node_modules/pi-lens/dist/clients/config-core/deny.js +221 -0
  41. package/node_modules/pi-lens/dist/clients/config-core/index.js +47 -0
  42. package/node_modules/pi-lens/dist/clients/config-core/merge.js +357 -0
  43. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +340 -0
  44. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +248 -0
  45. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +171 -0
  46. package/node_modules/pi-lens/dist/clients/config-core/records.js +218 -0
  47. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +125 -0
  48. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +78 -0
  49. package/node_modules/pi-lens/dist/clients/config-core/schema.js +167 -0
  50. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +281 -0
  51. package/node_modules/pi-lens/dist/clients/config-locations.js +160 -0
  52. package/node_modules/pi-lens/dist/clients/config-resolve.js +789 -0
  53. package/node_modules/pi-lens/dist/clients/config-schema.js +197 -0
  54. package/node_modules/pi-lens/dist/clients/config-warn.js +407 -0
  55. package/node_modules/pi-lens/dist/clients/dead-code-client.js +19 -14
  56. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +2 -2
  57. package/node_modules/pi-lens/dist/clients/deadline-utils.js +178 -0
  58. package/node_modules/pi-lens/dist/clients/debug-handles.js +2 -2
  59. package/node_modules/pi-lens/dist/clients/debug-heap.js +3 -3
  60. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +106 -0
  61. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +113 -7
  62. package/node_modules/pi-lens/dist/clients/dependency-checker.js +8 -19
  63. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +12 -9
  64. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +14 -4
  65. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +162 -18
  66. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +97 -0
  67. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +9 -7
  68. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -1
  69. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +380 -26
  70. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -1
  71. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -1
  72. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -1
  73. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -1
  74. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -1
  75. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -1
  76. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -1
  77. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -1
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -1
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -1
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -1
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -1
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +1 -3
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -1
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -1
  85. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -1
  86. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -1
  87. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -1
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -1
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -1
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -1
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +13 -18
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -1
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -1
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -1
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +7 -4
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +1 -3
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -1
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -1
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -1
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -1
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -1
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -1
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -1
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -1
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -1
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -1
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +194 -80
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +84 -15
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -1
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -1
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -1
  121. package/node_modules/pi-lens/dist/clients/disposition-logger.js +3 -3
  122. package/node_modules/pi-lens/dist/clients/effective-config.js +403 -0
  123. package/node_modules/pi-lens/dist/clients/error-class.js +23 -0
  124. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +274 -0
  125. package/node_modules/pi-lens/dist/clients/extension-log.js +2 -2
  126. package/node_modules/pi-lens/dist/clients/feature-hints.js +2 -1
  127. package/node_modules/pi-lens/dist/clients/file-time.js +6 -1
  128. package/node_modules/pi-lens/dist/clients/file-utils.js +112 -9
  129. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +50 -13
  130. package/node_modules/pi-lens/dist/clients/format-service.js +6 -1
  131. package/node_modules/pi-lens/dist/clients/formatters.js +276 -102
  132. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +17 -0
  133. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +114 -19
  134. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -9
  135. package/node_modules/pi-lens/dist/clients/git-guard.js +1 -1
  136. package/node_modules/pi-lens/dist/clients/go-client.js +39 -0
  137. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +252 -0
  138. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +424 -0
  139. package/node_modules/pi-lens/dist/clients/hook-budgets.js +76 -0
  140. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +5 -2
  141. package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
  142. package/node_modules/pi-lens/dist/clients/installer/index.js +71 -52
  143. package/node_modules/pi-lens/dist/clients/instance-reaper.js +104 -230
  144. package/node_modules/pi-lens/dist/clients/instance-registry.js +6 -3
  145. package/node_modules/pi-lens/dist/clients/language-registry.js +595 -0
  146. package/node_modules/pi-lens/dist/clients/latency-logger.js +160 -2
  147. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +13 -0
  148. package/node_modules/pi-lens/dist/clients/lens-config.js +141 -30
  149. package/node_modules/pi-lens/dist/clients/lens-engine.js +30 -37
  150. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +59 -6
  151. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -2
  152. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +3 -1
  153. package/node_modules/pi-lens/dist/clients/lsp/client.js +168 -30
  154. package/node_modules/pi-lens/dist/clients/lsp/config.js +483 -158
  155. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +1 -1
  156. package/node_modules/pi-lens/dist/clients/lsp/edits.js +15 -2
  157. package/node_modules/pi-lens/dist/clients/lsp/index.js +676 -71
  158. package/node_modules/pi-lens/dist/clients/lsp/language.js +18 -169
  159. package/node_modules/pi-lens/dist/clients/lsp/launch.js +2 -1
  160. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +5 -16
  161. package/node_modules/pi-lens/dist/clients/lsp/server.js +284 -147
  162. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +116 -23
  163. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +2 -7
  164. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +9 -2
  165. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +1 -1
  166. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +163 -21
  167. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +36 -0
  168. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +33 -4
  169. package/node_modules/pi-lens/dist/clients/mcp/session.js +5 -16
  170. package/node_modules/pi-lens/dist/clients/memory-sampler.js +8 -3
  171. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +2 -4
  172. package/node_modules/pi-lens/dist/clients/module-report.js +20 -45
  173. package/node_modules/pi-lens/dist/clients/mutating-tool.js +651 -0
  174. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +368 -0
  175. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +240 -0
  176. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +247 -20
  177. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +101 -0
  178. package/node_modules/pi-lens/dist/clients/observed-mutation.js +1215 -0
  179. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +68 -22
  180. package/node_modules/pi-lens/dist/clients/opengrep-config.js +4 -0
  181. package/node_modules/pi-lens/dist/clients/package-manager.js +195 -26
  182. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +359 -80
  183. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +23 -7
  184. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +21 -2
  185. package/node_modules/pi-lens/dist/clients/path-utils.js +428 -13
  186. package/node_modules/pi-lens/dist/clients/persist-debounce.js +8 -1
  187. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +114 -0
  188. package/node_modules/pi-lens/dist/clients/pipeline.js +77 -16
  189. package/node_modules/pi-lens/dist/clients/probe-home-state.js +228 -0
  190. package/node_modules/pi-lens/dist/clients/process-bridge.js +66 -0
  191. package/node_modules/pi-lens/dist/clients/process-snapshot.js +68 -0
  192. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
  193. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
  194. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +51 -20
  195. package/node_modules/pi-lens/dist/clients/project-lens-config.js +393 -122
  196. package/node_modules/pi-lens/dist/clients/project-snapshot.js +52 -24
  197. package/node_modules/pi-lens/dist/clients/python-environment.js +217 -0
  198. package/node_modules/pi-lens/dist/clients/python-provenance.js +639 -0
  199. package/node_modules/pi-lens/dist/clients/quiet-window.js +6 -1
  200. package/node_modules/pi-lens/dist/clients/read-bridge.js +13 -21
  201. package/node_modules/pi-lens/dist/clients/read-expansion.js +9 -46
  202. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +15 -4
  203. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +364 -212
  204. package/node_modules/pi-lens/dist/clients/read-guard.js +21 -0
  205. package/node_modules/pi-lens/dist/clients/resource-sampler.js +195 -96
  206. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +152 -109
  207. package/node_modules/pi-lens/dist/clients/review-graph/service.js +55 -3
  208. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +7 -15
  209. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +10 -74
  210. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +9 -3
  211. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +18 -2
  212. package/node_modules/pi-lens/dist/clients/runtime-context.js +49 -16
  213. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +25 -6
  214. package/node_modules/pi-lens/dist/clients/runtime-session.js +139 -22
  215. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +168 -35
  216. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +797 -243
  217. package/node_modules/pi-lens/dist/clients/runtime-turn.js +1028 -53
  218. package/node_modules/pi-lens/dist/clients/rust-client.js +31 -0
  219. package/node_modules/pi-lens/dist/clients/safe-spawn.js +33 -10
  220. package/node_modules/pi-lens/dist/clients/sanitize.js +7 -1
  221. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +1 -3
  222. package/node_modules/pi-lens/dist/clients/security-scan-client.js +3 -0
  223. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +1 -1
  224. package/node_modules/pi-lens/dist/clients/session-start-observability.js +79 -0
  225. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +12 -3
  226. package/node_modules/pi-lens/dist/clients/sgconfig.js +6 -1
  227. package/node_modules/pi-lens/dist/clients/skills-resolver.js +105 -0
  228. package/node_modules/pi-lens/dist/clients/smells-rollup.js +2 -2
  229. package/node_modules/pi-lens/dist/clients/string-utils.js +13 -0
  230. package/node_modules/pi-lens/dist/clients/subagent-mode.js +17 -4
  231. package/node_modules/pi-lens/dist/clients/test-runner-client.js +176 -39
  232. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +239 -0
  233. package/node_modules/pi-lens/dist/clients/tool-definition.js +41 -1
  234. package/node_modules/pi-lens/dist/clients/tool-policy.js +91 -21
  235. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +40 -40
  236. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +86 -48
  237. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +2 -2
  238. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +81 -1
  239. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +51 -46
  240. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +28 -0
  241. package/node_modules/pi-lens/dist/clients/typos-config.js +5 -0
  242. package/node_modules/pi-lens/dist/clients/user-notify.js +6 -2
  243. package/node_modules/pi-lens/dist/clients/widget-state.js +191 -26
  244. package/node_modules/pi-lens/dist/clients/word-index-logger.js +2 -2
  245. package/node_modules/pi-lens/dist/clients/word-index-store.js +20 -5
  246. package/node_modules/pi-lens/dist/clients/word-index.js +243 -31
  247. package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -1
  248. package/node_modules/pi-lens/dist/clients/zizmor-config.js +3 -0
  249. package/node_modules/pi-lens/dist/index.js +64335 -54300
  250. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -1
  251. package/node_modules/pi-lens/dist/mcp/server.js +127 -5
  252. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +583 -0
  253. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +129 -0
  254. package/node_modules/pi-lens/dist/tools/effective-config.js +89 -0
  255. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +95 -24
  256. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +67 -80
  257. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +37 -16
  258. package/node_modules/pi-lens/dist/tools/shared.js +0 -1
  259. package/node_modules/pi-lens/docs/agent-guide.md +64 -1
  260. package/node_modules/pi-lens/docs/configuration.md +222 -0
  261. package/node_modules/pi-lens/docs/dependencies.md +3 -3
  262. package/node_modules/pi-lens/docs/features.md +44 -5
  263. package/node_modules/pi-lens/docs/language-coverage.md +2 -2
  264. package/node_modules/pi-lens/docs/pi-lens-fixer.md +25 -0
  265. package/node_modules/pi-lens/docs/pi-lens-investigator.md +25 -0
  266. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +27 -0
  267. package/node_modules/pi-lens/docs/pi-lens-subagent.md +38 -0
  268. package/node_modules/pi-lens/docs/pi-lens-warden.md +55 -0
  269. package/node_modules/pi-lens/docs/public-api-stability.md +359 -0
  270. package/node_modules/pi-lens/docs/release-qa-baseline.md +182 -0
  271. package/node_modules/pi-lens/docs/subagent-compat.md +110 -30
  272. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +2 -2
  273. package/node_modules/pi-lens/package.json +15 -23
  274. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
  275. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
  276. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
  277. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +192 -1
  278. package/node_modules/pi-lens/scripts/install-selftest.mjs +58 -2
  279. package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +129 -0
  280. package/node_modules/pi-lens/scripts/rpc-load-check.mjs +3 -1
  281. package/node_modules/pi-web-access/CHANGELOG.md +28 -0
  282. package/node_modules/pi-web-access/README.md +62 -15
  283. package/node_modules/pi-web-access/curator-page.ts +3 -1
  284. package/node_modules/pi-web-access/curator-server.ts +5 -1
  285. package/node_modules/pi-web-access/gemini-search.ts +8 -4
  286. package/node_modules/pi-web-access/github-extract.ts +242 -1
  287. package/node_modules/pi-web-access/index.ts +118 -64
  288. package/node_modules/pi-web-access/mistral-search.ts +281 -0
  289. package/node_modules/pi-web-access/package.json +2 -2
  290. package/node_modules/pi-web-access/perplexity.ts +14 -1
  291. package/node_modules/pi-web-access/utils.ts +23 -6
  292. package/node_modules/pi-web-access/xai-search.ts +96 -33
  293. package/package.json +5 -5
@@ -4,35 +4,44 @@
4
4
  * Allows users to define custom LSP servers and override initialization options
5
5
  * for built-in servers via configuration.
6
6
  *
7
- * Config file: .pi-lens/lsp.json (or .pi-lens.json, pi-lsp.json)
7
+ * CANONICAL LOCATION (#2426): the `lsp` namespace of `.pi-lens.json` (project)
8
+ * and `~/.pi-lens/config.json` (machine-global). `.pi-lens/lsp.json`,
9
+ * `pi-lsp.json` and `~/.pi-lens/lsp.json` — and the four LSP keys at the ROOT
10
+ * of a canonical file — are still read for their deprecation window
11
+ * (`DEPRECATED_CONFIG_SURFACES`) and emit one migration warning per
12
+ * `(file, key)` naming where the setting moves. The canonical spelling wins
13
+ * every collision. `docs/configuration.md` documents the full lookup order;
14
+ * discovery itself lives in `clients/config-resolve.ts`, which this module and
15
+ * the other two loaders now share.
8
16
  *
9
- * Example — custom server:
17
+ * Example — custom server (canonical spelling, inside `.pi-lens.json`):
10
18
  * {
11
- * "servers": {
12
- * "my-server": {
13
- * "name": "My Custom LSP",
14
- * "extensions": [".myext"],
15
- * "command": "my-lsp-server",
16
- * "args": ["--stdio"],
17
- * "rootMarkers": ["package.json"]
19
+ * "lsp": {
20
+ * "servers": {
21
+ * "my-server": {
22
+ * "name": "My Custom LSP",
23
+ * "extensions": [".myext"],
24
+ * "command": "my-lsp-server",
25
+ * "args": ["--stdio"],
26
+ * "rootMarkers": ["package.json"]
27
+ * }
18
28
  * }
19
29
  * }
20
30
  * }
21
31
  *
22
32
  * Example — override initializationOptions for a built-in server:
23
33
  * {
24
- * "serverOverrides": {
25
- * "rust": {
26
- * "initializationOptions": {
27
- * "check": { "command": "clippy", "allTargets": true },
28
- * "cargo": { "features": "all", "targetDir": true }
29
- * }
30
- * },
31
- * "nix": {
32
- * "initializationOptions": {
33
- * "nixpkgs": { "expr": "import <nixpkgs> {}" },
34
- * "options": {
35
- * "home_manager": { "expr": "(builtins.getFlake (toString ./.)).homeConfigurations.me.options" }
34
+ * "lsp": {
35
+ * "serverOverrides": {
36
+ * "rust": {
37
+ * "initializationOptions": {
38
+ * "check": { "command": "clippy", "allTargets": true },
39
+ * "cargo": { "features": "all", "targetDir": true }
40
+ * }
41
+ * },
42
+ * "nix": {
43
+ * "initializationOptions": {
44
+ * "nixpkgs": { "expr": "import <nixpkgs> {}" }
36
45
  * }
37
46
  * }
38
47
  * }
@@ -46,98 +55,278 @@
46
55
  * Server IDs match the `id` field of each built-in server definition in
47
56
  * clients/lsp/server.ts (e.g. "rust", "nix", "bash", "python", "go", "ts").
48
57
  */
49
- import { logExtension } from "../extension-log.js";
50
- import { notifyUserDegradation } from "../user-notify.js";
51
- import fs from "node:fs/promises";
58
+ import { resetIgnoredConfigWarnCache } from "../config-warn.js";
59
+ import * as os from "node:os";
52
60
  import path from "node:path";
53
- import { BoundedLruCache } from "../bounded-cache.js";
61
+ import { lspSectionOf, reportConfigReadFailure, reportPiLensConfigRecords, resolvePiLensConfig, summarizeConfigResolution, } from "../config-resolve.js";
54
62
  import { getGlobalPiLensDir } from "../file-utils.js";
63
+ import { claimPhaseOncePerSession, currentSessionRecordId, logLatency, releaseOncePerSessionPhase, releasePhaseClaim, } from "../latency-logger.js";
64
+ import { getPiLensGlobalConfigPath } from "../lens-config.js";
65
+ import { normalizeFilePath } from "../path-utils.js";
66
+ import { logSessionStart } from "../sessionstart-logger.js";
55
67
  import { launchLSP } from "./launch.js";
56
- import { registerSessionRoot, resetSessionRootsForTests, } from "./session-roots.js";
68
+ import { registerSessionRoot, resetSessionRootsForTests, sessionRootConfigEntries, setSessionRootConfig, } from "./session-roots.js";
57
69
  import { createRootDetector, LSP_SERVERS, resetLSPCaseSensitivityState, } from "./server.js";
58
70
  // --- Config Loading ---
59
- const CONFIG_PATHS = [".pi-lens/lsp.json", ".pi-lens.json", "pi-lsp.json"];
60
- function warnInvalidLSPConfig(configPath, error) {
61
- const reason = error instanceof Error ? error.message : String(error);
62
- const message = `ignoring invalid LSP config ${configPath}: ${reason}`;
63
- logExtension({
64
- subsystem: "lsp-config",
65
- level: "warn",
66
- message,
67
- metadata: { configPath, reason },
68
- });
69
- // HUMAN-audience too: the user's own lsp.json is being ignored (#1333).
70
- notifyUserDegradation(`pi-lens: ${message}`);
71
+ /**
72
+ * For tests that need to force the warn-once cache to reset between cases —
73
+ * the LSP loader's counterpart to `resetGlobalConfigWarnCache` in
74
+ * lens-config.ts and to the clear folded into `resetProjectLensConfigCache`
75
+ * (#2418 review round 3, S3). Without it, this loader's cases had to lean on
76
+ * every fixture landing in a fresh temp path to stay unlatched, which is a
77
+ * property of the fixture rather than of the test.
78
+ */
79
+ export function resetLSPConfigWarnCache() {
80
+ resetIgnoredConfigWarnCache("lsp-config");
71
81
  }
72
- async function readLSPConfig(configPath) {
73
- let content;
74
- try {
75
- content = await fs.readFile(configPath, "utf-8");
76
- }
77
- catch (error) {
78
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
79
- return undefined;
80
- }
81
- warnInvalidLSPConfig(configPath, error);
82
- return undefined;
83
- }
84
- try {
85
- const parsed = JSON.parse(content);
86
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
87
- throw new TypeError("expected a JSON object");
88
- }
89
- return parsed;
90
- }
91
- catch (error) {
92
- warnInvalidLSPConfig(configPath, error);
93
- return undefined;
94
- }
82
+ function asRecord(value) {
83
+ return value && typeof value === "object" && !Array.isArray(value)
84
+ ? value
85
+ : undefined;
95
86
  }
96
- function mergeLSPConfigs(globalConfig, projectConfig) {
97
- const merged = { ...globalConfig, ...projectConfig };
98
- const servers = { ...globalConfig.servers, ...projectConfig.servers };
99
- if (Object.keys(servers).length > 0)
100
- merged.servers = servers;
101
- const serverOverrides = {
102
- ...globalConfig.serverOverrides,
103
- ...projectConfig.serverOverrides,
104
- };
105
- if (Object.keys(serverOverrides).length > 0) {
106
- merged.serverOverrides = serverOverrides;
107
- }
108
- if (!Object.hasOwn(projectConfig, "disabledServers")) {
109
- merged.disabledServers = globalConfig.disabledServers;
110
- }
111
- if (!Object.hasOwn(projectConfig, "warmFiles")) {
112
- merged.warmFiles = globalConfig.warmFiles;
87
+ /** The session-scoped phase this loader claims (#2526). */
88
+ const CONFIG_RESOLVED_PHASE = "config_resolved";
89
+ /**
90
+ * Write the session's ONE `config_resolved` record (#2526).
91
+ *
92
+ * Positive observability for the Phase 0 config stack: before this, a correct
93
+ * canonical-only resolution proved itself only by the ABSENCE of
94
+ * `PILENS_CFG_*` rows — the silent-success gap AGENTS.md warns about.
95
+ *
96
+ * REDACTED by construction, not by mode. Everything it can say comes from
97
+ * `summarizeConfigResolution`, the same projection `pilens_effective_config`
98
+ * embeds: document PATHS (home-relative), tiers, the legacy flag, per-tier
99
+ * leaf counts, and a record COUNT. No config value, and no absolute `$HOME`,
100
+ * can reach the log through it.
101
+ *
102
+ * NO SECOND RESOLUTION (#2513's facade rule): it reads the resolution its
103
+ * caller already performed. And it adds no `await` — `logLatency` and
104
+ * `logSessionStart` are synchronous buffered writers — so the session-start
105
+ * hook path gains nothing to wait on (#2523).
106
+ *
107
+ * ONE row per session AND SERVED ROOT, claimed through the logger's own
108
+ * session-scoped bookkeeping rather than a latch kept here. `loadLSPConfig` is
109
+ * the funnel every config resolution goes through (`initLSPConfig` at session
110
+ * start and at each served root, the MCP `ensureReady` boot,
111
+ * `ensureLSPConfigInitialized` on the first edit), so an unconditional write
112
+ * would emit one row per resolution and the per-session count the smell
113
+ * analyzer joins on would be unrecoverable from the log.
114
+ *
115
+ * The ROOT is part of the claim (#2526 review round 2, F3). A warm MCP server
116
+ * calls `ensureReady` per served root (`mcp/server.ts`), and a phase-only
117
+ * claim recorded the FIRST root's documents and nothing else — project B's
118
+ * legacy config document never reached a row, so the
119
+ * "legacy-document-with-no-records" smell structurally could not fire for it.
120
+ * `configResolutionKey` keys it (fold, canonicalize, fold — see below), the
121
+ * repo-wide rule for every path-keyed map (#210): a `/`-vs-`\` spelling of
122
+ * one root must not buy a second row.
123
+ */
124
+ /**
125
+ * THE (session, root) key of every config-resolution record: the
126
+ * `config_resolution_pending` mark, the `config_resolved` row and claim, and
127
+ * the release of that claim when the root is evicted.
128
+ *
129
+ * Fold separators, THEN canonicalize, then fold again — in that order, and
130
+ * neither step alone is enough (#2518 review F6). `normalizeFilePath` folds
131
+ * separators and Windows casing but is the IDENTITY on a POSIX path that
132
+ * merely needs canonicalizing, so `"/proj/"` and `"/proj"` produced two
133
+ * different claim keys. `path.resolve` fixes that and nothing else — and it
134
+ * cannot run first: on POSIX a backslash is an ordinary filename character,
135
+ * so resolving `"/proj\\sub"` before folding yields
136
+ * `<process.cwd()>/proj\sub`, a different root entirely. That is the #2526 R2
137
+ * spelling case (`tests/clients/config-resolved-phase.test.ts`, "a /-vs-\
138
+ * spelling of one root does not buy a second row"), which caught exactly this
139
+ * ordering while it was wrong. Resolving here rather than at
140
+ * each caller is what makes the keys identical BY CONSTRUCTION: the callers
141
+ * do not agree today (`initLSPConfig` passes its registry-resolved cwd,
142
+ * `clients/runtime-session.ts`'s two warm-path `loadLSPConfig` calls pass the
143
+ * session cwd verbatim, and `analysisRoot` from `.pi-lens.json` can carry a
144
+ * trailing slash), and a per-caller normalization is exactly the shape 1
145
+ * defect this key keeps being bitten by — the write form and the read form
146
+ * diverging because two sites each folded the path their own way.
147
+ *
148
+ * It is also the key `clients/lsp/session-roots.ts` stores its roots under,
149
+ * once composed: the registry key is `path.resolve(cwd)`, and this function
150
+ * applied to it is idempotent, so `forgetConfigResolvedClaims` releases
151
+ * exactly the claim `recordConfigResolved` took.
152
+ */
153
+ function configResolutionKey(cwd) {
154
+ // Fold first (separators, Windows casing), canonicalize second (trailing
155
+ // separators, relative segments), fold again so the result keeps the
156
+ // forward-slash shape every producer and the analyzer already compare on.
157
+ return normalizeFilePath(path.resolve(normalizeFilePath(cwd)));
158
+ }
159
+ /**
160
+ * Announce, at the instant a resolution is actually about to be attempted,
161
+ * that THIS session expects a `config_resolved` row (#2526 review round 3,
162
+ * S1).
163
+ *
164
+ * Round 2 PREDICTED this from three flags in `runtime-session.ts`
165
+ * (`no-lsp`/`subagent`/`warm-attach`), each mirroring one gate the resolution
166
+ * paths themselves check — and the mirror drifted: quick and minimal mode's
167
+ * SECOND session in one process schedule no resolution at all
168
+ * (`ensureLSPConfigInitialized`'s `_lspConfigInitializedCwds` memo skips the
169
+ * synchronous resolve past the first session, and only FULL mode's deferred
170
+ * `setImmediate` reschedules one), so the predicate kept saying
171
+ * `expected=true` for a session that was never going to resolve, and the
172
+ * analyzer flagged a real session every time under `PI_LENS_STARTUP_MODE=quick`
173
+ * or `=minimal`. STOP PREDICTING: this function is called from `loadLSPConfig`
174
+ * itself rather than from any of its callers' decision points, because
175
+ * `loadLSPConfig` is the ONE funnel every production caller reaches —
176
+ * `initLSPConfig` (`ensureLSPConfigInitialized`'s first-session-per-cwd path
177
+ * in index.ts, the MCP `ensureReady` boot via `ensureLspConfig`,
178
+ * `igniteWarmFiles`/`igniteDominantLanguageWarm`) and the two direct calls in
179
+ * `runtime-session.ts` (the full-mode deferred `setImmediate` load and the
180
+ * quick-mode warm-up's LSP pre-warm). Placing the mark here rather than at
181
+ * each of those four sites buys three things: (a) it fires exactly when a
182
+ * resolution is genuinely attempted, never merely decided likely; (b) a
183
+ * session that never reaches ANY of those sites — quick or minimal mode's
184
+ * second-and-later session in a process, for the same root — never gets a
185
+ * mark, so the analyzer silently excludes it instead of counting it against;
186
+ * (c) this mark and {@link recordConfigResolved}'s row are stamped from the
187
+ * SAME `currentSessionRecordId()` read, a few lines apart in one function
188
+ * call, so they can never disagree about which session they belong to —
189
+ * writing the mark at each scheduling site instead (e.g. before a
190
+ * `setImmediate`) would let a session boundary between scheduling and
191
+ * execution attribute the two halves to different sessions.
192
+ *
193
+ * Written unconditionally — before {@link resolvePiLensConfig} can throw —
194
+ * so a resolution that is entered but fails mid-flight leaves a mark with no
195
+ * row, which the analyzer's join reads as "expected and never happened"
196
+ * rather than silently matching "never expected at all".
197
+ *
198
+ * Carries `root=<configResolutionKey(cwd)>` (#2552 review round 4, MEDIUM): the
199
+ * warm MCP server keeps ONE session id for the life of the process but calls
200
+ * this once per SERVED ROOT (`ensureReady` per root, `mcp/server.ts`) — a
201
+ * session-id-only mark let one root's `config_resolved` row silently clear
202
+ * every OTHER root's deficit under the same id, reintroducing review round
203
+ * 2's F3 defect one layer up, at the analyzer's join instead of the claim.
204
+ * The value is the SAME {@link configResolutionKey} string
205
+ * {@link recordConfigResolved} uses for its claim scope and its row's
206
+ * `filePath`, so the mark and the row compare equal without the analyzer
207
+ * re-deriving any path normalization of its own.
208
+ */
209
+ function publishConfigResolutionPending(cwd) {
210
+ logSessionStart(`session_start config_resolution_pending session=${currentSessionRecordId()} ` +
211
+ `root=${configResolutionKey(cwd)}`);
212
+ }
213
+ function recordConfigResolved(cwd, resolution, lspConfig, homeDir, resolveMs) {
214
+ // #2552 review round 4: computed ONCE and reused for the claim scope, the
215
+ // row's `filePath`, and the sessionstart line's `root=` — one normalization
216
+ // of `cwd`, so the pending mark, the row, and the claim can never disagree
217
+ // about which root they name. #2518 review F6 moved that one normalization
218
+ // into `configResolutionKey`, so callers passing the same root spelled
219
+ // differently cannot disagree either.
220
+ const root = configResolutionKey(cwd);
221
+ if (!claimPhaseOncePerSession(CONFIG_RESOLVED_PHASE, root)) {
222
+ return;
113
223
  }
114
- return merged;
224
+ const summary = summarizeConfigResolution(resolution, homeDir);
225
+ // #2526 R2 F2: the session identity the analyzer joins on. Written on both
226
+ // sinks from this ONE call, so the latency row and the sessionstart line
227
+ // carry the same id by construction rather than by agreement.
228
+ const sessionId = currentSessionRecordId();
229
+ // The deny UNION's size, read off the same projection the gates consume
230
+ // (`lspConfigOf`) rather than re-read from the raw value — one definition of
231
+ // "which servers are denied", so the record cannot describe a different
232
+ // deny set than the one that actually suppresses a server.
233
+ const deniedServers = lspConfig.disabledServers?.length ?? 0;
234
+ const legacyDocuments = summary.documents.filter((document) => document.legacy).length;
235
+ logLatency({
236
+ type: "phase",
237
+ phase: CONFIG_RESOLVED_PHASE,
238
+ filePath: root,
239
+ durationMs: resolveMs,
240
+ metadata: {
241
+ sessionId,
242
+ documents: summary.documents,
243
+ countsByTier: summary.countsByTier,
244
+ recordCount: summary.recordCount,
245
+ deniedServers,
246
+ resolveMs,
247
+ },
248
+ });
249
+ logSessionStart(`config resolved documents=${summary.documents.length} legacy=${legacyDocuments} ` +
250
+ `records=${summary.recordCount} deniedServers=${deniedServers} resolveMs=${resolveMs} ` +
251
+ `session=${sessionId} root=${root}`);
252
+ }
253
+ export async function loadLSPConfig(cwd, homeDir = os.homedir(), options = {}) {
254
+ const reporting = options.report !== false;
255
+ // #2526 review round 3, S1: announce the attempt before anything that
256
+ // resolves it can throw — see `publishConfigResolutionPending`'s doc
257
+ // comment for why this lives here rather than at each caller.
258
+ publishConfigResolutionPending(cwd);
259
+ const resolveStartedAt = Date.now();
260
+ const resolution = resolvePiLensConfig({
261
+ cwd,
262
+ globalDir: getGlobalPiLensDir(),
263
+ // `homeDir` is threaded, not dropped: it is the `$HOME` this call resolves
264
+ // against, and the canonical global config is `$HOME/.pi-lens/config.json`
265
+ // whenever `PI_LENS_CONFIG_PATH` does not override it. Production behavior
266
+ // is unchanged (the default IS `os.homedir()`); what it buys is that the
267
+ // seam means the same `$HOME` on both sides of the resolution, so a test
268
+ // can exercise a relocated `PI_LENS_HOME` without reaching the real one.
269
+ globalConfigPath: getPiLensGlobalConfigPath(homeDir),
270
+ homeDir,
271
+ // The subsystem comes from the failing DOCUMENT, not from this loader
272
+ // (#2445). This resolution opens `~/.pi-lens/config.json` and
273
+ // `.pi-lens.json` as well as the LSP-scoped files, and reporting all of
274
+ // them as `lsp-config` announced an "invalid LSP config" for a file whose
275
+ // contents are pi-lens settings. An LSP-scoped file still reports here.
276
+ ...(reporting ? { onReadError: reportConfigReadFailure } : {}),
277
+ });
278
+ // EVERY record this resolution produced (#2426 review round 3, F1) — not
279
+ // filtered to what this loader "owns". `reportPiLensConfigRecords` derives
280
+ // the reporting subsystem per record; the warn-once latch collapses this
281
+ // loader's report with the pi-lens loaders' report of the SAME record into
282
+ // one notice. Filtering here (as round 2 did) silently dropped a pi-lens-
283
+ // owned record from a document only this multi-file resolution discovered.
284
+ if (reporting)
285
+ reportPiLensConfigRecords(resolution.records);
286
+ const config = lspConfigOf(resolution.value);
287
+ // #2526: the session's one positive record that config resolution HAPPENED,
288
+ // written where the resolution actually exists. `report: false` is not a
289
+ // gate here — the option suppresses USER-FACING notices, and a record in
290
+ // the latency log is not a notice; gating on it would let a session whose
291
+ // only resolution was a quiet one look, in the log, like a session that
292
+ // never resolved config at all.
293
+ recordConfigResolved(cwd, resolution, config, homeDir, Date.now() - resolveStartedAt);
294
+ return config;
115
295
  }
116
296
  /**
117
- * Load LSP configuration, with project settings overriding machine-global
118
- * settings from ~/.pi-lens/lsp.json.
297
+ * THE resolved-value → {@link LSPConfig} projection: read the `lsp` namespace
298
+ * and keep the four keys the gates consume, each only when the resolution
299
+ * actually produced it in the right shape.
300
+ *
301
+ * Exported and named in #2427 review round 5 (F-R4-1). `effectiveConfig`
302
+ * needs the LSP config AND the provenance of the same resolution, and
303
+ * `loadLSPConfig` returns only the former — it discards the resolution it
304
+ * just performed. Round 4 therefore had the query call `loadLSPConfig` for
305
+ * the gates and run a SECOND `resolvePiLensConfig` for the provenance, at a
306
+ * different root, and the two disagreed: the gates answered from the file's
307
+ * own directory while the reported spec, provenance and document list came
308
+ * from the workspace root. With the projection spelled here the query performs
309
+ * ONE resolution and derives both halves from it, and the projection is still
310
+ * a single definition, so a derived config and a session-registered one cannot
311
+ * disagree about what a document means.
119
312
  */
120
- export async function loadLSPConfig(cwd) {
121
- let projectConfig;
122
- let dir = path.resolve(cwd);
123
- while (true) {
124
- for (const configPath of CONFIG_PATHS) {
125
- const fullPath = path.join(dir, configPath);
126
- const config = await readLSPConfig(fullPath);
127
- if (config) {
128
- projectConfig = config;
129
- break;
130
- }
131
- }
132
- if (projectConfig)
133
- break;
134
- const parent = path.dirname(dir);
135
- if (parent === dir)
136
- break;
137
- dir = parent;
313
+ export function lspConfigOf(value) {
314
+ const section = lspSectionOf(value);
315
+ const config = {};
316
+ const servers = asRecord(section.servers);
317
+ if (servers)
318
+ config.servers = servers;
319
+ const serverOverrides = asRecord(section.serverOverrides);
320
+ if (serverOverrides) {
321
+ config.serverOverrides = serverOverrides;
322
+ }
323
+ if (Array.isArray(section.disabledServers)) {
324
+ config.disabledServers = section.disabledServers;
138
325
  }
139
- const globalConfig = (await readLSPConfig(path.join(getGlobalPiLensDir(), "lsp.json"))) ?? {};
140
- return mergeLSPConfigs(globalConfig, projectConfig ?? {});
326
+ if (Array.isArray(section.warmFiles)) {
327
+ config.warmFiles = section.warmFiles;
328
+ }
329
+ return config;
141
330
  }
142
331
  // --- Custom Server Factory ---
143
332
  /**
@@ -166,7 +355,6 @@ const EMPTY_CONFIG = {
166
355
  disabledServerIds: new Set(),
167
356
  serverOverrides: new Map(),
168
357
  };
169
- const workspaceConfigs = new BoundedLruCache(32);
170
358
  /** In-flight config initialization promises to prevent duplicate concurrent loads */
171
359
  const configInFlight = new Map();
172
360
  function normalizeWorkspacePath(cwd) {
@@ -180,7 +368,13 @@ function isSameOrChildPath(filePath, candidateRoot) {
180
368
  function getConfigForFile(filePath) {
181
369
  const resolvedFilePath = path.resolve(filePath);
182
370
  let bestMatch;
183
- for (const [root, config] of workspaceConfigs) {
371
+ // #2518: the per-root configs ARE the session-root registry's values, so
372
+ // this walk sees a config for exactly the roots that registry still serves.
373
+ // `undefined` is a root whose first load is still in flight — the same
374
+ // "no entry yet" state this walk skipped before the two stores merged.
375
+ for (const [root, config] of sessionRootConfigEntries()) {
376
+ if (config === undefined)
377
+ continue;
184
378
  if (!isSameOrChildPath(resolvedFilePath, root))
185
379
  continue;
186
380
  if (!bestMatch || root.length > bestMatch.root.length) {
@@ -190,55 +384,113 @@ function getConfigForFile(filePath) {
190
384
  return bestMatch?.config ?? EMPTY_CONFIG;
191
385
  }
192
386
  /**
193
- * Initialize LSP configuration (call at session start)
387
+ * THE `LSPConfig` → `RegisteredLSPConfig` conversion: construct the custom
388
+ * servers, index the deny list, index the overrides. Pure — it reads no
389
+ * module state and writes none.
390
+ *
391
+ * Extracted from `initLSPConfig`'s body in #2427 review round 3 so that
392
+ * `effectiveConfig` can build the config its question needs WITHOUT calling
393
+ * `initLSPConfig`. That call was the finding: a read-only query ran a full
394
+ * session initialization, which (a) registered the caller's cwd as a served
395
+ * session root, widening the #2052 access gate for a tree the session never
396
+ * opened, and (b) wrote the per-root config store, which was then a separate
397
+ * 32-entry LRU, so ~40 queries against other directories evicted a live root's
398
+ * config and silently lifted the operator's `disabledServers` denial — the
399
+ * exact inversion the surface promises cannot happen. With the conversion
400
+ * spelled here, both writes stop being something the query has to opt out of:
401
+ * it never reaches them. (#2518 later removed the second cap by making the
402
+ * config the registry's own value; a query that skips the registry still skips
403
+ * both.)
404
+ *
405
+ * Still ONE definition, so the derived config and the session-registered one
406
+ * cannot disagree about what a document means.
407
+ */
408
+ export function registerLSPConfig(config) {
409
+ const customServers = [];
410
+ const disabledServerIds = new Set(config.disabledServers ?? []);
411
+ if (config.servers) {
412
+ for (const [id, serverConfig] of Object.entries(config.servers)) {
413
+ try {
414
+ const server = createCustomServer(serverConfig, id);
415
+ customServers.push(server);
416
+ }
417
+ catch {
418
+ // pi-lens-ignore: missing-error-propagation — per-server registration, skip bad entries
419
+ }
420
+ }
421
+ }
422
+ const serverOverrides = new Map();
423
+ if (config.serverOverrides) {
424
+ for (const [id, entry] of Object.entries(config.serverOverrides)) {
425
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
426
+ const initOpts = entry
427
+ .initializationOptions;
428
+ if (initOpts !== undefined &&
429
+ typeof initOpts === "object" &&
430
+ initOpts !== null &&
431
+ !Array.isArray(initOpts)) {
432
+ serverOverrides.set(id, {
433
+ initializationOptions: initOpts,
434
+ });
435
+ }
436
+ }
437
+ }
438
+ }
439
+ return { customServers, disabledServerIds, serverOverrides };
440
+ }
441
+ /**
442
+ * Drop the `config_resolved` claim of every root the registry just evicted
443
+ * (#2518 review F1).
444
+ *
445
+ * `recordConfigResolved` claims that row once per (session, root), which is
446
+ * right while the resolved config is still in the store: a second
447
+ * `loadLSPConfig` for a root already resolved this session re-derives the same
448
+ * answer and needs no second row. An EVICTED root is the other case — its
449
+ * resolved config is gone, the reload is a genuine second resolution, and its
450
+ * row is the only record of what the reloaded config was. Without this release
451
+ * the reload publishes a `config_resolution_pending` mark that no row ever
452
+ * answers, which reads in the analyzer exactly like a resolution that never
453
+ * finished.
454
+ *
455
+ * Rows stay bounded because evictions do, and evictions are counted:
456
+ * `lsp-session-root-evicted` in the degradation ledger.
457
+ *
458
+ * The release derives its key with {@link configResolutionKey}, the ONE
459
+ * expression the claim itself is taken with — not a second normalization that
460
+ * happens to match. Round 2 shipped `normalizeFilePath(root)` here against a
461
+ * claim keyed on the caller's own `normalizeFilePath(cwd)`, which is the same
462
+ * string only when `path.resolve` is the identity on that cwd: a trailing
463
+ * slash (what `analysisRoot` from `.pi-lens.json` and the warm paths can pass)
464
+ * made the release miss and the reload silent again (review F6).
465
+ */
466
+ function forgetConfigResolvedClaims(evictedRoots) {
467
+ for (const root of evictedRoots) {
468
+ releasePhaseClaim(CONFIG_RESOLVED_PHASE, configResolutionKey(root));
469
+ }
470
+ }
471
+ /**
472
+ * Initialize LSP configuration (call at session start).
194
473
  * Deduplicates concurrent calls for the same workspace.
474
+ *
475
+ * It takes no options on purpose. Every one of its callers is a session
476
+ * DECLARING a root it will serve — `ensureLSPConfigInitialized`, `ensureReady`,
477
+ * `runtime-session.ts`, `lens-engine.ts` — and a session-start load is exactly
478
+ * the caller that must report its config notices. There is no mode in which
479
+ * this function runs silently, because there is no caller that is not a
480
+ * session (#2427 review round 3).
195
481
  */
196
482
  export async function initLSPConfig(cwd) {
197
483
  const normalizedCwd = normalizeWorkspacePath(cwd);
198
484
  // #2052: this cwd is now a served session root. Registered BEFORE the
199
485
  // in-flight dedup return below, so a concurrent duplicate init still
200
486
  // registers it rather than returning early with the root unrecorded.
201
- registerSessionRoot(normalizedCwd);
487
+ forgetConfigResolvedClaims(registerSessionRoot(normalizedCwd));
202
488
  const existing = configInFlight.get(normalizedCwd);
203
489
  if (existing)
204
490
  return existing;
205
491
  const promise = (async () => {
206
- const config = await loadLSPConfig(cwd);
207
- const customServers = [];
208
- const disabledServerIds = new Set(config.disabledServers ?? []);
209
- if (config.servers) {
210
- for (const [id, serverConfig] of Object.entries(config.servers)) {
211
- try {
212
- const server = createCustomServer(serverConfig, id);
213
- customServers.push(server);
214
- }
215
- catch {
216
- // pi-lens-ignore: missing-error-propagation — per-server registration, skip bad entries
217
- }
218
- }
219
- }
220
- const serverOverrides = new Map();
221
- if (config.serverOverrides) {
222
- for (const [id, entry] of Object.entries(config.serverOverrides)) {
223
- if (entry && typeof entry === "object" && !Array.isArray(entry)) {
224
- const initOpts = entry
225
- .initializationOptions;
226
- if (initOpts !== undefined &&
227
- typeof initOpts === "object" &&
228
- initOpts !== null &&
229
- !Array.isArray(initOpts)) {
230
- serverOverrides.set(id, {
231
- initializationOptions: initOpts,
232
- });
233
- }
234
- }
235
- }
236
- }
237
- workspaceConfigs.set(normalizedCwd, {
238
- customServers,
239
- disabledServerIds,
240
- serverOverrides,
241
- });
492
+ const config = registerLSPConfig(await loadLSPConfig(cwd, os.homedir()));
493
+ forgetConfigResolvedClaims(setSessionRootConfig(normalizedCwd, config));
242
494
  })();
243
495
  configInFlight.set(normalizedCwd, promise);
244
496
  try {
@@ -254,13 +506,24 @@ export async function initLSPConfig(cwd) {
254
506
  }
255
507
  }
256
508
  }
509
+ /**
510
+ * Every server a workspace knows about, in registry-then-custom order and
511
+ * BEFORE any gate is applied.
512
+ *
513
+ * Spelled once because two callers need the same list for opposite purposes:
514
+ * `getAllServers` DROPS the disabled ones, `explainServersForFile` REPORTS
515
+ * them. A custom server that only one of the two composed would be a server
516
+ * the runtime runs and the introspection cannot see, or the reverse.
517
+ */
518
+ function registeredServers(config) {
519
+ return [...LSP_SERVERS, ...config.customServers];
520
+ }
257
521
  /**
258
522
  * Get all available servers (built-in + custom, minus disabled)
259
523
  */
260
524
  export function getAllServers(filePath) {
261
525
  const config = filePath ? getConfigForFile(filePath) : EMPTY_CONFIG;
262
- const all = [...LSP_SERVERS, ...config.customServers];
263
- return all.filter((s) => !config.disabledServerIds.has(s.id));
526
+ return registeredServers(config).filter((s) => !config.disabledServerIds.has(s.id));
264
527
  }
265
528
  /**
266
529
  * Check if a server is disabled
@@ -269,22 +532,73 @@ export function isServerDisabled(serverId, filePath) {
269
532
  const config = filePath ? getConfigForFile(filePath) : EMPTY_CONFIG;
270
533
  return config.disabledServerIds.has(serverId);
271
534
  }
272
- // --- Override getServersForFile to include custom servers
273
- export function getServersForFileWithConfig(filePath) {
535
+ /**
536
+ * THE server-selection gate: why this server does or does not attach to this
537
+ * file (#2427).
538
+ *
539
+ * One evaluation, two projections. `getServersForFileWithConfig` asks it for a
540
+ * verdict and `explainServersForFile` asks it for a reason; before #2427 the
541
+ * verdict lived here and the reason did not exist, so answering "why is server
542
+ * X not running" meant re-implementing these three gates at the asking site —
543
+ * a second copy of a filter is a copy that drifts, which is what AGENTS.md's
544
+ * single-source-of-truth rule forbids.
545
+ *
546
+ * Returning a REASON rather than a boolean is also what keeps the verdict path
547
+ * allocation-free: `getServersForFileWithConfig` runs per file on the dispatch
548
+ * and cascade paths, and materializing one decision object per registered
549
+ * server per call would put ~46 short-lived objects on a hot path to serve a
550
+ * question only the introspection surface asks.
551
+ *
552
+ * Gate order is the ANSWER order, not just an implementation detail: a server
553
+ * the operator disabled reports `disabled-by-config` even when the file's
554
+ * extension would not have matched it anyway, because "you turned it off" is
555
+ * the fact the asker can act on.
556
+ */
557
+ function selectionReason(server, config, filePath, ext, base) {
558
+ if (config.disabledServerIds.has(server.id))
559
+ return "disabled-by-config";
560
+ let matched = false;
561
+ for (const value of server.extensions) {
562
+ const lower = value.toLowerCase();
563
+ if (lower === ext || lower === base) {
564
+ matched = true;
565
+ break;
566
+ }
567
+ }
568
+ if (!matched)
569
+ return "extension-mismatch";
570
+ // #636: a server's extension match can be intentionally broader than what
571
+ // it can usefully act on (zizmor attaches to "yaml" but only ever reports
572
+ // on GitHub Actions workflow/action/dependabot paths). `pathFilter`, when
573
+ // present, is an ADDITIONAL narrowing gate — never a widening one.
574
+ if (server.pathFilter && !server.pathFilter(filePath))
575
+ return "path-filter";
576
+ return "selected";
577
+ }
578
+ /**
579
+ * Every registered server's decision for a file, with the reason for each.
580
+ *
581
+ * `config` defaults to the SESSION's registered config for the file's tree —
582
+ * what the runtime would actually use. An explicit one is for a caller that
583
+ * must not touch session state to ask: `effectiveConfig` derives its own from
584
+ * `loadLSPConfig(..., { report: false })` rather than initializing the
585
+ * workspace (#2427 review round 3). Both spellings run the identical gate, and
586
+ * `tests/clients/effective-config.test.ts` pins them equal for the same cwd.
587
+ */
588
+ export function explainServersForFile(filePath, config = getConfigForFile(filePath)) {
274
589
  const ext = path.extname(filePath).toLowerCase();
275
590
  const base = path.basename(filePath).toLowerCase();
276
- return getAllServers(filePath).filter((server) => {
277
- const extensions = server.extensions.map((value) => value.toLowerCase());
278
- const extensionMatch = extensions.includes(ext) || extensions.includes(base);
279
- if (!extensionMatch)
280
- return false;
281
- // #636: a server's extension match can be intentionally broader than what
282
- // it can usefully act on (zizmor attaches to "yaml" but only ever reports
283
- // on GitHub Actions workflow/action/dependabot paths). `pathFilter`, when
284
- // present, is an ADDITIONAL narrowing gate — never a widening one.
285
- return server.pathFilter ? server.pathFilter(filePath) : true;
591
+ return registeredServers(config).map((server) => {
592
+ const reason = selectionReason(server, config, filePath, ext, base);
593
+ return { server, selected: reason === "selected", reason };
286
594
  });
287
595
  }
596
+ export function getServersForFileWithConfig(filePath) {
597
+ const config = getConfigForFile(filePath);
598
+ const ext = path.extname(filePath).toLowerCase();
599
+ const base = path.basename(filePath).toLowerCase();
600
+ return registeredServers(config).filter((server) => selectionReason(server, config, filePath, ext, base) === "selected");
601
+ }
288
602
  /**
289
603
  * The primary language server for a file (e.g. "typescript"), as opposed to a
290
604
  * cross-cutting auxiliary scanner attached via clientScope "all"/
@@ -316,11 +630,22 @@ export function getServerInitOverride(serverId, filePath) {
316
630
  return getConfigForFile(filePath).serverOverrides.get(serverId);
317
631
  }
318
632
  export function resetLSPConfigStateForTests() {
319
- workspaceConfigs.clear();
320
633
  resetLSPCaseSensitivityState();
321
- // Reset both together: a cleared config store beside a live session-root
322
- // registry would decline files for roots nothing can serve any more.
634
+ // One call clears both the served roots and their configs: since #2518 they
635
+ // are one store, so a reset cannot leave a cleared config store beside a
636
+ // live session-root registry declining files for roots nothing can serve.
323
637
  resetSessionRootsForTests();
638
+ // The warn latch is loader state too: a test that re-reads the same broken
639
+ // path after this reset must see the warning again, not a latched silence.
640
+ resetLSPConfigWarnCache();
641
+ // #2526: same reasoning for the per-session `config_resolved` claim — a
642
+ // test that resolves again after this reset must produce its record, not
643
+ // inherit a previous test's "already recorded" claim. Narrowed to THIS
644
+ // loader's own phase (review round 2, S1): the blanket
645
+ // `resetOncePerSessionPhases()` this used to call is the SESSION boundary's
646
+ // call to make — it also re-mints the session identity — and reaching for
647
+ // it from one producer's test reset cleared claims this module does not own.
648
+ releaseOncePerSessionPhase(CONFIG_RESOLVED_PHASE);
324
649
  }
325
650
  /**
326
651
  * Test hook — read the `initLSPConfig` in-flight map directly (#1968's ABA