@herbertgao/pi-extensions 2026.9.0 → 2026.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +13 -1
  2. package/node_modules/@czottmann/pi-automode/README.md +20 -0
  3. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +1 -1
  4. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +2 -1
  5. package/node_modules/@czottmann/pi-automode/docs/defaults.md +3 -2
  6. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +38 -2
  7. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +5 -3
  8. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +11 -3
  9. package/node_modules/@czottmann/pi-automode/package.json +1 -1
  10. package/node_modules/@narumitw/pi-btw/README.md +57 -141
  11. package/node_modules/@narumitw/pi-btw/dist/index.ts +466 -41
  12. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  13. package/node_modules/@narumitw/pi-btw/docs/workflows.md +74 -0
  14. package/node_modules/@narumitw/pi-btw/package.json +2 -1
  15. package/node_modules/@narumitw/pi-btw/src/btw.ts +15 -1
  16. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +32 -2
  17. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +330 -0
  18. package/node_modules/@narumitw/pi-btw/src/menu.ts +136 -19
  19. package/node_modules/@narumitw/pi-btw/src/settings.ts +32 -0
  20. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +57 -5
  21. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +52 -30
  22. package/node_modules/@pi-plugins/fast-mode/README.md +5 -3
  23. package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
  24. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20 -55
  25. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  26. package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
  27. package/node_modules/pi-lens/CHANGELOG.md +1072 -0
  28. package/node_modules/pi-lens/README.md +3 -0
  29. package/node_modules/pi-lens/config/biome/core.jsonc +11 -2
  30. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +13 -1
  31. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +2 -9
  32. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +927 -80
  33. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +1 -1
  34. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +13 -4
  35. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +123 -3
  36. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +60 -5
  37. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +2 -9
  38. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +2 -0
  39. package/node_modules/pi-lens/dist/clients/bash-file-access.js +133 -3
  40. package/node_modules/pi-lens/dist/clients/biome-client.js +9 -2
  41. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +14 -6
  42. package/node_modules/pi-lens/dist/clients/bootstrap.js +509 -73
  43. package/node_modules/pi-lens/dist/clients/bounded-cache.js +152 -12
  44. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +1 -1
  45. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +59 -9
  46. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +113 -0
  47. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +3 -11
  48. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +31 -4
  49. package/node_modules/pi-lens/dist/clients/cache-manager.js +105 -6
  50. package/node_modules/pi-lens/dist/clients/cache-observability.js +40 -19
  51. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +422 -0
  52. package/node_modules/pi-lens/dist/clients/cascade-logger.js +2 -9
  53. package/node_modules/pi-lens/dist/clients/child-unref.js +1 -19
  54. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +13 -3
  55. package/node_modules/pi-lens/dist/clients/complexity-client.js +16 -5
  56. package/node_modules/pi-lens/dist/clients/config-core/deny.js +221 -0
  57. package/node_modules/pi-lens/dist/clients/config-core/index.js +50 -0
  58. package/node_modules/pi-lens/dist/clients/config-core/merge.js +357 -0
  59. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +340 -0
  60. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +248 -0
  61. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +164 -0
  62. package/node_modules/pi-lens/dist/clients/config-core/records.js +218 -0
  63. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +125 -0
  64. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +78 -0
  65. package/node_modules/pi-lens/dist/clients/config-core/schema.js +165 -0
  66. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +281 -0
  67. package/node_modules/pi-lens/dist/clients/config-locations.js +160 -0
  68. package/node_modules/pi-lens/dist/clients/config-resolve.js +789 -0
  69. package/node_modules/pi-lens/dist/clients/config-schema.js +234 -0
  70. package/node_modules/pi-lens/dist/clients/config-warn.js +407 -0
  71. package/node_modules/pi-lens/dist/clients/dead-code-client.js +19 -14
  72. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +2 -6
  73. package/node_modules/pi-lens/dist/clients/deadline-utils.js +178 -0
  74. package/node_modules/pi-lens/dist/clients/debug-handles.js +2 -2
  75. package/node_modules/pi-lens/dist/clients/debug-heap.js +3 -3
  76. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +106 -0
  77. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +113 -7
  78. package/node_modules/pi-lens/dist/clients/dependency-checker.js +8 -19
  79. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +1 -1
  80. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +12 -9
  81. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +15 -5
  82. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -4
  83. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +162 -44
  84. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +97 -0
  85. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +27 -8
  86. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -6
  87. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +1 -1
  88. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -3
  89. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +2 -2
  90. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +1 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +383 -27
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +4 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -1
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -1
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -1
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -1
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -1
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -1
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +1 -3
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -1
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -1
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +1 -2
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -1
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -1
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -1
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -1
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -1
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -1
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +14 -18
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -1
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -1
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -1
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +6 -3
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +7 -4
  122. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -1
  123. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +1 -2
  124. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +1 -3
  125. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +1 -2
  126. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -1
  127. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +4 -3
  128. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -1
  129. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -1
  130. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -1
  131. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -1
  132. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -1
  133. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -1
  134. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -1
  135. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -1
  136. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -1
  137. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
  138. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -67
  139. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +194 -80
  140. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +84 -15
  141. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -21
  142. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -1
  143. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +2 -20
  144. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +1 -1
  145. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -1
  146. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +2 -2
  147. package/node_modules/pi-lens/dist/clients/disposition-logger.js +3 -10
  148. package/node_modules/pi-lens/dist/clients/effective-config.js +403 -0
  149. package/node_modules/pi-lens/dist/clients/error-class.js +23 -0
  150. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +274 -0
  151. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +2 -2
  152. package/node_modules/pi-lens/dist/clients/extension-log.js +2 -2
  153. package/node_modules/pi-lens/dist/clients/feature-hints.js +2 -1
  154. package/node_modules/pi-lens/dist/clients/file-kinds.js +1 -38
  155. package/node_modules/pi-lens/dist/clients/file-time.js +7 -2
  156. package/node_modules/pi-lens/dist/clients/file-utils.js +117 -10
  157. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +50 -13
  158. package/node_modules/pi-lens/dist/clients/format-service.js +6 -2
  159. package/node_modules/pi-lens/dist/clients/formatters.js +385 -104
  160. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +17 -0
  161. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +116 -22
  162. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -9
  163. package/node_modules/pi-lens/dist/clients/git-guard.js +6 -1
  164. package/node_modules/pi-lens/dist/clients/go-client.js +39 -0
  165. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +252 -0
  166. package/node_modules/pi-lens/dist/clients/grammar-source.js +3 -3
  167. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +424 -0
  168. package/node_modules/pi-lens/dist/clients/hook-budgets.js +76 -0
  169. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +5 -2
  170. package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
  171. package/node_modules/pi-lens/dist/clients/installer/index.js +289 -59
  172. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +84 -18
  173. package/node_modules/pi-lens/dist/clients/instance-reaper.js +110 -236
  174. package/node_modules/pi-lens/dist/clients/instance-registry.js +6 -3
  175. package/node_modules/pi-lens/dist/clients/language-profile.js +0 -9
  176. package/node_modules/pi-lens/dist/clients/language-registry.js +599 -0
  177. package/node_modules/pi-lens/dist/clients/latency-logger.js +165 -9
  178. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +34 -1
  179. package/node_modules/pi-lens/dist/clients/lens-config.js +160 -30
  180. package/node_modules/pi-lens/dist/clients/lens-engine.js +33 -41
  181. package/node_modules/pi-lens/dist/clients/lens-events.js +1 -1
  182. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +77 -6
  183. package/node_modules/pi-lens/dist/clients/lens-map.js +1 -1
  184. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -38
  185. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +3 -1
  186. package/node_modules/pi-lens/dist/clients/lsp/client.js +172 -33
  187. package/node_modules/pi-lens/dist/clients/lsp/config.js +484 -158
  188. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +1 -1
  189. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +1 -1
  190. package/node_modules/pi-lens/dist/clients/lsp/edits.js +18 -5
  191. package/node_modules/pi-lens/dist/clients/lsp/index.js +892 -96
  192. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +1 -1
  193. package/node_modules/pi-lens/dist/clients/lsp/language.js +18 -177
  194. package/node_modules/pi-lens/dist/clients/lsp/launch.js +2 -1
  195. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  196. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +5 -18
  197. package/node_modules/pi-lens/dist/clients/lsp/server.js +284 -147
  198. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +116 -23
  199. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +2 -7
  200. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -1
  201. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +14 -7
  202. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +6 -0
  203. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +2 -1
  204. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +1 -1
  205. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +1 -1
  206. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +163 -21
  207. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +36 -0
  208. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +33 -4
  209. package/node_modules/pi-lens/dist/clients/mcp/session.js +5 -16
  210. package/node_modules/pi-lens/dist/clients/memory-sampler.js +8 -3
  211. package/node_modules/pi-lens/dist/clients/metrics-history.js +28 -107
  212. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +3 -5
  213. package/node_modules/pi-lens/dist/clients/module-report.js +21 -46
  214. package/node_modules/pi-lens/dist/clients/mutating-tool.js +651 -0
  215. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +368 -0
  216. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +240 -0
  217. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +247 -24
  218. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +101 -0
  219. package/node_modules/pi-lens/dist/clients/observed-mutation.js +1215 -0
  220. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +70 -28
  221. package/node_modules/pi-lens/dist/clients/opengrep-config.js +5 -1
  222. package/node_modules/pi-lens/dist/clients/package-manager.js +195 -26
  223. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +359 -80
  224. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +23 -7
  225. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +21 -2
  226. package/node_modules/pi-lens/dist/clients/path-utils.js +451 -15
  227. package/node_modules/pi-lens/dist/clients/performance-report.js +2 -2
  228. package/node_modules/pi-lens/dist/clients/persist-debounce.js +8 -1
  229. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +114 -0
  230. package/node_modules/pi-lens/dist/clients/pipeline.js +79 -18
  231. package/node_modules/pi-lens/dist/clients/probe-home-state.js +228 -0
  232. package/node_modules/pi-lens/dist/clients/process-bridge.js +66 -0
  233. package/node_modules/pi-lens/dist/clients/process-snapshot.js +68 -0
  234. package/node_modules/pi-lens/dist/clients/project-changes.js +1 -1
  235. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +1 -1
  236. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
  237. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +1 -1
  238. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +1 -1
  239. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +1 -1
  240. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +1 -1
  241. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +28 -4
  242. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +51 -20
  243. package/node_modules/pi-lens/dist/clients/project-lens-config.js +438 -122
  244. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +3 -51
  245. package/node_modules/pi-lens/dist/clients/project-snapshot.js +52 -24
  246. package/node_modules/pi-lens/dist/clients/python-environment.js +217 -0
  247. package/node_modules/pi-lens/dist/clients/python-provenance.js +639 -0
  248. package/node_modules/pi-lens/dist/clients/quiet-window.js +6 -1
  249. package/node_modules/pi-lens/dist/clients/read-bridge.js +13 -21
  250. package/node_modules/pi-lens/dist/clients/read-expansion.js +9 -46
  251. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +16 -5
  252. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +364 -212
  253. package/node_modules/pi-lens/dist/clients/read-guard.js +26 -2
  254. package/node_modules/pi-lens/dist/clients/recent-touches.js +2 -2
  255. package/node_modules/pi-lens/dist/clients/resource-sampler.js +195 -96
  256. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +155 -114
  257. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +1 -1
  258. package/node_modules/pi-lens/dist/clients/review-graph/service.js +55 -3
  259. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +7 -15
  260. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +10 -74
  261. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +9 -3
  262. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +19 -3
  263. package/node_modules/pi-lens/dist/clients/runtime-context.js +49 -16
  264. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +25 -6
  265. package/node_modules/pi-lens/dist/clients/runtime-session.js +184 -32
  266. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +179 -36
  267. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +797 -243
  268. package/node_modules/pi-lens/dist/clients/runtime-turn.js +1028 -53
  269. package/node_modules/pi-lens/dist/clients/rust-client.js +31 -0
  270. package/node_modules/pi-lens/dist/clients/safe-spawn.js +33 -10
  271. package/node_modules/pi-lens/dist/clients/sanitize.js +7 -12
  272. package/node_modules/pi-lens/dist/clients/scan-utils.js +1 -56
  273. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +1 -3
  274. package/node_modules/pi-lens/dist/clients/security-scan-client.js +3 -0
  275. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +1 -1
  276. package/node_modules/pi-lens/dist/clients/session-start-observability.js +79 -0
  277. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -24
  278. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +12 -3
  279. package/node_modules/pi-lens/dist/clients/sgconfig.js +7 -6
  280. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +1 -1
  281. package/node_modules/pi-lens/dist/clients/skills-resolver.js +105 -0
  282. package/node_modules/pi-lens/dist/clients/smells-rollup.js +4 -4
  283. package/node_modules/pi-lens/dist/clients/startup-scan.js +2 -10
  284. package/node_modules/pi-lens/dist/clients/string-utils.js +13 -0
  285. package/node_modules/pi-lens/dist/clients/subagent-mode.js +17 -4
  286. package/node_modules/pi-lens/dist/clients/test-runner-client.js +176 -39
  287. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +244 -0
  288. package/node_modules/pi-lens/dist/clients/tool-definition.js +41 -1
  289. package/node_modules/pi-lens/dist/clients/tool-policy.js +92 -22
  290. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +40 -40
  291. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +87 -100
  292. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +2 -2
  293. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +81 -1
  294. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +51 -46
  295. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +28 -0
  296. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -4
  297. package/node_modules/pi-lens/dist/clients/typos-config.js +5 -0
  298. package/node_modules/pi-lens/dist/clients/user-notify.js +6 -6
  299. package/node_modules/pi-lens/dist/clients/widget-state.js +191 -26
  300. package/node_modules/pi-lens/dist/clients/word-index-logger.js +2 -9
  301. package/node_modules/pi-lens/dist/clients/word-index-store.js +23 -12
  302. package/node_modules/pi-lens/dist/clients/word-index.js +252 -47
  303. package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -1
  304. package/node_modules/pi-lens/dist/clients/zizmor-config.js +4 -1
  305. package/node_modules/pi-lens/dist/index.js +65899 -55340
  306. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -1
  307. package/node_modules/pi-lens/dist/mcp/build-staleness.js +1 -1
  308. package/node_modules/pi-lens/dist/mcp/server.js +127 -5
  309. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +583 -0
  310. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +129 -0
  311. package/node_modules/pi-lens/dist/tools/effective-config.js +89 -0
  312. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +96 -25
  313. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +104 -86
  314. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +37 -16
  315. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +3 -3
  316. package/node_modules/pi-lens/dist/tools/render-compact.js +1 -1
  317. package/node_modules/pi-lens/dist/tools/shared.js +0 -1
  318. package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -2
  319. package/node_modules/pi-lens/docs/agent-guide.md +64 -1
  320. package/node_modules/pi-lens/docs/configuration.md +222 -0
  321. package/node_modules/pi-lens/docs/dependencies.md +3 -3
  322. package/node_modules/pi-lens/docs/features.md +44 -5
  323. package/node_modules/pi-lens/docs/globalconfig.md +20 -0
  324. package/node_modules/pi-lens/docs/language-coverage.md +2 -2
  325. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +9 -1
  326. package/node_modules/pi-lens/docs/pi-lens-fixer.md +25 -0
  327. package/node_modules/pi-lens/docs/pi-lens-investigator.md +25 -0
  328. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +33 -0
  329. package/node_modules/pi-lens/docs/pi-lens-subagent.md +49 -0
  330. package/node_modules/pi-lens/docs/pi-lens-warden.md +55 -0
  331. package/node_modules/pi-lens/docs/public-api-stability.md +359 -0
  332. package/node_modules/pi-lens/docs/release-qa-baseline.md +190 -0
  333. package/node_modules/pi-lens/docs/servercapabilities.md +7 -3
  334. package/node_modules/pi-lens/docs/subagent-compat.md +110 -30
  335. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +2 -2
  336. package/node_modules/pi-lens/docs/word-index.md +1 -1
  337. package/node_modules/pi-lens/package.json +18 -35
  338. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
  339. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
  340. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
  341. package/node_modules/pi-lens/rules/typos/_typos.toml +4 -2
  342. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +192 -1
  343. package/node_modules/pi-lens/scripts/install-selftest.mjs +58 -2
  344. package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +129 -0
  345. package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +1 -1
  346. package/node_modules/pi-lens/scripts/rpc-load-check.mjs +3 -1
  347. package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +4 -2
  348. package/node_modules/pi-web-access/CHANGELOG.md +28 -0
  349. package/node_modules/pi-web-access/README.md +62 -15
  350. package/node_modules/pi-web-access/curator-page.ts +3 -1
  351. package/node_modules/pi-web-access/curator-server.ts +5 -1
  352. package/node_modules/pi-web-access/gemini-search.ts +8 -4
  353. package/node_modules/pi-web-access/github-extract.ts +242 -1
  354. package/node_modules/pi-web-access/index.ts +118 -64
  355. package/node_modules/pi-web-access/mistral-search.ts +281 -0
  356. package/node_modules/pi-web-access/package.json +2 -2
  357. package/node_modules/pi-web-access/perplexity.ts +14 -1
  358. package/node_modules/pi-web-access/utils.ts +23 -6
  359. package/node_modules/pi-web-access/xai-search.ts +96 -33
  360. package/package.json +6 -6
