@herbertgao/pi-extensions 2026.8.5 → 2026.8.7

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 (389) hide show
  1. package/README.md +5 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +19 -7
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +30 -9
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +272 -22
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +94 -77
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +48 -6
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +1 -1
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +14 -0
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -17
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +41 -2
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +75 -6
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +7 -3
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +64 -1
  17. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  18. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +16 -0
  19. package/node_modules/@herbertgao/pi-subagents/README.md +30 -22
  20. package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +5 -5
  21. package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
  22. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +7 -1
  23. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +48 -19
  24. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +22 -15
  25. package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +26 -2
  26. package/node_modules/@herbertgao/pi-subagents/src/index.ts +219 -40
  27. package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +101 -3
  28. package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +18 -4
  29. package/node_modules/@herbertgao/pi-subagents/src/settings.ts +99 -0
  30. package/node_modules/@herbertgao/pi-subagents/src/types.ts +22 -3
  31. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +51 -3
  32. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +20 -2
  33. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +34 -10
  34. package/node_modules/@herbertgao/pi-subagents/src/usage.ts +129 -1
  35. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +29 -5
  36. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +3 -0
  37. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +46 -10
  38. package/node_modules/@juicesharp/rpiv-ask-user-question/config.ts +25 -0
  39. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/configuration.md +13 -4
  40. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +8 -1
  41. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +3 -1
  42. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +2 -2
  43. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/zh.json +2 -2
  44. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  45. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +10 -0
  46. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +3 -1
  47. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +30 -7
  48. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
  49. package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +30 -4
  50. package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +26 -5
  51. package/node_modules/@narumitw/pi-btw/README.md +119 -103
  52. package/node_modules/@narumitw/pi-btw/dist/index.ts +2898 -0
  53. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +7 -0
  54. package/node_modules/@narumitw/pi-btw/package.json +13 -9
  55. package/node_modules/@narumitw/pi-btw/src/btw.ts +40 -42
  56. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +55 -10
  57. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +390 -0
  58. package/node_modules/@narumitw/pi-btw/src/menu.ts +78 -15
  59. package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
  60. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
  61. package/node_modules/pi-lens/CHANGELOG.md +1004 -0
  62. package/node_modules/pi-lens/README.md +11 -3
  63. package/node_modules/pi-lens/config/ruff/core.toml +42 -0
  64. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +130 -27
  65. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +427 -0
  66. package/node_modules/pi-lens/dist/clients/agent-nudge.js +79 -16
  67. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +9 -3
  68. package/node_modules/pi-lens/dist/clients/atomic-write.js +9 -3
  69. package/node_modules/pi-lens/dist/clients/bash-file-access.js +285 -8
  70. package/node_modules/pi-lens/dist/clients/biome-client.js +176 -37
  71. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
  72. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +119 -0
  73. package/node_modules/pi-lens/dist/clients/bootstrap.js +12 -1
  74. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +12 -3
  75. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +192 -0
  76. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
  77. package/node_modules/pi-lens/dist/clients/bus-publish.js +11 -3
  78. package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
  79. package/node_modules/pi-lens/dist/clients/cascade-format.js +76 -2
  80. package/node_modules/pi-lens/dist/clients/cascade-types.js +9 -1
  81. package/node_modules/pi-lens/dist/clients/child-unref.js +54 -6
  82. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +76 -24
  83. package/node_modules/pi-lens/dist/clients/console-guard-install.js +16 -4
  84. package/node_modules/pi-lens/dist/clients/dead-code-client.js +249 -52
  85. package/node_modules/pi-lens/dist/clients/deadline-utils.js +8 -1
  86. package/node_modules/pi-lens/dist/clients/debug-handles.js +5 -0
  87. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +62 -13
  88. package/node_modules/pi-lens/dist/clients/dependency-checker.js +43 -9
  89. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +153 -37
  90. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
  91. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  92. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +165 -11
  93. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +325 -72
  94. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +7 -4
  95. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +5 -0
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +234 -18
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +187 -19
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +6 -3
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +11 -9
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +398 -0
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +11 -3
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +12 -7
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +18 -0
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +895 -0
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +4 -0
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +16 -3
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +81 -41
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -0
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +23 -0
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +331 -55
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +38 -5
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +97 -19
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +14 -0
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +20 -0
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +22 -0
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +10 -0
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +18 -6
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +73 -12
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +488 -0
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +84 -0
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +260 -27
  122. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +943 -93
  123. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +86 -11
  124. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +72 -0
  125. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +106 -0
  126. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +9 -0
  127. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -1
  128. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
  129. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  130. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
  131. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
  132. package/node_modules/pi-lens/dist/clients/extension-log.js +317 -3
  133. package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
  134. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +294 -0
  135. package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
  136. package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
  137. package/node_modules/pi-lens/dist/clients/format-events-publish.js +39 -8
  138. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +3 -2
  139. package/node_modules/pi-lens/dist/clients/formatters.js +500 -115
  140. package/node_modules/pi-lens/dist/clients/generation-guard.js +216 -0
  141. package/node_modules/pi-lens/dist/clients/git-guard.js +49 -21
  142. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
  143. package/node_modules/pi-lens/dist/clients/go-client.js +21 -39
  144. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +219 -10
  145. package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
  146. package/node_modules/pi-lens/dist/clients/host-ports.js +9 -1
  147. package/node_modules/pi-lens/dist/clients/installer/index.js +1119 -101
  148. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +58 -0
  149. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +715 -0
  150. package/node_modules/pi-lens/dist/clients/instance-reaper.js +643 -97
  151. package/node_modules/pi-lens/dist/clients/instance-registry.js +87 -17
  152. package/node_modules/pi-lens/dist/clients/jscpd-client.js +86 -15
  153. package/node_modules/pi-lens/dist/clients/knip-client.js +361 -24
  154. package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
  155. package/node_modules/pi-lens/dist/clients/language-profile.js +2 -0
  156. package/node_modules/pi-lens/dist/clients/latency-logger.js +487 -9
  157. package/node_modules/pi-lens/dist/clients/lazy-import.js +21 -0
  158. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -0
  159. package/node_modules/pi-lens/dist/clients/lens-events.js +56 -25
  160. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +16 -0
  161. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +45 -2
  162. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
  163. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +160 -20
  164. package/node_modules/pi-lens/dist/clients/lsp/client.js +1666 -148
  165. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +111 -0
  166. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
  167. package/node_modules/pi-lens/dist/clients/lsp/index.js +2496 -171
  168. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +215 -0
  169. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +146 -14
  170. package/node_modules/pi-lens/dist/clients/lsp/language.js +48 -4
  171. package/node_modules/pi-lens/dist/clients/lsp/server.js +517 -69
  172. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +22 -0
  173. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +43 -0
  174. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +172 -0
  175. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
  176. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +61 -3
  177. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +422 -28
  178. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +30 -0
  179. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +175 -0
  180. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +3 -2
  181. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +4 -0
  182. package/node_modules/pi-lens/dist/clients/mcp/session.js +39 -18
  183. package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
  184. package/node_modules/pi-lens/dist/clients/module-report.js +8 -0
  185. package/node_modules/pi-lens/dist/clients/opengrep-client.js +15 -1
  186. package/node_modules/pi-lens/dist/clients/package-manager.js +170 -14
  187. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +27 -0
  188. package/node_modules/pi-lens/dist/clients/path-utils.js +160 -0
  189. package/node_modules/pi-lens/dist/clients/pipeline.js +178 -30
  190. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +37 -0
  191. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +17 -3
  192. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +89 -22
  193. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +85 -15
  194. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +15 -3
  195. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
  196. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +50 -2
  197. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +5 -0
  198. package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
  199. package/node_modules/pi-lens/dist/clients/project-snapshot.js +299 -0
  200. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +47 -0
  201. package/node_modules/pi-lens/dist/clients/quiet-window.js +11 -25
  202. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +1 -1
  203. package/node_modules/pi-lens/dist/clients/read-guard.js +184 -10
  204. package/node_modules/pi-lens/dist/clients/resource-sampler.js +49 -17
  205. package/node_modules/pi-lens/dist/clients/reverse-deps.js +50 -11
  206. package/node_modules/pi-lens/dist/clients/review-graph/query.js +76 -4
  207. package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
  208. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +248 -13
  209. package/node_modules/pi-lens/dist/clients/runtime-context.js +112 -12
  210. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +472 -61
  211. package/node_modules/pi-lens/dist/clients/runtime-session.js +359 -13
  212. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
  213. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +269 -22
  214. package/node_modules/pi-lens/dist/clients/runtime-turn.js +948 -87
  215. package/node_modules/pi-lens/dist/clients/rust-client.js +21 -37
  216. package/node_modules/pi-lens/dist/clients/safe-spawn.js +220 -7
  217. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +161 -0
  218. package/node_modules/pi-lens/dist/clients/search-read-registration.js +34 -6
  219. package/node_modules/pi-lens/dist/clients/security-scan-client.js +224 -8
  220. package/node_modules/pi-lens/dist/clients/sg-runner.js +249 -37
  221. package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
  222. package/node_modules/pi-lens/dist/clients/smells-rollup.js +18 -11
  223. package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
  224. package/node_modules/pi-lens/dist/clients/startup-timing.js +26 -2
  225. package/node_modules/pi-lens/dist/clients/test-runner-client.js +574 -32
  226. package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
  227. package/node_modules/pi-lens/dist/clients/tool-policy.js +127 -20
  228. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
  229. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +75 -1
  230. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +769 -64
  231. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +11 -0
  232. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +90 -0
  233. package/node_modules/pi-lens/dist/clients/trivy-client.js +19 -1
  234. package/node_modules/pi-lens/dist/clients/warm-attach.js +17 -0
  235. package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
  236. package/node_modules/pi-lens/dist/clients/word-index.js +305 -33
  237. package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
  238. package/node_modules/pi-lens/dist/index.js +64677 -49484
  239. package/node_modules/pi-lens/dist/mcp/server.js +8 -2
  240. package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
  241. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
  242. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
  243. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +469 -56
  244. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +190 -46
  245. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
  246. package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
  247. package/node_modules/pi-lens/docs/agent-guide.md +44 -14
  248. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +518 -501
  249. package/node_modules/pi-lens/docs/dispositions.md +21 -0
  250. package/node_modules/pi-lens/docs/features.md +33 -6
  251. package/node_modules/pi-lens/docs/globalconfig.md +14 -0
  252. package/node_modules/pi-lens/docs/language-coverage.md +71 -5
  253. package/node_modules/pi-lens/docs/mcp.md +2 -2
  254. package/node_modules/pi-lens/docs/servercapabilities.md +4 -1
  255. package/node_modules/pi-lens/docs/settings.md +25 -1
  256. package/node_modules/pi-lens/docs/usage.md +24 -5
  257. package/node_modules/pi-lens/docs/word-index.md +35 -0
  258. package/node_modules/pi-lens/package.json +10 -6
  259. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-js-snapshot.yml +23 -0
  260. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-snapshot.yml +23 -0
  261. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/ts-void-zero-snapshot.yml +23 -0
  262. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-object-param-test.yml +16 -0
  263. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +22 -0
  264. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-compile-call-test.yml +29 -1
  265. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
  266. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
  267. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-dupe-class-members-test.yml +33 -0
  268. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-js-test.yml +7 -1
  269. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-test.yml +7 -1
  270. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-known-value-widening-test.yml +9 -0
  271. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-non-null-assertion-test.yml +216 -0
  272. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +9 -0
  273. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +9 -0
  274. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +9 -0
  275. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +9 -0
  276. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-return-value-in-generator-test.yml +12 -0
  277. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-runtime-typeof-test.yml +33 -0
  278. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-server-bind-wildcard-test.yml +9 -0
  279. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-shape-in-symbol-names-test.yml +13 -0
  280. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-js-test.yml +5 -1
  281. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-test.yml +5 -1
  282. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
  283. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-parameters-test.yml +12 -0
  284. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-returns-test.yml +16 -0
  285. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-any-test.yml +10 -0
  286. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-unknown-test.yml +8 -0
  287. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/redundant-unsafe-function-test.yml +35 -0
  288. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/require-safety-comment-for-as-unknown-as-test.yml +177 -0
  289. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ts-void-zero-test.yml +1 -0
  290. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member-js.yml +13 -2
  291. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member.yml +13 -2
  292. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +2 -0
  293. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-object-param.yml +60 -0
  294. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +66 -0
  295. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-compile-call.yml +63 -5
  296. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
  297. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
  298. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-dupe-class-members.yml +52 -8
  299. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html-js.yml +27 -2
  300. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html.yml +27 -2
  301. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-known-value-widening.yml +71 -0
  302. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-non-null-assertion.yml +316 -0
  303. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +7 -0
  304. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +29 -0
  305. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +30 -0
  306. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +41 -0
  307. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +42 -0
  308. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-return-value-in-generator.yml +22 -5
  309. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-runtime-typeof.yml +121 -0
  310. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-server-bind-wildcard.yml +23 -7
  311. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-shape-in-symbol-names.yml +68 -0
  312. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +41 -2
  313. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code.yml +41 -2
  314. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +32 -0
  315. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-parameters.yml +67 -0
  316. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-returns.yml +106 -0
  317. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-any.yml +50 -0
  318. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-unknown.yml +56 -0
  319. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +2 -0
  320. package/node_modules/pi-lens/rules/ast-grep-rules/rules/redundant-unsafe-function.yml +32 -0
  321. package/node_modules/pi-lens/rules/ast-grep-rules/rules/require-safety-comment-for-as-unknown-as.yml +184 -0
  322. package/node_modules/pi-lens/rules/ast-grep-rules/rules/ts-void-zero.yml +5 -0
  323. package/node_modules/pi-lens/rules/ast-grep-rules/self-scan-baseline.json +5 -0
  324. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +132 -1
  325. package/node_modules/pi-lens/scripts/download-grammars.js +25 -0
  326. package/node_modules/pi-lens/scripts/grammars.lock.json +9 -0
  327. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +12 -11
  328. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
  329. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +87 -116
  330. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +170 -0
  331. package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
  332. package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
  333. package/node_modules/pi-mcp-adapter/CHANGELOG.md +49 -0
  334. package/node_modules/pi-mcp-adapter/README.md +51 -6
  335. package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
  336. package/node_modules/pi-mcp-adapter/cli.js +188 -5
  337. package/node_modules/pi-mcp-adapter/commands.ts +128 -22
  338. package/node_modules/pi-mcp-adapter/config.ts +28 -10
  339. package/node_modules/pi-mcp-adapter/direct-tools.ts +23 -19
  340. package/node_modules/pi-mcp-adapter/index.ts +219 -32
  341. package/node_modules/pi-mcp-adapter/init.ts +36 -4
  342. package/node_modules/pi-mcp-adapter/lifecycle.ts +315 -24
  343. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +39 -6
  344. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +387 -0
  345. package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +27 -3
  346. package/node_modules/pi-mcp-adapter/mcp-code.ts +4 -0
  347. package/node_modules/pi-mcp-adapter/mcp-panel.ts +42 -13
  348. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
  349. package/node_modules/pi-mcp-adapter/metadata-cache.ts +46 -25
  350. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
  351. package/node_modules/pi-mcp-adapter/package.json +4 -1
  352. package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
  353. package/node_modules/pi-mcp-adapter/request-headers-command.ts +330 -0
  354. package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
  355. package/node_modules/pi-mcp-adapter/server-manager.ts +145 -2
  356. package/node_modules/pi-mcp-adapter/session-recovery.ts +7 -0
  357. package/node_modules/pi-mcp-adapter/tool-approval.ts +15 -2
  358. package/node_modules/pi-mcp-adapter/tool-metadata.ts +38 -18
  359. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
  360. package/node_modules/pi-mcp-adapter/types.ts +95 -10
  361. package/node_modules/pi-mcp-adapter/ui-server.ts +18 -16
  362. package/node_modules/pi-mcp-adapter/utils.ts +70 -0
  363. package/node_modules/pi-web-access/CHANGELOG.md +37 -0
  364. package/node_modules/pi-web-access/README.md +38 -14
  365. package/node_modules/pi-web-access/anysearch.ts +4 -2
  366. package/node_modules/pi-web-access/auth-fetch.ts +148 -0
  367. package/node_modules/pi-web-access/brave.ts +16 -4
  368. package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
  369. package/node_modules/pi-web-access/curator-page.ts +8 -3
  370. package/node_modules/pi-web-access/curator-server.ts +5 -1
  371. package/node_modules/pi-web-access/exa.ts +18 -7
  372. package/node_modules/pi-web-access/extract.ts +134 -37
  373. package/node_modules/pi-web-access/fetch-params.ts +17 -3
  374. package/node_modules/pi-web-access/firecrawl.ts +172 -12
  375. package/node_modules/pi-web-access/gemini-search.ts +33 -7
  376. package/node_modules/pi-web-access/index.ts +160 -104
  377. package/node_modules/pi-web-access/package.json +2 -2
  378. package/node_modules/pi-web-access/page-query.ts +4 -2
  379. package/node_modules/pi-web-access/parallel-mcp.ts +249 -0
  380. package/node_modules/pi-web-access/parallel.ts +2 -2
  381. package/node_modules/pi-web-access/query-rewrite.ts +51 -0
  382. package/node_modules/pi-web-access/serper.ts +211 -0
  383. package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
  384. package/node_modules/pi-web-access/summary-review.ts +51 -23
  385. package/node_modules/pi-web-access/tavily.ts +16 -4
  386. package/node_modules/pi-web-access/utils.ts +87 -0
  387. package/node_modules/pi-web-access/valyu.ts +199 -0
  388. package/node_modules/pi-web-access/youtube-extract.ts +2 -2
  389. package/package.json +13 -13
