pi-lens 4.1.2 → 4.1.4

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 (359) hide show
  1. package/CHANGELOG.md +1308 -0
  2. package/README.md +3 -0
  3. package/config/biome/core.jsonc +11 -2
  4. package/config/dependency-cruiser-eager-allowlist.json +91 -0
  5. package/dist/clients/actionable-warnings-logger.js +18 -3
  6. package/dist/clients/actionable-warnings.js +927 -80
  7. package/dist/clients/agent-behavior-client.js +13 -4
  8. package/dist/clients/ast-grep-client.js +136 -9
  9. package/dist/clients/ast-grep-rule-manager.js +60 -5
  10. package/dist/clients/ast-grep-tool-logger.js +2 -2
  11. package/dist/clients/availability-probe-flight.js +12 -0
  12. package/dist/clients/bash-file-access.js +1 -3
  13. package/dist/clients/biome-client.js +16 -2
  14. package/dist/clients/blocker-freshness.js +34 -0
  15. package/dist/clients/bootstrap.js +509 -73
  16. package/dist/clients/bounded-cache.js +152 -12
  17. package/dist/clients/bounded-telemetry.js +115 -23
  18. package/dist/clients/build-identity.js +95 -0
  19. package/dist/clients/bundled-resource-health.js +113 -0
  20. package/dist/clients/bus-events-logger.js +2 -2
  21. package/dist/clients/bus-publish.js +3 -0
  22. package/dist/clients/cache/rule-cache.js +33 -5
  23. package/dist/clients/cache-manager.js +105 -6
  24. package/dist/clients/cache-observability.js +46 -19
  25. package/dist/clients/cargo-manifest.js +422 -0
  26. package/dist/clients/cascade-logger.js +17 -3
  27. package/dist/clients/code-quality-warnings.js +13 -3
  28. package/dist/clients/complexity-client.js +16 -5
  29. package/dist/clients/config-core/deny.js +221 -0
  30. package/dist/clients/config-core/index.js +47 -0
  31. package/dist/clients/config-core/merge.js +357 -0
  32. package/dist/clients/config-core/normalize.js +340 -0
  33. package/dist/clients/config-core/process-spec.js +248 -0
  34. package/dist/clients/config-core/provenance.js +171 -0
  35. package/dist/clients/config-core/records.js +218 -0
  36. package/dist/clients/config-core/resolve.js +125 -0
  37. package/dist/clients/config-core/safe-object.js +78 -0
  38. package/dist/clients/config-core/schema.js +167 -0
  39. package/dist/clients/config-diagnostic-codes.js +281 -0
  40. package/dist/clients/config-locations.js +160 -0
  41. package/dist/clients/config-resolve.js +789 -0
  42. package/dist/clients/config-schema.js +197 -0
  43. package/dist/clients/config-warn.js +407 -0
  44. package/dist/clients/dead-code-client.js +33 -25
  45. package/dist/clients/dead-code-logger.js +2 -2
  46. package/dist/clients/deadline-utils.js +178 -0
  47. package/dist/clients/debug-handles.js +2 -2
  48. package/dist/clients/debug-heap.js +3 -3
  49. package/dist/clients/deferred-lsp-work.js +106 -0
  50. package/dist/clients/degradation-ledger.js +166 -8
  51. package/dist/clients/demoted-finding-render.js +14 -0
  52. package/dist/clients/dependency-checker.js +28 -22
  53. package/dist/clients/deps/minimatch.js +1 -1
  54. package/dist/clients/diagnostic-line-freshness.js +12 -9
  55. package/dist/clients/diagnostic-logger.js +14 -4
  56. package/dist/clients/dispatch/collect-later-tier.js +39 -0
  57. package/dist/clients/dispatch/dispatcher.js +304 -19
  58. package/dist/clients/dispatch/fact-store.js +328 -9
  59. package/dist/clients/dispatch/integration.js +214 -107
  60. package/dist/clients/dispatch/pending-runner-findings.js +87 -0
  61. package/dist/clients/dispatch/plan.js +21 -5
  62. package/dist/clients/dispatch/runners/actionlint.js +0 -1
  63. package/dist/clients/dispatch/runners/ast-grep-napi.js +550 -77
  64. package/dist/clients/dispatch/runners/biome-check.js +0 -1
  65. package/dist/clients/dispatch/runners/cpp-check.js +0 -1
  66. package/dist/clients/dispatch/runners/credo.js +0 -1
  67. package/dist/clients/dispatch/runners/cue-vet.js +0 -1
  68. package/dist/clients/dispatch/runners/dart-analyze.js +0 -1
  69. package/dist/clients/dispatch/runners/detekt.js +0 -1
  70. package/dist/clients/dispatch/runners/dotnet-build.js +0 -1
  71. package/dist/clients/dispatch/runners/elixir-check.js +0 -1
  72. package/dist/clients/dispatch/runners/eslint.js +5 -2
  73. package/dist/clients/dispatch/runners/fact-rules.js +0 -1
  74. package/dist/clients/dispatch/runners/fish-indent.js +0 -1
  75. package/dist/clients/dispatch/runners/gleam-check.js +0 -1
  76. package/dist/clients/dispatch/runners/go-vet.js +1 -3
  77. package/dist/clients/dispatch/runners/golangci-lint.js +0 -1
  78. package/dist/clients/dispatch/runners/hadolint.js +0 -1
  79. package/dist/clients/dispatch/runners/helm-lint.js +16 -4
  80. package/dist/clients/dispatch/runners/helm-render.js +54 -10
  81. package/dist/clients/dispatch/runners/htmlhint.js +0 -1
  82. package/dist/clients/dispatch/runners/index.js +1 -3
  83. package/dist/clients/dispatch/runners/javac.js +0 -1
  84. package/dist/clients/dispatch/runners/ktlint.js +0 -1
  85. package/dist/clients/dispatch/runners/lsp.js +37 -2
  86. package/dist/clients/dispatch/runners/markdownlint.js +4 -2
  87. package/dist/clients/dispatch/runners/mypy.js +0 -1
  88. package/dist/clients/dispatch/runners/oxlint.js +24 -22
  89. package/dist/clients/dispatch/runners/php-lint.js +0 -1
  90. package/dist/clients/dispatch/runners/phpstan.js +0 -1
  91. package/dist/clients/dispatch/runners/prisma-validate.js +0 -1
  92. package/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -1
  93. package/dist/clients/dispatch/runners/pyright.js +7 -4
  94. package/dist/clients/dispatch/runners/rubocop.js +0 -1
  95. package/dist/clients/dispatch/runners/ruff.js +0 -1
  96. package/dist/clients/dispatch/runners/rust-clippy.js +9 -6
  97. package/dist/clients/dispatch/runners/shellcheck.js +0 -1
  98. package/dist/clients/dispatch/runners/shfmt.js +0 -1
  99. package/dist/clients/dispatch/runners/spellcheck.js +0 -1
  100. package/dist/clients/dispatch/runners/spotbugs.js +0 -1
  101. package/dist/clients/dispatch/runners/sqlfluff.js +0 -1
  102. package/dist/clients/dispatch/runners/stylelint.js +0 -1
  103. package/dist/clients/dispatch/runners/swiftlint.js +0 -1
  104. package/dist/clients/dispatch/runners/taplo.js +0 -1
  105. package/dist/clients/dispatch/runners/terragrunt.js +0 -1
  106. package/dist/clients/dispatch/runners/tflint.js +0 -1
  107. package/dist/clients/dispatch/runners/tree-sitter.js +0 -1
  108. package/dist/clients/dispatch/runners/trivy-config.js +0 -1
  109. package/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
  110. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
  111. package/dist/clients/dispatch/runners/utils/runner-helpers.js +270 -105
  112. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
  113. package/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
  114. package/dist/clients/dispatch/runners/utils/toolchain-availability.js +89 -26
  115. package/dist/clients/dispatch/runners/vale.js +0 -1
  116. package/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
  117. package/dist/clients/dispatch/runners/yamllint.js +0 -1
  118. package/dist/clients/dispatch/runners/zig-check.js +0 -1
  119. package/dist/clients/disposition-logger.js +3 -3
  120. package/dist/clients/effective-config.js +403 -0
  121. package/dist/clients/error-class.js +23 -0
  122. package/dist/clients/event-loop-hold.js +274 -0
  123. package/dist/clients/event-loop-monitor.js +66 -1
  124. package/dist/clients/extension-log.js +2 -2
  125. package/dist/clients/feature-hints.js +2 -1
  126. package/dist/clients/file-time.js +6 -1
  127. package/dist/clients/file-utils.js +266 -13
  128. package/dist/clients/finding-delivery-gate.js +55 -13
  129. package/dist/clients/format-service.js +6 -1
  130. package/dist/clients/formatters.js +303 -104
  131. package/dist/clients/freshness-cadence.js +17 -0
  132. package/dist/clients/generated-artifacts.js +114 -19
  133. package/dist/clients/generation-guard.js +4 -9
  134. package/dist/clients/git-guard.js +198 -48
  135. package/dist/clients/git-tracked-ignore.js +32 -1
  136. package/dist/clients/go-client.js +39 -0
  137. package/dist/clients/govulncheck-client.js +5 -1
  138. package/dist/clients/gradle-ktfmt-style.js +252 -0
  139. package/dist/clients/gzip-stage-write.js +7 -0
  140. package/dist/clients/hashline-anchor.js +424 -0
  141. package/dist/clients/hook-budgets.js +76 -0
  142. package/dist/clients/host-edit-normalize.js +5 -2
  143. package/dist/clients/host-ports.js +1 -1
  144. package/dist/clients/installer/index.js +200 -84
  145. package/dist/clients/installer/managed-tool-refresh.js +3 -1
  146. package/dist/clients/instance-reaper.js +108 -243
  147. package/dist/clients/instance-registry-lock.js +145 -0
  148. package/dist/clients/instance-registry.js +435 -122
  149. package/dist/clients/jscpd-client.js +6 -1
  150. package/dist/clients/language-profile.js +4 -2
  151. package/dist/clients/language-registry.js +595 -0
  152. package/dist/clients/latency-logger.js +179 -3
  153. package/dist/clients/ledger-bounds.js +13 -0
  154. package/dist/clients/lens-config.js +141 -30
  155. package/dist/clients/lens-engine.js +30 -37
  156. package/dist/clients/lens-flag-registry.js +67 -6
  157. package/dist/clients/lens-map.js +3 -1
  158. package/dist/clients/log-cleanup.js +2 -2
  159. package/dist/clients/lsp/aggregation.js +3 -1
  160. package/dist/clients/lsp/cascade-tier.js +4 -0
  161. package/dist/clients/lsp/client.js +672 -49
  162. package/dist/clients/lsp/config.js +504 -156
  163. package/dist/clients/lsp/diagnostic-binding.js +10 -5
  164. package/dist/clients/lsp/edits.js +15 -2
  165. package/dist/clients/lsp/index.js +1179 -247
  166. package/dist/clients/lsp/jvm-runtime.js +5 -0
  167. package/dist/clients/lsp/language.js +18 -169
  168. package/dist/clients/lsp/launch.js +38 -9
  169. package/dist/clients/lsp/pending-aux-coverage.js +113 -12
  170. package/dist/clients/lsp/server.js +426 -160
  171. package/dist/clients/lsp/session-roots.js +192 -0
  172. package/dist/clients/lsp/spawn-history.js +2 -7
  173. package/dist/clients/lsp/tsserver-sync.js +9 -2
  174. package/dist/clients/lsp/workspace-diagnostics-cache.js +17 -7
  175. package/dist/clients/lsp/workspace-sweep-hold.js +32 -23
  176. package/dist/clients/lsp-mutation.js +179 -23
  177. package/dist/clients/map-with-concurrency.js +36 -0
  178. package/dist/clients/mcp/analyze.js +45 -10
  179. package/dist/clients/mcp/session.js +19 -17
  180. package/dist/clients/memory-sampler.js +28 -1
  181. package/dist/clients/message-end-attribution.js +30 -0
  182. package/dist/clients/metrics-history.js +21 -4
  183. package/dist/clients/middle-man-analysis.js +2 -4
  184. package/dist/clients/module-report.js +20 -45
  185. package/dist/clients/mutating-tool.js +651 -0
  186. package/dist/clients/mutation-attribution.js +368 -0
  187. package/dist/clients/mutation-bridge.js +240 -0
  188. package/dist/clients/ndjson-logger.js +247 -20
  189. package/dist/clients/observed-mutation-sources.js +101 -0
  190. package/dist/clients/observed-mutation.js +1215 -0
  191. package/dist/clients/opaque-mutation-scan.js +300 -40
  192. package/dist/clients/opengrep-config.js +4 -0
  193. package/dist/clients/package-manager.js +201 -44
  194. package/dist/clients/partial-edit-apply.js +359 -80
  195. package/dist/clients/path-attribution-telemetry.js +23 -7
  196. package/dist/clients/path-keyed-map.js +35 -3
  197. package/dist/clients/path-utils.js +460 -13
  198. package/dist/clients/persist-debounce.js +8 -1
  199. package/dist/clients/php-cs-fixer-config.js +114 -0
  200. package/dist/clients/pipeline.js +77 -24
  201. package/dist/clients/probe-home-state.js +228 -0
  202. package/dist/clients/process-bridge.js +66 -0
  203. package/dist/clients/process-singletons.js +210 -0
  204. package/dist/clients/process-snapshot.js +68 -0
  205. package/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
  206. package/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
  207. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
  208. package/dist/clients/project-diagnostics/scanner.js +61 -24
  209. package/dist/clients/project-lens-config.js +393 -122
  210. package/dist/clients/project-snapshot.js +85 -35
  211. package/dist/clients/python-environment.js +217 -0
  212. package/dist/clients/python-provenance.js +639 -0
  213. package/dist/clients/quiet-window.js +6 -1
  214. package/dist/clients/read-bridge.js +13 -21
  215. package/dist/clients/read-expansion.js +9 -46
  216. package/dist/clients/read-guard-logger.js +39 -6
  217. package/dist/clients/read-guard-tool-lines.js +364 -212
  218. package/dist/clients/read-guard.js +95 -7
  219. package/dist/clients/recent-touches.js +17 -5
  220. package/dist/clients/resource-sampler.js +195 -96
  221. package/dist/clients/review-graph/builder.js +752 -132
  222. package/dist/clients/review-graph/service.js +55 -3
  223. package/dist/clients/review-graph/shared-extraction-ir.js +7 -15
  224. package/dist/clients/review-graph/tsconfig-paths.js +5 -1
  225. package/dist/clients/review-graph/workspace-modules.js +10 -74
  226. package/dist/clients/review-graph-logger.js +30 -4
  227. package/dist/clients/runtime-agent-end.js +18 -2
  228. package/dist/clients/runtime-context.js +49 -16
  229. package/dist/clients/runtime-coordinator.js +93 -6
  230. package/dist/clients/runtime-session.js +169 -34
  231. package/dist/clients/runtime-tool-call.js +183 -35
  232. package/dist/clients/runtime-tool-result.js +894 -241
  233. package/dist/clients/runtime-turn.js +1264 -74
  234. package/dist/clients/rust-client.js +31 -0
  235. package/dist/clients/safe-spawn.js +191 -28
  236. package/dist/clients/sanitize.js +7 -1
  237. package/dist/clients/scratch-tree-policy.js +1 -3
  238. package/dist/clients/security-scan-client.js +51 -17
  239. package/dist/clients/session-lifecycle.js +238 -42
  240. package/dist/clients/session-start-observability.js +79 -0
  241. package/dist/clients/sessionstart-logger.js +12 -3
  242. package/dist/clients/sg-runner.js +27 -15
  243. package/dist/clients/sgconfig.js +6 -1
  244. package/dist/clients/shared-checkout-guard.js +338 -0
  245. package/dist/clients/skills-resolver.js +105 -0
  246. package/dist/clients/smells-rollup.js +2 -2
  247. package/dist/clients/spawn-output-cap.js +37 -0
  248. package/dist/clients/spawn-timeout-cooldown.js +15 -1
  249. package/dist/clients/startup-scan.js +6 -7
  250. package/dist/clients/startup-timing.js +69 -4
  251. package/dist/clients/string-utils.js +33 -0
  252. package/dist/clients/subagent-mode.js +17 -4
  253. package/dist/clients/test-runner-client.js +586 -118
  254. package/dist/clients/test-runner-delivery.js +239 -0
  255. package/dist/clients/tool-definition.js +41 -1
  256. package/dist/clients/tool-policy.js +92 -22
  257. package/dist/clients/tree-sitter-cache.js +40 -40
  258. package/dist/clients/tree-sitter-client.js +97 -53
  259. package/dist/clients/tree-sitter-logger.js +19 -3
  260. package/dist/clients/tree-sitter-query-loader.js +81 -1
  261. package/dist/clients/tree-sitter-shared.js +51 -46
  262. package/dist/clients/tree-sitter-symbol-extractor.js +28 -0
  263. package/dist/clients/typos-config.js +5 -0
  264. package/dist/clients/user-notify.js +6 -2
  265. package/dist/clients/warm-attach.js +8 -10
  266. package/dist/clients/widget-state.js +191 -26
  267. package/dist/clients/word-index-logger.js +15 -3
  268. package/dist/clients/word-index-store.js +463 -0
  269. package/dist/clients/word-index.js +815 -125
  270. package/dist/clients/workspace-topology.js +25 -1
  271. package/dist/clients/zizmor-config.js +29 -6
  272. package/dist/index.js +68577 -53950
  273. package/dist/mcp/analyze-cli.js +3 -1
  274. package/dist/mcp/build-staleness.js +8 -7
  275. package/dist/mcp/cli.js +3 -1
  276. package/dist/mcp/server.js +133 -6
  277. package/dist/scripts/lib/process-scan.mjs +583 -0
  278. package/dist/scripts/lib/skills-predicate.mjs +129 -0
  279. package/dist/tools/effective-config.js +89 -0
  280. package/dist/tools/lens-diagnostics.js +99 -24
  281. package/dist/tools/lsp-diagnostics.js +81 -85
  282. package/dist/tools/lsp-navigation.js +40 -17
  283. package/dist/tools/shared.js +0 -1
  284. package/docs/agent-guide.md +68 -3
  285. package/docs/ast-grep_rules_catalog.md +41 -10
  286. package/docs/astplayground.md +26 -17
  287. package/docs/configuration.md +222 -0
  288. package/docs/dependencies.md +3 -3
  289. package/docs/features.md +44 -5
  290. package/docs/language-coverage.md +2 -2
  291. package/docs/pi-lens-fixer.md +25 -0
  292. package/docs/pi-lens-investigator.md +25 -0
  293. package/docs/pi-lens-reviewer.md +27 -0
  294. package/docs/pi-lens-subagent.md +38 -0
  295. package/docs/pi-lens-warden.md +55 -0
  296. package/docs/public-api-stability.md +359 -0
  297. package/docs/release-qa-baseline.md +182 -0
  298. package/docs/subagent-compat.md +110 -30
  299. package/docs/tree-sitter_rules_catalog.md +2 -2
  300. package/package.json +17 -21
  301. package/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
  302. package/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
  303. package/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
  304. package/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
  305. package/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
  306. package/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
  307. package/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
  308. package/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
  309. package/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
  310. package/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
  311. package/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
  312. package/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
  313. package/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
  314. package/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
  315. package/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
  316. package/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
  317. package/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
  318. package/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
  319. package/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
  320. package/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
  321. package/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
  322. package/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
  323. package/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
  324. package/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
  325. package/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
  326. package/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
  327. package/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
  328. package/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
  329. package/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
  330. package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
  331. package/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
  332. package/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
  333. package/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
  334. package/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
  335. package/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
  336. package/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
  337. package/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
  338. package/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
  339. package/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
  340. package/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
  341. package/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
  342. package/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
  343. package/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
  344. package/rules/ast-grep-rules/rules/no-important.yml +21 -0
  345. package/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
  346. package/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
  347. package/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
  348. package/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
  349. package/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
  350. package/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
  351. package/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
  352. package/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
  353. package/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
  354. package/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
  355. package/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
  356. package/scripts/analyze-pi-lens-logs.mjs +192 -1
  357. package/scripts/install-selftest.mjs +58 -2
  358. package/scripts/lib/skills-predicate.mjs +129 -0
  359. package/scripts/rpc-load-check.mjs +3 -1