@@ -11,15 +11,21 @@
11
11
  import { logExtension } from "./extension-log.js";
12
12
  import { AsyncLocalStorage } from "node:async_hooks";
13
13
  import * as fs from "node:fs/promises";
14
+ import * as os from "node:os";
14
15
  import * as path from "node:path";
15
16
  import { BoundedLruCache } from "./bounded-cache.js";
16
- import { normalizeMapKey } from "./path-utils.js";
17
+ import { createGenerationSource } from "./generation-guard.js";
18
+ import { findNearestMarkerRoot, isRealGitMarker, normalizeMapKey, } from "./path-utils.js";
19
+ import { resolveCargoPackageEdition } from "./cargo-manifest.js";
20
+ import { resolveKtfmtGradleStyle } from "./gradle-ktfmt-style.js";
21
+ import { resolvePhpCsFixerConfig } from "./php-cs-fixer-config.js";
17
22
  import { TERRAGRUNT_FILENAMES } from "./file-kinds.js";
23
+ import { stripAnsi } from "./sanitize.js";
18
24
  import { detectIndentation, hasDetectableIndentation, } from "./dispatch/indent-detect.js";
19
25
  import { logLatency } from "./latency-logger.js";
20
26
  import { compareOrdinal } from "./string-utils.js";
21
27
  import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