@@ -9,6 +9,7 @@
9
9
  * Inspired by OpenCode's formatter.ts pattern
10
10
  */
11
11
  import { logExtension } from "./extension-log.js";
12
+ import { AsyncLocalStorage } from "node:async_hooks";
12
13
  import * as fs from "node:fs/promises";
13
14
  import * as path from "node:path";
14
15
  import { BoundedLruCache } from "./bounded-cache.js";
@@ -16,47 +17,26 @@ import { normalizeMapKey } from "./path-utils.js";
16
17
  import { TERRAGRUNT_FILENAMES } from "./file-kinds.js";
17
18
  import { detectIndentation, hasDetectableIndentation, } from "./dispatch/indent-detect.js";
18
19
  import { logLatency } from "./latency-logger.js";
19
- import { findGlobalBinary } from "./package-manager.js";
20
+ import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
21
+ import { findGlobalBinary, findLocalBinUpwards } from "./package-manager.js";
20
22
  import { safeSpawnAsync } from "./safe-spawn.js";
21
23
  import { assertInstallAllowed } from "./project-trust.js";
22
- import { getAutoInstallToolIdForFormatter, getFormatterPolicyForFile, getSmartDefaultFormatterName, hasBiomeConfig, hasBlackConfig, hasClangFormatConfig, hasCljfmtConfig, hasCmakeFormatConfig, hasGoogleJavaFormatConfig, hasKtfmtConfig, hasKtlintConfig, hasNearestPackageJsonDependency, hasNearestPackageJsonField, hasOcamlformatConfig, hasOxfmtConfig, hasOxfmtSvelteConfig, hasPhpCsFixerConfig, hasPrettierConfig, hasRubocopConfig, hasRuffConfig, hasSqlfluffConfig, hasStandardrbConfig, hasStyluaConfig, hasVitePlusConfig, OXFMT_SUPPORTED_EXTENSIONS, } from "./tool-policy.js";
23
- const _lazyInstallAttempts = new Set();
24
+ import { tryLazyInstallForFormatter } from "./dispatch/runners/utils/lazy-installer.js";
25
+ import { findPSScriptAnalyzerConfigPath, getAutoInstallToolIdForFormatter, getFormatterPolicyForFile, getSmartDefaultFormatterName, hasBiomeConfig, hasBlackConfig, hasClangFormatConfig, hasCljfmtConfig, hasCmakeFormatConfig, hasCsharpierConfig, hasFantomasConfig, hasGoogleJavaFormatConfig, hasKtfmtConfig, hasKtlintConfig, hasMixFormatConfig, hasNearestPackageJsonDependency, hasNearestPackageJsonField, hasOcamlformatConfig, hasOrmoluConfig, hasOxfmtConfig, hasOxfmtSvelteConfig, hasPhpCsFixerConfig, hasPrettierConfig, hasPSScriptAnalyzerConfig, hasRubocopConfig, hasRuffConfig, hasSqlfluffConfig, hasStandardrbConfig, hasStyluaConfig, hasSwiftformatConfig, hasTaploConfig, hasTerraformConfig, hasVitePlusConfig, OXFMT_SUPPORTED_EXTENSIONS, } from "./tool-policy.js";
26
+ /**
27
+ * Lazy-install a formatter's tool, through the shared seam (#1537).
28
+ *
29
+ * This used to own a second copy of the attempt guard — `_lazyInstallAttempts`,
30
+ * a Set keyed before the install ran and never cleared — so a `gem install
31
+ * rubocop` that died on a network blip was never retried for the session. The
32
+ * state, the transient/durable classification and the retry ladder now live in
33
+ * `lazy-installer.ts`; what stays here is the one thing that is formatter
34
+ * business: a fresh binary on PATH invalidates every "not found" verdict (#1495).
35
+ */
24
36
  export async function tryLazyInstallFormatterTool(tool, cwd) {
25
- if (!assertInstallAllowed(`formatter lazy install: ${tool}`))
26
- return false;
27
- const attemptKey = `${tool}:${cwd}`;
28
- if (_lazyInstallAttempts.has(attemptKey))
29
- return false;
30
- _lazyInstallAttempts.add(attemptKey);
31
- if (tool === "rubocop") {
32
- const res = await safeSpawnAsync("gem", ["install", "rubocop", "--no-document"], {
33
- timeout: 180000,
34
- cwd,
35
- ignoreAmbientSignal: true,
36
- });
37
- const ok = !res.error && res.status === 0;
38
- if (!ok) {
39
- logExtension({
40
- subsystem: "format",
41
- message: `lazy-install rubocop failed: ${res.error?.message ?? res.stderr ?? "exit " + res.status}`,
42
- metadata: { tool: "rubocop", cwd },
43
- });
44
- }
45
- return ok;
46
- }
47
- const res = await safeSpawnAsync("rustup", ["component", "add", "rustfmt"], {
48
- timeout: 180000,
49
- cwd,
50
- ignoreAmbientSignal: true,
51
- });
52
- const ok = !res.error && res.status === 0;
53
- if (!ok) {
54
- logExtension({
55
- subsystem: "format",
56
- message: `lazy-install rustfmt failed: ${res.error?.message ?? res.stderr ?? "exit " + res.status}`,
57
- metadata: { tool: "rustfmt", cwd },
58
- });
59
- }
37
+ const ok = await tryLazyInstallForFormatter(tool, cwd);
38
+ if (ok)
39
+ resetWhichLatches();
60
40
  return ok;
61
41
  }
