@herbertgao/pi-extensions 2026.9.0 → 2026.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +13 -1
  2. package/node_modules/@czottmann/pi-automode/README.md +20 -0
  3. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +1 -1
  4. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +2 -1
  5. package/node_modules/@czottmann/pi-automode/docs/defaults.md +3 -2
  6. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +38 -2
  7. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +5 -3
  8. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +11 -3
  9. package/node_modules/@czottmann/pi-automode/package.json +1 -1
  10. package/node_modules/@pi-plugins/fast-mode/README.md +5 -3
  11. package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
  12. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20 -55
  13. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  14. package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
  15. package/node_modules/pi-lens/CHANGELOG.md +988 -0
  16. package/node_modules/pi-lens/README.md +3 -0
  17. package/node_modules/pi-lens/config/biome/core.jsonc +11 -2
  18. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +13 -1
  19. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +2 -2
  20. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +927 -80
  21. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +13 -4
  22. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +123 -3
  23. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +60 -5
  24. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +2 -2
  25. package/node_modules/pi-lens/dist/clients/bash-file-access.js +1 -3
  26. package/node_modules/pi-lens/dist/clients/biome-client.js +9 -2
  27. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +14 -0
  28. package/node_modules/pi-lens/dist/clients/bootstrap.js +509 -73
  29. package/node_modules/pi-lens/dist/clients/bounded-cache.js +152 -12
  30. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +59 -4
  31. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +113 -0
  32. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +2 -2
  33. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +31 -4
  34. package/node_modules/pi-lens/dist/clients/cache-manager.js +105 -6
  35. package/node_modules/pi-lens/dist/clients/cache-observability.js +44 -18
  36. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +422 -0
  37. package/node_modules/pi-lens/dist/clients/cascade-logger.js +2 -2
  38. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +13 -3
  39. package/node_modules/pi-lens/dist/clients/complexity-client.js +16 -5
  40. package/node_modules/pi-lens/dist/clients/config-core/deny.js +221 -0
  41. package/node_modules/pi-lens/dist/clients/config-core/index.js +47 -0
  42. package/node_modules/pi-lens/dist/clients/config-core/merge.js +357 -0
  43. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +340 -0
  44. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +248 -0
  45. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +171 -0
  46. package/node_modules/pi-lens/dist/clients/config-core/records.js +218 -0
  47. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +125 -0
  48. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +78 -0
  49. package/node_modules/pi-lens/dist/clients/config-core/schema.js +167 -0
  50. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +281 -0
  51. package/node_modules/pi-lens/dist/clients/config-locations.js +160 -0
  52. package/node_modules/pi-lens/dist/clients/config-resolve.js +789 -0
  53. package/node_modules/pi-lens/dist/clients/config-schema.js +197 -0
  54. package/node_modules/pi-lens/dist/clients/config-warn.js +407 -0
  55. package/node_modules/pi-lens/dist/clients/dead-code-client.js +19 -14
  56. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +2 -2
  57. package/node_modules/pi-lens/dist/clients/deadline-utils.js +178 -0
  58. package/node_modules/pi-lens/dist/clients/debug-handles.js +2 -2
  59. package/node_modules/pi-lens/dist/clients/debug-heap.js +3 -3
  60. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +106 -0
  61. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +113 -7
  62. package/node_modules/pi-lens/dist/clients/dependency-checker.js +8 -19
  63. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +12 -9
  64. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +14 -4
  65. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +162 -18
  66. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +97 -0
  67. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +9 -7
  68. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -1
  69. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +380 -26
  70. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -1
  71. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -1
  72. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -1
  73. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -1
  74. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -1
  75. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -1
  76. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -1
  77. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -1
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -1
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -1
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -1
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -1
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +1 -3
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -1
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -1
  85. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -1
  86. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -1
  87. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -1
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -1
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -1
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -1
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +13 -18
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -1
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -1
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -1
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +7 -4
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +1 -3
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -1
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -1
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -1
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -1
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -1
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -1
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -1
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -1
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -1
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -1
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +194 -80
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +84 -15
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -1
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -1
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -1
  121. package/node_modules/pi-lens/dist/clients/disposition-logger.js +3 -3
  122. package/node_modules/pi-lens/dist/clients/effective-config.js +403 -0
  123. package/node_modules/pi-lens/dist/clients/error-class.js +23 -0
  124. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +274 -0
  125. package/node_modules/pi-lens/dist/clients/extension-log.js +2 -2
  126. package/node_modules/pi-lens/dist/clients/feature-hints.js +2 -1
  127. package/node_modules/pi-lens/dist/clients/file-time.js +6 -1
  128. package/node_modules/pi-lens/dist/clients/file-utils.js +112 -9
  129. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +50 -13
  130. package/node_modules/pi-lens/dist/clients/format-service.js +6 -1
  131. package/node_modules/pi-lens/dist/clients/formatters.js +276 -102
  132. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +17 -0
  133. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +114 -19
  134. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -9
  135. package/node_modules/pi-lens/dist/clients/git-guard.js +1 -1
  136. package/node_modules/pi-lens/dist/clients/go-client.js +39 -0
  137. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +252 -0
  138. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +424 -0
  139. package/node_modules/pi-lens/dist/clients/hook-budgets.js +76 -0
  140. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +5 -2
  141. package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
  142. package/node_modules/pi-lens/dist/clients/installer/index.js +71 -52
  143. package/node_modules/pi-lens/dist/clients/instance-reaper.js +104 -230
  144. package/node_modules/pi-lens/dist/clients/instance-registry.js +6 -3
  145. package/node_modules/pi-lens/dist/clients/language-registry.js +595 -0
  146. package/node_modules/pi-lens/dist/clients/latency-logger.js +160 -2
  147. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +13 -0
  148. package/node_modules/pi-lens/dist/clients/lens-config.js +141 -30
  149. package/node_modules/pi-lens/dist/clients/lens-engine.js +30 -37
  150. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +59 -6
  151. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -2
  152. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +3 -1
  153. package/node_modules/pi-lens/dist/clients/lsp/client.js +168 -30
  154. package/node_modules/pi-lens/dist/clients/lsp/config.js +483 -158
  155. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +1 -1
  156. package/node_modules/pi-lens/dist/clients/lsp/edits.js +15 -2
  157. package/node_modules/pi-lens/dist/clients/lsp/index.js +676 -71
  158. package/node_modules/pi-lens/dist/clients/lsp/language.js +18 -169
  159. package/node_modules/pi-lens/dist/clients/lsp/launch.js +2 -1
  160. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +5 -16
  161. package/node_modules/pi-lens/dist/clients/lsp/server.js +284 -147
  162. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +116 -23
  163. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +2 -7
  164. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +9 -2
  165. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +1 -1
  166. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +163 -21
  167. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +36 -0
  168. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +33 -4
  169. package/node_modules/pi-lens/dist/clients/mcp/session.js +5 -16
  170. package/node_modules/pi-lens/dist/clients/memory-sampler.js +8 -3
  171. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +2 -4
  172. package/node_modules/pi-lens/dist/clients/module-report.js +20 -45
  173. package/node_modules/pi-lens/dist/clients/mutating-tool.js +651 -0
  174. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +368 -0
  175. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +240 -0
  176. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +247 -20
  177. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +101 -0
  178. package/node_modules/pi-lens/dist/clients/observed-mutation.js +1215 -0
  179. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +68 -22
  180. package/node_modules/pi-lens/dist/clients/opengrep-config.js +4 -0
  181. package/node_modules/pi-lens/dist/clients/package-manager.js +195 -26
  182. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +359 -80
  183. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +23 -7
  184. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +21 -2
  185. package/node_modules/pi-lens/dist/clients/path-utils.js +428 -13
  186. package/node_modules/pi-lens/dist/clients/persist-debounce.js +8 -1
  187. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +114 -0
  188. package/node_modules/pi-lens/dist/clients/pipeline.js +77 -16
  189. package/node_modules/pi-lens/dist/clients/probe-home-state.js +228 -0
  190. package/node_modules/pi-lens/dist/clients/process-bridge.js +66 -0
  191. package/node_modules/pi-lens/dist/clients/process-snapshot.js +68 -0
  192. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
  193. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
  194. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +51 -20
  195. package/node_modules/pi-lens/dist/clients/project-lens-config.js +393 -122
  196. package/node_modules/pi-lens/dist/clients/project-snapshot.js +52 -24
  197. package/node_modules/pi-lens/dist/clients/python-environment.js +217 -0
  198. package/node_modules/pi-lens/dist/clients/python-provenance.js +639 -0
  199. package/node_modules/pi-lens/dist/clients/quiet-window.js +6 -1
  200. package/node_modules/pi-lens/dist/clients/read-bridge.js +13 -21
  201. package/node_modules/pi-lens/dist/clients/read-expansion.js +9 -46
  202. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +15 -4
  203. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +364 -212
  204. package/node_modules/pi-lens/dist/clients/read-guard.js +21 -0
  205. package/node_modules/pi-lens/dist/clients/resource-sampler.js +195 -96
  206. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +152 -109
  207. package/node_modules/pi-lens/dist/clients/review-graph/service.js +55 -3
  208. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +7 -15
  209. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +10 -74
  210. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +9 -3
  211. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +18 -2
  212. package/node_modules/pi-lens/dist/clients/runtime-context.js +49 -16
  213. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +25 -6
  214. package/node_modules/pi-lens/dist/clients/runtime-session.js +139 -22
  215. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +168 -35
  216. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +797 -243
  217. package/node_modules/pi-lens/dist/clients/runtime-turn.js +1028 -53
  218. package/node_modules/pi-lens/dist/clients/rust-client.js +31 -0
  219. package/node_modules/pi-lens/dist/clients/safe-spawn.js +33 -10
  220. package/node_modules/pi-lens/dist/clients/sanitize.js +7 -1
  221. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +1 -3
  222. package/node_modules/pi-lens/dist/clients/security-scan-client.js +3 -0
  223. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +1 -1
  224. package/node_modules/pi-lens/dist/clients/session-start-observability.js +79 -0
  225. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +12 -3
  226. package/node_modules/pi-lens/dist/clients/sgconfig.js +6 -1
  227. package/node_modules/pi-lens/dist/clients/skills-resolver.js +105 -0
  228. package/node_modules/pi-lens/dist/clients/smells-rollup.js +2 -2
  229. package/node_modules/pi-lens/dist/clients/string-utils.js +13 -0
  230. package/node_modules/pi-lens/dist/clients/subagent-mode.js +17 -4
  231. package/node_modules/pi-lens/dist/clients/test-runner-client.js +176 -39
  232. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +239 -0
  233. package/node_modules/pi-lens/dist/clients/tool-definition.js +41 -1
  234. package/node_modules/pi-lens/dist/clients/tool-policy.js +91 -21
  235. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +40 -40
  236. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +86 -48
  237. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +2 -2
  238. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +81 -1
  239. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +51 -46
  240. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +28 -0
  241. package/node_modules/pi-lens/dist/clients/typos-config.js +5 -0
  242. package/node_modules/pi-lens/dist/clients/user-notify.js +6 -2
  243. package/node_modules/pi-lens/dist/clients/widget-state.js +191 -26
  244. package/node_modules/pi-lens/dist/clients/word-index-logger.js +2 -2
  245. package/node_modules/pi-lens/dist/clients/word-index-store.js +20 -5
  246. package/node_modules/pi-lens/dist/clients/word-index.js +243 -31
  247. package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -1
  248. package/node_modules/pi-lens/dist/clients/zizmor-config.js +3 -0
  249. package/node_modules/pi-lens/dist/index.js +64335 -54300
  250. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -1
  251. package/node_modules/pi-lens/dist/mcp/server.js +127 -5
  252. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +583 -0
  253. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +129 -0
  254. package/node_modules/pi-lens/dist/tools/effective-config.js +89 -0
  255. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +95 -24
  256. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +67 -80
  257. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +37 -16
  258. package/node_modules/pi-lens/dist/tools/shared.js +0 -1
  259. package/node_modules/pi-lens/docs/agent-guide.md +64 -1
  260. package/node_modules/pi-lens/docs/configuration.md +222 -0
  261. package/node_modules/pi-lens/docs/dependencies.md +3 -3
  262. package/node_modules/pi-lens/docs/features.md +44 -5
  263. package/node_modules/pi-lens/docs/language-coverage.md +2 -2
  264. package/node_modules/pi-lens/docs/pi-lens-fixer.md +25 -0
  265. package/node_modules/pi-lens/docs/pi-lens-investigator.md +25 -0
  266. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +27 -0
  267. package/node_modules/pi-lens/docs/pi-lens-subagent.md +38 -0
  268. package/node_modules/pi-lens/docs/pi-lens-warden.md +55 -0
  269. package/node_modules/pi-lens/docs/public-api-stability.md +359 -0
  270. package/node_modules/pi-lens/docs/release-qa-baseline.md +182 -0
  271. package/node_modules/pi-lens/docs/subagent-compat.md +110 -30
  272. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +2 -2
  273. package/node_modules/pi-lens/package.json +15 -23
  274. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
  275. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
  276. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
  277. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +192 -1
  278. package/node_modules/pi-lens/scripts/install-selftest.mjs +58 -2
  279. package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +129 -0
  280. package/node_modules/pi-lens/scripts/rpc-load-check.mjs +3 -1
  281. package/node_modules/pi-web-access/CHANGELOG.md +28 -0
  282. package/node_modules/pi-web-access/README.md +62 -15
  283. package/node_modules/pi-web-access/curator-page.ts +3 -1
  284. package/node_modules/pi-web-access/curator-server.ts +5 -1
  285. package/node_modules/pi-web-access/gemini-search.ts +8 -4
  286. package/node_modules/pi-web-access/github-extract.ts +242 -1
  287. package/node_modules/pi-web-access/index.ts +118 -64
  288. package/node_modules/pi-web-access/mistral-search.ts +281 -0
  289. package/node_modules/pi-web-access/package.json +2 -2
  290. package/node_modules/pi-web-access/perplexity.ts +14 -1
  291. package/node_modules/pi-web-access/utils.ts +23 -6
  292. package/node_modules/pi-web-access/xai-search.ts +96 -33
  293. package/package.json +5 -5
