@herbertgao/pi-extensions 2026.9.0 → 2026.9.2

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 (360) 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/@narumitw/pi-btw/README.md +57 -141
  11. package/node_modules/@narumitw/pi-btw/dist/index.ts +466 -41
  12. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  13. package/node_modules/@narumitw/pi-btw/docs/workflows.md +74 -0
  14. package/node_modules/@narumitw/pi-btw/package.json +2 -1
  15. package/node_modules/@narumitw/pi-btw/src/btw.ts +15 -1
  16. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +32 -2
  17. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +330 -0
  18. package/node_modules/@narumitw/pi-btw/src/menu.ts +136 -19
  19. package/node_modules/@narumitw/pi-btw/src/settings.ts +32 -0
  20. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +57 -5
  21. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +52 -30
  22. package/node_modules/@pi-plugins/fast-mode/README.md +5 -3
  23. package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
  24. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20 -55
  25. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  26. package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
  27. package/node_modules/pi-lens/CHANGELOG.md +1072 -0
  28. package/node_modules/pi-lens/README.md +3 -0
  29. package/node_modules/pi-lens/config/biome/core.jsonc +11 -2
  30. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +13 -1
  31. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +2 -9
  32. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +927 -80
  33. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +1 -1
  34. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +13 -4
  35. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +123 -3
  36. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +60 -5
  37. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +2 -9
  38. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +2 -0
  39. package/node_modules/pi-lens/dist/clients/bash-file-access.js +133 -3
  40. package/node_modules/pi-lens/dist/clients/biome-client.js +9 -2
  41. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +14 -6
  42. package/node_modules/pi-lens/dist/clients/bootstrap.js +509 -73
  43. package/node_modules/pi-lens/dist/clients/bounded-cache.js +152 -12
  44. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +1 -1
  45. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +59 -9
  46. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +113 -0
  47. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +3 -11
  48. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +31 -4
  49. package/node_modules/pi-lens/dist/clients/cache-manager.js +105 -6
  50. package/node_modules/pi-lens/dist/clients/cache-observability.js +40 -19
  51. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +422 -0
  52. package/node_modules/pi-lens/dist/clients/cascade-logger.js +2 -9
  53. package/node_modules/pi-lens/dist/clients/child-unref.js +1 -19
  54. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +13 -3
  55. package/node_modules/pi-lens/dist/clients/complexity-client.js +16 -5
  56. package/node_modules/pi-lens/dist/clients/config-core/deny.js +221 -0
  57. package/node_modules/pi-lens/dist/clients/config-core/index.js +50 -0
  58. package/node_modules/pi-lens/dist/clients/config-core/merge.js +357 -0
  59. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +340 -0
  60. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +248 -0
  61. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +164 -0
  62. package/node_modules/pi-lens/dist/clients/config-core/records.js +218 -0
  63. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +125 -0
  64. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +78 -0
  65. package/node_modules/pi-lens/dist/clients/config-core/schema.js +165 -0
  66. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +281 -0
  67. package/node_modules/pi-lens/dist/clients/config-locations.js +160 -0
  68. package/node_modules/pi-lens/dist/clients/config-resolve.js +789 -0
  69. package/node_modules/pi-lens/dist/clients/config-schema.js +234 -0
  70. package/node_modules/pi-lens/dist/clients/config-warn.js +407 -0
  71. package/node_modules/pi-lens/dist/clients/dead-code-client.js +19 -14
  72. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +2 -6
  73. package/node_modules/pi-lens/dist/clients/deadline-utils.js +178 -0
  74. package/node_modules/pi-lens/dist/clients/debug-handles.js +2 -2
  75. package/node_modules/pi-lens/dist/clients/debug-heap.js +3 -3
  76. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +106 -0
  77. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +113 -7
  78. package/node_modules/pi-lens/dist/clients/dependency-checker.js +8 -19
  79. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +1 -1
  80. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +12 -9
  81. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +15 -5
  82. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -4
  83. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +162 -44
  84. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +97 -0
  85. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +27 -8
  86. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -6
  87. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +1 -1
  88. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -3
  89. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +2 -2
  90. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +1 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +383 -27
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +4 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -1
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -1
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -1
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -1
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -1
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -1
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +1 -3
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -1
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -1
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +1 -2
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -1
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -1
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -1
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -1
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -1
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -1
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +14 -18
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -1
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -1
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -1
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +6 -3
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +7 -4
  122. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -1
  123. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +1 -2
  124. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +1 -3
  125. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +1 -2
  126. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -1
  127. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +4 -3
  128. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -1
  129. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -1
  130. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -1
  131. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -1
  132. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -1
  133. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -1
  134. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -1
  135. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -1
  136. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -1
  137. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
  138. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -67
  139. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +194 -80
  140. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +84 -15
  141. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -21
  142. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -1
  143. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +2 -20
  144. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +1 -1
  145. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -1
  146. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +2 -2
  147. package/node_modules/pi-lens/dist/clients/disposition-logger.js +3 -10
  148. package/node_modules/pi-lens/dist/clients/effective-config.js +403 -0
  149. package/node_modules/pi-lens/dist/clients/error-class.js +23 -0
  150. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +274 -0
  151. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +2 -2
  152. package/node_modules/pi-lens/dist/clients/extension-log.js +2 -2
  153. package/node_modules/pi-lens/dist/clients/feature-hints.js +2 -1
  154. package/node_modules/pi-lens/dist/clients/file-kinds.js +1 -38
  155. package/node_modules/pi-lens/dist/clients/file-time.js +7 -2
  156. package/node_modules/pi-lens/dist/clients/file-utils.js +117 -10
  157. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +50 -13
  158. package/node_modules/pi-lens/dist/clients/format-service.js +6 -2
  159. package/node_modules/pi-lens/dist/clients/formatters.js +385 -104
  160. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +17 -0
  161. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +116 -22
  162. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -9
  163. package/node_modules/pi-lens/dist/clients/git-guard.js +6 -1
  164. package/node_modules/pi-lens/dist/clients/go-client.js +39 -0
  165. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +252 -0
  166. package/node_modules/pi-lens/dist/clients/grammar-source.js +3 -3
  167. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +424 -0
  168. package/node_modules/pi-lens/dist/clients/hook-budgets.js +76 -0
  169. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +5 -2
  170. package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
  171. package/node_modules/pi-lens/dist/clients/installer/index.js +289 -59
  172. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +84 -18
  173. package/node_modules/pi-lens/dist/clients/instance-reaper.js +110 -236
  174. package/node_modules/pi-lens/dist/clients/instance-registry.js +6 -3
  175. package/node_modules/pi-lens/dist/clients/language-profile.js +0 -9
  176. package/node_modules/pi-lens/dist/clients/language-registry.js +599 -0
  177. package/node_modules/pi-lens/dist/clients/latency-logger.js +165 -9
  178. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +34 -1
  179. package/node_modules/pi-lens/dist/clients/lens-config.js +160 -30
  180. package/node_modules/pi-lens/dist/clients/lens-engine.js +33 -41
  181. package/node_modules/pi-lens/dist/clients/lens-events.js +1 -1
  182. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +77 -6
  183. package/node_modules/pi-lens/dist/clients/lens-map.js +1 -1
  184. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -38
  185. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +3 -1
  186. package/node_modules/pi-lens/dist/clients/lsp/client.js +172 -33
  187. package/node_modules/pi-lens/dist/clients/lsp/config.js +484 -158
  188. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +1 -1
  189. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +1 -1
  190. package/node_modules/pi-lens/dist/clients/lsp/edits.js +18 -5
  191. package/node_modules/pi-lens/dist/clients/lsp/index.js +892 -96
  192. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +1 -1
  193. package/node_modules/pi-lens/dist/clients/lsp/language.js +18 -177
  194. package/node_modules/pi-lens/dist/clients/lsp/launch.js +2 -1
  195. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  196. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +5 -18
  197. package/node_modules/pi-lens/dist/clients/lsp/server.js +284 -147
  198. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +116 -23
  199. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +2 -7
  200. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -1
  201. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +14 -7
  202. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +6 -0
  203. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +2 -1
  204. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +1 -1
  205. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +1 -1
  206. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +163 -21
  207. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +36 -0
  208. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +33 -4
  209. package/node_modules/pi-lens/dist/clients/mcp/session.js +5 -16
  210. package/node_modules/pi-lens/dist/clients/memory-sampler.js +8 -3
  211. package/node_modules/pi-lens/dist/clients/metrics-history.js +28 -107
  212. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +3 -5
  213. package/node_modules/pi-lens/dist/clients/module-report.js +21 -46
  214. package/node_modules/pi-lens/dist/clients/mutating-tool.js +651 -0
  215. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +368 -0
  216. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +240 -0
  217. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +247 -24
  218. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +101 -0
  219. package/node_modules/pi-lens/dist/clients/observed-mutation.js +1215 -0
  220. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +70 -28
  221. package/node_modules/pi-lens/dist/clients/opengrep-config.js +5 -1
  222. package/node_modules/pi-lens/dist/clients/package-manager.js +195 -26
  223. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +359 -80
  224. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +23 -7
  225. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +21 -2
  226. package/node_modules/pi-lens/dist/clients/path-utils.js +451 -15
  227. package/node_modules/pi-lens/dist/clients/performance-report.js +2 -2
  228. package/node_modules/pi-lens/dist/clients/persist-debounce.js +8 -1
  229. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +114 -0
  230. package/node_modules/pi-lens/dist/clients/pipeline.js +79 -18
  231. package/node_modules/pi-lens/dist/clients/probe-home-state.js +228 -0
  232. package/node_modules/pi-lens/dist/clients/process-bridge.js +66 -0
  233. package/node_modules/pi-lens/dist/clients/process-snapshot.js +68 -0
  234. package/node_modules/pi-lens/dist/clients/project-changes.js +1 -1
  235. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +1 -1
  236. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
  237. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +1 -1
  238. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +1 -1
  239. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +1 -1
  240. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +1 -1
  241. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +28 -4
  242. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +51 -20
  243. package/node_modules/pi-lens/dist/clients/project-lens-config.js +438 -122
  244. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +3 -51
  245. package/node_modules/pi-lens/dist/clients/project-snapshot.js +52 -24
  246. package/node_modules/pi-lens/dist/clients/python-environment.js +217 -0
  247. package/node_modules/pi-lens/dist/clients/python-provenance.js +639 -0
  248. package/node_modules/pi-lens/dist/clients/quiet-window.js +6 -1
  249. package/node_modules/pi-lens/dist/clients/read-bridge.js +13 -21
  250. package/node_modules/pi-lens/dist/clients/read-expansion.js +9 -46
  251. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +16 -5
  252. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +364 -212
  253. package/node_modules/pi-lens/dist/clients/read-guard.js +26 -2
  254. package/node_modules/pi-lens/dist/clients/recent-touches.js +2 -2
  255. package/node_modules/pi-lens/dist/clients/resource-sampler.js +195 -96
  256. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +155 -114
  257. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +1 -1
  258. package/node_modules/pi-lens/dist/clients/review-graph/service.js +55 -3
  259. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +7 -15
  260. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +10 -74
  261. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +9 -3
  262. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +19 -3
  263. package/node_modules/pi-lens/dist/clients/runtime-context.js +49 -16
  264. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +25 -6
  265. package/node_modules/pi-lens/dist/clients/runtime-session.js +184 -32
  266. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +179 -36
  267. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +797 -243
  268. package/node_modules/pi-lens/dist/clients/runtime-turn.js +1028 -53
  269. package/node_modules/pi-lens/dist/clients/rust-client.js +31 -0
  270. package/node_modules/pi-lens/dist/clients/safe-spawn.js +33 -10
  271. package/node_modules/pi-lens/dist/clients/sanitize.js +7 -12
  272. package/node_modules/pi-lens/dist/clients/scan-utils.js +1 -56
  273. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +1 -3
  274. package/node_modules/pi-lens/dist/clients/security-scan-client.js +3 -0
  275. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +1 -1
  276. package/node_modules/pi-lens/dist/clients/session-start-observability.js +79 -0
  277. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -24
  278. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +12 -3
  279. package/node_modules/pi-lens/dist/clients/sgconfig.js +7 -6
  280. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +1 -1
  281. package/node_modules/pi-lens/dist/clients/skills-resolver.js +105 -0
  282. package/node_modules/pi-lens/dist/clients/smells-rollup.js +4 -4
  283. package/node_modules/pi-lens/dist/clients/startup-scan.js +2 -10
  284. package/node_modules/pi-lens/dist/clients/string-utils.js +13 -0
  285. package/node_modules/pi-lens/dist/clients/subagent-mode.js +17 -4
  286. package/node_modules/pi-lens/dist/clients/test-runner-client.js +176 -39
  287. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +244 -0
  288. package/node_modules/pi-lens/dist/clients/tool-definition.js +41 -1
  289. package/node_modules/pi-lens/dist/clients/tool-policy.js +92 -22
  290. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +40 -40
  291. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +87 -100
  292. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +2 -2
  293. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +81 -1
  294. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +51 -46
  295. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +28 -0
  296. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -4
  297. package/node_modules/pi-lens/dist/clients/typos-config.js +5 -0
  298. package/node_modules/pi-lens/dist/clients/user-notify.js +6 -6
  299. package/node_modules/pi-lens/dist/clients/widget-state.js +191 -26
  300. package/node_modules/pi-lens/dist/clients/word-index-logger.js +2 -9
  301. package/node_modules/pi-lens/dist/clients/word-index-store.js +23 -12
  302. package/node_modules/pi-lens/dist/clients/word-index.js +252 -47
  303. package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -1
  304. package/node_modules/pi-lens/dist/clients/zizmor-config.js +4 -1
  305. package/node_modules/pi-lens/dist/index.js +65899 -55340
  306. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -1
  307. package/node_modules/pi-lens/dist/mcp/build-staleness.js +1 -1
  308. package/node_modules/pi-lens/dist/mcp/server.js +127 -5
  309. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +583 -0
  310. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +129 -0
  311. package/node_modules/pi-lens/dist/tools/effective-config.js +89 -0
  312. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +96 -25
  313. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +104 -86
  314. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +37 -16
  315. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +3 -3
  316. package/node_modules/pi-lens/dist/tools/render-compact.js +1 -1
  317. package/node_modules/pi-lens/dist/tools/shared.js +0 -1
  318. package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -2
  319. package/node_modules/pi-lens/docs/agent-guide.md +64 -1
  320. package/node_modules/pi-lens/docs/configuration.md +222 -0
  321. package/node_modules/pi-lens/docs/dependencies.md +3 -3
  322. package/node_modules/pi-lens/docs/features.md +44 -5
  323. package/node_modules/pi-lens/docs/globalconfig.md +20 -0
  324. package/node_modules/pi-lens/docs/language-coverage.md +2 -2
  325. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +9 -1
  326. package/node_modules/pi-lens/docs/pi-lens-fixer.md +25 -0
  327. package/node_modules/pi-lens/docs/pi-lens-investigator.md +25 -0
  328. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +33 -0
  329. package/node_modules/pi-lens/docs/pi-lens-subagent.md +49 -0
  330. package/node_modules/pi-lens/docs/pi-lens-warden.md +55 -0
  331. package/node_modules/pi-lens/docs/public-api-stability.md +359 -0
  332. package/node_modules/pi-lens/docs/release-qa-baseline.md +190 -0
  333. package/node_modules/pi-lens/docs/servercapabilities.md +7 -3
  334. package/node_modules/pi-lens/docs/subagent-compat.md +110 -30
  335. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +2 -2
  336. package/node_modules/pi-lens/docs/word-index.md +1 -1
  337. package/node_modules/pi-lens/package.json +18 -35
  338. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
  339. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
  340. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
  341. package/node_modules/pi-lens/rules/typos/_typos.toml +4 -2
  342. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +192 -1
  343. package/node_modules/pi-lens/scripts/install-selftest.mjs +58 -2
  344. package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +129 -0
  345. package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +1 -1
  346. package/node_modules/pi-lens/scripts/rpc-load-check.mjs +3 -1
  347. package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +4 -2
  348. package/node_modules/pi-web-access/CHANGELOG.md +28 -0
  349. package/node_modules/pi-web-access/README.md +62 -15
  350. package/node_modules/pi-web-access/curator-page.ts +3 -1
  351. package/node_modules/pi-web-access/curator-server.ts +5 -1
  352. package/node_modules/pi-web-access/gemini-search.ts +8 -4
  353. package/node_modules/pi-web-access/github-extract.ts +242 -1
  354. package/node_modules/pi-web-access/index.ts +118 -64
  355. package/node_modules/pi-web-access/mistral-search.ts +281 -0
  356. package/node_modules/pi-web-access/package.json +2 -2
  357. package/node_modules/pi-web-access/perplexity.ts +14 -1
  358. package/node_modules/pi-web-access/utils.ts +23 -6
  359. package/node_modules/pi-web-access/xai-search.ts +96 -33
  360. package/package.json +6 -6