62
42
  // --- Types ---
@@ -95,11 +75,236 @@ async function findUp(targets, startDir, stopDir = path.parse(startDir).root) {
95
75
  }
96
76
  return found;
97
77
  }
78
+ const WHICH_BUDGET_MS = 5000;
79
+ /**
80
+ * PATH lookups, governed by the shared availability policy (#1495).
81
+ *
82
+ * `which()` is a spawn on a 5 s budget, not a filesystem check, and around a
83
+ * dozen `detect*` implementations gate on it. A transient timeout used to drop
84
+ * the formatter AND get written to `detectionCache` as an empty enabled-list —
85
+ * a cache invalidated only by a config file's mtime or size, never by time. One
86
+ * stalled `which rustfmt` therefore disabled Rust formatting for the session
87
+ * unless the user happened to edit a config file.
88
+ *
89
+ * Now: only a genuine absence latches, a stall expires on a cooldown, and the
90
+ * detection pass refuses to cache an empty result a stall caused.
91
+ */
92
+ const whichLatchByCommand = new Map();
93
+ /**
94
+ * Commands whose CURRENT `which` verdict is transient — a probe that never got a
95
+ * fair hearing, still inside its cooldown.
96
+ *
97
+ * Scoped per command on purpose (#1495 review). The first cut counted transient
98
+ * verdicts process-wide and compared the count across a detection pass, which
99
+ * over-suppressed: one stalled `which rustfmt` stopped an unrelated Python or
100
+ * shell detection from caching, and stamped `reason: "probe-timeout"` on
101
+ * selections where nothing had timed out. A command leaves the set as soon as
102
+ * its verdict becomes trustworthy — a resolved path or a genuine absence.
103
+ */
104
+ const whichTransientCommands = new Set();
105
+ /**
106
+ * The binaries `which()` was asked about inside the current `detect()` call
107
+ * (#1539).
108
+ *
109
+ * This replaces the `command[0]` approximation the poison guard used to run on.
110
+ * `command[0]` is the formatter's own binary, which is exact for most
111
+ * `detect()`s and wrong for the ones that consult a second binary — and the
112
+ * "extras are harmless" argument does not hold: `rustfmt` answering a GENUINE
113
+ * absence while `which rustup` stalls skips the lazy install and leaves nothing
114
+ * in the transient set for `command[0]` to match. Recording the actual probes
115
+ * makes the guard exact in both directions: it also stops a leftover transient
116
+ * verdict for some other pass's binary from being blamed on this decision.
117
+ *
118
+ * `AsyncLocalStorage` because `detect()` is async and two selection passes can
119
+ * be in flight at once (`tests/clients/formatters-which-latch.test.ts` runs
120
+ * exactly that); a module-level set would let one pass's probes leak into the
121
+ * other's verdict. Built eagerly — unlike `extension-log.ts`'s lazy console
122
+ * capture, this store is entered on every detection pass, so there is no window
123
+ * where paying for it is avoidable.
124
+ */
125
+ const probedCommandsStorage = new AsyncLocalStorage();
126
+ /**
127
+ * Run one candidate's `detect()` and read the per-candidate verdict back out of
128
+ * the PATH latch (#1539). `detected` is the plain boolean the caller still needs
129
+ * for selection; `verdict` is why.
130
+ */
131
+ async function detectCandidate(formatter, cwd) {
132
+ const probed = new Set();
133
+ let detected = false;
134
+ let error;
135
+ try {
136
+ detected = await probedCommandsStorage.run(probed, () => formatter.detect(cwd));
137
+ }
138
+ catch (err) {
139
+ // A `detect()` that threw still probed whatever it probed first, and those
140
+ // probes are the fact the guard needs. Swallowing them here would let a
141
+ // stall inside a throwing detection cache as a clean "not this project".
142
+ error = err;
143
+ }
144
+ const stalledCommands = [...probed].filter((command) => whichTransientCommands.has(command));
145
+ return {
146
+ detected,
147
+ ...(error !== undefined && { error }),
148
+ outcome: {
149
+ name: formatter.name,
150
+ verdict: detected
151
+ ? "available"
152
+ : stalledCommands.length > 0
153
+ ? "unreachable"
154
+ : "missing",
155
+ stalledCommands,
156
+ },
157
+ };
158
+ }
159
+ /**
160
+ * Drop the PATH verdicts, so a newly installed binary is visible at once.
161
+ *
162
+ * Module-private on purpose. Clearing the latches alone does NOT re-arm
163
+ * formatter availability: `getFormattersForFile` answers from `detectionCache`
164
+ * before it ever probes PATH, so a caller that wants a genuine re-probe must
165
+ * drop both. `clearFormatterCache` is that pair, and it is the only reset a
166
+ * session boundary should call (#1895 review round).
167
+ */
168
+ function resetWhichLatches() {
169
+ whichLatchByCommand.clear();
170
+ whichTransientCommands.clear();
171
+ cooldownRecordedForRetryAtMs.clear();
172
+ }
173
+ /**
174
+ * Last `retryAtMs` a cooldown-served record was emitted for, per command.
175
+ *
176
+ * The bound (#1539): the memo branch used to return before
177
+ * `logAvailabilityDecision`, so a formatter held off by a transient cooldown
178
+ * produced ONE record for arbitrarily many decisions — a reader counting
179
+ * `availability_decision` rows undercounted how long it had been off. Logging
180
+ * every cache hit is not an option either: this seam is consulted per save.
181
+ * One extra row per cooldown WINDOW is the compromise, and `retryAtMs` is the
182
+ * window's identity, so the ladder itself does the rate limiting.
183
+ */
184
+ const cooldownRecordedForRetryAtMs = new Map();
185
+ /**
186
+ * Record that a still-cooling verdict was served from the latch, at most once
187
+ * per cooldown window (#1539's second defect).
188
+ */
189
+ function noteCooldownServedVerdict(command, latch) {
190
+ if (latch.getOutcome() !== "transient")
191
+ return;
192
+ const retryAtMs = latch.getRetryAtMs();
193
+ if (retryAtMs <= 0)
194
+ return;
195
+ if (cooldownRecordedForRetryAtMs.get(command) === retryAtMs)
196
+ return;
197
+ cooldownRecordedForRetryAtMs.set(command, retryAtMs);
198
+ const cause = latch.getCause();
199
+ if (cause === null) {
200
+ // `getOutcome() === "transient"` is only ever set by `noteUnavailable` in
201
+ // the same call that sets `cause`, so this is unreachable today. It is
202
+ // still not a throw (#1539 review F2): this runs inside `which()`, which
203
+ // runs inside `detect()`, and the smart-default branch of the selection
204
+ // pass rethrows a detection failure — so an invariant break in a LOGGING
205
+ // helper could take down formatting for the file. Nothing is fabricated
206
+ // either (#1535's rule: a made-up cause would mislabel WHY a formatter is
207
+ // off, in the record this fix exists to make honest). The row is dropped
208
+ // and the anomaly is reported, bounded by the same once-per-window gate
209
+ // this function already passed.
210
+ logExtension({
211
+ subsystem: "format",
212
+ message: `which latch: transient outcome with no cause for ${command}; cooldown-served record dropped`,
213
+ metadata: { tool: command, retryAtMs },
214
+ });
215
+ return;
216
+ }
217
+ logAvailabilityDecision({
218
+ tool: command,
219
+ verdict: "unavailable",
220
+ outcome: "transient",
221
+ cause,
222
+ elapsedMs: 0,
223
+ latched: false,
224
+ retryAfterMs: Math.max(1, retryAtMs - Date.now()),
225
+ budgetMs: WHICH_BUDGET_MS,
226
+ servedFromCooldown: true,
227
+ });
228
+ }
98
229
  async function which(command) {
99
- const result = await safeSpawnAsync(process.platform === "win32" ? "where" : "which", [command], { timeout: 5000 });
100
- if (result.error || result.status !== 0)
101
- return null;
102
- return result.stdout?.trim().split(/\r?\n/)[0] ?? null;
230
+ probedCommandsStorage.getStore()?.add(command);
231
+ let entry = whichLatchByCommand.get(command);
232
+ if (!entry) {
233
+ entry = { latch: createAvailabilityLatch(), resolved: null };
234
+ whichLatchByCommand.set(command, entry);
235
+ }
236
+ const memo = entry.latch.read();
237
+ if (memo !== null) {
238
+ if (!memo)
239
+ noteCooldownServedVerdict(command, entry.latch);
240
+ return memo ? entry.resolved : null;
241
+ }
242
+ const stallSampler = startHostStallSampler();
243
+ const startedAt = Date.now();
244
+ const result = await safeSpawnAsync(process.platform === "win32" ? "where" : "which", [command], { timeout: WHICH_BUDGET_MS });
245
+ const hostStallMs = stallSampler.stop();
246
+ const elapsedMs = Date.now() - startedAt;
247
+ const resolved = !result.error && result.status === 0
248
+ ? (result.stdout?.trim().split(/\r?\n/)[0] ?? null)
249
+ : null;
250
+ if (resolved) {
251
+ entry.resolved = resolved;
252
+ entry.latch.noteAvailable();
253
+ whichTransientCommands.delete(command);
254
+ cooldownRecordedForRetryAtMs.delete(command);
255
+ logAvailabilityDecision({
256
+ tool: command,
257
+ verdict: "available",
258
+ outcome: "success",
259
+ cause: "ok",
260
+ elapsedMs,
261
+ latched: true,
262
+ hostStallMs,
263
+ budgetMs: WHICH_BUDGET_MS,
264
+ });
265
+ return resolved;
266
+ }
267
+ entry.resolved = null;
268
+ // A `which`/`where` that RAN and found nothing exits nonzero with NO spawn
269
+ // error, and only that is a genuine absence an install would fix.
270
+ //
271
+ // An UNSPAWNABLE prober is the opposite (#1495 review): EACCES, `spawn
272
+ // UNKNOWN`, EMFILE, an unresolvable cwd, or `where`/`which` itself missing
273
+ // says nothing about the tool that was asked for. Claiming `missing` there
274
+ // was the worst version of this bug, because the prober is shared by every
275
+ // which-gated formatter — one EACCES would have latched a dozen of them off
276
+ // for the session. So the `missing` override is granted only to a prober that
277
+ // ran, and anything that stopped it from running stays transient.
278
+ const proberRan = !result.error;
279
+ const classified = classifyProbeFailure(result, {
280
+ hostStallMs,
281
+ ...(proberRan && { unclassifiedFailureOutcome: "missing" }),
282
+ });
283
+ let { outcome, cause } = classified;
284
+ if (!proberRan && outcome !== "transient") {
285
+ outcome = "transient";
286
+ cause = "probe-rejected";
287
+ }
288
+ const retryAfterMs = entry.latch.noteUnavailable(outcome, cause);
289
+ if (outcome === "transient")
290
+ whichTransientCommands.add(command);
291
+ else
292
+ whichTransientCommands.delete(command);
293
+ // A fresh probe opens a new cooldown window (or ends the cooldown), so the
294
+ // next cache hit is entitled to its own record.
295
+ cooldownRecordedForRetryAtMs.delete(command);
296
+ logAvailabilityDecision({
297
+ tool: command,
298
+ verdict: "unavailable",
299
+ outcome,
300
+ cause,
301
+ elapsedMs,
302
+ latched: retryAfterMs === 0,
303
+ hostStallMs,
304
+ ...(retryAfterMs > 0 && { retryAfterMs }),
305
+ budgetMs: WHICH_BUDGET_MS,
306
+ });
307
+ return null;
103
308
  }