22
- import { findGlobalBinary, findLocalBinUpwards } from "./package-manager.js";
28
+ import { findGlobalBinary, findLocalBinUpwards, VENDOR_BIN_DIRS, VENV_BIN_DIRS, } from "./package-manager.js";
23
29
  import { safeSpawnAsync } from "./safe-spawn.js";
24
30
  import { assertInstallAllowed } from "./project-trust.js";
25
31
  import { tryLazyInstallForFormatter } from "./dispatch/runners/utils/lazy-installer.js";
@@ -49,6 +55,19 @@ export async function tryLazyInstallFormatterTool(tool, cwd) {
49
55
  * stock command there is exactly the stock-style imposition the fix bans.
50
56
  */
51
57
  export const SKIP_FORMATTING = "skip-formatting";
58
+ /**
59
+ * Sentinel a `resolveCommand` returns when it has PROVEN the selected
60
+ * formatter has no executable — the binary is absent from every place the
61
+ * resolver probed (node_modules/.bin, a venv, a managed install dir, PATH) and
62
+ * no alternative resolved (#2413). Distinct from `null` ("no override, fall
63
+ * back to the static `command`") and from SKIP_FORMATTING ("style-preserving
64
+ * refusal"): the static fallback for these formatters is the SAME bare binary
65
+ * the resolver just proved missing, so spawning it only re-triggers the ENOENT
66
+ * the resolver already observed — and reporting that as a formatting failure is
67
+ * the exact defect this fixes (recurring defect shape 10: an unavailable
68
+ * producer collapsed into an ordinary failed result).
69
+ */
70
+ export const FORMATTER_UNAVAILABLE = "formatter-unavailable";
52
71
  // --- Utility Functions ---
53
72
  async function fileExists(filePath) {
54
73
  try {
@@ -63,12 +82,47 @@ async function findUp(targets, startDir, stopDir = path.parse(startDir).root) {
63
82
  const found = [];
64
83
  let currentDir = startDir;
65
84
  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);
85
+ // One `readdir` per directory instead of one `access` per target: the
86
+ // per-target probe made this loop's cost O(targets), so a long target
87
+ // list (e.g. `FORMATTER_CONFIG_FILES`) paid for every entry at every
88
+ // ancestor directory even when almost none of them exist (#1603).
89
+ let entries;
90
+ try {
91
+ entries = await fs.readdir(currentDir);
92
+ }
93
+ catch {
94
+ entries = [];
95
+ }
96
+ if (entries.length > 0) {
97
+ // On default-case-insensitive platforms the old `fs.access` probe
98
+ // matched a config file regardless of on-disk casing; fold so the
99
+ // readdir membership check keeps that behavior.
100
+ const foldCase = process.platform === "win32" || process.platform === "darwin";
101
+ const entrySet = new Set(foldCase ? entries.map((e) => e.toLowerCase()) : entries);
102
+ for (const target of targets) {
103
+ // Nested candidates (for example node_modules/.bin/biome) do
104
+ // not appear as direct readdir members. They retain the old
105
+ // bounded candidate probe; direct names use the O(depth + matches)
106
+ // directory-membership path above.
107
+ if (target.includes("/") || target.includes("\\"))
108
+ continue;
109
+ if (entrySet.has(foldCase ? target.toLowerCase() : target)) {
110
+ const candidate = path.join(currentDir, target);
111
+ // Directory membership is only a cheap candidate filter. Keep the
112
+ // old access check for matched entries so dangling links and entries
113
+ // that cannot be read do not become config evidence (#1603 R2).
114
+ if (await fileExists(candidate))
115
+ found.push(candidate);
116
+ }
70
117
  }
71
118
  }
119
+ for (const target of targets) {
120
+ if (!target.includes("/") && !target.includes("\\"))
121
+ continue;
122
+ const candidate = path.join(currentDir, target);
123
+ if (await fileExists(candidate))
124
+ found.push(candidate);
125
+ }
72
126
  const parent = path.dirname(currentDir);
73
127
  if (parent === currentDir)
74
128
  break;
@@ -76,6 +130,10 @@ async function findUp(targets, startDir, stopDir = path.parse(startDir).root) {
76
130
  }
77
131
  return found;
78
132
  }
133
+ /** Test-only access to the real filesystem walker and its candidate filter. */
134
+ export function _findUpForTests(targets, startDir, stopDir) {
135
+ return findUp(targets, startDir, stopDir);
136
+ }
79
137
  const WHICH_BUDGET_MS = 5000;
80
138
  /**
81
139
  * PATH lookups, governed by the shared availability policy (#1495).
@@ -337,79 +395,58 @@ async function resolveGoFmtBinary() {
337
395
  /**
338
396
  * Walk up from cwd looking for a binary in .venv or venv.
339
397
  * Returns the absolute path if found, null otherwise.
398
+ *
399
+ * Tool-resolution walker, not a config lookup (#2514/#2517 policy): escaping
400
+ * the project upward past HOME means STOP, not keep reading — a `.venv`/`venv`
401
+ * bin found at or above HOME can never be THIS project's own virtualenv. The
402
+ * ceiling lives in `findLocalBinUpwards` and is default-on there; the tests
403
+ * that pin a fake HOME drive that walker directly rather than threading a
404
+ * `homeDir` no production caller ever passed (#2544 review F3).
405
+ *
406
+ * On Windows the candidate order is EXT-OUTER, DIR-INNER — `.venv/Scripts/x.exe`,
407
+ * `venv/Scripts/x.exe`, then the bare names — which is what the pre-fold copy
408
+ * did and what `findLocalBinUpwards` preserves.
340
409
  */
341
410
  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;
411
+ return (findLocalBinUpwards(binary, cwd, {
412
+ windowsExt: ".exe",
413
+ binDirs: VENV_BIN_DIRS,
414
+ }) ?? null);
365
415
  }
366
416
  /**
367
417
  * Check vendor/bin for PHP Composer-managed tools.
368
418
  * Walks up from cwd to find vendor/bin/<binary>.
419
+ *
420
+ * Tool-resolution walker, not a config lookup (#2514/#2517 policy): escaping
421
+ * the project upward past HOME means STOP, not keep reading — a `vendor/bin`
422
+ * match found at or above HOME can never be THIS project's own Composer
423
+ * install. Same single walker as `findInVenv` above; only `binDirs` and the
424
+ * Windows extension differ.
369
425
  */
370
426
  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;
427
+ return (findLocalBinUpwards(binary, cwd, {
428
+ windowsExt: ".bat",
429
+ binDirs: VENDOR_BIN_DIRS,
430
+ }) ?? null);
387
431
  }
