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
@@ -29,16 +29,17 @@
29
29
  * - rust-analyzer (Rust LSP) [GitHub release]
30
30
  * - golangci-lint (Go linting) [GitHub release]
31
31
  *
32
- * Manual install required (25+ tools):
33
- * - yaml-language-server: npm install -g yaml-language-server
34
- * - vscode-json-languageserver: npm install -g vscode-langservers-extracted
35
- * - bash-language-server: npm install -g bash-language-server
36
- * - svelte-language-server: npm install -g svelte-language-server
37
- * - vscode-css-languageserver: npm install -g vscode-langservers-extracted
38
- * - @prisma/language-server: npm install -g @prisma/language-server
39
- * - dockerfile-language-server: npm install -g dockerfile-language-server-nodejs
40
- * - @vue/language-server: npm install -g @vue/language-server
41
- * - And all language-specific servers (gopls, rust-analyzer, etc.)
32
+ * Every other managed tool (including several the list above omits, e.g.
33
+ * bash-language-server, yaml-language-server, svelte-language-server,
34
+ * @prisma/language-server, @vue/language-server, dockerfile-language-server-nodejs,
35
+ * and vscode-css-languageserver — all of which ARE auto-installed) is
36
+ * documented by the `TOOLS` array below, not restated here. #2638: a
37
+ * hand-kept "manual install" list naming specific package specs used to live
38
+ * in this comment, drifted from `TOOLS` (it named a since-unpublished npm
39
+ * package for the CSS entry, and several tools it called "manual" were
40
+ * already auto-install above) and nothing caught it. `TOOLS` is the single
41
+ * source of truth for every id, package, and strategy; deleted rather than
42
+ * re-derived, since a comment cannot be generated from code at write time.
42
43
  *
43
44
  * Strategies:
44
45
  * - npm packages via npx/bun
@@ -234,7 +235,12 @@ export const TOOLS = [
234
235
  checkCommand: "typescript-language-server",
235
236
  checkArgs: ["--version"],
236
237
  installStrategy: "npm",
237
- packageName: "typescript-language-server",
238
+ // Pinned below the package's own floor: 6.0.0 declares
239
+ // engines.node >=22.22.2, above the pi host's own floor (pi-lens
240
+ // must never require more than pi does — #2633) — an unpinned
241
+ // install here resolves latest and prints EBADENGINE on any pi
242
+ // host's supported Node. 5.3.0's own floor is engines.node >=20.
243
+ packageName: "typescript-language-server@5.3.0",
238
244
  binaryName: "typescript-language-server",
239
245
  },