@@ -6,15 +6,19 @@
6
6
  * - Auto-installation strategies
7
7
  * - Platform-specific handling
8
8
  */
9
- import { existsSync, mkdirSync } from "node:fs";
9
+ import { existsSync, mkdirSync, statSync } from "node:fs";
10
10
  import { access, readFile, readdir, stat } from "node:fs/promises";
11
11
  import os from "node:os";
12
12
  import path from "node:path";
13
13
  import { getGlobalPiLensDir, getProjectIgnoreGlobs, isPathIgnoredByProject, } from "../file-utils.js";
14
+ import { augmentPythonEnvironment, detectPythonEnvironment, detectPythonVenv, pythonEnvironmentToolCandidates, } from "../python-environment.js";
15
+ export { detectPythonVenv };
14
16
  import { STAGE_TMP_PATTERN } from "../atomic-write-staging.js";
15
17
  import { DOTNET_CSHARP_ROOT_MARKERS, DOTNET_FSHARP_ROOT_MARKERS, KIND_EXTENSIONS, } from "../file-kinds.js";
16
- import { direntsHaveMarkerGlobMatch, isAtOrAboveHomeDir, isFullyQualified, isWindowsPath, pathsEqual, } from "../path-utils.js";
17
- import { ensureTool, getToolEnvironment, getToolPath, } from "../installer/index.js";
18
+ import { CARGO_WORKSPACE_MEMBER_DIALECT, direntsHaveMarkerGlobMatch, isAtOrAboveHomeDir, isFullyQualified, isWindowsPath, matchesWorkspaceMemberPattern, pathsEqual, } from "../path-utils.js";
19
+ import { ensureTool, findManagedToolBinary, getToolEnvironment, getToolPath, } from "../installer/index.js";
20
+ import * as installer from "../installer/index.js";
21
+ import { classifyProbeFailure, describeInstallAttempt, logAvailabilityDecision, } from "../dispatch/runners/utils/availability-policy.js";
18
22
  import { resolveOpengrepConfig } from "../opengrep-config.js";