104
309
  async function resolveGoFmtBinary() {
105
310
  const inPath = await which("gofmt");
@@ -214,61 +419,85 @@ async function resolveManagedSmartDefaultCommand(formatterName, filePath, args)
214
419
  return null;
215
420
  return [installed, ...args, filePath];
216
421
  }
217
- function hasExplicitFormatterConfig(formatterName, cwd, ext) {
218
- switch (formatterName) {
219
- case "biome":
220
- return hasBiomeConfig(cwd);
221
- case "prettier":
222
- return (hasPrettierConfig(cwd) || hasNearestPackageJsonField(cwd, "prettier"));
223
- case "oxfmt":
224
- // .svelte is conditional beyond "an oxfmt config exists": oxfmt
225
- // requires the `svelte` package installed AND the config's `svelte`
226
- // flag enabled (verified empirically — see hasOxfmtSvelteConfig).
227
- // The generic checks below are NOT sufficient for .svelte — an
228
- // oxfmt.toml with no svelte flag, or an oxfmt dependency alone,
229
- // both fail at runtime for .svelte specifically (other extensions
230
- // are unaffected by this stricter gate).
231
- if (ext === ".svelte") {
232
- return hasOxfmtSvelteConfig(cwd);
233
- }
234
- return (hasOxfmtConfig(cwd) ||
422
+ /**
423
+ * One entry per formatter that can be selected via explicit project config
424
+ * (the `formatterPolicy` "explicit-config" branch of `getFormattersForFile`).
425
+ * A formatter with NO entry here can never win that branch — the switch this
426
+ * table replaced had exactly that failure mode for `psscriptanalyzer-format`
427
+ * (#1572: `.ps1`'s policy sets `defaultWhenUnconfigured: false` AND the
428
+ * formatter had no config check, so neither selection branch could ever pick
429
+ * it).
430
+ *
431
+ * This table IS the source of truth for "which formatters have an explicit-
432
+ * config check" — `FORMATTERS_WITH_EXPLICIT_CONFIG_CHECK` below derives its
433
+ * membership from these keys rather than hand-listing them a second time, so
434
+ * the coverage guard in formatter-policy-consistency.test.ts (#1572) can never
435
+ * drift from what this function actually does.
436
+ */
437
+ // A `Map`, not an object literal: an object literal's lookup inherits
438
+ // `Object.prototype`, so `checks["toString"]` or `checks["constructor"]`
439
+ // resolves to a real (non-formatter) function instead of `undefined` —
440
+ // a formatter genuinely NAMED one of those prototype properties would read
441
+ // as "has an explicit-config check" when it has none (review finding, #1572).
442
+ // `Map.prototype.get` has no such inherited-key hazard.
443
+ const EXPLICIT_FORMATTER_CONFIG_CHECKS = new Map([
444
+ ["biome", (cwd) => hasBiomeConfig(cwd)],
445
+ [
446
+ "prettier",
447
+ (cwd) => hasPrettierConfig(cwd) || hasNearestPackageJsonField(cwd, "prettier"),
448
+ ],
449
+ // .svelte is conditional beyond "an oxfmt config exists": oxfmt requires the
450
+ // `svelte` package installed AND the config's `svelte` flag enabled
451
+ // (verified empirically — see hasOxfmtSvelteConfig). The generic checks
452
+ // below are NOT sufficient for .svelte — an oxfmt.toml with no svelte flag,
453
+ // or an oxfmt dependency alone, both fail at runtime for .svelte
454
+ // specifically (other extensions are unaffected by this stricter gate).
455
+ [
456
+ "oxfmt",
457
+ (cwd, ext) => ext === ".svelte"
458
+ ? hasOxfmtSvelteConfig(cwd)
459
+ : hasOxfmtConfig(cwd) ||
235
460
  hasVitePlusConfig(cwd) ||
236
461
  // The published package is `oxfmt`; `@oxc-project/oxfmt` does not
237
462
  // exist on npm. Accept both (scoped kept for forward-compat).
238
463
  hasNearestPackageJsonDependency(cwd, "oxfmt") ||
239
- hasNearestPackageJsonDependency(cwd, "@oxc-project/oxfmt"));
240
- case "ruff":
241
- return hasRuffConfig(cwd);
242
- case "black":
243
- return hasBlackConfig(cwd);
244
- case "sqlfluff":
245
- return hasSqlfluffConfig(cwd);
246
- case "rubocop":
247
- return hasRubocopConfig(cwd);
248
- case "standardrb":
249
- return hasStandardrbConfig(cwd);
250
- case "clang-format":
251
- return hasClangFormatConfig(cwd);
252
- case "php-cs-fixer":
253
- return hasPhpCsFixerConfig(cwd);
254
- case "stylua":
255
- return hasStyluaConfig(cwd);
256
- case "ocamlformat":
257
- return hasOcamlformatConfig(cwd);
258
- case "google-java-format":
259
- return hasGoogleJavaFormatConfig(cwd);
260
- case "ktfmt":
261
- return hasKtfmtConfig(cwd);
262
- case "ktlint":
263
- return hasKtlintConfig(cwd);
264
- case "cljfmt":
265
- return hasCljfmtConfig(cwd);
266
- case "cmake-format":
267
- return hasCmakeFormatConfig(cwd);
268
- default:
269
- return false;
270
- }
464
+ hasNearestPackageJsonDependency(cwd, "@oxc-project/oxfmt"),
465
+ ],
466
+ ["ruff", (cwd) => hasRuffConfig(cwd)],
467
+ ["black", (cwd) => hasBlackConfig(cwd)],
468
+ ["sqlfluff", (cwd) => hasSqlfluffConfig(cwd)],
469
+ ["rubocop", (cwd) => hasRubocopConfig(cwd)],
470
+ ["standardrb", (cwd) => hasStandardrbConfig(cwd)],
471
+ ["clang-format", (cwd) => hasClangFormatConfig(cwd)],
472
+ ["php-cs-fixer", (cwd) => hasPhpCsFixerConfig(cwd)],
473
+ ["stylua", (cwd) => hasStyluaConfig(cwd)],
474
+ ["ocamlformat", (cwd) => hasOcamlformatConfig(cwd)],
475
+ ["google-java-format", (cwd) => hasGoogleJavaFormatConfig(cwd)],
476
+ ["ktfmt", (cwd) => hasKtfmtConfig(cwd)],
477
+ ["ktlint", (cwd) => hasKtlintConfig(cwd)],
478
+ ["cljfmt", (cwd) => hasCljfmtConfig(cwd)],
479
+ ["cmake-format", (cwd) => hasCmakeFormatConfig(cwd)],
480
+ ["psscriptanalyzer-format", (cwd) => hasPSScriptAnalyzerConfig(cwd)],
481
+ // #1595 sweep — see the comment above hasCsharpierConfig et al. in
482
+ // tool-policy.ts for why nixfmt (the 8th formatter #1572 flagged) is NOT
483
+ // wired here: it has no config-file convention and no manifest-marker
484
+ // equivalent to `.terraform.lock.hcl`, so there is no honest opt-in signal.
485
+ ["csharpier", (cwd) => hasCsharpierConfig(cwd)],
486
+ ["ormolu", (cwd) => hasOrmoluConfig(cwd)],
487
+ ["taplo", (cwd) => hasTaploConfig(cwd)],
488
+ ["terraform", (cwd) => hasTerraformConfig(cwd)],
489
+ ["swiftformat", (cwd) => hasSwiftformatConfig(cwd)],
490
+ ["fantomas", (cwd) => hasFantomasConfig(cwd)],
491
+ ["mix", (cwd) => hasMixFormatConfig(cwd)],
492
+ ]);
493
+ function hasExplicitFormatterConfig(formatterName, cwd, ext) {
494
+ return EXPLICIT_FORMATTER_CONFIG_CHECKS.get(formatterName)?.(cwd, ext) ?? false;
271
495
  }
496
+ // Exported for the "every registered formatter is selectable" coverage guard
497
+ // (formatter-policy-consistency.test.ts, #1572): derived from the table above,
498
+ // not hand-listed, so it cannot drift from what `hasExplicitFormatterConfig`
499
+ // actually checks.
500
+ export const FORMATTERS_WITH_EXPLICIT_CONFIG_CHECK = new Set(EXPLICIT_FORMATTER_CONFIG_CHECKS.keys());
272
501
  // --- Formatter Definitions ---
273
502
  async function hasEditorConfig(cwd) {
274
503
  try {
@@ -851,8 +1080,17 @@ export const styluaFormatter = {
851
1080
  name: "stylua",
852
1081
  command: ["stylua", "$FILE"],
853
1082
  extensions: [".lua"],
1083
+ async resolveCommand(filePath, cwd) {
1084
+ // Project binary first (#1731, discipline B): stylua has no pi-lens
1085
+ // managed install, so before this the ONLY resolution was a bare
1086
+ // `stylua` PATH lookup — a project-local install via npm
1087
+ // `@johnnymorganz/stylua` (`node_modules/.bin/stylua`) was invisible.
1088
+ const local = findLocalBinUpwards("stylua", cwd);
1089
+ return local ? [local, filePath] : null;
1090
+ },
854
1091
  async detect(cwd) {
855
- if ((await which("stylua")) === null)
1092
+ const local = findLocalBinUpwards("stylua", cwd);
1093
+ if (!local && (await which("stylua")) === null)
856
1094
  return false;
857
1095
  // Prefer explicit config but also run if binary is present in a Lua project
858
1096
  const configs = ["stylua.toml", ".stylua.toml"];
@@ -915,6 +1153,18 @@ export const cmakeFormatFormatter = {
915
1153
  return hasCmakeFormatConfig(cwd);
916
1154
  },
917
1155
  };
1156
+ export const cueFormatter = {
1157
+ name: "cue",
1158
+ // `cue fmt <file>` rewrites in place already. There is no `-w`: the only
1159
+ // flags are --check, -d/--diff and --files, so `cue fmt -w` aborts with
1160
+ // "unknown shorthand flag: 'w'" on every .cue write (verified on cue
1161
+ // v0.17.1).
1162
+ command: ["cue", "fmt", "$FILE"],
1163
+ extensions: [".cue"],
1164
+ async detect(_cwd) {
1165
+ return (await which("cue")) !== null;
1166
+ },
1167
+ };
918
1168
  /**
919
1169
  * The PowerShell one-liner behind `psscriptanalyzer-format`.
920
1170
  *
@@ -930,16 +1180,25 @@ export const cmakeFormatFormatter = {
930
1180
  * - single-quoted interpolation broke on any path containing an apostrophe.
931
1181
  * An empty/whitespace-only file exits 0 without touching anything, so "nothing
932
1182
  * to format" stays a clean no-op rather than a reported failure.
1183
+ *
1184
+ * `settingsPath`, when the project has one (#1572 review F2), is passed
1185
+ * straight to `Invoke-Formatter -Settings`. Gating selection on the file's
1186
+ * presence without ever reading its rules would run the project through the
1187
+ * stock `CodeFormatting` ruleset regardless of what it declared — the same
1188
+ * stock-style imposition #1144 banned for the other config-first formatters.
933
1189
  */
934
- function psScriptAnalyzerCommand(filePath) {
1190
+ function psScriptAnalyzerCommand(filePath, settingsPath) {
935
1191
  // PowerShell single-quoted strings escape an apostrophe by doubling it.
936
1192
  const quoted = filePath.replace(/'/g, "''");
1193
+ const settingsArg = settingsPath
1194
+ ? ` -Settings '${settingsPath.replace(/'/g, "''")}'`
1195
+ : "";
937
1196
  return [
938
1197
  "$ErrorActionPreference = 'Stop'",
939
1198
  `$p = '${quoted}'`,
940
1199
  "$content = Get-Content -Raw -LiteralPath $p",
941
1200
  "if ([string]::IsNullOrWhiteSpace($content)) { exit 0 }",
942
- "$formatted = Invoke-Formatter -ScriptDefinition $content",
1201
+ `$formatted = Invoke-Formatter -ScriptDefinition $content${settingsArg}`,
943
1202
  "if ($null -eq $formatted) { throw 'Invoke-Formatter returned no output' }",
944
1203
  "Set-Content -LiteralPath $p -Value $formatted",
945
1204
  ].join("; ");
@@ -948,11 +1207,17 @@ export const psscriptanalyzerFormatFormatter = {
948
1207
  name: "psscriptanalyzer-format",
949
1208
  command: ["pwsh", "-NoProfile", "-Command", psScriptAnalyzerCommand("$FILE")],
950
1209
  extensions: [".ps1", ".psm1", ".psd1"],
951
- async resolveCommand(filePath, _cwd) {
1210
+ async resolveCommand(filePath, cwd) {
952
1211
  const pwsh = (await which("pwsh")) ?? (await which("powershell"));
953
1212
  if (!pwsh)
954
1213
  return null;
955
- return [pwsh, "-NoProfile", "-Command", psScriptAnalyzerCommand(filePath)];
1214
+ const settingsPath = findPSScriptAnalyzerConfigPath(cwd);
1215
+ return [
1216
+ pwsh,
1217
+ "-NoProfile",
1218
+ "-Command",
1219
+ psScriptAnalyzerCommand(filePath, settingsPath),
1220
+ ];
956
1221
  },
957
1222
  async detect(_cwd) {
958
1223
  const pwsh = (await which("pwsh")) ?? (await which("powershell"));
@@ -1005,6 +1270,7 @@ export const ALL_FORMATTERS = [
1005
1270
  googleJavaFormatFormatter,
1006
1271
  cljfmtFormatter,
1007
1272
  cmakeFormatFormatter,
1273
+ cueFormatter,
1008
1274
  psscriptanalyzerFormatFormatter,
1009
1275
  ];
1010
1276
  // Basenames claimed by a filename-keyed formatter, e.g. terragrunt.hcl.
@@ -1013,17 +1279,35 @@ const FILENAME_FORMATTER_BASENAMES = new Set(ALL_FORMATTERS.flatMap((f) => f.fil
1013
1279
  const detectionCache = new BoundedLruCache(32);
1014
1280
  // These are the formatter configuration files consulted by the policy helpers
1015
1281
  // above. Their metadata is part of detection cache identity: changing a file
1016
- // must re-run detection even when PATH and installed tools are unchanged.
1282
+ // must re-run detection even when PATH and installed tools are unchanged. A
1283
+ // filename a `has*Config` check reads but this list omits is invisible to the
1284
+ // cache: the signature never moves when that file is added, so a project that
1285
+ // opts in AFTER the first `getFormattersForFile` call for its cwd keeps
1286
+ // getting the stale (pre-opt-in) cached answer for the rest of the session
1287
+ // (#1572 review F1 — proved for psscriptanalyzer-format's settings file;
1288
+ // swept against every `EXPLICIT_FORMATTER_CONFIG_CHECKS` entry below, which
1289
+ // turned up the same gap for google-java-format, cljfmt, cmake-format, the
1290
+ // Kotlin/Spotless gradle files, sqlfluff's setup.cfg, and oxfmt's
1291
+ // vite-plus.json / additional vite.config extensions).
1017
1292
  const FORMATTER_CONFIG_FILES = [
1018
1293
  "package.json", "biome.json", "biome.jsonc", ".prettierrc", ".prettierrc.json",
1019
1294
  ".prettierrc.yml", ".prettierrc.yaml", ".prettierrc.js", ".prettierrc.cjs",
1020
1295
  ".prettierrc.mjs", "prettier.config.js", "prettier.config.cjs", "prettier.config.mjs",
1021
1296
  "prettier.config.ts", "pyproject.toml", "ruff.toml", ".ruff.toml", "black.toml",
1022
- ".black", "tox.ini", "requirements.txt", "Pipfile", ".sqlfluff", ".rubocop.yml", ".rubocop.yaml", "Gemfile", ".clang-format",
1297
+ ".black", "tox.ini", "setup.cfg", "requirements.txt", "Pipfile", ".sqlfluff", ".rubocop.yml", ".rubocop.yaml", "Gemfile", ".clang-format",
1023
1298
  "_clang-format", ".php-cs-fixer.php", ".php-cs-fixer.dist.php", "stylua.toml",
1024
- ".stylua.toml", ".ocamlformat", ".editorconfig", ".ktfmt", ".ktfmt.kts",
1025
- ".cljfmt.edn", "cmake-format.py", ".cmake-format.yaml", ".cmake-format.json",
1026
- "oxfmt.toml", ".oxfmtrc.json", "vite.config.ts", "vite.config.js", "vite.config.mjs",
1299
+ ".stylua.toml", ".ocamlformat", ".editorconfig", ".google-java-format",
1300
+ ".ktfmt", ".ktfmt.kts", "build.gradle.kts", "build.gradle", "settings.gradle.kts", "settings.gradle",
1301
+ ".cljfmt.edn", "cljfmt.edn", ".cljfmt",
1302
+ ".cmake-format", ".cmake-format.yaml", ".cmake-format.yml", ".cmake-format.json", ".cmake-format.py",
1303
+ "cmake-format.py", "cmake-format.yaml", "cmake-format.yml",
1304
+ "oxfmt.toml", ".oxfmtrc.json", "vite-plus.json",
1305
+ "vite.config.ts", "vite.config.mts", "vite.config.cts", "vite.config.js", "vite.config.mjs", "vite.config.cjs",
1306
+ "PSScriptAnalyzerSettings.psd1", "ScriptAnalyzerSettings.psd1",
1307
+ // #1595 sweep additions.
1308
+ ".csharpierrc", ".csharpierrc.json", ".csharpierrc.yaml", ".csharpierrc.yml",
1309
+ ".ormolu", "taplo.toml", ".taplo.toml", ".terraform.lock.hcl", ".swiftformat",
1310
+ ".fantomasignore", ".formatter.exs",
1027
1311
  ];
1028
1312
  async function formatterConfigSignature(cwd) {
1029
1313
  const paths = await findUp(FORMATTER_CONFIG_FILES, cwd);
@@ -1054,9 +1338,16 @@ export async function getFormattersForFile(filePath, cwd) {
1054
1338
  const configSignature = await formatterConfigSignature(cwd);
1055
1339
  // Check cache
1056
1340
  let cached = detectionCache.get(normalizedCwd);
1057
- if (cached?.signature !== configSignature) {
1058
- cached = undefined;
1341
+ if (cached && cached.signature !== configSignature) {
1342
+ // An EXISTING entry whose signature moved is the user telling us the world
1343
+ // changed. Re-probe PATH too, so "install the tool, touch the config" still
1344
+ // works within one session now that a durable absence latches (#1495).
1345
+ // Scoped to a real change on purpose: keying this off "no entry yet" would
1346
+ // drop every PATH verdict on the first save in each new directory, which in
1347
+ // a monorepo means re-probing forever.
1059
1348
  detectionCache.delete(normalizedCwd);
1349
+ cached = undefined;
1350
+ resetWhichLatches();
1060
1351
  }
1061
1352
  if (!cached) {
1062
1353
  cached = { signature: configSignature, entries: new Map() };
@@ -1077,6 +1368,13 @@ export async function getFormattersForFile(filePath, cwd) {
1077
1368
  ? matching.filter((f) => formatterPolicy.formatterNames.includes(f.name))
1078
1369
  : matching;
1079
1370
  let selected;
1371
+ /**
1372
+ * One entry per candidate this pass actually ASKED, in the order it asked
1373
+ * them (#1539). Candidates eliminated without a probe — an explicit-config
1374
+ * decision is pure filesystem — contribute nothing, which is the point: a
1375
+ * decision that never probed cannot have been degraded by a probe.
1376
+ */
1377
+ const candidateOutcomes = [];
1080
1378
  if (formatterPolicy) {
1081
1379
  const explicitlyConfigured = candidateFormatters.filter((formatter) => hasExplicitFormatterConfig(formatter.name, cwd, ext));
1082
1380
  if (explicitlyConfigured.length > 0) {
@@ -1093,33 +1391,93 @@ export async function getFormattersForFile(filePath, cwd) {
1093
1391
  const smartDefaultFormatter = candidateFormatters.find((f) => f.name === smartDefaultFormatterName);
1094
1392
  if (smartDefaultFormatter) {
1095
1393
  const autoInstallToolId = getAutoInstallToolIdForFormatter(smartDefaultFormatter.name);
1096
- if (autoInstallToolId || (await smartDefaultFormatter.detect(cwd))) {
1394
+ if (autoInstallToolId) {
1097
1395
  selected = smartDefaultFormatter;
1098
1396
  }
1397
+ else {
1398
+ const { detected, error, outcome } = await detectCandidate(smartDefaultFormatter, cwd);
1399
+ candidateOutcomes.push(outcome);
1400
+ // This branch never caught a throwing detection, and still
1401
+ // does not: `detectCandidate` catches only so the probes survive.
1402
+ if (error !== undefined)
1403
+ throw error;
1404
+ if (detected)
1405
+ selected = smartDefaultFormatter;
1406
+ }
1099
1407
  }
1100
1408
  }
1101
1409
  }
1102
1410
  else {
1103
1411
  for (const formatter of candidateFormatters) {
1104
- try {
1105
- if (await formatter.detect(cwd)) {
1106
- selected = formatter;
1107
- break;
1108
- }
1109
- }
1110
- catch (err) {
1412
+ const { detected, error, outcome } = await detectCandidate(formatter, cwd);
1413
+ candidateOutcomes.push(outcome);
1414
+ if (error !== undefined) {
1111
1415
  // pi-lens-ignore: missing-error-propagation — optional formatter detection, skip on failure
1112
1416
  logExtension({
1113
1417
  subsystem: "format",
1114
- message: `Detection failed for ${formatter.name}: ${err instanceof Error ? err.message : String(err)}`,
1418
+ message: `Detection failed for ${formatter.name}: ${error instanceof Error ? error.message : String(error)}`,
1115
1419
  metadata: { formatter: formatter.name, cwd },
1116
1420
  });
1421
+ continue;
1422
+ }
1423
+ if (detected) {
1424
+ selected = formatter;
1425
+ break;
1117
1426
  }
1118
1427
  }
1119
1428
  }
1120
1429
  const enabled = selected ? [selected] : [];
1430
+ // The #925/#1467 `wouldPoisonCache` shape: an empty result produced while a
1431
+ // PATH probe was timing out is not a finding about this project. Caching it
1432
+ // would survive until a config file's mtime or size changed, so leave the
1433
+ // cache untouched and let the next turn re-detect.
1434
+ // Which of THIS file's candidates are currently un-answered? Read off the
1435
+ // binaries the candidates' own `detect()`s actually probed (#1539), so a
1436
+ // stalled `which rustfmt` cannot stop a shell or Python detection from
1437
+ // caching (#1495 review) AND a leftover transient verdict for a binary this
1438
+ // pass never consulted cannot be blamed on this decision.
1439
+ //
1440
+ // This replaces the `command[0]` approximation, which missed both shapes of
1441
+ // extra probe: a co-equal ALTERNATIVE (`pwsh` ?? `powershell`) and — the
1442
+ // case the old comment wrongly called harmless — an install FALLBACK reached
1443
+ // after the primary answered a GENUINE absence. `rustfmt` missing plus a
1444
+ // stalled `which rustup` skips the lazy install, and nothing named `rustfmt`
1445
+ // is transient, so the empty result used to cache for the session.
1446
+ //
1447
+ // Residual: the record covers this module's own `which()` only. A detection
1448
+ // that reaches PATH some other way is invisible to it — ktlint's `detect()`
1449
+ // falls back to the installer's `getToolPath("ktlint")`, which has its own
1450
+ // probe and its own 24-hour cache. Widening the record to those is deferred to
1451
+ // the installer-side follow-up rather than bolted on here.
1452
+ const stalledCandidateCommands = [
1453
+ ...new Set(candidateOutcomes.flatMap((outcome) => outcome.stalledCommands)),
1454
+ ];
1455
+ const poisonedByTransientProbe = enabled.length === 0 && stalledCandidateCommands.length > 0;
1456
+ // A NON-empty result the poison guard cannot see (#1539): the winner won only
1457
+ // because a candidate ahead of it was never asked. `candidateOutcomes` is in
1458
+ // ask order and the loop stops at the winner, so every entry before the
1459
+ // winner's is a candidate that lost — and an `unreachable` one did not lose
1460
+ // on the merits. Caching this would hand the session to the runner-up until
1461
+ // a config file's mtime or size changed.
1462
+ const winnerIndex = selected
1463
+ ? candidateOutcomes.findIndex((outcome) => outcome.name === selected.name)
1464
+ : -1;
1465
+ const unreachablePreferred = candidateOutcomes
1466
+ .slice(0, winnerIndex === -1 ? 0 : winnerIndex)
1467
+ .filter((outcome) => outcome.verdict === "unreachable")
1468
+ .map((outcome) => outcome.name);
1469
+ const degradedSelection = unreachablePreferred.length > 0;
1121
1470
  let selectionReason;
1122
- if (!selected) {
1471
+ if (poisonedByTransientProbe) {
1472
+ selectionReason = "probe-timeout";
1473
+ }
1474
+ else if (degradedSelection) {
1475
+ // The reason now describes how the winner WON, not just what the config
1476
+ // looks like. "explicit-config" on a selection whose preferred candidate
1477
+ // was never reachable was the most misleading record in this seam.
1478
+ selectionReason = "preferred-unreachable";
1479
+ }
1480
+ else if (!selected) {
1123
1481
  selectionReason = "none";
1124
1482
  }
1125
1483
  else if (!formatterPolicy) {
@@ -1130,6 +1488,7 @@ export async function getFormattersForFile(filePath, cwd) {
1130
1488
  ? "explicit-config"
1131
1489
  : "smart-default";
1132
1490
  }
1491
+ const provisional = poisonedByTransientProbe || degradedSelection;
1133
1492
  logLatency({
1134
1493
  type: "phase",
1135
1494
  phase: "formatter_selected",
@@ -1139,19 +1498,45 @@ export async function getFormattersForFile(filePath, cwd) {
1139
1498
  formatter: selected?.name ?? null,
1140
1499
  reason: selectionReason,
1141
1500
  cwd,
1501
+ ...(provisional && {
1502
+ cached: false,
1503
+ stalledProbes: stalledCandidateCommands,
1504
+ }),
1505
+ ...(degradedSelection && { unreachablePreferred }),
1142
1506
  },
1143
1507
  });
1508
+ // Provisional either way: not cached, so the next pass re-detects once the
1509
+ // cooldown expires and the preferred formatter's recovery takes effect
1510
+ // without waiting on a config-file edit.
1511
+ if (provisional)
1512
+ return enabled;
1144
1513
  // Store the list of enabled formatter names in cache
1145
1514
  const enabledNames = enabled.map((f) => f.name);
1146
1515
  cached.entries.set(cacheKey, enabledNames);
1147
1516
  return enabled;
1148
1517
  }
1518
+ /**
1519
+ * Re-arm formatter availability: drop both the per-cwd selection cache and the
1520
+ * PATH latches.
1521
+ *
1522
+ * #1895: this is the session-boundary reset. Both halves are required. The
1523
+ * latches decide whether `which` runs at all, but `detectionCache` short-
1524
+ * circuits ahead of them — a same-cwd lookup returns the previous verdict's
1525
+ * formatter names without reaching a probe. Clearing only the latches
1526
+ * therefore re-arms every directory EXCEPT the one the user is working in.
1527
+ */
1149
1528
  export function clearFormatterCache() {
1150
1529
  detectionCache.clear();
1530
+ resetWhichLatches();
1151
1531
  }
1152
1532
  export function clearFormatterRuntimeState() {
1153
1533
  detectionCache.clear();
1154
- _lazyInstallAttempts.clear();
1534
+ resetWhichLatches();
1535
+ // NO `resetLazyInstallAttempts()` here (#1537 review F1). This function runs
1536
+ // from `resetFormatService()`, which `handleTurnEnd` calls every turn — so
1537
+ // clearing the lazy-install hold here made "held for the session" mean "held
1538
+ // for a turn", and a failing install re-ran every turn. The hold's only reset
1539
+ // is `session_start`'s block in runtime-session.ts.
1155
1540
  }
1156
1541
  // ESC is built via fromCharCode so no raw control byte sits in the source.
1157
1542
  const ANSI_ESCAPE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "g");