240
246
  {
@@ -383,6 +389,12 @@ export const TOOLS = [
383
389
  name: "Bash Language Server",
384
390
  checkCommand: "bash-language-server",
385
391
  checkArgs: ["--version"],
392
+ // The #2188 sweep measured a 9,667ms cold `--version` start with closed
393
+ // stdin — close enough to the 10s installer default that modest host
394
+ // contention pushes it over and emits a false verification degradation.
395
+ // 20s gives the same kind of headroom Vue's 30s bound gives its own
396
+ // slower cold start (#2176), without inventing a shared literal (#2194).
397
+ verificationTimeoutMs: 20_000,
386
398
  installStrategy: "npm",
387
399
  packageName: "bash-language-server",
388
400
  binaryName: "bash-language-server",
@@ -419,6 +431,11 @@ export const TOOLS = [
419
431
  name: "VSCode JSON Language Server",
420
432
  checkCommand: "vscode-json-language-server",
421
433
  checkArgs: ["--version"],
434
+ // The #2188 sweep measured an 11,047ms cold `--version` start with closed
435
+ // stdin — over the 10s installer default, so a cold or contended host can
436
+ // see a false verification degradation before the binary ever answers.
437
+ // 20s mirrors the bash-language-server bound above (#2194).
438
+ verificationTimeoutMs: 20_000,
422
439
  installStrategy: "npm",
423
440
  packageName: "vscode-langservers-extracted",
424
441
  binaryName: "vscode-json-language-server",
@@ -429,7 +446,7 @@ export const TOOLS = [
429
446
  checkCommand: "vscode-html-language-server",
430
447
  checkArgs: ["--version"],
431
448
  installStrategy: "npm",
432
- packageName: "vscode-html-languageserver-bin",
449
+ packageName: "vscode-langservers-extracted",
433
450
  binaryName: "vscode-html-language-server",
434
451
  },
435
452
  {
@@ -465,7 +482,8 @@ export const TOOLS = [
465
482
  id: "helm",
466
483
  name: "Helm",
467
484
  checkCommand: "helm",
468
- checkArgs: ["version", "--short"],
485
+ // intended: version --short — unverified against real binary (shape 16), do not wire until probed
486
+ checkArgs: ["--version"],
469
487
  installStrategy: "github",
470
488
  binaryName: "helm",
471
489
  github: {
@@ -517,7 +535,7 @@ export const TOOLS = [
517
535
  checkCommand: "vscode-css-language-server",
518
536
  checkArgs: ["--version"],
519
537
  installStrategy: "npm",
520
- packageName: "vscode-css-languageserver",
538
+ packageName: "vscode-langservers-extracted",
521
539
  binaryName: "vscode-css-language-server",
522
540
  },
523
541
  {
@@ -543,6 +561,11 @@ export const TOOLS = [
543
561
  name: "Prisma Language Server",
544
562
  checkCommand: "prisma-language-server",
545
563
  checkArgs: ["--version"],
564
+ // #2169: a real closed-stdin cold run measured 27,265ms, and a warm-cache
565
+ // rerun still took 9,860ms — well past the 10s installer default. 40s
566
+ // keeps the same margin-over-worst-observed ratio Vue's 30s bound uses
567
+ // (#2176) rather than trimming it for a slower binary.
568
+ verificationTimeoutMs: 40_000,
546
569
  installStrategy: "npm",
547
570
  packageName: "@prisma/language-server",
548
571
  binaryName: "prisma-language-server",
@@ -552,6 +575,10 @@ export const TOOLS = [
552
575
  name: "Vue Language Server",
553
576
  checkCommand: "vue-language-server",
554
577
  checkArgs: ["--version"],
578
+ // Vue's launcher loads the full language-service bundle before answering
579
+ // --version. Its cold start exceeds the dispatch probe budget on some hosts
580
+ // even though warm starts complete quickly (#2176).
581
+ verificationTimeoutMs: 30_000,
555
582
  installStrategy: "npm",
556
583
  packageName: "@vue/language-server",
557
584
  binaryName: "vue-language-server",
@@ -561,6 +588,10 @@ export const TOOLS = [
561
588
  name: "Svelte Language Server",
562
589
  checkCommand: "svelteserver",
563
590
  checkArgs: ["--version"],
591
+ // #2169: a real closed-stdin cold run measured 12,410ms — over the 10s
592
+ // installer default, matching the bash/JSON class of false verification
593
+ // degradation from a cold-cache host (#2194). 20s mirrors that bound.
594
+ verificationTimeoutMs: 20_000,
564
595
  installStrategy: "npm",
565
596
  packageName: "svelte-language-server",
566
597
  binaryName: "svelteserver",
@@ -569,7 +600,9 @@ export const TOOLS = [
569
600
  id: "markdownlint",
570
601
  name: "markdownlint-cli2",
571
602
  checkCommand: "markdownlint-cli2",
572
- checkArgs: ["--version"],
603
+ // `--version` is interpreted as a file glob by markdownlint-cli2. Use
604
+ // stdin with globs disabled so verification cannot scan the workspace.
605
+ checkArgs: ["--no-globs", "-"],
573
606
  installStrategy: "npm",
574
607
  packageName: "markdownlint-cli2",
575
608
  binaryName: "markdownlint-cli2",
@@ -784,7 +817,8 @@ export const TOOLS = [
784
817
  id: "spotbugs",
785
818
  name: "SpotBugs",
786
819
  checkCommand: "spotbugs",
787
- checkArgs: ["-version"],
820
+ // intended: -version — unverified against real binary (shape 16), do not wire until probed
821
+ checkArgs: ["--version"],
788
822
  installStrategy: "archive",
789
823
  binaryName: "spotbugs",
790
824
  archive: {
@@ -1030,7 +1064,8 @@ export const TOOLS = [
1030
1064
  id: "gitleaks",
1031
1065
  name: "gitleaks",
1032
1066
  checkCommand: "gitleaks",
1033
- checkArgs: ["version"],
1067
+ // intended: version — unverified against real binary (shape 16), do not wire until probed
1068
+ checkArgs: ["--version"],
1034
1069
  installStrategy: "github",
1035
1070
  binaryName: "gitleaks",
1036
1071
  github: {
@@ -1144,7 +1179,8 @@ export const TOOLS = [
1144
1179
  id: "terraform-ls",
1145
1180
  name: "terraform-ls",
1146
1181
  checkCommand: "terraform-ls",
1147
- checkArgs: ["version"],
1182
+ // intended: version — unverified against real binary (shape 16), do not wire until probed
1183
+ checkArgs: ["--version"],
1148
1184
  installStrategy: "github",
1149
1185
  binaryName: "terraform-ls",
1150
1186
  github: {
@@ -1216,6 +1252,7 @@ export const TOOLS = [
1216
1252
  id: "cue",
1217
1253
  name: "CUE",
1218
1254
  checkCommand: "cue",
1255
+ // Probed locally with CUE v0.17.1: `cue version` exits 0.
1219
1256
  checkArgs: ["version"],
1220
1257
  installStrategy: "github",
1221
1258
  binaryName: "cue",
@@ -1324,6 +1361,10 @@ export const TOOLS = [
1324
1361
  },
1325
1362
  },
1326
1363
  ];
1364
+ /** Return the bounded managed-binary verification budget for a registered tool. */
1365
+ export function getToolVerificationTimeout(tool) {
1366
+ return tool.verificationTimeoutMs ?? 10_000;
1367
+ }
1327
1368
  const ensureInFlight = new Map();
1328
1369
  const installFailureReasons = new Map();
1329
1370
  /** Last honest install refusal/failure reason for callers that need diagnostics. */
@@ -1621,8 +1662,13 @@ export async function checkProbeCache(toolId) {
1621
1662
  try {
1622
1663
  await fs.access(entry.path);
1623
1664
  const stat = await fs.stat(entry.path);
1624
- if (stat.mtimeMs !== entry.mtimeMs) {
1625
- logSessionStart(`auto-install probe-cache ${toolId}: miss (mtime changed)`);
1665
+ // #2300: size alongside mtime, from the same stat — a binary replaced
1666
+ // in-place within the same coarse-granularity mtime tick would otherwise
1667
+ // pass this check unnoticed. `entry.sizeBytes` absent (pre-#2300 entry)
1668
+ // fails open to the mtime-only check (see the field's doc comment).
1669
+ if (stat.mtimeMs !== entry.mtimeMs ||
1670
+ (entry.sizeBytes !== undefined && stat.size !== entry.sizeBytes)) {
1671
+ logSessionStart(`auto-install probe-cache ${toolId}: miss (mtime/size changed)`);
1626
1672
  delete cache[toolId];
1627
1673
  markProbeCacheChange(toolId, null);
1628
1674
  return undefined;
@@ -1650,6 +1696,7 @@ export async function updateProbeCache(toolId, resolvedPath, transient = false)
1650
1696
  const entry = {
1651
1697
  path: resolvedPath,
1652
1698
  mtimeMs: stat.mtimeMs,
1699
+ sizeBytes: stat.size,
1653
1700
  cachedAt: Date.now(),
1654
1701
  ...(transient && { transient: true }),
1655
1702
  };
@@ -1681,6 +1728,14 @@ export function resetProbeCacheStateForTesting() {
1681
1728
  _probeCacheFlushTimer = null;
1682
1729
  }
1683
1730
  }
1731
+ /**
1732
+ * Exported for testing only. Read the `ensureTool` in-flight map directly
1733
+ * (#1968's ABA regression: a second writer replacing an entry mid-flight,
1734
+ * then a late-settling first run evicting it with a bare delete-by-key).
1735
+ */
1736
+ export function _peekEnsureInFlightForTesting() {
1737
+ return ensureInFlight;
1738
+ }
1684
1739
  // --- Check Functions ---
1685
1740
  const pathWalkMemo = new Map();
1686
1741
  function hashSync(value) {
@@ -1765,6 +1820,7 @@ export async function isSpawnableCommand(command) {
1765
1820
  export function isLspTransportRequiredError(output) {
1766
1821
  return /Connection (?:input|output) stream is not set|Use arguments of createConnection/i.test(output);
1767
1822
  }
1823
+ const lspTransportRequiredMatcher = /Connection (?:input|output) stream is not set|Use arguments of createConnection/i;
1768
1824
  /**
1769
1825
  * Parse the exact version pinned in a TOOLS `packageName` spec, e.g.
1770
1826
  * `"jscpd@3.5.10"` -> `"3.5.10"`. Scoped packages (`"@ast-grep/cli"`) have no
@@ -1773,7 +1829,7 @@ export function isLspTransportRequiredError(output) {
1773
1829
  * correctly reports "no pin" rather than mistaking the scope for a version.
1774
1830
  * Returns undefined when packageName has no explicit `@version` suffix.
1775
1831
  */
1776
- function parsePinnedVersion(packageName) {
1832
+ export function parsePinnedVersion(packageName) {
1777
1833
  const at = packageName.lastIndexOf("@");
1778
1834
  if (at <= 0)
1779
1835
  return undefined;
@@ -1796,7 +1852,7 @@ function extractVersionToken(output) {
1796
1852
  */
1797
1853
  const lastManagedInstallVersion = new Map();
1798
1854
  /**
1799
- * Verify a tool binary actually works by running --version
1855
+ * Verify a tool binary actually works by running its configured checkArgs.
1800
1856
  * This catches broken symlinks, partial installs, and corrupted binaries.
1801
1857
  * `onVersionOutput`, when provided, receives the raw stdout on a successful
1802
1858
  * (exit 0) probe — used to piggyback version-pin drift detection onto this
@@ -1822,7 +1878,7 @@ onTransient,
1822
1878
  * sensitive caller on the dispatch hot path passes a shorter one and treats
1823
1879
  * the expiry as transient rather than as a verdict (#1657).
1824
1880
  */
1825
- timeoutMs = 10000) {
1881
+ timeoutMs = 10000, verificationArgs = ["--version"]) {
1826
1882
  // #2015: safeSpawnAsync instead of raw spawn. Raw spawn's timeout
1827
1883
  // SIGTERMed only cmd.exe on Windows (.cmd shims run shell:true), orphaning
1828
1884
  // the grandchild node process - which kept scanning, held handles against
@@ -1849,16 +1905,29 @@ timeoutMs = 10000) {
1849
1905
  // there), so no shell option exists or is needed here.
1850
1906
  void useShell;
1851
1907
  try {
1852
- const result = await safeSpawnAsync(execPath, ["--version"], {
1908
+ const result = await safeSpawnAsync(execPath, verificationArgs, {
1853
1909
  timeout: timeoutMs,
1910
+ input: "",
1911
+ // A broken or version-blind language server can emit a bundled
1912
+ // megabytes-long diagnostic on stderr. Keep verification bounded even
1913
+ // when the child reaches its normal timeout first.
1914
+ maxOutputBytes: 64 * 1024,
1915
+ matchWhileStreaming: lspTransportRequiredMatcher,
1854
1916
  });
1855
1917
  const output = `${result.stdout}\n${result.stderr}`;
1918
+ if (result.outputTruncated) {
1919
+ recordDegradationOnce({
1920
+ kind: "installer-verification-output-truncated",
1921
+ subject: binPath,
1922
+ reason: `verification output exceeded 65536 bytes (${verificationArgs.join(" ")})`,
1923
+ });
1924
+ }
1856
1925
  if (result.status === 0 && !result.error) {
1857
- debugLog(`Verified: ${binPath} (version: ${result.stdout.trim()})`);
1926
+ debugLog(`Verified: ${binPath} (${verificationArgs.join(" ")}: ${result.stdout.trim()})`);
1858
1927
  onVersionOutput?.(result.stdout);
1859
1928
  return true;
1860
1929
  }
1861
- if (isLspTransportRequiredError(output)) {
1930
+ if (result.streamingMatch || isLspTransportRequiredError(output)) {
1862
1931
  // Valid stdio LSP server that rejects `--version` (#208) — the
1863
1932
  // transport-required error proves the binary works.
1864
1933
  debugLog(`Verified (stdio LSP, transport-required): ${binPath}`);
@@ -1868,7 +1937,13 @@ timeoutMs = 10000) {
1868
1937
  // verdict from the binary (#1569 transient semantics).
1869
1938
  if (result.signal !== undefined || result.spawnFailure)
1870
1939
  onTransient?.();
1871
- logSessionStart(`auto-install verify: failed for ${binPath} (kind=${result.signal ? `killed-signal=${result.signal}` : result.error ? "spawn-error" : "exit-nonzero"}${result.status !== null ? `, exit=${result.status}` : ""})`);
1940
+ const kind = result.spawnFailure?.kind ??
1941
+ (result.signal
1942
+ ? `killed-signal=${result.signal}`
1943
+ : result.error
1944
+ ? "spawn-error"
1945
+ : "exit-nonzero");
1946
+ logSessionStart(`auto-install verify: failed for ${binPath} (check=${verificationArgs.join(" ")}, kind=${kind}${result.status !== null ? `, exit=${result.status}` : ""})`);
1872
1947
  return false;
1873
1948
  }
1874
1949
  catch (err) {
@@ -1938,7 +2013,7 @@ export async function getAllToolStatuses() {
1938
2013
  }
1939
2014
  // 2. Check npm global
1940
2015
  if (tool.installStrategy === "npm") {
1941
- const npmPath = await findNpmGlobalToolPath(tool.binaryName || tool.id);
2016
+ const npmPath = await findNpmGlobalToolPath(tool.binaryName || tool.id, undefined, tool.checkArgs, getToolVerificationTimeout(tool));
1942
2017
  if (npmPath) {
1943
2018
  status.installed = true;
1944
2019
  status.source = "npm-global";
@@ -1949,7 +2024,7 @@ export async function getAllToolStatuses() {
1949
2024
  }
1950
2025
  // 3. Check pip user install
1951
2026
  if (tool.installStrategy === "pip") {
1952
- const pipPath = await findPipUserToolPath(tool.binaryName || tool.id);
2027
+ const pipPath = await findPipUserToolPath(tool.binaryName || tool.id, undefined, tool.checkArgs, getToolVerificationTimeout(tool));
1953
2028
  if (pipPath) {
1954
2029
  status.installed = true;
1955
2030
  status.source = "pip-user";
@@ -1981,7 +2056,7 @@ export async function getAllToolStatuses() {
1981
2056
  const localPath = installerPlatform() === "win32" ? `${localBase}.cmd` : localBase;
1982
2057
  try {
1983
2058
  await fs.access(localPath);
1984
- if (await verifyToolBinary(localPath)) {
2059
+ if (await verifyToolBinary(localPath, undefined, undefined, getToolVerificationTimeout(tool), tool.checkArgs)) {
1985
2060
  status.installed = true;
1986
2061
  status.source = "pi-lens-auto";
1987
2062
  status.path = localPath;
@@ -2150,7 +2225,7 @@ async function getToolPathResolved(toolId, onTransient) {
2150
2225
  const cmdPath = `${localBase}.cmd`;
2151
2226
  try {
2152
2227
  await fs.access(cmdPath);
2153
- if (await verifyToolBinary(cmdPath, recordVersion, onTransient)) {
2228
+ if (await verifyToolBinary(cmdPath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
2154
2229
  return cmdPath;
2155
2230
  }
2156
2231
  logSessionStart(`auto-install verify: ${cmdPath} exists but is broken, will reinstall`);
@@ -2163,7 +2238,7 @@ async function getToolPathResolved(toolId, onTransient) {
2163
2238
  const exePath = `${localBase}.exe`;
2164
2239
  try {
2165
2240
  await fs.access(exePath);
2166
- if (await verifyToolBinary(exePath, recordVersion, onTransient)) {
2241
+ if (await verifyToolBinary(exePath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
2167
2242
  return exePath;
2168
2243
  }
2169
2244
  logSessionStart(`auto-install verify: ${exePath} exists but is broken, will reinstall`);
@@ -2175,7 +2250,7 @@ async function getToolPathResolved(toolId, onTransient) {
2175
2250
  if (installerPlatform() !== "win32") {
2176
2251
  try {
2177
2252
  await fs.access(localBase);
2178
- if (await verifyToolBinary(localBase, recordVersion, onTransient)) {
2253
+ if (await verifyToolBinary(localBase, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
2179
2254
  return localBase;
2180
2255
  }
2181
2256
  logSessionStart(`auto-install verify: ${localBase} exists but is broken, will reinstall`);
@@ -2191,7 +2266,7 @@ async function getToolPathResolved(toolId, onTransient) {
2191
2266
  if (tool.platformPackage) {
2192
2267
  const platformBin = resolvePlatformPackageBinary(tool);
2193
2268
  if (platformBin &&
2194
- (await verifyToolBinary(platformBin, undefined, onTransient))) {
2269
+ (await verifyToolBinary(platformBin, undefined, onTransient, getToolVerificationTimeout(tool), tool.checkArgs))) {
2195
2270
  logSessionStart(`auto-install ${toolId}: resolved platform-package binary at ${platformBin}`);
2196
2271
  return platformBin;
2197
2272
  }
@@ -2213,14 +2288,14 @@ async function getToolPathResolved(toolId, onTransient) {
2213
2288
  return tool.checkCommand;
2214
2289
  }
2215
2290
  if (tool.installStrategy === "npm") {
2216
- const npmPath = await findNpmGlobalToolPath(tool.binaryName || tool.id, onTransient);
2291
+ const npmPath = await findNpmGlobalToolPath(tool.binaryName || tool.id, onTransient, tool.checkArgs, getToolVerificationTimeout(tool));
2217
2292
  if (npmPath) {
2218
2293
  return npmPath;
2219
2294
  }
2220
2295
  }
2221
2296
  // For pip tools, also probe user-level script locations
2222
2297
  if (tool.installStrategy === "pip") {
2223
- const pipPath = await findPipUserToolPath(tool.binaryName || tool.id, onTransient);
2298
+ const pipPath = await findPipUserToolPath(tool.binaryName || tool.id, onTransient, tool.checkArgs, getToolVerificationTimeout(tool));
2224
2299
  if (pipPath) {
2225
2300
  return pipPath;
2226
2301
  }
@@ -2248,6 +2323,18 @@ async function findGitHubToolPath(binaryName) {
2248
2323
  }
2249
2324
  return undefined;
2250
2325
  }
2326
+ /** Resolve a release-managed binary without probing PATH first. */
2327
+ export async function findManagedToolBinary(toolId) {
2328
+ const tool = TOOLS.find((candidate) => candidate.id === toolId);
2329
+ if (!tool)
2330
+ return undefined;
2331
+ if (tool.installStrategy !== "github" &&
2332
+ tool.installStrategy !== "maven" &&
2333
+ tool.installStrategy !== "archive") {
2334
+ return undefined;
2335
+ }
2336
+ return findGitHubToolPath(tool.binaryName || tool.id);
2337
+ }
2251
2338
  function hasExecutableExtension(name) {
2252
2339
  return /\.(exe|bat|cmd|ps1)$/i.test(name);
2253
2340
  }
@@ -2278,7 +2365,7 @@ function getArchiveBinaryCandidates(binaryName, platform, assetName) {
2278
2365
  candidates.add(`${binaryName}.cmd`);
2279
2366
  return [...candidates];
2280
2367
  }
2281
- async function findNpmGlobalToolPath(binaryName, onTransient) {
2368
+ async function findNpmGlobalToolPath(binaryName, onTransient, verificationArgs = ["--version"], verificationTimeoutMs = 10_000) {
2282
2369
  const isWindows = process.platform === "win32";
2283
2370
  const binDirs = await getNpmGlobalBinCandidates(onTransient);
2284
2371
  for (const dir of binDirs) {
@@ -2291,7 +2378,7 @@ async function findNpmGlobalToolPath(binaryName, onTransient) {
2291
2378
  for (const candidate of candidates) {
2292
2379
  try {
2293
2380
  await fs.access(candidate);
2294
- if (await verifyToolBinary(candidate, undefined, onTransient)) {
2381
+ if (await verifyToolBinary(candidate, undefined, onTransient, verificationTimeoutMs, verificationArgs)) {
2295
2382
  return candidate;
2296
2383
  }
2297
2384
  }
@@ -2332,7 +2419,7 @@ async function getNpmGlobalBinCandidates(onTransient) {
2332
2419
  }
2333
2420
  return dirs;
2334
2421
  }
2335
- async function findPipUserToolPath(binaryName, onTransient) {
2422
+ async function findPipUserToolPath(binaryName, onTransient, verificationArgs = ["--version"], verificationTimeoutMs = 10_000) {
2336
2423
  const isWindows = process.platform === "win32";
2337
2424
  const userBaseCandidates = await getPythonUserBaseCandidates();
2338
2425
  for (const userBase of userBaseCandidates) {
@@ -2365,7 +2452,7 @@ async function findPipUserToolPath(binaryName, onTransient) {
2365
2452
  for (const candidate of candidates) {
2366
2453
  try {
2367
2454
  await fs.access(candidate);
2368
- if (await verifyToolBinary(candidate, undefined, onTransient)) {
2455
+ if (await verifyToolBinary(candidate, undefined, onTransient, verificationTimeoutMs, verificationArgs)) {
2369
2456
  return candidate;
2370
2457
  }
2371
2458
  }
@@ -2837,8 +2924,10 @@ export function getRefreshableManagedTools() {
2837
2924
  refreshable.push({
2838
2925
  toolId: tool.id,
2839
2926
  strategy: "npm",
2927
+ checkArgs: tool.checkArgs,
2840
2928
  packageName: tool.packageName,
2841
2929
  binaryName: tool.binaryName,
2930
+ verificationTimeoutMs: tool.verificationTimeoutMs,
2842
2931
  });
2843
2932
  break;
2844
2933
  }
@@ -2849,6 +2938,7 @@ export function getRefreshableManagedTools() {
2849
2938
  refreshable.push({
2850
2939
  toolId: tool.id,
2851
2940
  strategy: tool.installStrategy,
2941
+ checkArgs: tool.checkArgs,
2852
2942
  packageName: tool.packageName,
2853
2943
  });
2854
2944
  break;
@@ -2856,7 +2946,11 @@ export function getRefreshableManagedTools() {
2856
2946
  case "github": {
2857
2947
  if (!tool.github)
2858
2948
  continue;
2859
- refreshable.push({ toolId: tool.id, strategy: "github" });
2949
+ refreshable.push({
2950
+ toolId: tool.id,
2951
+ strategy: "github",
2952
+ checkArgs: tool.checkArgs,
2953
+ });
2860
2954
  break;
2861
2955
  }
2862
2956
  case "maven": {
@@ -2865,6 +2959,7 @@ export function getRefreshableManagedTools() {
2865
2959
  refreshable.push({
2866
2960
  toolId: tool.id,
2867
2961
  strategy: "maven",
2962
+ checkArgs: tool.checkArgs,
2868
2963
  pinnedCoordinate: mavenCoordinate(tool.maven),
2869
2964
  });
2870
2965
  break;
@@ -2880,6 +2975,7 @@ export function getRefreshableManagedTools() {
2880
2975
  refreshable.push({
2881
2976
  toolId: tool.id,
2882
2977
  strategy: "archive",
2978
+ checkArgs: tool.checkArgs,
2883
2979
  pinnedCoordinate: url,
2884
2980
  });
2885
2981
  break;
@@ -2951,7 +3047,8 @@ async function probeManagedToolVersion(tool) {
2951
3047
  return undefined;
2952
3048
  try {
2953
3049
  const result = await safeSpawnAsync(cached.path, tool.checkArgs, {
2954
- timeout: 10_000,
3050
+ timeout: getToolVerificationTimeout(tool),
3051
+ input: "",
2955
3052
  ignoreAmbientSignal: true,
2956
3053
  resourceLabel: `tool-refresh-version:${tool.id}`,
2957
3054
  });
@@ -3187,8 +3284,8 @@ async function verifyRefreshedArtifact(tool, installedPath) {
3187
3284
  await updateProbeCache(tool.id, installedPath);
3188
3285
  return true;
3189
3286
  }
3190
- if (!(await verifyToolBinary(installedPath))) {
3191
- logSessionStart(`managed-tool-refresh ${tool.id}: refreshed artifact at ${installedPath} failed its --version check`);
3287
+ if (!(await verifyToolBinary(installedPath, undefined, undefined, getToolVerificationTimeout(tool), tool.checkArgs))) {
3288
+ logSessionStart(`managed-tool-refresh ${tool.id}: refreshed artifact at ${installedPath} failed its verification check`);
3192
3289
  return false;
3193
3290
  }
3194
3291
  // Record the NEW mtime rather than leaving the persisted entry to miss its
@@ -3212,10 +3309,10 @@ async function refreshPackageManagerManagedTool(tool) {
3212
3309
  const installed = tool.installStrategy === "pip"
3213
3310
  ? // `-U` is the whole fix: without it pip treats the installed copy as
3214
3311
  // satisfying the requirement and the day-one version never moves.
3215
- await installPipTool(tool.packageName, { upgrade: true })
3312
+ await installPipTool(tool.id, tool.packageName, { upgrade: true })
3216
3313
  : // `gem install` always fetches the newest version that satisfies the
3217
3314
  // requirement, so the install command IS the upgrade command.
3218
- await installGemTool(tool.packageName);
3315
+ await installGemTool(tool.id, tool.packageName);
3219
3316
  if (!installed) {
3220
3317
  return {
3221
3318
  ok: false,
@@ -3643,7 +3740,24 @@ async function installArchiveTool(tool) {
3643
3740
  return undefined;
3644
3741
  }
3645
3742
  }
3646
- async function installNpmTool(packageName, binaryName) {
3743
+ /**
3744
+ * Record a genuine package-manager install exception against `toolId` and
3745
+ * log it — the one place `installNpmTool`/`installPipTool`/`installGemTool`'s
3746
+ * catch blocks funnel through (#2661 review F3/Sonar: the three catches were
3747
+ * byte-identical but for the strategy label, a duplication SonarCloud's new-
3748
+ * code gate correctly flagged). The real error (registry E404, EBADENGINE, a
3749
+ * network failure, "no version satisfies…") lands in `installFailureReasons`
3750
+ * so a caller that distinguishes a genuine installer defect from a policy
3751
+ * decline (the tool-smoke lane, #2638) sees the actual message, never the
3752
+ * generic fallback `finishInstallAttempt` uses when nothing set it.
3753
+ */
3754
+ function recordPackageManagerInstallException(toolId, strategyLabel, packageName, err) {
3755
+ const message = err.message;
3756
+ logSessionStart(`auto-install ${strategyLabel} ${packageName}: exception: ${message}`);
3757
+ installFailureReasons.set(toolId, message);
3758
+ return undefined;
3759
+ }
3760
+ async function installNpmTool(toolId, packageName, binaryName, verificationArgs = ["--version"], verificationTimeoutMs = 10_000) {
3647
3761
  try {
3648
3762
  // Ensure tools directory exists
3649
3763
  await fs.mkdir(TOOLS_DIR, { recursive: true });
@@ -3727,7 +3841,7 @@ async function installNpmTool(packageName, binaryName) {
3727
3841
  lastAttemptTransient = false;
3728
3842
  isValid = await verifyToolBinary(binPath, undefined, () => {
3729
3843
  lastAttemptTransient = true;
3730
- });
3844
+ }, verificationTimeoutMs, verificationArgs);
3731
3845
  if (isValid)
3732
3846
  break;
3733
3847
  if (attempt < 3) {
@@ -3767,14 +3881,26 @@ async function installNpmTool(packageName, binaryName) {
3767
3881
  return binPath;
3768
3882
  }
3769
3883
  catch (err) {
3770
- logSessionStart(`auto-install npm ${packageName}: exception: ${err.message}`);
3771
- return undefined;
3884
+ return recordPackageManagerInstallException(toolId, "npm", packageName, err);
3772
3885
  }
3773
3886
  }
3887
+ /**
3888
+ * The pip command ladder `installPipTool` tries, per platform, in priority
3889
+ * order — exported so a caller that needs to know "is ANY pip toolchain
3890
+ * reachable on this runner" (without running an install) probes the SAME
3891
+ * candidates rather than a second, narrower guess (#2661 review F4/S5: the
3892
+ * tool-smoke lane's own presence check tried bare `pip` only and missed a
3893
+ * `pip3`-only or python-module-only runner).
3894
+ */
3895
+ export function pipCommandCandidates() {
3896
+ return process.platform === "win32"
3897
+ ? ["pip", "py", "python"]
3898
+ : ["pip3", "pip", "python3", "python"];
3899
+ }
3774
3900
  /**
3775
3901
  * Install a pip package tool
3776
3902
  */
3777
- async function installPipTool(packageName,
3903
+ async function installPipTool(toolId, packageName,
3778
3904
  /**
3779
3905
  * Add `-U`, turning the install into an upgrade. Without it `pip install`
3780
3906
  * treats an already-present package as satisfied and leaves the day-one
@@ -3789,30 +3915,16 @@ options = {}) {
3789
3915
  const verb = options.upgrade
3790
3916
  ? ["install", "-U", "--user"]
3791
3917
  : ["install", "--user"];
3792
- const pipCandidates = isWindows
3793
- ? [
3794
- { command: "pip", args: [...verb, packageName] },
3795
- {
3796
- command: "py",
3797
- args: ["-m", "pip", ...verb, packageName],
3798
- },
3799
- {
3800
- command: "python",
3801
- args: ["-m", "pip", ...verb, packageName],
3802
- },
3803
- ]
3804
- : [
3805
- { command: "pip3", args: [...verb, packageName] },
3806
- { command: "pip", args: [...verb, packageName] },
3807
- {
3808
- command: "python3",
3809
- args: ["-m", "pip", ...verb, packageName],
3810
- },
3811
- {
3812
- command: "python",
3813
- args: ["-m", "pip", ...verb, packageName],
3814
- },
3815
- ];
3918
+ // Built from `pipCommandCandidates()` — the single source of truth this
3919
+ // module and any other caller (the tool-smoke lane's toolchain-presence
3920
+ // probe, #2661 review) share, rather than a second, independently
3921
+ // maintained ladder that can drift.
3922
+ const pipCandidates = pipCommandCandidates().map((command) => ({
3923
+ command,
3924
+ args: command === "pip" || command === "pip3"
3925
+ ? [...verb, packageName]
3926
+ : ["-m", "pip", ...verb, packageName],
3927
+ }));
3816
3928
  let lastError = "";
3817
3929
  for (const candidate of pipCandidates) {
3818
3930
  const pipResult = await safeSpawnAsync(candidate.command, candidate.args, {
@@ -3909,11 +4021,10 @@ options = {}) {
3909
4021
  throw new Error(`Failed to install ${packageName}: no usable pip command found (${lastError || "unknown error"})`);
3910
4022
  }
3911
4023
  catch (err) {
3912
- logSessionStart(`auto-install pip ${packageName}: exception: ${err.message}`);
3913
- return undefined;
4024
+ return recordPackageManagerInstallException(toolId, "pip", packageName, err);
3914
4025
  }
3915
4026
  }
3916
- async function installGemTool(packageName) {
4027
+ async function installGemTool(toolId, packageName) {
3917
4028
  try {
3918
4029
  const gemResult = await safeSpawnAsync("gem", ["install", packageName, "--no-document"], {
3919
4030
  timeout: 120_000,
@@ -3930,8 +4041,7 @@ async function installGemTool(packageName) {
3930
4041
  return packageName;
3931
4042
  }
3932
4043
  catch (err) {
3933
- logSessionStart(`auto-install gem ${packageName}: exception: ${err.message}`);
3934
- return undefined;
4044
+ return recordPackageManagerInstallException(toolId, "gem", packageName, err);
3935
4045
  }
3936
4046
  }
3937
4047
  /**
@@ -4022,7 +4132,7 @@ export async function installTool(toolId) {
4022
4132
  case "npm": {
4023
4133
  if (!tool.packageName || !tool.binaryName)
4024
4134
  return false;
4025
- const npmPath = await installNpmTool(tool.packageName, tool.binaryName);
4135
+ const npmPath = await installNpmTool(tool.id, tool.packageName, tool.binaryName, tool.checkArgs, getToolVerificationTimeout(tool));
4026
4136
  if (npmPath !== undefined) {
4027
4137
  // #1746 review F4: an install just resolved this package's range
4028
4138
  // against the registry, so record it as freshly checked. Otherwise a
@@ -4048,13 +4158,13 @@ export async function installTool(toolId) {
4048
4158
  case "pip": {
4049
4159
  if (!tool.packageName)
4050
4160
  return false;
4051
- const pipPath = await installPipTool(tool.packageName);
4161
+ const pipPath = await installPipTool(tool.id, tool.packageName);
4052
4162
  return finishInstallAttempt(tool.id, pipPath !== undefined, startedAt);
4053
4163
  }
4054
4164
  case "gem": {
4055
4165
  if (!tool.packageName)
4056
4166
  return false;
4057
- const gemPath = await installGemTool(tool.packageName);
4167
+ const gemPath = await installGemTool(tool.id, tool.packageName);
4058
4168
  return finishInstallAttempt(tool.id, gemPath !== undefined, startedAt);
4059
4169
  }
4060
4170
  case "github": {
@@ -4316,7 +4426,13 @@ async function ensureToolResolved(toolId, opts) {
4316
4426
  return await ensurePromise;
4317
4427
  }
4318
4428
  finally {
4319
- ensureInFlight.delete(inFlightKey);
4429
+ // Identity-guarded release (#1968's pattern): delete only if THIS run is
4430
+ // still the registered one. A bare delete-by-key lets a late-settling run
4431
+ // evict a live successor a second writer registered under the same key
4432
+ // mid-flight, after which the next caller starts a duplicate ensure/install.
4433
+ if (ensureInFlight.get(inFlightKey) === ensurePromise) {
4434
+ ensureInFlight.delete(inFlightKey);
4435
+ }
4320
4436
  }
4321
4437
  }
4322
4438
  // --- Integration Helpers ---