19
23
  import { isZizmorAuditTarget, resolveZizmorGitHubToken, } from "../zizmor-config.js";
20
24
  import { logLatency } from "../latency-logger.js";
@@ -22,6 +26,7 @@ import { logSessionStart } from "../sessionstart-logger.js";
22
26
  import { findLocalSgconfig, resolveBaselineSgconfig } from "../sgconfig.js";
23
27
  import { findLocalTyposConfig } from "../typos-config.js";
24
28
  import { resolvePackagePath } from "../package-root.js";
29
+ import { hasCargoWorkspaceTable, readCargoWorkspaceExclude, readCargoWorkspaceMembers, readTextFileOrUndefined, } from "../cargo-manifest.js";
25
30
  import { resolveAstGrepNativeExe } from "./wait-policy/index.js";
26
31
  import { hasSpawnFailureKind, isCommandAvailableAsync, safeSpawnAsync, } from "../safe-spawn.js";
27
32
  import { launchLSP } from "./launch.js";
@@ -29,6 +34,8 @@ import { createLombokJdtlsArgs } from "./lombok.js";
29
34
  import { resolveJavaRuntimeEnv } from "./jvm-runtime.js";
30
35
  import { normalizeMapKey } from "./path-utils.js";
31
36
  import { getRubyVersionDirNamesSync } from "./ruby-drive-dirs.js";
37
+ import { getProcessSingleton } from "../process-singletons.js";
38
+ import { createGenerationSource, } from "../generation-guard.js";
32
39
  const FIXTURE_ROOT_SEGMENTS = new Set(["__fixtures__", "testdata"]);