388
432
  /**
389
433
  * Check node_modules/.bin for locally installed Node tools.
390
434
  * Walks up from cwd to find node_modules/.bin/<binary>.
435
+ *
436
+ * #2514: this WAS a private, byte-for-byte duplicate of
437
+ * `findLocalBinUpwards` (`package-manager.ts`) — the same walk `stylua`
438
+ * below, `taplo.ts`, `knip-client.ts`, and (via `findNodeToolBinary`)
439
+ * `dependency-checker.ts`/`jscpd-client.ts` already reuse. A stray
440
+ * `~/node_modules/.bin/oxfmt.cmd` (the home-level pi-extensions manifest
441
+ * installs its own bins) was picked up as the project's formatter because
442
+ * this copy had no HOME ceiling; fixing only this copy would have left every
443
+ * other caller of `findLocalBinUpwards` with the same defect. Delegating
444
+ * here instead of hand-rolling a second ceiling keeps there being exactly one
445
+ * `node_modules/.bin` walker — see that function for the tool-resolution
446
+ * "escaping the project means STOP" policy (#2517).
391
447
  */
392
448
  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;
449
+ return findLocalBinUpwards(binary, cwd) ?? null;
413
450
  }
414
451
  /**
415
452
  * Returns true if `bundle exec <gem>` should be used:
@@ -566,6 +603,110 @@ async function indentationArgs(filePath, tool, cwd) {
566
603
  String(indentation.width),
567
604
  ];
568
605
  }
606
+ const FORMATTER_MARKERS_BY_NAME = new Map([
607
+ ["biome", ["biome.json", "biome.jsonc", "package.json"]],
608
+ [
609
+ "prettier",
610
+ [
611
+ ".prettierrc",
612
+ ".prettierrc.json",
613
+ ".prettierrc.yaml",
614
+ ".prettierrc.yml",
615
+ ".prettierrc.js",
616
+ ".prettierrc.cjs",
617
+ ".prettierrc.mjs",
618
+ "prettier.config.js",
619
+ "prettier.config.cjs",
620
+ "prettier.config.mjs",
621
+ ".prettierignore",
622
+ "package.json",
623
+ ],
624
+ ],
625
+ [
626
+ "oxfmt",
627
+ ["oxfmt.toml", ".oxfmtrc.json", "vite-plus.json", "package.json"],
628
+ ],
629
+ ["ruff", ["pyproject.toml", "ruff.toml", ".ruff.toml"]],
630
+ ["black", ["pyproject.toml", "black.toml", ".black"]],
631
+ ["sqlfluff", [".sqlfluff", "pyproject.toml", "setup.cfg"]],
632
+ ["rustfmt", ["rustfmt.toml", ".rustfmt.toml", "Cargo.toml"]],
633
+ ["rubocop", [".rubocop.yml", ".rubocop.yaml"]],
634
+ ["standardrb", [".standard.yml", ".standard.yaml"]],
635
+ ["clang-format", [".clang-format", "_clang-format"]],
636
+ ["php-cs-fixer", [".php-cs-fixer.php", ".php-cs-fixer.dist.php"]],
637
+ ["stylua", ["stylua.toml", ".stylua.toml"]],
638
+ ["ocamlformat", [".ocamlformat"]],
639
+ ["google-java-format", [".google-java-format", ".editorconfig"]],
640
+ ["cljfmt", [".cljfmt.edn", "cljfmt.edn", ".cljfmt"]],
641
+ [
642
+ "cmake-format",
643
+ [
644
+ ".cmake-format",
645
+ ".cmake-format.yaml",
646
+ ".cmake-format.yml",
647
+ ".cmake-format.json",
648
+ ".cmake-format.py",
649
+ "cmake-format.yaml",
650
+ "cmake-format.yml",
651
+ ".editorconfig",
652
+ ],
653
+ ],
654
+ [
655
+ "psscriptanalyzer-format",
656
+ ["PSScriptAnalyzerSettings.psd1", "ScriptAnalyzerSettings.psd1"],
657
+ ],
658
+ [
659
+ "csharpier",
660
+ [
661
+ ".csharpierrc",
662
+ ".csharpierrc.json",
663
+ ".csharpierrc.yaml",
664
+ ".csharpierrc.yml",
665
+ ],
666
+ ],
667
+ ["ormolu", [".ormolu"]],
668
+ ["taplo", ["taplo.toml", ".taplo.toml"]],
669
+ ["terraform", [".terraform.lock.hcl"]],
670
+ ["swiftformat", [".swiftformat"]],
671
+ ["fantomas", [".fantomasignore", ".editorconfig"]],
672
+ ["mix", [".formatter.exs"]],
673
+ ["shfmt", [".editorconfig"]],
674
+ ["ktlint", [".editorconfig"]],
675
+ [
676
+ "ktfmt",
677
+ [
678
+ ".editorconfig",
679
+ ".ktfmt",
680
+ ".ktfmt.kts",
681
+ "build.gradle",
682
+ "build.gradle.kts",
683
+ "settings.gradle",
684
+ "settings.gradle.kts",
685
+ ],
686
+ ],
687
+ ]);
688
+ /**
689
+ * Resolve the cwd for the formatter child, capped at the user's home
690
+ * directory. Files outside a project retain the historical file-directory
691
+ * cwd. The optional homeDir is only for the ceiling regression test.
692
+ */
693
+ export function resolveFormatterCwd(absolutePath, formatterName, homeDir = os.homedir()) {
694
+ const fileDir = path.dirname(path.resolve(absolutePath));
695
+ const effectiveHome = homeDir || undefined;
696
+ const markers = formatterName
697
+ ? [...(FORMATTER_MARKERS_BY_NAME.get(formatterName) ?? []), ".gitignore"]
698
+ : [".gitignore"];
699
+ const root = findNearestMarkerRoot(fileDir, markers, {
700
+ homeDir: effectiveHome,
701
+ });
702
+ if (root)
703
+ return root;
704
+ const gitRoot = findNearestMarkerRoot(fileDir, [".git"], {
705
+ homeDir: effectiveHome,
706
+ markerPredicate: isRealGitMarker,
707
+ });
708
+ return gitRoot ?? fileDir;
709
+ }
569
710
  /**
570
711
  * Every biome invocation must carry this. Biome exits 1 with "No files were
571
712
  * processed in the specified paths" when the path is ignored by the repo's own
@@ -747,7 +888,11 @@ export const oxfmtFormatter = {
747
888
  const found = await which("oxfmt");
748
889
  if (found)
749
890
  return [found, OXFMT_NO_ERROR_ON_UNMATCHED, filePath];
750
- return null;
891
+ // #2413: neither node_modules/.bin nor PATH has oxfmt, and `detect()` is
892
+ // config-only (it never probes the binary), so selection can reach here
893
+ // with nothing installed. The static command is bare `oxfmt` — spawning it
894
+ // only reproduces the reported `spawn oxfmt ENOENT`. Prove it unavailable.
895
+ return FORMATTER_UNAVAILABLE;
751
896
  },
752
897
  // Single source of truth: OXFMT_SUPPORTED_EXTENSIONS in tool-policy.ts.
753
898
  // Do not hand-maintain a second copy of this list (#1134 — previously two
@@ -784,7 +929,12 @@ export const ruffFormatter = {
784
929
  const installed = await ensureTool(toolId);
785
930
  if (installed)
786
931
  return [installed, ...args, filePath];
787
- return null;
932
+ // #2413: `ensureTool` already resolves PATH, global bins and the managed
933
+ // dir (and attempts an install) before returning falsy, so reaching here
934
+ // means ruff is genuinely nowhere. `detect()`'s `hasRuffConfig` branch has
935
+ // no binary check, so a config-only project selects ruff with nothing
936
+ // installed; the static bare `ruff` would then ENOENT. Prove it unavailable.
937
+ return FORMATTER_UNAVAILABLE;
788
938
  },
789
939
  async detect(cwd) {
790
940
  if (hasRuffConfig(cwd))
@@ -847,6 +997,18 @@ export const rustfmtFormatter = {
847
997
  name: "rustfmt",
848
998
  command: ["rustfmt", "$FILE"],
849
999
  extensions: [".rs"],
1000
+ // #2466: rustfmt defaults to an OLDER edition than the file's actual Cargo
1001
+ // package when invoked bare, so it can reject valid newer-edition syntax
1002
+ // (e.g. Rust 2024). Carry the nearest package's `edition` (honoring
1003
+ // `edition.workspace = true` inheritance) through `--edition`; `undefined`
1004
+ // (unreadable/unparseable manifest) falls back to the static command
1005
+ // above, unchanged from pre-#2466 behavior.
1006
+ async resolveCommand(filePath, _cwd) {
1007
+ const edition = await resolveCargoPackageEdition(filePath);
1008
+ if (edition === undefined)
1009
+ return null;
1010
+ return ["rustfmt", "--edition", edition, filePath];
1011
+ },
850
1012
  async detect(cwd) {
851
1013
  if ((await which("rustfmt")) !== null)
852
1014
  return true;
@@ -971,13 +1133,27 @@ export const ktfmtFormatter = {
971
1133
  // ktfmt formats in place when given a file path (no flag needed).
972
1134
  command: ["ktfmt", "$FILE"],
973
1135
  extensions: [".kt", ".kts"],
1136
+ // #2468: ktfmt's CLI never reads a project's Gradle `ktfmt { googleStyle()
1137
+ // | kotlinLangStyle() }` selection — style is CLI-flag-only
1138
+ // (`--google-style`/`--kotlinlang-style`, verified against ktfmt v0.63's
1139
+ // own arg parser). Carry the nearest module's declared style through;
1140
+ // `undefined` (no declaration, unreadable/unparseable manifest, or an
1141
+ // unsupported style like the removed `dropboxStyle()`) falls back to the
1142
+ // bare invocation, unchanged from pre-#2468 behavior.
974
1143
  async resolveCommand(filePath, _cwd) {
1144
+ const styleFlag = await resolveKtfmtGradleStyle(filePath);
975
1145
  const inPath = await which("ktfmt");
976
1146
  if (inPath)
977
- return [inPath, filePath];
1147
+ return styleFlag ? [inPath, styleFlag, filePath] : [inPath, filePath];
978
1148
  const { ensureTool } = await import("./installer/index.js");
979
1149
  const installed = await ensureTool("ktfmt");
980
- return installed ? [installed, filePath] : null;
1150
+ // #2413: which() and ensureTool (PATH/global/managed + install) both
1151
+ // failed, and `detect()` gates only on hasKtfmtConfig — no binary probe —
1152
+ // so a config-only project reaches here with ktfmt absent. The static
1153
+ // command is bare `ktfmt`; spawning it reproduces the ENOENT class.
1154
+ if (!installed)
1155
+ return FORMATTER_UNAVAILABLE;
1156
+ return styleFlag ? [installed, styleFlag, filePath] : [installed, filePath];
981
1157
  },
982
1158
  async detect(cwd) {
983
1159
  // Opt-in only: ktfmt becomes the formatter when the project elects it,
@@ -1063,18 +1239,49 @@ export const phpCsFixerFormatter = {
1063
1239
  name: "php-cs-fixer",
1064
1240
  command: ["php-cs-fixer", "fix", "$FILE"],
1065
1241
  extensions: [".php"],
1242
+ // #2472: php-cs-fixer does NOT walk up parent directories looking for its
1243
+ // own config the way prettier/biome/eslint do (verified against upstream
1244
+ // `computeConfigFiles()` — see `resolvePhpCsFixerConfig`'s doc comment),
1245
+ // and `formatFile` spawns with cwd = the FILE's own directory, which is
1246
+ // not necessarily where the ancestor config `detect()` found actually
1247
+ // lives. Always resolve the binary explicitly here (vendor/bin first,
1248
+ // then global) rather than falling through to the static `command` above
1249
+ // — that static command can never carry `--config`, so a config found at
1250
+ // an ancestor would silently be dropped whenever the vendor lookup here
1251
+ // missed but a global binary still resolved. `--config` is attached
1252
+ // whenever a config resolves, even when it sits in the file's own
1253
+ // directory (AC3): unlike the pre-#2472 code, correctness no longer
1254
+ // depends on php-cs-fixer's own (nonexistent) upward search.
1066
1255
  async resolveCommand(filePath, cwd) {
1067
- const vendor = await findInVendorBin("php-cs-fixer", cwd);
1068
- if (vendor)
1069
- return [vendor, "fix", filePath];
1070
- return null;
1256
+ const configPath = resolvePhpCsFixerConfig(filePath);
1257
+ const binary = (await findInVendorBin("php-cs-fixer", cwd)) ??
1258
+ (await which("php-cs-fixer"));
1259
+ // #2413/#2472 review F4: both probes (vendor/bin, then PATH) have
1260
+ // PROVEN the binary is absent — returning `null` here would fall back
1261
+ // to the static `command` above, which is the SAME bare
1262
+ // `php-cs-fixer` this just failed to find, spawning it only to
1263
+ // re-observe the ENOENT already known. Report the proven-missing
1264
+ // state instead so `formatFile` skips the wasted spawn.
1265
+ if (!binary)
1266
+ return FORMATTER_UNAVAILABLE;
1267
+ return configPath
1268
+ ? [binary, "fix", "--config", configPath, filePath]
1269
+ : [binary, "fix", filePath];
1071
1270
  },
1072
1271
  async detect(cwd) {
1073
1272
  const vendorBin = await findInVendorBin("php-cs-fixer", cwd);
1074
1273
  const globalBin = await which("php-cs-fixer");
1075
1274
  if (!vendorBin && !globalBin)
1076
1275
  return false;
1077
- // Only run if project has explicit config
1276
+ // Only run if project has explicit config. This is a presence-only
1277
+ // climb from the project `cwd` (not necessarily the formatted file's
1278
+ // own directory) via this file's own `findUp` — deliberately NOT
1279
+ // merged with `resolvePhpCsFixerConfig` above (#2472 AC4): that
1280
+ // resolver climbs from the FILE's directory and needs the exact
1281
+ // winning path for `--config`, while this only needs a yes/no answer
1282
+ // from whatever `cwd` the caller passed. `rustfmtFormatter.detect`
1283
+ // keeps the same non-merged shape against `resolveCargoPackageEdition`
1284
+ // for the identical reason.
1078
1285
  const configs = [".php-cs-fixer.php", ".php-cs-fixer.dist.php"];
1079
1286
  const found = await findUp(configs, cwd);
1080
1287
  return found.length > 0;
@@ -1333,9 +1540,15 @@ export const ALL_FORMATTERS = [
1333
1540
  const FILENAME_FORMATTER_BASENAMES = new Set(ALL_FORMATTERS.flatMap((f) => f.filenames ?? []));
1334
1541
  // Cache for detection results - stores array of enabled formatter names per cwd+ext
1335
1542
  const detectionCache = new BoundedLruCache(32);
1543
+ // The signature is immutable for a cache generation. This memo is separate
1544
+ // from detectionCache because a cwd can have several extension entries, and a
1545
+ // warm lookup must not repeat the ancestor walk or stat matched configs.
1546
+ const formatterSignatureFlights = new BoundedLruCache(32);
1547
+ const formatterCacheGeneration = createGenerationSource("formatter-cache");
1336
1548
  // 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
1549
+ // above. Their metadata is captured by the cold detection signature. The
1550
+ // write-result seam invalidates that signature when a config path changes, so
1551
+ // detection re-runs even when PATH and installed tools are unchanged. A
1339
1552
  // filename a `has*Config` check reads but this list omits is invisible to the
1340
1553
  // cache: the signature never moves when that file is added, so a project that
1341
1554
  // opts in AFTER the first `getFormattersForFile` call for its cwd keeps
@@ -1436,6 +1649,18 @@ async function formatterConfigSignature(cwd) {
1436
1649
  }));
1437
1650
  return parts.join("|");
1438
1651
  }
1652
+ async function getFormatterConfigSignature(cwd, normalizedCwd) {
1653
+ const existing = formatterSignatureFlights.get(normalizedCwd);
1654
+ if (existing)
1655
+ return existing.promise;
1656
+ const promise = formatterConfigSignature(cwd).finally(() => {
1657
+ const current = formatterSignatureFlights.get(normalizedCwd);
1658
+ if (current?.promise === promise)
1659
+ formatterSignatureFlights.delete(normalizedCwd);
1660
+ });
1661
+ formatterSignatureFlights.set(normalizedCwd, { promise });
1662
+ return promise;
1663
+ }
1439
1664
  // --- Public API ---
1440
1665
  export async function getFormattersForFile(filePath, cwd) {
1441
1666
  const ext = path.extname(filePath).toLowerCase();
@@ -1449,31 +1674,19 @@ export async function getFormattersForFile(filePath, cwd) {
1449
1674
  const cacheKey = FILENAME_FORMATTER_BASENAMES.has(base)
1450
1675
  ? `${normalizedCwd}:${ext}:${base}`
1451
1676
  : `${normalizedCwd}:${ext}`;
1452
- const configSignature = await formatterConfigSignature(cwd);
1453
- // Check cache
1677
+ // A warm entry is authoritative until the write-result seam invalidates its
1678
+ // config path. This is the only way to make the warm path free of config
1679
+ // polling while still reacting immediately to pi-authored create/remove/
1680
+ // change events. External editor changes remain a documented session-boundary
1681
+ // limitation, like other write-result-owned freshness seams.
1454
1682
  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)) {
1683
+ if (cached?.entries.has(cacheKey)) {
1471
1684
  const enabledNames = cached.entries.get(cacheKey);
1472
1685
  const selectedFormatterName = enabledNames && enabledNames.length > 0 ? enabledNames[0] : null;
1473
1686
  logLatency({
1474
1687
  type: "phase",
1475
1688
  phase: "formatter_selected",
1476
- filePath: filePath,
1689
+ filePath,
1477
1690
  durationMs: 0,
1478
1691
  metadata: {
1479
1692
  formatter: selectedFormatterName,
@@ -1485,9 +1698,26 @@ export async function getFormattersForFile(filePath, cwd) {
1485
1698
  });
1486
1699
  if (!enabledNames || enabledNames.length === 0)
1487
1700
  return [];
1488
- // Return cached formatters by name (preserves priority order)
1489
1701
  return ALL_FORMATTERS.filter((f) => enabledNames.includes(f.name));
1490
1702
  }
1703
+ if (!cached) {
1704
+ const generation = formatterCacheGeneration.capture();
1705
+ const configSignature = await getFormatterConfigSignature(cwd, normalizedCwd);
1706
+ // An invalidation can happen while the first signature walk is in flight.
1707
+ // Do not publish a pre-invalidation signature into the new generation.
1708
+ if (!generation.isCurrent()) {
1709
+ return getFormattersForFile(filePath, cwd);
1710
+ }
1711
+ // Another cold caller for this cwd can finish the shared signature flight
1712
+ // first. Re-read the LRU before installing an object so the later caller
1713
+ // merges its extension entry into the existing object instead of replacing
1714
+ // the earlier entry.
1715
+ cached = detectionCache.get(normalizedCwd);
1716
+ if (!cached) {
1717
+ cached = { signature: configSignature, entries: new Map() };
1718
+ detectionCache.set(normalizedCwd, cached);
1719
+ }
1720
+ }
1491
1721
  // Detect formatters for this extension (or exact filename, e.g. terragrunt.hcl)
1492
1722
  const matching = ALL_FORMATTERS.filter((f) => f.extensions.includes(ext) || f.filenames?.includes(base));
1493
1723
  const formatterPolicy = getFormatterPolicyForFile(filePath);
@@ -1641,7 +1871,11 @@ export async function getFormattersForFile(filePath, cwd) {
1641
1871
  return enabled;
1642
1872
  // Store the list of enabled formatter names in cache
1643
1873
  const enabledNames = enabled.map((f) => f.name);
1644
- cached.entries.set(cacheKey, enabledNames);
1874
+ // An invalidation may have removed or replaced this object while detection
1875
+ // awaited a tool probe. Never repopulate the old generation.
1876
+ if (detectionCache.get(normalizedCwd) === cached) {
1877
+ cached.entries.set(cacheKey, enabledNames);
1878
+ }
1645
1879
  return enabled;
1646
1880
  }
1647
1881
  /**
@@ -1655,9 +1889,21 @@ export async function getFormattersForFile(filePath, cwd) {
1655
1889
  * therefore re-arms every directory EXCEPT the one the user is working in.
1656
1890
  */