@@ -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
  /**
@@ -147,6 +336,7 @@ export function createCustomServer(config, id) {
147
336
  return {
148
337
  id,
149
338
  name: config.name,
339
+ custom: true,
150
340
  extensions: config.extensions,
151
341
  root: config.rootMarkers
152
342
  ? createRootDetector(config.rootMarkers)
@@ -166,7 +356,6 @@ const EMPTY_CONFIG = {
166
356
  disabledServerIds: new Set(),
167
357
  serverOverrides: new Map(),
168
358
  };
169
- const workspaceConfigs = new BoundedLruCache(32);
170
359
  /** In-flight config initialization promises to prevent duplicate concurrent loads */
171
360
  const configInFlight = new Map();
172
361
  function normalizeWorkspacePath(cwd) {
@@ -180,7 +369,13 @@ function isSameOrChildPath(filePath, candidateRoot) {
180
369
  function getConfigForFile(filePath) {
181
370
  const resolvedFilePath = path.resolve(filePath);
182
371
  let bestMatch;
183
- for (const [root, config] of workspaceConfigs) {
372
+ // #2518: the per-root configs ARE the session-root registry's values, so
373
+ // this walk sees a config for exactly the roots that registry still serves.
374
+ // `undefined` is a root whose first load is still in flight — the same
375
+ // "no entry yet" state this walk skipped before the two stores merged.
376
+ for (const [root, config] of sessionRootConfigEntries()) {
377
+ if (config === undefined)
378
+ continue;
184
379
  if (!isSameOrChildPath(resolvedFilePath, root))
185
380
  continue;
186
381
  if (!bestMatch || root.length > bestMatch.root.length) {
@@ -190,55 +385,113 @@ function getConfigForFile(filePath) {
190
385
  return bestMatch?.config ?? EMPTY_CONFIG;
191
386
  }
192
387
  /**
193
- * Initialize LSP configuration (call at session start)
388
+ * THE `LSPConfig` → `RegisteredLSPConfig` conversion: construct the custom
389
+ * servers, index the deny list, index the overrides. Pure — it reads no
390
+ * module state and writes none.
391
+ *
392
+ * Extracted from `initLSPConfig`'s body in #2427 review round 3 so that
393
+ * `effectiveConfig` can build the config its question needs WITHOUT calling
394
+ * `initLSPConfig`. That call was the finding: a read-only query ran a full
395
+ * session initialization, which (a) registered the caller's cwd as a served
396
+ * session root, widening the #2052 access gate for a tree the session never
397
+ * opened, and (b) wrote the per-root config store, which was then a separate
398
+ * 32-entry LRU, so ~40 queries against other directories evicted a live root's
399
+ * config and silently lifted the operator's `disabledServers` denial — the
400
+ * exact inversion the surface promises cannot happen. With the conversion
401
+ * spelled here, both writes stop being something the query has to opt out of:
402
+ * it never reaches them. (#2518 later removed the second cap by making the
403
+ * config the registry's own value; a query that skips the registry still skips
404
+ * both.)
405
+ *
406
+ * Still ONE definition, so the derived config and the session-registered one
407
+ * cannot disagree about what a document means.
408
+ */
409
+ export function registerLSPConfig(config) {
410
+ const customServers = [];
411
+ const disabledServerIds = new Set(config.disabledServers ?? []);
412
+ if (config.servers) {
413
+ for (const [id, serverConfig] of Object.entries(config.servers)) {
414
+ try {
415
+ const server = createCustomServer(serverConfig, id);
416
+ customServers.push(server);
417
+ }
418
+ catch {
419
+ // pi-lens-ignore: missing-error-propagation — per-server registration, skip bad entries
420
+ }
421
+ }
422
+ }
423
+ const serverOverrides = new Map();
424
+ if (config.serverOverrides) {
425
+ for (const [id, entry] of Object.entries(config.serverOverrides)) {
426
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
427
+ const initOpts = entry
428
+ .initializationOptions;
429
+ if (initOpts !== undefined &&
430
+ typeof initOpts === "object" &&
431
+ initOpts !== null &&
432
+ !Array.isArray(initOpts)) {
433
+ serverOverrides.set(id, {
434
+ initializationOptions: initOpts,
435
+ });
436
+ }
437
+ }
438
+ }
439
+ }
440
+ return { customServers, disabledServerIds, serverOverrides };
441
+ }
442
+ /**
443
+ * Drop the `config_resolved` claim of every root the registry just evicted
444
+ * (#2518 review F1).
445
+ *
446
+ * `recordConfigResolved` claims that row once per (session, root), which is
447
+ * right while the resolved config is still in the store: a second
448
+ * `loadLSPConfig` for a root already resolved this session re-derives the same
449
+ * answer and needs no second row. An EVICTED root is the other case — its
450
+ * resolved config is gone, the reload is a genuine second resolution, and its
451
+ * row is the only record of what the reloaded config was. Without this release
452
+ * the reload publishes a `config_resolution_pending` mark that no row ever
453
+ * answers, which reads in the analyzer exactly like a resolution that never
454
+ * finished.
455
+ *
456
+ * Rows stay bounded because evictions do, and evictions are counted:
457
+ * `lsp-session-root-evicted` in the degradation ledger.
458
+ *
459
+ * The release derives its key with {@link configResolutionKey}, the ONE
460
+ * expression the claim itself is taken with — not a second normalization that
461
+ * happens to match. Round 2 shipped `normalizeFilePath(root)` here against a
462
+ * claim keyed on the caller's own `normalizeFilePath(cwd)`, which is the same
463
+ * string only when `path.resolve` is the identity on that cwd: a trailing
464
+ * slash (what `analysisRoot` from `.pi-lens.json` and the warm paths can pass)
465
+ * made the release miss and the reload silent again (review F6).
466
+ */
467
+ function forgetConfigResolvedClaims(evictedRoots) {
468
+ for (const root of evictedRoots) {
469
+ releasePhaseClaim(CONFIG_RESOLVED_PHASE, configResolutionKey(root));
470
+ }
471
+ }
472
+ /**
473
+ * Initialize LSP configuration (call at session start).
194
474
  * Deduplicates concurrent calls for the same workspace.
475
+ *
476
+ * It takes no options on purpose. Every one of its callers is a session
477
+ * DECLARING a root it will serve — `ensureLSPConfigInitialized`, `ensureReady`,
478
+ * `runtime-session.ts`, `lens-engine.ts` — and a session-start load is exactly
479
+ * the caller that must report its config notices. There is no mode in which
480
+ * this function runs silently, because there is no caller that is not a
481
+ * session (#2427 review round 3).
195
482
  */
196
483
  export async function initLSPConfig(cwd) {
197
484
  const normalizedCwd = normalizeWorkspacePath(cwd);
198
485
  // #2052: this cwd is now a served session root. Registered BEFORE the
199
486
  // in-flight dedup return below, so a concurrent duplicate init still
200
487
  // registers it rather than returning early with the root unrecorded.
201
- registerSessionRoot(normalizedCwd);
488
+ forgetConfigResolvedClaims(registerSessionRoot(normalizedCwd));
202
489
  const existing = configInFlight.get(normalizedCwd);
203
490
  if (existing)
204
491
  return existing;
205
492
  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
- });
493
+ const config = registerLSPConfig(await loadLSPConfig(cwd, os.homedir()));
494
+ forgetConfigResolvedClaims(setSessionRootConfig(normalizedCwd, config));
242
495
  })();
243
496
  configInFlight.set(normalizedCwd, promise);
244
497
  try {
@@ -254,13 +507,24 @@ export async function initLSPConfig(cwd) {
254
507
  }
255
508
  }
256
509
  }
510
+ /**
511
+ * Every server a workspace knows about, in registry-then-custom order and
512
+ * BEFORE any gate is applied.
513
+ *
514
+ * Spelled once because two callers need the same list for opposite purposes:
515
+ * `getAllServers` DROPS the disabled ones, `explainServersForFile` REPORTS
516
+ * them. A custom server that only one of the two composed would be a server
517
+ * the runtime runs and the introspection cannot see, or the reverse.
518
+ */
519
+ function registeredServers(config) {
520
+ return [...LSP_SERVERS, ...config.customServers];
521
+ }
257
522
  /**
258
523
  * Get all available servers (built-in + custom, minus disabled)
259
524
  */
260
525
  export function getAllServers(filePath) {
261
526
  const config = filePath ? getConfigForFile(filePath) : EMPTY_CONFIG;
262
- const all = [...LSP_SERVERS, ...config.customServers];
263
- return all.filter((s) => !config.disabledServerIds.has(s.id));
527
+ return registeredServers(config).filter((s) => !config.disabledServerIds.has(s.id));
264
528
  }
265
529
  /**
266
530
  * Check if a server is disabled
@@ -269,22 +533,73 @@ export function isServerDisabled(serverId, filePath) {
269
533
  const config = filePath ? getConfigForFile(filePath) : EMPTY_CONFIG;
270
534
  return config.disabledServerIds.has(serverId);
271
535
  }
272
- // --- Override getServersForFile to include custom servers
273
- export function getServersForFileWithConfig(filePath) {
536
+ /**
537
+ * THE server-selection gate: why this server does or does not attach to this
538
+ * file (#2427).
539
+ *
540
+ * One evaluation, two projections. `getServersForFileWithConfig` asks it for a
541
+ * verdict and `explainServersForFile` asks it for a reason; before #2427 the
542
+ * verdict lived here and the reason did not exist, so answering "why is server
543
+ * X not running" meant re-implementing these three gates at the asking site —
544
+ * a second copy of a filter is a copy that drifts, which is what AGENTS.md's
545
+ * single-source-of-truth rule forbids.
546
+ *
547
+ * Returning a REASON rather than a boolean is also what keeps the verdict path
548
+ * allocation-free: `getServersForFileWithConfig` runs per file on the dispatch
549
+ * and cascade paths, and materializing one decision object per registered
550
+ * server per call would put ~46 short-lived objects on a hot path to serve a
551
+ * question only the introspection surface asks.
552
+ *
553
+ * Gate order is the ANSWER order, not just an implementation detail: a server
554
+ * the operator disabled reports `disabled-by-config` even when the file's
555
+ * extension would not have matched it anyway, because "you turned it off" is
556
+ * the fact the asker can act on.
557
+ */
558
+ function selectionReason(server, config, filePath, ext, base) {
559
+ if (config.disabledServerIds.has(server.id))
560
+ return "disabled-by-config";
561
+ let matched = false;
562
+ for (const value of server.extensions) {
563
+ const lower = value.toLowerCase();
564
+ if (lower === ext || lower === base) {
565
+ matched = true;
566
+ break;
567
+ }
568
+ }
569
+ if (!matched)
570
+ return "extension-mismatch";
571
+ // #636: a server's extension match can be intentionally broader than what
572
+ // it can usefully act on (zizmor attaches to "yaml" but only ever reports
573
+ // on GitHub Actions workflow/action/dependabot paths). `pathFilter`, when
574
+ // present, is an ADDITIONAL narrowing gate — never a widening one.
575
+ if (server.pathFilter && !server.pathFilter(filePath))
576
+ return "path-filter";
577
+ return "selected";
578
+ }
579
+ /**
580
+ * Every registered server's decision for a file, with the reason for each.
581
+ *
582
+ * `config` defaults to the SESSION's registered config for the file's tree —
583
+ * what the runtime would actually use. An explicit one is for a caller that
584
+ * must not touch session state to ask: `effectiveConfig` derives its own from
585
+ * `loadLSPConfig(..., { report: false })` rather than initializing the
586
+ * workspace (#2427 review round 3). Both spellings run the identical gate, and
587
+ * `tests/clients/effective-config.test.ts` pins them equal for the same cwd.
588
+ */
589
+ export function explainServersForFile(filePath, config = getConfigForFile(filePath)) {
274
590
  const ext = path.extname(filePath).toLowerCase();
275
591
  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;
592
+ return registeredServers(config).map((server) => {
593
+ const reason = selectionReason(server, config, filePath, ext, base);
594
+ return { server, selected: reason === "selected", reason };
286
595
  });
287
596
  }
597
+ export function getServersForFileWithConfig(filePath) {
598
+ const config = getConfigForFile(filePath);
599
+ const ext = path.extname(filePath).toLowerCase();
600
+ const base = path.basename(filePath).toLowerCase();
601
+ return registeredServers(config).filter((server) => selectionReason(server, config, filePath, ext, base) === "selected");
602
+ }
288
603
  /**
289
604
  * The primary language server for a file (e.g. "typescript"), as opposed to a
290
605
  * cross-cutting auxiliary scanner attached via clientScope "all"/
@@ -316,11 +631,22 @@ export function getServerInitOverride(serverId, filePath) {
316
631
  return getConfigForFile(filePath).serverOverrides.get(serverId);
317
632
  }
318
633
  export function resetLSPConfigStateForTests() {
319
- workspaceConfigs.clear();
320
634
  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.
635
+ // One call clears both the served roots and their configs: since #2518 they
636
+ // are one store, so a reset cannot leave a cleared config store beside a
637
+ // live session-root registry declining files for roots nothing can serve.
323
638
  resetSessionRootsForTests();
639
+ // The warn latch is loader state too: a test that re-reads the same broken
640
+ // path after this reset must see the warning again, not a latched silence.
641
+ resetLSPConfigWarnCache();
642
+ // #2526: same reasoning for the per-session `config_resolved` claim — a
643
+ // test that resolves again after this reset must produce its record, not
644
+ // inherit a previous test's "already recorded" claim. Narrowed to THIS
645
+ // loader's own phase (review round 2, S1): the blanket
646
+ // `resetOncePerSessionPhases()` this used to call is the SESSION boundary's
647
+ // call to make — it also re-mints the session identity — and reaching for
648
+ // it from one producer's test reset cleared claims this module does not own.
649
+ releaseOncePerSessionPhase(CONFIG_RESOLVED_PHASE);
324
650
  }
325
651
  /**
326
652
  * Test hook — read the `initLSPConfig` in-flight map directly (#1968's ABA