@@ -13,13 +13,18 @@ import { AsyncLocalStorage } from "node:async_hooks";
13
13
  import * as fs from "node:fs/promises";
14
14
  import * as path from "node:path";
15
15
  import { BoundedLruCache } from "./bounded-cache.js";
16
+ import { createGenerationSource } from "./generation-guard.js";
16
17
  import { normalizeMapKey } from "./path-utils.js";
18
+ import { resolveCargoPackageEdition } from "./cargo-manifest.js";
19
+ import { resolveKtfmtGradleStyle } from "./gradle-ktfmt-style.js";
20
+ import { resolvePhpCsFixerConfig } from "./php-cs-fixer-config.js";
17
21
  import { TERRAGRUNT_FILENAMES } from "./file-kinds.js";
22
+ import { stripAnsi } from "./sanitize.js";
18
23
  import { detectIndentation, hasDetectableIndentation, } from "./dispatch/indent-detect.js";
19
24
  import { logLatency } from "./latency-logger.js";
20
25
  import { compareOrdinal } from "./string-utils.js";
21
26
  import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
22
- import { findGlobalBinary, findLocalBinUpwards } from "./package-manager.js";
27
+ import { findGlobalBinary, findLocalBinUpwards, VENDOR_BIN_DIRS, VENV_BIN_DIRS, } from "./package-manager.js";
23
28
  import { safeSpawnAsync } from "./safe-spawn.js";