1657
1891
  export function clearFormatterCache() {
1892
+ formatterCacheGeneration.bump();
1893
+ formatterSignatureFlights.clear();
1658
1894
  detectionCache.clear();
1659
1895
  resetWhichLatches();
1660
1896
  }
1897
+ const formatterConfigBasenames = new Set(FORMATTER_CONFIG_FILES.map((fileName) => fileName.toLowerCase()));
1898
+ /** Invalidate selection when the write-result seam reports a config path. */
1899
+ export function invalidateFormatterCacheForPath(filePath) {
1900
+ const basename = filePath.includes("\\")
1901
+ ? path.win32.basename(filePath)
1902
+ : path.basename(filePath);
1903
+ if (!formatterConfigBasenames.has(basename.toLowerCase()))
1904
+ return;
1905
+ clearFormatterCache();
1906
+ }
1661
1907
  /**
1662
1908
  * Test-only internals access for the session-state registry probe (#1895):
1663
1909
  * the conformance suite must arm all four pieces of state this reset claims
@@ -1669,19 +1915,17 @@ export function _getFormatterResetStateForTests() {
1669
1915
  whichTransientCommands,
1670
1916
  cooldownRecordedForRetryAtMs,
1671
1917
  detectionCache,
1918
+ formatterSignatureFlights,
1672
1919
  };
1673
1920
  }
1674
1921
  export function clearFormatterRuntimeState() {
1675
- detectionCache.clear();
1676
- resetWhichLatches();
1922
+ clearFormatterCache();
1677
1923
  // NO `resetLazyInstallAttempts()` here (#1537 review F1). This function runs
1678
1924
  // from `resetFormatService()`, which `handleTurnEnd` calls every turn — so
1679
1925
  // clearing the lazy-install hold here made "held for the session" mean "held
1680
1926
  // for a turn", and a failing install re-ran every turn. The hold's only reset
1681
1927
  // is `session_start`'s block in runtime-session.ts.
1682
1928
  }
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
1929
  const BOX_DRAWING_GLOBAL = /[\u2500-\u257F]/g;
1686
1930
  const HAS_BOX_DRAWING = /[\u2500-\u257F]/;
1687
1931
  /**
@@ -1698,7 +1942,7 @@ const HAS_BOX_DRAWING = /[\u2500-\u257F]/;
1698
1942
  */