33
40
  const FALLBACK_PROJECT_MARKERS = [
34
41
  ".git",
@@ -77,10 +84,69 @@ const PROJECT_BOUNDARY_MARKERS = [
77
84
  // process (not once per session). Keep the root-boundary marker below aligned
78
85
  // with FALLBACK_PROJECT_MARKERS, the shared fallback root-policy marker set.
79
86
  const loggedRootCeilingClamps = new Set();
80
- function isSameOrWithin(ancestor, candidate) {
87
+ const posixCaseInsensitiveByPath = new Map();
88
+ function posixFilesystemIsCaseInsensitive(root) {
89
+ const resolved = path.resolve(root);
90
+ const cached = posixCaseInsensitiveByPath.get(resolved);
91
+ if (cached !== undefined)
92
+ return cached;
93
+ if (!existsSync(resolved)) {
94
+ return false;
95
+ }
96
+ const name = path.basename(resolved);
97
+ const alternate = name.toLowerCase() === name ? name.toUpperCase() : name.toLowerCase();
98
+ let insensitive = false;
99
+ const alternatePath = path.join(path.dirname(resolved), alternate);
100
+ if (alternate !== name && existsSync(alternatePath)) {
101
+ try {
102
+ const actual = statSync(resolved);
103
+ const alternateStat = statSync(alternatePath);
104
+ insensitive =
105
+ actual.dev === alternateStat.dev && actual.ino === alternateStat.ino;
106
+ }
107
+ catch {
108
+ insensitive = false;
109
+ }
110
+ }
111
+ posixCaseInsensitiveByPath.set(resolved, insensitive);
112
+ return insensitive;
113
+ }
114
+ export function resetLSPCaseSensitivityState() {
115
+ posixCaseInsensitiveByPath.clear();
116
+ }
117
+ export function _getPosixCaseSensitivityCacheSizeForTests() {
118
+ return posixCaseInsensitiveByPath.size;
119
+ }
120
+ /**
121
+ * Path-shape-aware containment test: is `candidate` the same path as
122
+ * `ancestor`, or inside it?
123
+ *
124
+ * Exported so the session-cwd registry (`clients/lsp/config.ts`) and the
125
+ * foreign-root decline gate (`clients/lsp/index.ts`) test containment with the
126
+ * SAME comparator the root ceiling uses. A second hand-rolled `path.relative`
127
+ * helper alongside this one is how the two drift apart on Windows-shaped
128
+ * paths (shape 2 / #1150): `path.isAbsolute("C:\\repo")` is false on POSIX, so
129
+ * a host-default comparator silently mis-answers a win32 path on Linux CI.
130
+ */
131
+ export function isSameOrWithin(ancestor, candidate, deps = {}) {
81
132
  const windowsShaped = isWindowsPath(ancestor) || isWindowsPath(candidate);
82
133
  const pathApi = windowsShaped ? path.win32 : path;
83
- const relative = pathApi.relative(pathApi.resolve(ancestor), pathApi.resolve(candidate));
134
+ const resolvedAncestor = pathApi.resolve(ancestor);
135
+ const resolvedCandidate = pathApi.resolve(candidate);
136
+ let caseInsensitive = false;
137
+ if (!windowsShaped) {
138
+ if (deps.caseInsensitiveProbe) {
139
+ const cached = posixCaseInsensitiveByPath.get(resolvedAncestor);
140
+ caseInsensitive = cached ?? deps.caseInsensitiveProbe(resolvedAncestor);
141
+ if (cached === undefined) {
142
+ posixCaseInsensitiveByPath.set(resolvedAncestor, caseInsensitive);
143
+ }
144
+ }
145
+ else {
146
+ caseInsensitive = posixFilesystemIsCaseInsensitive(resolvedAncestor);
147
+ }
148
+ }
149
+ const relative = pathApi.relative(caseInsensitive ? resolvedAncestor.toLowerCase() : resolvedAncestor, caseInsensitive ? resolvedCandidate.toLowerCase() : resolvedCandidate);
84
150
  return (relative === "" ||
85
151
  (!relative.startsWith("..") && !pathApi.isAbsolute(relative)));
86
152
  }
@@ -192,6 +258,49 @@ function canInstall(allowInstall) {
192
258
  const DIRECT_LSP_NEGATIVE_TTL_MS = Math.max(30_000, Number.parseInt(process.env.PI_LENS_DIRECT_LSP_NEGATIVE_TTL_MS ?? "600000", 10) || 600_000);
193
259
  const directLspCommandUnavailableUntil = new Map();
194
260
  const directLspCommandSkipLoggedUntil = new Map();
261
+ // Availability rows are emitted from the same async launch path that owns the
262
+ // live LSP generation. A session reset can retire that generation while a
263
+ // managed lookup, install, or launch is still awaiting; stale work must not
264
+ // publish into the replacement session (#2351, shape 22).
265
+ const lspLaunchAvailabilityGeneration = createGenerationSource("lsp-launch-availability");
266
+ export function resetLspLaunchAvailabilityGeneration() {
267
+ lspLaunchAvailabilityGeneration.bump();
268
+ }
269
+ function staleLaunch(generation, subject, proc) {
270
+ if (generation.isCurrent())
271
+ return false;
272
+ try {
273
+ proc?.process?.kill();
274
+ }
275
+ catch {
276
+ // Best-effort cleanup for a process returned after service retirement.
277
+ }
278
+ generation.guardedWrite(subject, () => undefined);
279
+ return true;
280
+ }
281
+ async function installEvidenceForLaunch(toolId, installed, attempt) {
282
+ const evidence = describeInstallAttempt(attempt);
283
+ const confirmedManagedPath = await findManagedToolBinary(toolId);
284
+ return {
285
+ ...evidence,
286
+ binary: path.basename(installed),
287
+ ...(confirmedManagedPath !== undefined &&
288
+ pathsEqual(confirmedManagedPath, installed) && {
289
+ source: "managed-dir",
290
+ }),
291
+ };
292
+ }
293
+ function captureInstallAttempt(toolId) {
294
+ try {
295
+ // Capture synchronously after ensureTool resolves. Reading this later, after
296
+ // launch/evidence awaits, can observe another concurrent ensure's outcome.
297
+ return installer.getInstallAttempt?.(toolId);
298
+ }
299
+ catch {
300
+ // Older test doubles do not expose this production export.
301
+ }
302
+ return undefined;
303
+ }
195
304
  /** Re-arm direct-command availability for the next session. */
196
305
  export function resetDirectLspCommandAvailability() {
197
306
  directLspCommandUnavailableUntil.clear();
@@ -241,6 +350,7 @@ function markDirectLspCommandUnavailable(command) {
241
350
  }
242
351
  const PI_LENS_BIN_DIR = path.join(getGlobalPiLensDir(), "bin");
243
352
  export async function resolveAndLaunch(spec, allowInstall) {
353
+ const generation = lspLaunchAvailabilityGeneration.capture();
244
354
  const toolLabel = spec.managedToolId ??
245
355
  spec.candidates[spec.candidates.length - 1] ??
246
356
  "unknown";
@@ -251,6 +361,28 @@ export async function resolveAndLaunch(spec, allowInstall) {
251
361
  lastRuntimeFailure = err instanceof Error ? err : new Error(message);
252
362
  }
253
363
  };
364
+ // #2140: consult the release-managed binary directory (~/.pi-lens/bin)
365
+ // BEFORE the bare-PATH candidates below. A bare `candidates: ["opengrep"]`
366
+ // entry only ever resolves through the OS's own PATH lookup — never that
367
+ // directory — so a github/maven/archive-strategy tool installed there with
368
+ // no PATH entry ENOENTs every direct candidate first, only to have the
369
+ // managed-install step further down find the very same binary a few
370
+ // hundred ms later. `findManagedToolBinary` does a bare `fs.access`, no
371
+ // spawn, so this costs at most a few stat calls per server launch attempt
372
+ // (not per file/dispatch — launches are session-scoped). Mirrors
373
+ // `findManagedNodeToolBinary`'s npm-managed fast path (runner-helpers.ts)
374
+ // and `SecurityScanClient.probeVersion`'s equivalent fix for the CLI-scan
375
+ // half of this same issue (#2140, landed in PR #2148/#2137).
376
+ const managedProbeStartedAt = Date.now();
377
+ const managedCandidate = spec.managedToolId
378
+ ? await findManagedToolBinary(spec.managedToolId)
379
+ : undefined;
380
+ if (staleLaunch(generation, `${toolLabel}:findManagedToolBinary`)) {
381
+ return undefined;
382
+ }
383
+ const candidates = managedCandidate && !spec.candidates.includes(managedCandidate)
384
+ ? [managedCandidate, ...spec.candidates]
385
+ : spec.candidates;
254
386
  // A candidate that fails while a LATER candidate (or managed install)
255
387
  // succeeds is just fallback, not a failure — logging each immediately floods
256
388
  // the logs with scary "candidate failed / npm shim failed / Run npm install"
@@ -258,7 +390,7 @@ export async function resolveAndLaunch(spec, allowInstall) {
258
390
  // surface only if ALL direct candidates fail.
259
391
  const candidateFailures = [];
260
392
  // Step 1 & 2 — try all explicit candidates (includes bare command = PATH lookup)
261
- for (const [index, command] of spec.candidates.entries()) {
393
+ for (const [index, command] of candidates.entries()) {
262
394
  logLatency({
263
395
  type: "phase",
264
396
  phase: "lsp_launch_candidate_attempt",
@@ -268,16 +400,19 @@ export async function resolveAndLaunch(spec, allowInstall) {
268
400
  tool: toolLabel,
269
401
  command,
270
402
  index,
271
- totalCandidates: spec.candidates.length,
403
+ totalCandidates: candidates.length,
272
404
  allowInstall: canInstall(allowInstall),
273
405
  },
274
406
  });
275
- logSessionStart(`lsp launch candidate attempt tool=${toolLabel} idx=${index}/${spec.candidates.length - 1} command=${command} cwd=${spec.cwd}`);
407
+ logSessionStart(`lsp launch candidate attempt tool=${toolLabel} idx=${index}/${candidates.length - 1} command=${command} cwd=${spec.cwd}`);
276
408
  try {
277
409
  const proc = await launchLSP(command, spec.args, {
278
410
  cwd: spec.cwd,
279
411
  env: spec.env,
280
412
  });
413
+ if (staleLaunch(generation, `${toolLabel}:launchLSP:${command}`, proc)) {
414
+ return undefined;
415
+ }
281
416
  logLatency({
282
417
  type: "phase",
283
418
  phase: "lsp_launch_candidate_success",
@@ -291,9 +426,36 @@ export async function resolveAndLaunch(spec, allowInstall) {
291
426
  },
292
427
  });
293
428
  logSessionStart(`lsp launch candidate success tool=${toolLabel} idx=${index} command=${command} source=direct`);
429
+ // The managed-dir fast path (#2140) IS the availability probe for a
430
+ // release-managed tool: a real spawn just confirmed the binary
431
+ // findManagedToolBinary resolved actually launches. Gated on the exact
432
+ // managed candidate (never a later bare-PATH fallback), so a session
433
+ // start with the binary present emits exactly one decision and a
434
+ // developer-PATH-resolved copy (no managed binary at all) emits none —
435
+ // unchanged from before this fix.
436
+ if (managedCandidate !== undefined && command === managedCandidate) {
437
+ logAvailabilityDecision({
438
+ tool: toolLabel,
439
+ verdict: "available",
440
+ outcome: "success",
441
+ cause: "ok",
442
+ elapsedMs: Date.now() - managedProbeStartedAt,
443
+ latched: false,
444
+ producer: "lsp-launch",
445
+ classifiedBy: "probe",
446
+ evidence: {
447
+ binary: path.basename(managedCandidate),
448
+ source: "managed-dir",
449
+ correctsLatchedRow: false,
450
+ },
451
+ });
452
+ }
294
453
  return { process: proc, source: "direct" };
295
454
  }
296
455
  catch (err) {
456
+ if (staleLaunch(generation, `${toolLabel}:launchLSP:${command}`)) {
457
+ return undefined;
458
+ }
297
459
  const message = err instanceof Error ? err.message : String(err);
298
460
  // Defer logging: only a failure if no later candidate/install succeeds.
299
461
  candidateFailures.push({ index, command, message, err });
@@ -341,8 +503,40 @@ export async function resolveAndLaunch(spec, allowInstall) {
341
503
  }
342
504
  // Step 3 — managed install via installer registry
343
505
  if (spec.managedToolId) {
506
+ // Neither the managed-dir stat (#2140) nor any bare-PATH candidate
507
+ // resolved this tool — the negative counterpart to the fast-path
508
+ // "available" decision above, mirroring `SecurityScanClient.probeVersion`'s
509
+ // failure record so the negative case is visible in latency.log rather
510
+ // than swallowed by going straight to the install attempt.
511
+ const failedCandidate = candidateFailures.at(-1);
512
+ const classifiedFailure = classifyProbeFailure({
513
+ error: failedCandidate?.err instanceof Error
514
+ ? failedCandidate.err
515
+ : undefined,
516
+ spawnFailure: {
517
+ kind: hasSpawnFailureKind(failedCandidate?.err, "tool-not-found")
518
+ ? "tool-not-found"
519
+ : undefined,
520
+ },
521
+ }, { command: failedCandidate?.command });
522
+ logAvailabilityDecision({
523
+ tool: spec.managedToolId,
524
+ verdict: "unavailable",
525
+ outcome: classifiedFailure.outcome,
526
+ cause: classifiedFailure.cause,
527
+ elapsedMs: Date.now() - managedProbeStartedAt,
528
+ latched: false,
529
+ classifiedBy: "probe",
530
+ producer: "lsp-launch",
531
+ evidence: classifiedFailure.evidence,
532
+ });
344
533
  logSessionStart(`lsp launch ensure-tool start tool=${spec.managedToolId} cwd=${spec.cwd}`);
534
+ const installStartedAt = Date.now();
345
535
  const installed = await ensureTool(spec.managedToolId);
536
+ const installAttempt = captureInstallAttempt(spec.managedToolId);
537
+ if (staleLaunch(generation, `${toolLabel}:ensureTool`)) {
538
+ return undefined;
539
+ }
346
540
  logSessionStart(`lsp launch ensure-tool result tool=${spec.managedToolId} installed=${installed ? "yes" : "no"} path=${installed ?? ""}`);
347
541
  logLatency({
348
542
  type: "phase",
@@ -361,6 +555,9 @@ export async function resolveAndLaunch(spec, allowInstall) {
361
555
  cwd: spec.cwd,
362
556
  env: spec.env,
363
557
  });
558
+ if (staleLaunch(generation, `${toolLabel}:launchLSP:managed`, proc)) {
559
+ return undefined;
560
+ }
364
561
  logSessionStart(`lsp launch managed success tool=${spec.managedToolId} command=${installed} source=managed`);
365
562
  logLatency({
366
563
  type: "phase",
@@ -372,9 +569,37 @@ export async function resolveAndLaunch(spec, allowInstall) {
372
569
  command: installed,
373
570
  },
374
571
  });
572
+ // Compensating row for the "unavailable" decision logged above (#1606
573
+ // shape): the install fixed exactly what the fast path found missing,
574
+ // so the durable record must not be left saying the tool is off.
575
+ // `source` is only asserted when a fresh managed-dir lookup confirms
576
+ // the install actually landed in ~/.pi-lens/bin (github/maven/archive
577
+ // strategies) rather than an npm/pip/gem install elsewhere, so the
578
+ // evidence never claims a resolution this call didn't derive.
579
+ const evidence = await installEvidenceForLaunch(spec.managedToolId, installed, installAttempt);
580
+ if (staleLaunch(generation, `${toolLabel}:managed-evidence`, proc)) {
581
+ return undefined;
582
+ }
583
+ logAvailabilityDecision({
584
+ tool: spec.managedToolId,
585
+ verdict: "available",
586
+ outcome: "success",
587
+ cause: "ok",
588
+ elapsedMs: Date.now() - installStartedAt,
589
+ latched: false,
590
+ classifiedBy: "caller",
591
+ producer: "lsp-launch",
592
+ evidence: {
593
+ ...evidence,
594
+ correctsLatchedRow: false,
595
+ },
596
+ });
375
597
  return { process: proc, source: "managed" };
376
598
  }
377
599
  catch (err) {
600
+ if (staleLaunch(generation, `${toolLabel}:launchLSP:managed-rejection`)) {
601
+ return undefined;
602
+ }
378
603
  const message = err instanceof Error ? err.message : String(err);
379
604
  logSessionStart(`lsp launch managed failed tool=${spec.managedToolId} command=${installed} error=${message}`);
380
605
  logLatency({
@@ -398,12 +623,19 @@ export async function resolveAndLaunch(spec, allowInstall) {
398
623
  const reinstalled = await ensureTool(spec.managedToolId, {
399
624
  forceReinstall: true,
400
625
  });
626
+ const reinstallAttempt = captureInstallAttempt(spec.managedToolId);
627
+ if (staleLaunch(generation, `${toolLabel}:forceReinstall`)) {
628
+ return undefined;
629
+ }
401
630
  if (reinstalled) {
402
631
  try {
403
632
  const proc = await launchLSP(reinstalled, spec.args, {
404
633
  cwd: spec.cwd,
405
634
  env: spec.env,
406
635
  });
636
+ if (staleLaunch(generation, `${toolLabel}:launchLSP:forceReinstall`, proc)) {
637
+ return undefined;
638
+ }
407
639
  logSessionStart(`lsp launch managed force-reinstall success tool=${spec.managedToolId} command=${reinstalled}`);
408
640
  logLatency({
409
641
  type: "phase",
@@ -415,6 +647,24 @@ export async function resolveAndLaunch(spec, allowInstall) {
415
647
  command: reinstalled,
416
648
  },
417
649
  });
650
+ const evidence = await installEvidenceForLaunch(spec.managedToolId, reinstalled, reinstallAttempt);
651
+ if (staleLaunch(generation, `${toolLabel}:forceReinstall-evidence`, proc)) {
652
+ return undefined;
653
+ }
654
+ logAvailabilityDecision({
655
+ tool: spec.managedToolId,
656
+ verdict: "available",
657
+ outcome: "success",
658
+ cause: "ok",
659
+ elapsedMs: Date.now() - installStartedAt,
660
+ latched: false,
661
+ classifiedBy: "caller",
662
+ producer: "lsp-launch",
663
+ evidence: {
664
+ ...evidence,
665
+ correctsLatchedRow: false,
666
+ },
667
+ });
418
668
  return { process: proc, source: "managed" };
419
669
  }
420
670
  catch (retryErr) {
@@ -427,20 +677,35 @@ export async function resolveAndLaunch(spec, allowInstall) {
427
677
  }
428
678
  }
429
679
  // Step 4 — language-native runtime install (go install, gem install, …)
430
- if (spec.runtimeInstall &&
431
- (await isOnPath(spec.runtimeInstall.runtimeCommand))) {
432
- const ok = await spec.runtimeInstall.install();
680
+ const runtimeInstall = spec.runtimeInstall;
681
+ const runtimeAvailable = runtimeInstall
682
+ ? await isOnPath(runtimeInstall.runtimeCommand)
683
+ : false;
684
+ if (staleLaunch(generation, `${toolLabel}:runtimeAvailability`)) {
685
+ return undefined;
686
+ }
687
+ if (runtimeInstall && runtimeAvailable) {
688
+ const ok = await runtimeInstall.install();
689
+ if (staleLaunch(generation, `${toolLabel}:runtimeInstall`)) {
690
+ return undefined;
691
+ }
433
692
  if (ok) {
434
- const retry = spec.runtimeInstall.retryCandidates ?? spec.candidates;
693
+ const retry = runtimeInstall.retryCandidates ?? spec.candidates;
435
694
  for (const command of retry) {
436
695
  try {
437
696
  const proc = await launchLSP(command, spec.args, {
438
697
  cwd: spec.cwd,
439
698
  env: spec.env,
440
699
  });
700
+ if (staleLaunch(generation, `${toolLabel}:launchLSP:runtime`, proc)) {
701
+ return undefined;
702
+ }
441
703
  return { process: proc, source: "managed" };
442
704
  }
443
705
  catch (err) {
706
+ if (staleLaunch(generation, `${toolLabel}:launchLSP:runtime`)) {
707
+ return undefined;
708
+ }
444
709
  trackRuntimeFailure(err);
445
710
  // try next
446
711
  }
@@ -551,12 +816,15 @@ async function resolveAndLaunchTreeBinary(spec, allowInstall) {
551
816
  return undefined;
552
817
  }
553
818
  }
554
- function nodeBinCandidates(root, baseName) {
819
+ function nodeBinLocalCandidates(root, baseName) {
555
820
  const localBase = path.join(root, "node_modules", ".bin", baseName);
556
821
  if (process.platform === "win32") {
557
- return [`${localBase}.cmd`, `${localBase}.exe`, baseName];
822
+ return [`${localBase}.cmd`, `${localBase}.exe`];
558
823
  }
559
- return [localBase, baseName];
824
+ return [localBase];
825
+ }
826
+ function nodeBinCandidates(root, baseName) {
827
+ return [...nodeBinLocalCandidates(root, baseName), baseName];
560
828
  }
561
829
  function normalizeSlashKey(value) {
562
830
  const normalized = path.resolve(value).replace(/\\/g, "/");
@@ -611,6 +879,7 @@ function createInteractiveServer(spec) {
611
879
  name: spec.name,
612
880
  extensions: spec.extensions,
613
881
  root: spec.root,
882
+ fallbackFor: spec.fallbackFor,
614
883
  availabilityKey: typeof spec.command === "string" && isSimpleCommand(spec.command)
615
884
  ? spec.command
616
885
  : undefined,
@@ -896,19 +1165,23 @@ async function findAncestorFileAmong(startDir, candidateSegmentLists) {
896
1165
  * Svelte). Without this guard an offline or partial install re-runs a 120 s
897
1166
  * forced reinstall on every spawn.
898
1167
  *
899
- * The guard is a plain module-level flag, so without an explicit re-arm it
1168
+ * The guard is process-singleton state, so without an explicit re-arm it
900
1169
  * would latch for the whole extension-host process — a repair that failed
901
1170
  * once (transient registry hiccup) would stay unrepairable for every later
902
1171
  * session in that process. `resetClassicTsRepairGuard` re-arms it; callers
903
1172
  * wire that into `session_start` alongside the other per-session resets
904
1173
  * (#1570).
905
1174
  */
906
- let classicTsRepairAttempted = false;
1175
+ const CLASSIC_TS_REPAIR_FAMILY = "lsp.classic-ts-repair-guard";
1176
+ const CLASSIC_TS_REPAIR_VERSION = 1;
1177
+ function classicTsRepairState() {
1178
+ return getProcessSingleton(CLASSIC_TS_REPAIR_FAMILY, CLASSIC_TS_REPAIR_VERSION, () => ({ attempted: false }));
1179
+ }
907
1180
  /** Re-arm the classic-repair guard so a new session gets its own attempt. */
908
1181
  export function resetClassicTsRepairGuard() {
909
- classicTsRepairAttempted = false;
1182
+ classicTsRepairState().attempted = false;
910
1183
  }
911
- /** Test hook — clears the per-process classic-repair guard. */
1184
+ /** Test hook — clears the process-singleton classic-repair guard. */
912
1185
  export function _resetClassicTsRepairForTests() {
913
1186
  resetClassicTsRepairGuard();
914
1187
  }
@@ -1008,7 +1281,7 @@ async function findTsserverPath(root, allowInstall) {
1008
1281
  if (discoveredTsserver ||
1009
1282
  !discoveredTsc ||
1010
1283
  !installAllowed ||
1011
- classicTsRepairAttempted) {
1284
+ classicTsRepairState().attempted) {
1012
1285
  return discoveredTsserver;
1013
1286
  }
1014
1287
  // Repair only a compiler we can prove is TypeScript 7+. TypeScript 7 dropped
@@ -1021,7 +1294,7 @@ async function findTsserverPath(root, allowInstall) {
1021
1294
  // An older managed tree took `latest` before the registry pinned the classic
1022
1295
  // compiler. Reinstall the pinned version once so that tree self-heals,
1023
1296
  // without deleting user or project-local TypeScript installations.
1024
- classicTsRepairAttempted = true;
1297
+ classicTsRepairState().attempted = true;
1025
1298
  logSessionStart(`lsp typescript: managed compiler resolved to TypeScript ${discoveredVersion.version}, which ships no tsserver.js; reinstalling pinned classic fallback`);
1026
1299
  const repairedTsc = await ensureTool("typescript", {
1027
1300
  allowInstall: true,
@@ -1178,33 +1451,6 @@ export function DenoExcludeRoot(primary) {
1178
1451
  return primary(file);
1179
1452
  };
1180
1453
  }
1181
- /**
1182
- * Find the active Python interpreter inside the nearest virtual environment.
1183
- * Search order: VIRTUAL_ENV → CONDA_PREFIX → .venv → venv (all under root).
1184
- * Returns undefined when no venv python binary is found.
1185
- */
1186
- export async function detectPythonVenv(root) {
1187
- const isWin = process.platform === "win32";
1188
- const candidates = [
1189
- process.env.VIRTUAL_ENV,
1190
- process.env.CONDA_PREFIX,
1191
- path.join(root, ".venv"),
1192
- path.join(root, "venv"),
1193
- ].filter((v) => Boolean(v));
1194
- for (const venv of candidates) {
1195
- const pythonPath = isWin
1196
- ? path.join(venv, "Scripts", "python.exe")
1197
- : path.join(venv, "bin", "python");
1198
- try {
1199
- await access(pythonPath);
1200
- return pythonPath;
1201
- }
1202
- catch {
1203
- // not found — try next candidate
1204
- }
1205
- }
1206
- return undefined;
1207
- }
1208
1454
  // --- Server Definitions ---
1209
1455
  const JS_TS_LSP_EXTENSIONS = KIND_EXTENSIONS["jsts"].filter((ext) => ext !== ".svelte" && ext !== ".vue");
1210
1456
  // TypeScript identity and tooling discovery deliberately use separate marker
@@ -1436,6 +1682,7 @@ export const TypeScriptServer = {
1436
1682
  export const DenoServer = {
1437
1683
  id: "deno",
1438
1684
  name: "Deno Language Server",
1685
+ fallbackFor: "typescript",
1439
1686
  extensions: JS_TS_LSP_EXTENSIONS,
1440
1687
  autoPropagateDiagnostics: true,
1441
1688
  root: createRootDetector(["deno.json", "deno.jsonc"]),
@@ -1462,7 +1709,8 @@ export const PythonServer = {
1462
1709
  "poetry.lock",
1463
1710
  ])),
1464
1711
  async spawn(root, options) {
1465
- const env = await getToolEnvironment();
1712
+ const pythonEnvironment = await detectPythonEnvironment(root);
1713
+ const env = augmentPythonEnvironment(await getToolEnvironment(), pythonEnvironment);
1466
1714
  let source = "direct";
1467
1715
  // openFilesOnly: true — analyse only open files rather than the full workspace.
1468
1716
  // Avoids the 5–14 s cold-start on large projects caused by workspace-wide
@@ -1472,36 +1720,65 @@ export const PythonServer = {
1472
1720
  ...(pythonPath ? { pythonPath } : {}),
1473
1721
  openFilesOnly: true,
1474
1722
  });
1475
- // Prefer pyright-langserver; basedpyright-langserver is a drop-in fork with
1476
- // the same --stdio protocol and additional rules (e.g. reportUnusedExpression).
1477
- const localCandidates = [
1478
- ...nodeBinCandidates(root, "pyright-langserver"),
1479
- ...nodeBinCandidates(root, "basedpyright-langserver"),
1480
- ];
1481
- const direct = await resolveAndLaunch({ candidates: localCandidates, args: ["--stdio"], cwd: root, env }, false);
1482
- if (direct) {
1483
- const pythonPath = await detectPythonVenv(root);
1723
+ // Project ownership outranks checker preference: exhaust explicit project
1724
+ // candidates before a bare command can resolve to pi-lens's managed bin or
1725
+ // the host PATH. Within the project tier, preserve the established
1726
+ // pyright → basedpyright → ty preference.
1727
+ const projectPyright = await resolveAndLaunch({
1728
+ candidates: [
1729
+ ...pythonEnvironmentToolCandidates(pythonEnvironment, "pyright-langserver"),
1730
+ ...nodeBinLocalCandidates(root, "pyright-langserver"),
1731
+ ...pythonEnvironmentToolCandidates(pythonEnvironment, "basedpyright-langserver"),
1732
+ ...nodeBinLocalCandidates(root, "basedpyright-langserver"),
1733
+ ],
1734
+ args: ["--stdio"],
1735
+ cwd: root,
1736
+ env,
1737
+ }, false);
1738
+ if (projectPyright) {
1484
1739
  return {
1485
- process: direct.process,
1486
- source: direct.source,
1487
- initialization: pyrightInit(pythonPath),
1740
+ process: projectPyright.process,
1741
+ source: projectPyright.source,
1742
+ initialization: pyrightInit(pythonEnvironment?.pythonPath),
1488
1743
  };
1489
1744
  }
1490
- // ty (astral-sh/ty, #717) — an alternative Python checker/language server,
1491
- // tried ONLY when neither pyright nor basedpyright was found locally, and
1492
- // ONLY on PATH (allowInstall: false below — no managed/auto-install, unlike
1493
- // pyright's fallback right after this block). That keeps ty strictly
1494
- // opt-in: it never displaces an already-installed pyright/basedpyright,
1495
- // and it's never silently auto-installed as a default — a user only gets
1496
- // it by having installed `ty` themselves (e.g. `uv tool install ty` /
1497
- // `pip install ty`). Unlike pyright-langserver's `--stdio` flag, ty's CLI
1498
- // launches its language server via the `server` subcommand; it has no
1499
- // stable initializationOptions equivalent to pyright's `pythonPath` yet
1500
- // (astral-sh/ty#2032) — it auto-discovers `.venv`/`VIRTUAL_ENV` from cwd,
1501
- // so no `initialization` payload is sent.
1502
- const ty = await resolveAndLaunch({ candidates: ["ty"], args: ["server"], cwd: root, env }, false);
1503
- if (ty) {
1504
- return { process: ty.process, source: ty.source };
1745
+ // ty uses `ty server`, so it needs a separate launch phase from the
1746
+ // Pyright-compatible servers. It has no stable initializationOptions
1747
+ // equivalent to pyright's `pythonPath` (astral-sh/ty#2032); the child
1748
+ // environment and cwd provide interpreter discovery instead.
1749
+ const projectTy = await resolveAndLaunch({
1750
+ candidates: pythonEnvironmentToolCandidates(pythonEnvironment, "ty"),
1751
+ args: ["server"],
1752
+ cwd: root,
1753
+ env,
1754
+ }, false);
1755
+ if (projectTy) {
1756
+ return { process: projectTy.process, source: projectTy.source };
1757
+ }
1758
+ // With no project-owned checker, retain the existing PATH preference and
1759
+ // keep ty opt-in: pyright, then basedpyright, then ty. The augmented child
1760
+ // PATH includes pi-lens-managed bins before the inherited global PATH.
1761
+ const pathPyright = await resolveAndLaunch({
1762
+ candidates: ["pyright-langserver", "basedpyright-langserver"],
1763
+ args: ["--stdio"],
1764
+ cwd: root,
1765
+ env,
1766
+ }, false);
1767
+ if (pathPyright) {
1768
+ return {
1769
+ process: pathPyright.process,
1770
+ source: pathPyright.source,
1771
+ initialization: pyrightInit(pythonEnvironment?.pythonPath),
1772
+ };
1773
+ }
1774
+ const pathTy = await resolveAndLaunch({
1775
+ candidates: ["ty"],
1776
+ args: ["server"],
1777
+ cwd: root,
1778
+ env,
1779
+ }, false);
1780
+ if (pathTy) {
1781
+ return { process: pathTy.process, source: pathTy.source };
1505
1782
  }
1506
1783
  // Discover a globally-installed pyright even when install is disabled;
1507
1784
  // only the download is gated by canInstall.
@@ -1523,17 +1800,17 @@ export const PythonServer = {
1523
1800
  const resolved = await resolveAndLaunch({ candidates: managedCandidates, args: ["--stdio"], cwd: root, env }, false);
1524
1801
  if (!resolved)
1525
1802
  return undefined;
1526
- const pythonPath = await detectPythonVenv(root);
1527
1803
  return {
1528
1804
  process: resolved.process,
1529
1805
  source,
1530
- initialization: pyrightInit(pythonPath),
1806
+ initialization: pyrightInit(pythonEnvironment?.pythonPath),
1531
1807
  };
1532
1808
  },
1533
1809
  };
1534
1810
  export const PythonJediServer = {
1535
1811
  id: "python-jedi",
1536
1812
  name: "Jedi Language Server",
1813
+ fallbackFor: "python",
1537
1814
  extensions: KIND_EXTENSIONS["python"],
1538
1815
  root: RootWithFallback(createRootDetector([
1539
1816
  ".git",
@@ -1585,79 +1862,6 @@ export const GoServer = {
1585
1862
  return { ...result, initialization: { ui: { semanticTokens: true } } };
1586
1863
  },
1587
1864
  };
1588
- async function readTextFileOrUndefined(filePath) {
1589
- try {
1590
- return await readFile(filePath, "utf-8");
1591
- }
1592
- catch {
1593
- return undefined;
1594
- }
1595
- }
1596
- /**
1597
- * Slice out ONE top-level TOML table's raw body — from its `[name]` heading
1598
- * to the next top-level `[...]`/`[[...]]` heading or EOF. `members`/`exclude`
1599
- * must be read from the `[workspace]` table specifically: `[package]` has its
1600
- * OWN `exclude` key (the standard cargo-publish exclude list, conventionally
1601
- * written above `[workspace]` in a virtual-manifest-less root crate), and a
1602
- * whole-file regex would misread it as workspace membership (#1671 F4).
1603
- */
1604
- function extractTomlTableSection(content, tableName) {
1605
- const heading = new RegExp(`^\\[${tableName}\\][ \\t]*(?:#.*)?$`, "m");
1606
- const match = heading.exec(content);
1607
- if (!match)
1608
- return "";
1609
- const rest = content.slice(match.index + match[0].length);
1610
- const nextHeading = rest.match(/^\[{1,2}[^\]]+\]{1,2}[ \t]*(?:#.*)?$/m);
1611
- return nextHeading?.index !== undefined
1612
- ? rest.slice(0, nextHeading.index)
1613
- : rest;
1614
- }
1615
- function parseTomlStringArray(content, key) {
1616
- const match = content.match(new RegExp(`^[ \\t]*${key}[ \\t]*=[ \\t]*\\[([\\s\\S]*?)\\]`, "m"));
1617
- if (!match)
1618
- return [];
1619
- return [...match[1].matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => (m[1] ?? m[2] ?? "").trim());
1620
- }
1621
- /** Turn one `/`-delimited glob SEGMENT into a regex source: `*` matches any
1622
- * run of characters within the segment, `?` matches exactly one character,
1623
- * everything else is escaped and literal. */
1624
- function segmentGlobToRegExpSource(segment) {
1625
- let out = "";
1626
- for (const ch of segment) {
1627
- if (ch === "*")
1628
- out += "[^/]*";
1629
- else if (ch === "?")
1630
- out += "[^/]";
1631
- else
1632
- out += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1633
- }
1634
- return out;
1635
- }
1636
- /**
1637
- * A `members`/`exclude` entry names a path exactly, or globs it segment by
1638
- * segment: `*` and `?` inside a segment work at any depth — one wildcard
1639
- * segment (`crates/*`), a bare `*`, or several chained together for a
1640
- * deeper fixed-depth layout — matching cargo's own `glob`-crate semantics
1641
- * for a fixed number of path components.
1642
- *
1643
- * KNOWN LIMITATION (#1671 F6, documented rather than implemented): a
1644
- * recursive `**` segment (matching a variable number of path components) is
1645
- * NOT supported and never matches — cargo workspaces that rely on `**` to
1646
- * pull in an arbitrarily-nested crate tree will under-hoist (the crate stays
1647
- * independently rooted instead of joining the workspace). This is
1648
- * deliberately out of #1671's scope: the common, and the issue's fixture,
1649
- * shape is an explicit fixed-depth `members` list.
1650
- */
1651
- function matchesCargoWorkspacePattern(pattern, relativePath) {
1652
- const normalized = pattern.replace(/\/+$/, "");
1653
- if (normalized.includes("**"))
1654
- return false;
1655
- const patternSegments = normalized.split("/");
1656
- const pathSegments = relativePath.split("/");
1657
- if (patternSegments.length !== pathSegments.length)
1658
- return false;
1659
- return patternSegments.every((segment, i) => new RegExp(`^${segmentGlobToRegExpSource(segment)}$`).test(pathSegments[i]));
1660
- }
1661
1865
  /**
1662
1866
  * Given an ancestor Cargo.toml's raw contents that already contains a
1663
1867
  * `[workspace]` table, decide whether it actually claims `childDir` as a
@@ -1684,13 +1888,21 @@ function cargoWorkspaceDeclaresMember(workspaceContent, parentDir, childDir) {
1684
1888
  .join("/");
1685
1889
  if (relativePath === "" || relativePath.startsWith(".."))
1686
1890
  return false;
1687
- const workspaceSection = extractTomlTableSection(workspaceContent, "workspace");
1688
- const excluded = parseTomlStringArray(workspaceSection, "exclude");
1689
- if (excluded.some((pattern) => matchesCargoWorkspacePattern(pattern, relativePath))) {
1891
+ // Reuse the shared, `[workspace]`-scoped readers (review round 2, F4)
1892
+ // instead of hand-composing `extractTomlTableSection` +
1893
+ // `parseTomlStringArray` here — that hand-composition duplicated exactly
1894
+ // what `readCargoWorkspaceMembers`/`readCargoWorkspaceExclude` do, the
1895
+ // single-source-of-truth violation #2473 was filed to close for the OTHER
1896
+ // two Cargo.toml readers. Pattern matching goes through the ONE
1897
+ // workspace-member matcher for the same reason (#2591); cargo's dialect —
1898
+ // `**` never matches (#1671 F6), wildcards confined to one component, so
1899
+ // pattern and path must have the same component count — is
1900
+ // `CARGO_WORKSPACE_MEMBER_DIALECT`, not a private compiler here.
1901
+ const matches = (pattern) => matchesWorkspaceMemberPattern(pattern, relativePath, CARGO_WORKSPACE_MEMBER_DIALECT);
1902
+ const excluded = readCargoWorkspaceExclude(workspaceContent);
1903
+ if (excluded.some(matches))
1690
1904
  return false;
1691
- }
1692
- const members = parseTomlStringArray(workspaceSection, "members");
1693
- return members.some((pattern) => matchesCargoWorkspacePattern(pattern, relativePath));
1905
+ return readCargoWorkspaceMembers(workspaceContent).some(matches);
1694
1906
  }
1695
1907
  /**
1696
1908
  * Bound a monorepo-hoist walk-up by the session ceiling: when `file` is inside
@@ -1729,7 +1941,7 @@ function RustWorkspaceRoot() {
1729
1941
  const parentCargoPath = path.join(parent, "Cargo.toml");
1730
1942
  const parentCargoContent = await readTextFileOrUndefined(parentCargoPath);
1731
1943
  if (parentCargoContent !== undefined &&
1732
- /^\s*\[workspace\]/m.test(parentCargoContent)) {
1944
+ hasCargoWorkspaceTable(parentCargoContent)) {
1733
1945
  // Test membership against the ORIGINAL crate dir (`root`), never the
1734
1946
  // walk cursor (`current`, which may already have climbed through
1735
1947
  // gap directories that have no Cargo.toml of their own) — a
@@ -2037,6 +2249,7 @@ export const CSharpServer = {
2037
2249
  export const OmniSharpServer = createInteractiveServer({
2038
2250
  id: "omnisharp",
2039
2251
  name: "OmniSharp",
2252
+ fallbackFor: "csharp",
2040
2253
  extensions: KIND_EXTENSIONS["csharp"],
2041
2254
  root: createRootDetector([...DOTNET_CSHARP_ROOT_MARKERS]),
2042
2255
  language: "csharp",
@@ -2203,6 +2416,7 @@ export const ElixirServer = createInteractiveServer({
2203
2416
  export const ElixirExpertServer = {
2204
2417
  id: "expert",
2205
2418
  name: "Expert",
2419
+ fallbackFor: "elixir",
2206
2420
  extensions: KIND_EXTENSIONS["elixir"],
2207
2421
  root: RootWithFallback(createRootDetector(["mix.exs"])),
2208
2422
  availabilityKey: "expert",
@@ -2316,6 +2530,19 @@ export const BashServer = {
2316
2530
  name: "Bash Language Server",
2317
2531
  extensions: [".bash", ".sh", ".zsh"],
2318
2532
  root: FileDirRoot,
2533
+ // #2194 bounded the installer's own verification at 20s; the dispatch
2534
+ // touch's client-wait floor stayed at the shared 5s default, so a cold
2535
+ // spawn could still race that budget and read as unavailable even after
2536
+ // installer verification cleared it. Match the installer bound (#2169).
2537
+ // `initializeTimeoutMs` matches it too: the measured cold start
2538
+ // (9,667ms, #2194) has headroom under the unraised 15s default, but
2539
+ // leaving the two fields mismatched is the same latent shape that made
2540
+ // the Prisma/Vue raise a no-op (fix-round F1, #2233) — a slower host
2541
+ // than the one measured would still hit the 15s inner kill despite the
2542
+ // caller being willing to wait 20s. Equalizing costs nothing on the
2543
+ // success path and removes the mismatch outright.
2544
+ clientWaitTimeoutMs: 20_000,
2545
+ initializeTimeoutMs: 20_000,
2319
2546
  spawn(root, options) {
2320
2547
  return resolveAndLaunch({
2321
2548
  candidates: nodeBinCandidates(root, "bash-language-server"),
@@ -2402,6 +2629,13 @@ export const JsonServer = {
2402
2629
  ["package.json", "tsconfig.json", "jsconfig.json"],
2403
2630
  [".git"],
2404
2631
  ])),
2632
+ // See BashServer above: the installer's 20s verification bound (#2194)
2633
+ // does not, on its own, raise the dispatch touch's cold-spawn wait floor.
2634
+ // Mirror it here so a cold spawn cannot lose that race either (#2169).
2635
+ // `initializeTimeoutMs` matches it for the same reason as BashServer
2636
+ // above (fix-round F1, #2233).
2637
+ clientWaitTimeoutMs: 20_000,
2638
+ initializeTimeoutMs: 20_000,
2405
2639
  spawn(root, options) {
2406
2640
  return resolveAndLaunch({
2407
2641
  candidates: ["vscode-json-language-server"],
@@ -2444,6 +2678,17 @@ export const PrismaServer = {
2444
2678
  name: "Prisma Language Server",
2445
2679
  extensions: KIND_EXTENSIONS["prisma"],
2446
2680
  root: RootWithFallback(createRootDetector(["prisma/schema.prisma", "schema.prisma"])),
2681
+ // Matches the installer's 40s verification bound above (#2169): the
2682
+ // dispatch touch's cold-spawn wait floor defaults to 5s and would
2683
+ // otherwise time the client out well before the binary answers.
2684
+ // `initializeTimeoutMs` MUST be set alongside it (RubyServer's own
2685
+ // precedent below): unset, it falls back to the 15s
2686
+ // `INITIALIZE_TIMEOUT_MS` default in `clients/lsp/client.ts`, which
2687
+ // hard-kills the child mid-handshake — a real cold Prisma run measured
2688
+ // up to 27.3s, so the spawn would die there before this wait ever gets
2689
+ // a chance to matter (fix-round F1, #2233).
2690
+ clientWaitTimeoutMs: 40_000,
2691
+ initializeTimeoutMs: 40_000,
2447
2692
  spawn(root, options) {
2448
2693
  return resolveAndLaunch({
2449
2694
  candidates: nodeBinCandidates(root, "prisma-language-server"),
@@ -2466,6 +2711,15 @@ export const VueServer = {
2466
2711
  "pnpm-lock.yaml",
2467
2712
  "yarn.lock",
2468
2713
  ]))),
2714
+ // Vue's launcher loads the full language-service bundle before answering,
2715
+ // matching the installer's 30s verification bound (#2176). The dispatch
2716
+ // touch's cold-spawn wait floor defaults to 5s and needs the same raise so
2717
+ // a cold spawn cannot time out there instead. `initializeTimeoutMs` MUST
2718
+ // match: unset, it falls back to the 15s default in `clients/lsp/client.ts`
2719
+ // and hard-kills a cold spawn (measured up to 22.6s in #2188, 16.2s here)
2720
+ // mid-handshake before this wait can matter (fix-round F1, #2233).
2721
+ clientWaitTimeoutMs: 30_000,
2722
+ initializeTimeoutMs: 30_000,
2469
2723
  async spawn(root, options) {
2470
2724
  const tsserverPath = await findTsserverPath(root, options?.allowInstall);
2471
2725
  // Vue Language Server needs Vue dependencies installed to resolve types.
@@ -2505,6 +2759,13 @@ export const SvelteServer = {
2505
2759
  "pnpm-lock.yaml",
2506
2760
  "yarn.lock",
2507
2761
  ]))),
2762
+ // Matches the installer's 20s verification bound above (#2169): the
2763
+ // dispatch touch's cold-spawn wait floor defaults to 5s and would
2764
+ // otherwise time the client out well before the binary answers.
2765
+ // `initializeTimeoutMs` matches it for the same reason as BashServer
2766
+ // above (fix-round F1, #2233).
2767
+ clientWaitTimeoutMs: 20_000,
2768
+ initializeTimeoutMs: 20_000,
2508
2769
  async spawn(root, options) {
2509
2770
  const tsserverPath = await findTsserverPath(root, options?.allowInstall);
2510
2771
  const proc = await resolveAndLaunch({
@@ -2629,7 +2890,13 @@ export const OpengrepServer = {
2629
2890
  // Gate B). NOTE: the napi runner is NOT a subset — it delegates to napi's native
2630
2891
  // engine via root.findAll({rule}) (#206), the SAME Rust core as this LSP and the
2631
2892
  // ast-grep CLI, so rule semantics are identical across all three. The LSP's edge
2632
- // is engine-driven codeAction fixes, not faithfulness of matching.
2893
+ // is engine-driven codeAction fixes, not faithfulness of matching. #2347 closed
2894
+ // the one known divergence under Gate B: the LSP/CLI resolve embedded `<script>`
2895
+ // bodies in HTML and run `language: JavaScript` rules inside them, and the napi
2896
+ // fallback now mirrors that (each script body is reparsed as JS and findings are
2897
+ // translated back to file coordinates). A future ast-grep embedded-content
2898
+ // surface (for example `<style>` bodies, which 0.45.1 does NOT inject) must land
2899
+ // on both routes together or be recorded here as an accepted divergence.
2633
2900
  const AST_GREP_KINDS = [
2634
2901
  "csharp",
2635
2902
  "cxx",
@@ -2650,7 +2917,6 @@ const AST_GREP_KINDS = [
2650
2917
  "rust",
2651
2918
  "scala",
2652
2919
  "shell",
2653
- "solidity",
2654
2920
  "swift",
2655
2921
  "yaml",
2656
2922
  ];