24
29
  import { assertInstallAllowed } from "./project-trust.js";
25
30
  import { tryLazyInstallForFormatter } from "./dispatch/runners/utils/lazy-installer.js";
@@ -49,6 +54,19 @@ export async function tryLazyInstallFormatterTool(tool, cwd) {
49
54
  * stock command there is exactly the stock-style imposition the fix bans.
50
55
  */
51
56
  export const SKIP_FORMATTING = "skip-formatting";
57
+ /**
58
+ * Sentinel a `resolveCommand` returns when it has PROVEN the selected
59
+ * formatter has no executable — the binary is absent from every place the
60
+ * resolver probed (node_modules/.bin, a venv, a managed install dir, PATH) and
61
+ * no alternative resolved (#2413). Distinct from `null` ("no override, fall
62
+ * back to the static `command`") and from SKIP_FORMATTING ("style-preserving
63
+ * refusal"): the static fallback for these formatters is the SAME bare binary
64
+ * the resolver just proved missing, so spawning it only re-triggers the ENOENT
65
+ * the resolver already observed — and reporting that as a formatting failure is
66
+ * the exact defect this fixes (recurring defect shape 10: an unavailable
67
+ * producer collapsed into an ordinary failed result).
68
+ */
69
+ export const FORMATTER_UNAVAILABLE = "formatter-unavailable";
52
70
  // --- Utility Functions ---
53
71
  async function fileExists(filePath) {
54
72
  try {
@@ -63,12 +81,47 @@ async function findUp(targets, startDir, stopDir = path.parse(startDir).root) {
63
81
  const found = [];
64
82
  let currentDir = startDir;
65
83
  while (currentDir !== stopDir) {
66
- for (const target of targets) {
67
- const checkPath = path.join(currentDir, target);
68
- if (await fileExists(checkPath)) {
69
- found.push(checkPath);
84
+ // One `readdir` per directory instead of one `access` per target: the
85
+ // per-target probe made this loop's cost O(targets), so a long target
86
+ // list (e.g. `FORMATTER_CONFIG_FILES`) paid for every entry at every
87
+ // ancestor directory even when almost none of them exist (#1603).
88
+ let entries;
89
+ try {
90
+ entries = await fs.readdir(currentDir);
91
+ }
92
+ catch {
93
+ entries = [];
94
+ }
95
+ if (entries.length > 0) {
96
+ // On default-case-insensitive platforms the old `fs.access` probe
97
+ // matched a config file regardless of on-disk casing; fold so the
98
+ // readdir membership check keeps that behavior.
99
+ const foldCase = process.platform === "win32" || process.platform === "darwin";
100
+ const entrySet = new Set(foldCase ? entries.map((e) => e.toLowerCase()) : entries);
101
+ for (const target of targets) {
102
+ // Nested candidates (for example node_modules/.bin/biome) do
103
+ // not appear as direct readdir members. They retain the old
104
+ // bounded candidate probe; direct names use the O(depth + matches)
105
+ // directory-membership path above.
106
+ if (target.includes("/") || target.includes("\\"))
107
+ continue;
108
+ if (entrySet.has(foldCase ? target.toLowerCase() : target)) {
109
+ const candidate = path.join(currentDir, target);
110
+ // Directory membership is only a cheap candidate filter. Keep the
111
+ // old access check for matched entries so dangling links and entries
112
+ // that cannot be read do not become config evidence (#1603 R2).
113
+ if (await fileExists(candidate))
114
+ found.push(candidate);
115
+ }
70
116
  }
71
117
  }
118
+ for (const target of targets) {
119
+ if (!target.includes("/") && !target.includes("\\"))
120
+ continue;
121
+ const candidate = path.join(currentDir, target);
122
+ if (await fileExists(candidate))
123
+ found.push(candidate);
124
+ }
72
125
  const parent = path.dirname(currentDir);
73
126
  if (parent === currentDir)
74
127
  break;
@@ -76,6 +129,10 @@ async function findUp(targets, startDir, stopDir = path.parse(startDir).root) {
76
129
  }
77
130
  return found;
78
131
  }
132
+ /** Test-only access to the real filesystem walker and its candidate filter. */
133
+ export function _findUpForTests(targets, startDir, stopDir) {
134
+ return findUp(targets, startDir, stopDir);
135
+ }
79
136
  const WHICH_BUDGET_MS = 5000;
80
137
  /**
81
138
  * PATH lookups, governed by the shared availability policy (#1495).
@@ -337,79 +394,58 @@ async function resolveGoFmtBinary() {
337
394
  /**
338
395
  * Walk up from cwd looking for a binary in .venv or venv.
339
396
  * Returns the absolute path if found, null otherwise.
397
+ *
398
+ * Tool-resolution walker, not a config lookup (#2514/#2517 policy): escaping
399
+ * the project upward past HOME means STOP, not keep reading — a `.venv`/`venv`
400
+ * bin found at or above HOME can never be THIS project's own virtualenv. The
401
+ * ceiling lives in `findLocalBinUpwards` and is default-on there; the tests
402
+ * that pin a fake HOME drive that walker directly rather than threading a
403
+ * `homeDir` no production caller ever passed (#2544 review F3).
404
+ *
405
+ * On Windows the candidate order is EXT-OUTER, DIR-INNER — `.venv/Scripts/x.exe`,
406
+ * `venv/Scripts/x.exe`, then the bare names — which is what the pre-fold copy
407
+ * did and what `findLocalBinUpwards` preserves.
340
408
  */
341
409
  async function findInVenv(binary, cwd) {
342
- const isWin = process.platform === "win32";
343
- const candidates = isWin
344
- ? [
345
- `.venv/Scripts/${binary}.exe`,
346
- `venv/Scripts/${binary}.exe`,
347
- `.venv/Scripts/${binary}`,
348
- `venv/Scripts/${binary}`,
349
- ]
350
- : [`.venv/bin/${binary}`, `venv/bin/${binary}`];
351
- let dir = cwd;
352
- const root = path.parse(dir).root;
353
- while (dir !== root) {
354
- for (const candidate of candidates) {
355
- const full = path.join(dir, candidate);
356
- if (await fileExists(full))
357
- return full;
358
- }
359
- const parent = path.dirname(dir);
360
- if (parent === dir)
361
- break;
362
- dir = parent;
363
- }
364
- return null;
410
+ return (findLocalBinUpwards(binary, cwd, {
411
+ windowsExt: ".exe",
412
+ binDirs: VENV_BIN_DIRS,
413
+ }) ?? null);
365
414
  }
366
415
  /**
367
416
  * Check vendor/bin for PHP Composer-managed tools.
368
417
  * Walks up from cwd to find vendor/bin/<binary>.
418
+ *
419
+ * Tool-resolution walker, not a config lookup (#2514/#2517 policy): escaping
420
+ * the project upward past HOME means STOP, not keep reading — a `vendor/bin`
421
+ * match found at or above HOME can never be THIS project's own Composer
422
+ * install. Same single walker as `findInVenv` above; only `binDirs` and the
423
+ * Windows extension differ.
369
424
  */
370
425
  async function findInVendorBin(binary, cwd) {
371
- const isWin = process.platform === "win32";
372
- const names = isWin ? [`${binary}.bat`, binary] : [binary];
373
- let dir = cwd;
374
- const root = path.parse(dir).root;
375
- while (dir !== root) {
376
- for (const name of names) {
377
- const full = path.join(dir, "vendor", "bin", name);
378
- if (await fileExists(full))
379
- return full;
380
- }
381
- const parent = path.dirname(dir);
382
- if (parent === dir)
383
- break;
384
- dir = parent;
385
- }
386
- return null;
426
+ return (findLocalBinUpwards(binary, cwd, {
427
+ windowsExt: ".bat",
428
+ binDirs: VENDOR_BIN_DIRS,
429
+ }) ?? null);
387
430
  }
388
431
  /**
389
432
  * Check node_modules/.bin for locally installed Node tools.
390
433
  * Walks up from cwd to find node_modules/.bin/<binary>.
434
+ *
435
+ * #2514: this WAS a private, byte-for-byte duplicate of
436
+ * `findLocalBinUpwards` (`package-manager.ts`) — the same walk `stylua`
437
+ * below, `taplo.ts`, `knip-client.ts`, and (via `findNodeToolBinary`)
438
+ * `dependency-checker.ts`/`jscpd-client.ts` already reuse. A stray
439
+ * `~/node_modules/.bin/oxfmt.cmd` (the home-level pi-extensions manifest
440
+ * installs its own bins) was picked up as the project's formatter because
441
+ * this copy had no HOME ceiling; fixing only this copy would have left every
442
+ * other caller of `findLocalBinUpwards` with the same defect. Delegating
443
+ * here instead of hand-rolling a second ceiling keeps there being exactly one
444
+ * `node_modules/.bin` walker — see that function for the tool-resolution
445
+ * "escaping the project means STOP" policy (#2517).
391
446
  */
392
447
  async function findInNodeModules(binary, cwd) {
393
- const isWin = process.platform === "win32";
394
- let dir = cwd;
395
- const root = path.parse(dir).root;
396
- while (dir !== root) {
397
- const candidates = isWin
398
- ? [
399
- path.join(dir, "node_modules", ".bin", `${binary}.cmd`),
400
- path.join(dir, "node_modules", ".bin", binary),
401
- ]
402
- : [path.join(dir, "node_modules", ".bin", binary)];
403
- for (const full of candidates) {
404
- if (await fileExists(full))
405
- return full;
406
- }
407
- const parent = path.dirname(dir);
408
- if (parent === dir)
409
- break;
410
- dir = parent;
411
- }
412
- return null;
448
+ return findLocalBinUpwards(binary, cwd) ?? null;
413
449
  }
414
450
  /**
415
451
  * Returns true if `bundle exec <gem>` should be used:
@@ -747,7 +783,11 @@ export const oxfmtFormatter = {
747
783
  const found = await which("oxfmt");
748
784
  if (found)
749
785
  return [found, OXFMT_NO_ERROR_ON_UNMATCHED, filePath];
750
- return null;
786
+ // #2413: neither node_modules/.bin nor PATH has oxfmt, and `detect()` is
787
+ // config-only (it never probes the binary), so selection can reach here
788
+ // with nothing installed. The static command is bare `oxfmt` — spawning it
789
+ // only reproduces the reported `spawn oxfmt ENOENT`. Prove it unavailable.
790
+ return FORMATTER_UNAVAILABLE;
751
791
  },
752
792
  // Single source of truth: OXFMT_SUPPORTED_EXTENSIONS in tool-policy.ts.
753
793
  // Do not hand-maintain a second copy of this list (#1134 — previously two
@@ -784,7 +824,12 @@ export const ruffFormatter = {
784
824
  const installed = await ensureTool(toolId);
785
825
  if (installed)
786
826
  return [installed, ...args, filePath];
787
- return null;
827
+ // #2413: `ensureTool` already resolves PATH, global bins and the managed
828
+ // dir (and attempts an install) before returning falsy, so reaching here
829
+ // means ruff is genuinely nowhere. `detect()`'s `hasRuffConfig` branch has
830
+ // no binary check, so a config-only project selects ruff with nothing
831
+ // installed; the static bare `ruff` would then ENOENT. Prove it unavailable.
832
+ return FORMATTER_UNAVAILABLE;
788
833
  },
789
834
  async detect(cwd) {
790
835
  if (hasRuffConfig(cwd))
@@ -847,6 +892,18 @@ export const rustfmtFormatter = {
847
892
  name: "rustfmt",
848
893
  command: ["rustfmt", "$FILE"],
849
894
  extensions: [".rs"],
895
+ // #2466: rustfmt defaults to an OLDER edition than the file's actual Cargo
896
+ // package when invoked bare, so it can reject valid newer-edition syntax
897
+ // (e.g. Rust 2024). Carry the nearest package's `edition` (honoring
898
+ // `edition.workspace = true` inheritance) through `--edition`; `undefined`
899
+ // (unreadable/unparseable manifest) falls back to the static command
900
+ // above, unchanged from pre-#2466 behavior.
901
+ async resolveCommand(filePath, _cwd) {
902
+ const edition = await resolveCargoPackageEdition(filePath);
903
+ if (edition === undefined)
904
+ return null;
905
+ return ["rustfmt", "--edition", edition, filePath];
906
+ },
850
907
  async detect(cwd) {
851
908
  if ((await which("rustfmt")) !== null)
852
909
  return true;
@@ -971,13 +1028,27 @@ export const ktfmtFormatter = {
971
1028
  // ktfmt formats in place when given a file path (no flag needed).
972
1029
  command: ["ktfmt", "$FILE"],
973
1030
  extensions: [".kt", ".kts"],
1031
+ // #2468: ktfmt's CLI never reads a project's Gradle `ktfmt { googleStyle()
1032
+ // | kotlinLangStyle() }` selection — style is CLI-flag-only
1033
+ // (`--google-style`/`--kotlinlang-style`, verified against ktfmt v0.63's
1034
+ // own arg parser). Carry the nearest module's declared style through;
1035
+ // `undefined` (no declaration, unreadable/unparseable manifest, or an
1036
+ // unsupported style like the removed `dropboxStyle()`) falls back to the
1037
+ // bare invocation, unchanged from pre-#2468 behavior.
974
1038
  async resolveCommand(filePath, _cwd) {
1039
+ const styleFlag = await resolveKtfmtGradleStyle(filePath);
975
1040
  const inPath = await which("ktfmt");
976
1041
  if (inPath)
977
- return [inPath, filePath];
1042
+ return styleFlag ? [inPath, styleFlag, filePath] : [inPath, filePath];
978
1043
  const { ensureTool } = await import("./installer/index.js");
979
1044
  const installed = await ensureTool("ktfmt");
980
- return installed ? [installed, filePath] : null;
1045
+ // #2413: which() and ensureTool (PATH/global/managed + install) both
1046
+ // failed, and `detect()` gates only on hasKtfmtConfig — no binary probe —
1047
+ // so a config-only project reaches here with ktfmt absent. The static
1048
+ // command is bare `ktfmt`; spawning it reproduces the ENOENT class.
1049
+ if (!installed)
1050
+ return FORMATTER_UNAVAILABLE;
1051
+ return styleFlag ? [installed, styleFlag, filePath] : [installed, filePath];
981
1052
  },
982
1053
  async detect(cwd) {
983
1054
  // Opt-in only: ktfmt becomes the formatter when the project elects it,
@@ -1063,18 +1134,49 @@ export const phpCsFixerFormatter = {
1063
1134
  name: "php-cs-fixer",
1064
1135
  command: ["php-cs-fixer", "fix", "$FILE"],
1065
1136
  extensions: [".php"],
1137
+ // #2472: php-cs-fixer does NOT walk up parent directories looking for its
1138
+ // own config the way prettier/biome/eslint do (verified against upstream
1139
+ // `computeConfigFiles()` — see `resolvePhpCsFixerConfig`'s doc comment),
1140
+ // and `formatFile` spawns with cwd = the FILE's own directory, which is
1141
+ // not necessarily where the ancestor config `detect()` found actually
1142
+ // lives. Always resolve the binary explicitly here (vendor/bin first,
1143
+ // then global) rather than falling through to the static `command` above
1144
+ // — that static command can never carry `--config`, so a config found at
1145
+ // an ancestor would silently be dropped whenever the vendor lookup here
1146
+ // missed but a global binary still resolved. `--config` is attached
1147
+ // whenever a config resolves, even when it sits in the file's own
1148
+ // directory (AC3): unlike the pre-#2472 code, correctness no longer
1149
+ // depends on php-cs-fixer's own (nonexistent) upward search.
1066
1150
  async resolveCommand(filePath, cwd) {
1067
- const vendor = await findInVendorBin("php-cs-fixer", cwd);
1068
- if (vendor)
1069
- return [vendor, "fix", filePath];
1070
- return null;
1151
+ const configPath = resolvePhpCsFixerConfig(filePath);
1152
+ const binary = (await findInVendorBin("php-cs-fixer", cwd)) ??
1153
+ (await which("php-cs-fixer"));
1154
+ // #2413/#2472 review F4: both probes (vendor/bin, then PATH) have
1155
+ // PROVEN the binary is absent — returning `null` here would fall back
1156
+ // to the static `command` above, which is the SAME bare
1157
+ // `php-cs-fixer` this just failed to find, spawning it only to
1158
+ // re-observe the ENOENT already known. Report the proven-missing
1159
+ // state instead so `formatFile` skips the wasted spawn.
1160
+ if (!binary)
1161
+ return FORMATTER_UNAVAILABLE;
1162
+ return configPath
1163
+ ? [binary, "fix", "--config", configPath, filePath]
1164
+ : [binary, "fix", filePath];
1071
1165
  },
1072
1166
  async detect(cwd) {
1073
1167
  const vendorBin = await findInVendorBin("php-cs-fixer", cwd);
1074
1168
  const globalBin = await which("php-cs-fixer");
1075
1169
  if (!vendorBin && !globalBin)
1076
1170
  return false;
1077
- // Only run if project has explicit config
1171
+ // Only run if project has explicit config. This is a presence-only
1172
+ // climb from the project `cwd` (not necessarily the formatted file's
1173
+ // own directory) via this file's own `findUp` — deliberately NOT
1174
+ // merged with `resolvePhpCsFixerConfig` above (#2472 AC4): that
1175
+ // resolver climbs from the FILE's directory and needs the exact
1176
+ // winning path for `--config`, while this only needs a yes/no answer
1177
+ // from whatever `cwd` the caller passed. `rustfmtFormatter.detect`
1178
+ // keeps the same non-merged shape against `resolveCargoPackageEdition`
1179
+ // for the identical reason.
1078
1180
  const configs = [".php-cs-fixer.php", ".php-cs-fixer.dist.php"];
1079
1181
  const found = await findUp(configs, cwd);
1080
1182
  return found.length > 0;
@@ -1333,9 +1435,15 @@ export const ALL_FORMATTERS = [
1333
1435
  const FILENAME_FORMATTER_BASENAMES = new Set(ALL_FORMATTERS.flatMap((f) => f.filenames ?? []));
1334
1436
  // Cache for detection results - stores array of enabled formatter names per cwd+ext
1335
1437
  const detectionCache = new BoundedLruCache(32);
1438
+ // The signature is immutable for a cache generation. This memo is separate
1439
+ // from detectionCache because a cwd can have several extension entries, and a
1440
+ // warm lookup must not repeat the ancestor walk or stat matched configs.
1441
+ const formatterSignatureFlights = new Map();
1442
+ const formatterCacheGeneration = createGenerationSource("formatter-cache");
1336
1443
  // These are the formatter configuration files consulted by the policy helpers
1337
- // above. Their metadata is part of detection cache identity: changing a file
1338
- // must re-run detection even when PATH and installed tools are unchanged. A
1444
+ // above. Their metadata is captured by the cold detection signature. The
1445
+ // write-result seam invalidates that signature when a config path changes, so
1446
+ // detection re-runs even when PATH and installed tools are unchanged. A
1339
1447
  // filename a `has*Config` check reads but this list omits is invisible to the
1340
1448
  // cache: the signature never moves when that file is added, so a project that
1341
1449
  // opts in AFTER the first `getFormattersForFile` call for its cwd keeps
@@ -1436,6 +1544,18 @@ async function formatterConfigSignature(cwd) {
1436
1544
  }));
1437
1545
  return parts.join("|");
1438
1546
  }
1547
+ async function getFormatterConfigSignature(cwd, normalizedCwd) {
1548
+ const existing = formatterSignatureFlights.get(normalizedCwd);
1549
+ if (existing)
1550
+ return existing.promise;
1551
+ const promise = formatterConfigSignature(cwd).finally(() => {
1552
+ const current = formatterSignatureFlights.get(normalizedCwd);
1553
+ if (current?.promise === promise)
1554
+ formatterSignatureFlights.delete(normalizedCwd);
1555
+ });
1556
+ formatterSignatureFlights.set(normalizedCwd, { promise });
1557
+ return promise;
1558
+ }
1439
1559
  // --- Public API ---
1440
1560
  export async function getFormattersForFile(filePath, cwd) {
1441
1561
  const ext = path.extname(filePath).toLowerCase();
@@ -1449,31 +1569,19 @@ export async function getFormattersForFile(filePath, cwd) {
1449
1569
  const cacheKey = FILENAME_FORMATTER_BASENAMES.has(base)
1450
1570
  ? `${normalizedCwd}:${ext}:${base}`
1451
1571
  : `${normalizedCwd}:${ext}`;
1452
- const configSignature = await formatterConfigSignature(cwd);
1453
- // Check cache
1572
+ // A warm entry is authoritative until the write-result seam invalidates its
1573
+ // config path. This is the only way to make the warm path free of config
1574
+ // polling while still reacting immediately to pi-authored create/remove/
1575
+ // change events. External editor changes remain a documented session-boundary
1576
+ // limitation, like other write-result-owned freshness seams.
1454
1577
  let cached = detectionCache.get(normalizedCwd);
1455
- if (cached && cached.signature !== configSignature) {
1456
- // An EXISTING entry whose signature moved is the user telling us the world
1457
- // changed. Re-probe PATH too, so "install the tool, touch the config" still
1458
- // works within one session now that a durable absence latches (#1495).
1459
- // Scoped to a real change on purpose: keying this off "no entry yet" would
1460
- // drop every PATH verdict on the first save in each new directory, which in
1461
- // a monorepo means re-probing forever.
1462
- detectionCache.delete(normalizedCwd);
1463
- cached = undefined;
1464
- resetWhichLatches();
1465
- }
1466
- if (!cached) {
1467
- cached = { signature: configSignature, entries: new Map() };
1468
- detectionCache.set(normalizedCwd, cached);
1469
- }
1470
- if (cached.entries.has(cacheKey)) {
1578
+ if (cached?.entries.has(cacheKey)) {
1471
1579
  const enabledNames = cached.entries.get(cacheKey);
1472
1580
  const selectedFormatterName = enabledNames && enabledNames.length > 0 ? enabledNames[0] : null;
1473
1581
  logLatency({
1474
1582
  type: "phase",
1475
1583
  phase: "formatter_selected",
1476
- filePath: filePath,
1584
+ filePath,
1477
1585
  durationMs: 0,
1478
1586
  metadata: {
1479
1587
  formatter: selectedFormatterName,
@@ -1485,9 +1593,26 @@ export async function getFormattersForFile(filePath, cwd) {
1485
1593
  });
1486
1594
  if (!enabledNames || enabledNames.length === 0)
1487
1595
  return [];
1488
- // Return cached formatters by name (preserves priority order)
1489
1596
  return ALL_FORMATTERS.filter((f) => enabledNames.includes(f.name));
1490
1597
  }
1598
+ if (!cached) {
1599
+ const generation = formatterCacheGeneration.capture();
1600
+ const configSignature = await getFormatterConfigSignature(cwd, normalizedCwd);
1601
+ // An invalidation can happen while the first signature walk is in flight.
1602
+ // Do not publish a pre-invalidation signature into the new generation.
1603
+ if (!generation.isCurrent()) {
1604
+ return getFormattersForFile(filePath, cwd);
1605
+ }
1606
+ // Another cold caller for this cwd can finish the shared signature flight
1607
+ // first. Re-read the LRU before installing an object so the later caller
1608
+ // merges its extension entry into the existing object instead of replacing
1609
+ // the earlier entry.
1610
+ cached = detectionCache.get(normalizedCwd);
1611
+ if (!cached) {
1612
+ cached = { signature: configSignature, entries: new Map() };
1613
+ detectionCache.set(normalizedCwd, cached);
1614
+ }
1615
+ }
1491
1616
  // Detect formatters for this extension (or exact filename, e.g. terragrunt.hcl)
1492
1617
  const matching = ALL_FORMATTERS.filter((f) => f.extensions.includes(ext) || f.filenames?.includes(base));
1493
1618
  const formatterPolicy = getFormatterPolicyForFile(filePath);
@@ -1641,7 +1766,11 @@ export async function getFormattersForFile(filePath, cwd) {
1641
1766
  return enabled;
1642
1767
  // Store the list of enabled formatter names in cache
1643
1768
  const enabledNames = enabled.map((f) => f.name);
1644
- cached.entries.set(cacheKey, enabledNames);
1769
+ // An invalidation may have removed or replaced this object while detection
1770
+ // awaited a tool probe. Never repopulate the old generation.
1771
+ if (detectionCache.get(normalizedCwd) === cached) {
1772
+ cached.entries.set(cacheKey, enabledNames);
1773
+ }
1645
1774
  return enabled;
1646
1775
  }
1647
1776
  /**
@@ -1655,9 +1784,21 @@ export async function getFormattersForFile(filePath, cwd) {
1655
1784
  * therefore re-arms every directory EXCEPT the one the user is working in.
1656
1785
  */
1657
1786
  export function clearFormatterCache() {
1787
+ formatterCacheGeneration.bump();
1788
+ formatterSignatureFlights.clear();
1658
1789
  detectionCache.clear();
1659
1790
  resetWhichLatches();
1660
1791
  }
1792
+ const formatterConfigBasenames = new Set(FORMATTER_CONFIG_FILES.map((fileName) => fileName.toLowerCase()));
1793
+ /** Invalidate selection when the write-result seam reports a config path. */
1794
+ export function invalidateFormatterCacheForPath(filePath) {
1795
+ const basename = filePath.includes("\\")
1796
+ ? path.win32.basename(filePath)
1797
+ : path.basename(filePath);
1798
+ if (!formatterConfigBasenames.has(basename.toLowerCase()))
1799
+ return;
1800
+ clearFormatterCache();
1801
+ }
1661
1802
  /**
1662
1803
  * Test-only internals access for the session-state registry probe (#1895):
1663
1804
  * the conformance suite must arm all four pieces of state this reset claims
@@ -1672,16 +1813,13 @@ export function _getFormatterResetStateForTests() {
1672
1813
  };
1673
1814
  }
1674
1815
  export function clearFormatterRuntimeState() {
1675
- detectionCache.clear();
1676
- resetWhichLatches();
1816
+ clearFormatterCache();
1677
1817
  // NO `resetLazyInstallAttempts()` here (#1537 review F1). This function runs
1678
1818
  // from `resetFormatService()`, which `handleTurnEnd` calls every turn — so
1679
1819
  // clearing the lazy-install hold here made "held for the session" mean "held
1680
1820
  // for a turn", and a failing install re-ran every turn. The hold's only reset
1681
1821
  // is `session_start`'s block in runtime-session.ts.
1682
1822
  }
1683
- // ESC is built via fromCharCode so no raw control byte sits in the source.
1684
- const ANSI_ESCAPE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "g");
1685
1823
  const BOX_DRAWING_GLOBAL = /[\u2500-\u257F]/g;
1686
1824
  const HAS_BOX_DRAWING = /[\u2500-\u257F]/;
1687
1825
  /**
@@ -1698,7 +1836,7 @@ const HAS_BOX_DRAWING = /[\u2500-\u257F]/;
1698
1836
  */
1699
1837
  export function firstDiagnosticLine(text) {
1700
1838
  for (const raw of (text ?? "").split("\n")) {
1701
- const line = raw.replace(ANSI_ESCAPE, "").trimEnd();
1839
+ const line = stripAnsi(raw).trimEnd();
1702
1840
  const stripped = line.replace(BOX_DRAWING_GLOBAL, "").trim();
1703
1841
  if (!stripped)
1704
1842
  continue;
@@ -1724,6 +1862,12 @@ async function resolveFormatterCommand(formatter, absolutePath, cwd) {
1724
1862
  : null;
1725
1863
  if (resolved === SKIP_FORMATTING)
1726
1864
  return SKIP_FORMATTING;
1865
+ // The resolver proved the executable absent — DO NOT fall through to the
1866
+ // static bare command, which is the same binary it just probed and missing
1867
+ // (#2413). The caller turns this into a typed `unavailable` outcome, never a
1868
+ // spawn and never a failure.
1869
+ if (resolved === FORMATTER_UNAVAILABLE)
1870
+ return FORMATTER_UNAVAILABLE;
1727
1871
  if (resolved !== null)
1728
1872
  return resolved;
1729
1873
  const fallback = formatter.command.map((c) => c.replace("$FILE", absolutePath));
@@ -1751,13 +1895,40 @@ export async function formatFile(filePath, formatter) {
1751
1895
  if (cmd === SKIP_FORMATTING) {
1752
1896
  // Style-preserving refusal (#1144): no repo config and no detectable
1753
1897
  // indentation to pin — formatting would impose the tool's stock style.
1754
- return { success: true, changed: false };
1898
+ return { success: true, changed: false, outcome: "skipped" };
1899
+ }
1900
+ if (cmd === FORMATTER_UNAVAILABLE) {
1901
+ // The resolver proved the executable absent (#2413): the oxfmt ENOENT
1902
+ // trap and its siblings. This is unavailable infrastructure, not a code
1903
+ // failure — success stays true so it is never counted as a failed file
1904
+ // or requeued, and the typed outcome keeps it out of the failure bucket.
1905
+ return {
1906
+ success: true,
1907
+ changed: false,
1908
+ outcome: "unavailable",
1909
+ error: `${formatter.name}: formatter executable not found`,
1910
+ };
1755
1911
  }
1756
1912
  // Run formatter without blocking the event loop.
1757
1913
  const result = await safeSpawnAsync(cmd[0], cmd.slice(1), {
1758
1914
  timeout: 15000,
1759
1915
  cwd,
1760
1916
  });
1917
+ // A resolver that could NOT prove absence (it never probed PATH — e.g.
1918
+ // black/sqlfluff, which only look in a venv) legitimately falls back to the
1919
+ // bare static command. When that bare command is missing, the spawn boundary
1920
+ // reports a typed `tool-not-found` failure — never a nonzero exit. Treat it
1921
+ // as unavailable, not a formatting failure, closing the same #2413 class for
1922
+ // the formatters whose `null` correctly means "static untried" (no
1923
+ // oxfmt-specific string matching — this reads the typed spawn-failure kind).
1924
+ if (result.spawnFailure?.kind === "tool-not-found") {
1925
+ return {
1926
+ success: true,
1927
+ changed: false,
1928
+ outcome: "unavailable",
1929
+ error: result.spawnFailure.message,
1930
+ };
1931
+ }
1761
1932
  // Strict by default (#1337): only a formatter with a documented
1762
1933
  // benign-nonzero mode (`lenientExitCode`) may exit nonzero and still be
1763
1934
  // read as a successful run. Everything else that exits nonzero never
@@ -1770,6 +1941,7 @@ export async function formatFile(filePath, formatter) {
1770
1941
  return {
1771
1942
  success: false,
1772
1943
  changed: false,
1944
+ outcome: "failed",
1773
1945
  error: result.error?.message ||
1774
1946
  firstDiagnosticLine(result.stderr) ||
1775
1947
  // biome, ktlint and `mix format` report on STDOUT; without this
@@ -1785,12 +1957,14 @@ export async function formatFile(filePath, formatter) {
1785
1957
  return {
1786
1958
  success: true,
1787
1959
  changed,
1960
+ outcome: changed ? "formatted" : "unchanged",
1788
1961
  };
1789
1962
  }
1790
1963
  catch (err) {
1791
1964
  return {
1792
1965
  success: false,
1793
1966
  changed: false,
1967
+ outcome: "failed",
1794
1968
  error: err instanceof Error ? err.message : String(err),
1795
1969
  };
1796
1970
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The shared re-check cadence for a cached "nothing here" verdict — the
3
+ * bound both `file-utils.ts`'s nested `.gitignore`/`.pi-lens.json` matcher
4
+ * (#2159/#2071) and `project-lens-config.ts`'s no-config discovery cache
5
+ * (#2483 round 2) use so a negative result is neither re-statted on every
6
+ * call nor cached for the life of the process.
7
+ *
8
+ * A declared leaf module, the same shape as `ledger-bounds.ts` /
9
+ * `spawn-output-cap.ts`: it imports nothing from `clients/`, because
10
+ * `file-utils.ts` already imports `project-lens-config.ts`, and the
11
+ * `clients/` acyclic-imports rule (`no-client-cycles`,
12
+ * `.dependency-cruiser.cjs`) would reject either of those two files
13
+ * importing a constant that lived inside the other. A shared value neither
14
+ * owns is the only way both stay on the same cadence without one importing
15
+ * the other.
16
+ */
17
+ export const FRESHNESS_CADENCE_MS = 2_000;