1699
1943
  export function firstDiagnosticLine(text) {
1700
1944
  for (const raw of (text ?? "").split("\n")) {
1701
- const line = raw.replace(ANSI_ESCAPE, "").trimEnd();
1945
+ const line = stripAnsi(raw).trimEnd();
1702
1946
  const stripped = line.replace(BOX_DRAWING_GLOBAL, "").trim();
1703
1947
  if (!stripped)
1704
1948
  continue;
@@ -1724,6 +1968,12 @@ async function resolveFormatterCommand(formatter, absolutePath, cwd) {
1724
1968
  : null;
1725
1969
  if (resolved === SKIP_FORMATTING)
1726
1970
  return SKIP_FORMATTING;
1971
+ // The resolver proved the executable absent — DO NOT fall through to the
1972
+ // static bare command, which is the same binary it just probed and missing
1973
+ // (#2413). The caller turns this into a typed `unavailable` outcome, never a
1974
+ // spawn and never a failure.
1975
+ if (resolved === FORMATTER_UNAVAILABLE)
1976
+ return FORMATTER_UNAVAILABLE;
1727
1977
  if (resolved !== null)
1728
1978
  return resolved;
1729
1979
  const fallback = formatter.command.map((c) => c.replace("$FILE", absolutePath));
@@ -1743,6 +1993,7 @@ export async function formatFile(filePath, formatter) {
1743
1993
  try {
1744
1994
  const absolutePath = path.resolve(filePath);
1745
1995
  const cwd = path.dirname(absolutePath);
1996
+ const formatterCwd = resolveFormatterCwd(absolutePath, formatter.name);
1746
1997
  const contentBefore = await fs.readFile(absolutePath, "utf-8");
1747
1998
  // Resolve command: prefer local (venv/vendor/node_modules) over global.
1748
1999
  // The shared seam must honor SKIP_FORMATTING before selecting the static
@@ -1751,13 +2002,40 @@ export async function formatFile(filePath, formatter) {
1751
2002
  if (cmd === SKIP_FORMATTING) {
1752
2003
  // Style-preserving refusal (#1144): no repo config and no detectable
1753
2004
  // indentation to pin — formatting would impose the tool's stock style.
1754
- return { success: true, changed: false };
2005
+ return { success: true, changed: false, outcome: "skipped" };
2006
+ }
2007
+ if (cmd === FORMATTER_UNAVAILABLE) {
2008
+ // The resolver proved the executable absent (#2413): the oxfmt ENOENT
2009
+ // trap and its siblings. This is unavailable infrastructure, not a code
2010
+ // failure — success stays true so it is never counted as a failed file
2011
+ // or requeued, and the typed outcome keeps it out of the failure bucket.
2012
+ return {
2013
+ success: true,
2014
+ changed: false,
2015
+ outcome: "unavailable",
2016
+ error: `${formatter.name}: formatter executable not found`,
2017
+ };
1755
2018
  }
1756
2019
  // Run formatter without blocking the event loop.
1757
2020
  const result = await safeSpawnAsync(cmd[0], cmd.slice(1), {
1758
2021
  timeout: 15000,
1759
- cwd,
2022
+ cwd: formatterCwd,
1760
2023
  });
2024
+ // A resolver that could NOT prove absence (it never probed PATH — e.g.
2025
+ // black/sqlfluff, which only look in a venv) legitimately falls back to the
2026
+ // bare static command. When that bare command is missing, the spawn boundary
2027
+ // reports a typed `tool-not-found` failure — never a nonzero exit. Treat it
2028
+ // as unavailable, not a formatting failure, closing the same #2413 class for
2029
+ // the formatters whose `null` correctly means "static untried" (no
2030
+ // oxfmt-specific string matching — this reads the typed spawn-failure kind).
2031
+ if (result.spawnFailure?.kind === "tool-not-found") {
2032
+ return {
2033
+ success: true,
2034
+ changed: false,
2035
+ outcome: "unavailable",
2036
+ error: result.spawnFailure.message,
2037
+ };
2038
+ }
1761
2039
  // Strict by default (#1337): only a formatter with a documented
1762
2040
  // benign-nonzero mode (`lenientExitCode`) may exit nonzero and still be
1763
2041
  // read as a successful run. Everything else that exits nonzero never
@@ -1770,6 +2048,7 @@ export async function formatFile(filePath, formatter) {
1770
2048
  return {
1771
2049
  success: false,
1772
2050
  changed: false,
2051
+ outcome: "failed",
1773
2052
  error: result.error?.message ||
1774
2053
  firstDiagnosticLine(result.stderr) ||
1775
2054
  // biome, ktlint and `mix format` report on STDOUT; without this
@@ -1785,12 +2064,14 @@ export async function formatFile(filePath, formatter) {
1785
2064
  return {
1786
2065
  success: true,
1787
2066
  changed,
2067
+ outcome: changed ? "formatted" : "unchanged",
1788
2068
  };
1789
2069
  }
1790
2070
  catch (err) {
1791
2071
  return {
1792
2072
  success: false,
1793
2073
  changed: false,
2074
+ outcome: "failed",
1794
2075
  error: err instanceof Error ? err.message : String(err),
1795
2076
  };
1796
2077
  }