@herbertgao/pi-extensions 2026.9.1 → 2026.9.3

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 (341) hide show
  1. package/README.md +3 -2
  2. package/THIRD_PARTY_NOTICES.md +24 -0
  3. package/node_modules/@herbertgao/sol-pi/LICENSE +19 -0
  4. package/node_modules/@herbertgao/sol-pi/README.md +159 -0
  5. package/node_modules/@herbertgao/sol-pi/SECURITY.md +26 -0
  6. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +19 -0
  7. package/node_modules/@herbertgao/sol-pi/agents-install.md +150 -0
  8. package/node_modules/@herbertgao/sol-pi/assets/sol-pi-hero.png +0 -0
  9. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +69 -0
  10. package/node_modules/@herbertgao/sol-pi/docs/configuration.md +75 -0
  11. package/node_modules/@herbertgao/sol-pi/package.json +76 -0
  12. package/node_modules/@herbertgao/sol-pi/scripts/check-pi-compat.mjs +32 -0
  13. package/node_modules/@herbertgao/sol-pi/scripts/check-sol-pi-config.mjs +120 -0
  14. package/node_modules/@herbertgao/sol-pi/sol-pi.example.json +10 -0
  15. package/node_modules/@herbertgao/sol-pi/src/sol-pi/config.ts +135 -0
  16. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/file-queue.ts +74 -0
  17. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +185 -0
  18. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/then-run.ts +128 -0
  19. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/archive.ts +53 -0
  20. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/candidate.ts +101 -0
  21. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/config.ts +71 -0
  22. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/index.ts +220 -0
  23. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/journal.ts +25 -0
  24. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/provider.ts +164 -0
  25. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/receipt.ts +177 -0
  26. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/index.ts +227 -0
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/ledger.ts +20 -0
  28. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/observation.ts +252 -0
  29. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +237 -0
  30. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +455 -0
  31. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/index.ts +49 -0
  32. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/plan.ts +79 -0
  33. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +208 -0
  34. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/tools.ts +100 -0
  35. package/node_modules/@herbertgao/sol-pi/src/sol-pi/index.ts +42 -0
  36. package/node_modules/@herbertgao/sol-pi/src/sol-pi/runtime-paths.ts +17 -0
  37. package/node_modules/@herbertgao/sol-pi/src/sol-pi/tui.ts +71 -0
  38. package/node_modules/@narumitw/pi-btw/README.md +57 -141
  39. package/node_modules/@narumitw/pi-btw/dist/index.ts +478 -43
  40. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  41. package/node_modules/@narumitw/pi-btw/docs/workflows.md +74 -0
  42. package/node_modules/@narumitw/pi-btw/package.json +2 -1
  43. package/node_modules/@narumitw/pi-btw/src/btw.ts +28 -3
  44. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +32 -2
  45. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +330 -0
  46. package/node_modules/@narumitw/pi-btw/src/menu.ts +136 -19
  47. package/node_modules/@narumitw/pi-btw/src/settings.ts +32 -0
  48. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +57 -5
  49. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +52 -30
  50. package/node_modules/pi-lens/CHANGELOG.md +260 -0
  51. package/node_modules/pi-lens/README.md +13 -8
  52. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -1
  53. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -7
  54. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +1 -1
  55. package/node_modules/pi-lens/dist/clients/analysed-root.js +1 -0
  56. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +1 -1
  57. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +1 -8
  58. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +2 -0
  59. package/node_modules/pi-lens/dist/clients/bash-file-access.js +133 -1
  60. package/node_modules/pi-lens/dist/clients/biome-client.js +13 -2
  61. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -6
  62. package/node_modules/pi-lens/dist/clients/bootstrap.js +4 -4
  63. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +1 -1
  64. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +2 -5
  65. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +1 -9
  66. package/node_modules/pi-lens/dist/clients/cache-observability.js +122 -6
  67. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +2 -2
  68. package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -7
  69. package/node_modules/pi-lens/dist/clients/child-unref.js +1 -19
  70. package/node_modules/pi-lens/dist/clients/complexity-client.js +5 -0
  71. package/node_modules/pi-lens/dist/clients/config-core/index.js +11 -8
  72. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +1 -1
  73. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +2 -9
  74. package/node_modules/pi-lens/dist/clients/config-core/records.js +1 -1
  75. package/node_modules/pi-lens/dist/clients/config-core/schema.js +3 -5
  76. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +3 -1
  77. package/node_modules/pi-lens/dist/clients/config-resolve.js +10 -7
  78. package/node_modules/pi-lens/dist/clients/config-schema.js +56 -2
  79. package/node_modules/pi-lens/dist/clients/config-warn.js +2 -2
  80. package/node_modules/pi-lens/dist/clients/dead-code-client.js +8 -1
  81. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -4
  82. package/node_modules/pi-lens/dist/clients/dependency-checker.js +5 -1
  83. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +1 -1
  84. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +1 -1
  85. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -4
  86. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +38 -41
  87. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +1 -1
  88. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +5 -1
  89. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +18 -1
  90. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -6
  91. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +1 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -3
  93. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +2 -2
  94. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +1 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +2 -1
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +3 -1
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +2 -3
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +7 -2
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +2 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +2 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +2 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +2 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +2 -1
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +2 -1
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +2 -1
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +2 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +2 -1
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +2 -1
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +2 -1
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +3 -1
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +3 -2
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +2 -1
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +2 -1
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +2 -1
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +19 -1
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +3 -2
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +3 -1
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +2 -1
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +2 -1
  122. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +2 -1
  123. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +15 -11
  124. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +2 -1
  125. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +2 -1
  126. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +3 -3
  127. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +12 -11
  128. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +3 -2
  129. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -1
  130. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +5 -3
  131. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  132. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +3 -2
  133. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +3 -2
  134. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +2 -1
  135. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  136. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +2 -1
  137. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +2 -1
  138. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +2 -1
  139. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
  140. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
  141. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +1 -64
  142. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
  143. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -21
  144. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +2 -1
  145. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +2 -20
  146. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +5 -3
  147. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +2 -1
  148. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +2 -2
  149. package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -7
  150. package/node_modules/pi-lens/dist/clients/effective-config.js +12 -2
  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 -0
  153. package/node_modules/pi-lens/dist/clients/file-kinds.js +1 -38
  154. package/node_modules/pi-lens/dist/clients/file-role.js +18 -1
  155. package/node_modules/pi-lens/dist/clients/file-time.js +1 -1
  156. package/node_modules/pi-lens/dist/clients/file-utils.js +5 -1
  157. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +4 -0
  158. package/node_modules/pi-lens/dist/clients/format-service.js +0 -1
  159. package/node_modules/pi-lens/dist/clients/formatters.js +66 -93
  160. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +3 -4
  161. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -0
  162. package/node_modules/pi-lens/dist/clients/git-guard.js +5 -0
  163. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +42 -6
  164. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +48 -7
  165. package/node_modules/pi-lens/dist/clients/grammar-source.js +3 -3
  166. package/node_modules/pi-lens/dist/clients/installer/index.js +416 -61
  167. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +84 -18
  168. package/node_modules/pi-lens/dist/clients/instance-reaper.js +6 -6
  169. package/node_modules/pi-lens/dist/clients/jscpd-client.js +9 -1
  170. package/node_modules/pi-lens/dist/clients/knip-client.js +9 -2
  171. package/node_modules/pi-lens/dist/clients/language-profile.js +12 -11
  172. package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
  173. package/node_modules/pi-lens/dist/clients/latency-logger.js +7 -7
  174. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -1
  175. package/node_modules/pi-lens/dist/clients/lens-config.js +23 -0
  176. package/node_modules/pi-lens/dist/clients/lens-engine.js +3 -4
  177. package/node_modules/pi-lens/dist/clients/lens-events.js +1 -1
  178. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +18 -0
  179. package/node_modules/pi-lens/dist/clients/lens-map.js +1 -1
  180. package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -36
  181. package/node_modules/pi-lens/dist/clients/lsp/client.js +4 -3
  182. package/node_modules/pi-lens/dist/clients/lsp/config.js +37 -7
  183. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +24 -1
  184. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +79 -1
  185. package/node_modules/pi-lens/dist/clients/lsp/edits.js +3 -3
  186. package/node_modules/pi-lens/dist/clients/lsp/index.js +491 -64
  187. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +1 -1
  188. package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -8
  189. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  190. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -2
  191. package/node_modules/pi-lens/dist/clients/lsp/server.js +62 -15
  192. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -1
  193. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +5 -5
  194. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +6 -0
  195. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +2 -1
  196. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +37 -4
  197. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +1 -1
  198. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +3 -0
  199. package/node_modules/pi-lens/dist/clients/mcp/session.js +2 -0
  200. package/node_modules/pi-lens/dist/clients/metrics-history.js +28 -107
  201. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +1 -1
  202. package/node_modules/pi-lens/dist/clients/module-report.js +1 -1
  203. package/node_modules/pi-lens/dist/clients/mutating-tool.js +3 -3
  204. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +3 -3
  205. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -4
  206. package/node_modules/pi-lens/dist/clients/observed-mutation.js +9 -9
  207. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +2 -6
  208. package/node_modules/pi-lens/dist/clients/opengrep-client.js +2 -0
  209. package/node_modules/pi-lens/dist/clients/opengrep-config.js +1 -1
  210. package/node_modules/pi-lens/dist/clients/package-manager.js +2 -1
  211. package/node_modules/pi-lens/dist/clients/path-utils.js +23 -2
  212. package/node_modules/pi-lens/dist/clients/performance-report.js +2 -2
  213. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +9 -4
  214. package/node_modules/pi-lens/dist/clients/pipeline.js +5 -4
  215. package/node_modules/pi-lens/dist/clients/project-changes.js +1 -1
  216. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +1 -1
  217. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +38 -10
  218. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
  219. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +1 -1
  220. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +1 -1
  221. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +1 -1
  222. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +1 -1
  223. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +1 -1
  224. package/node_modules/pi-lens/dist/clients/project-lens-config.js +49 -1
  225. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +3 -51
  226. package/node_modules/pi-lens/dist/clients/python-provenance.js +1 -1
  227. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +3 -1
  228. package/node_modules/pi-lens/dist/clients/read-guard.js +5 -2
  229. package/node_modules/pi-lens/dist/clients/recent-touches.js +2 -2
  230. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +3 -5
  231. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +1 -1
  232. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +2 -0
  233. package/node_modules/pi-lens/dist/clients/ruff-client.js +18 -9
  234. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +10 -1
  235. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +5 -0
  236. package/node_modules/pi-lens/dist/clients/runtime-session.js +58 -14
  237. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +11 -1
  238. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +4 -1
  239. package/node_modules/pi-lens/dist/clients/runtime-turn.js +48 -1
  240. package/node_modules/pi-lens/dist/clients/sanitize.js +0 -11
  241. package/node_modules/pi-lens/dist/clients/scan-utils.js +1 -56
  242. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -2
  243. package/node_modules/pi-lens/dist/clients/session-event-guard.js +69 -1
  244. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -24
  245. package/node_modules/pi-lens/dist/clients/sg-runner.js +2 -1
  246. package/node_modules/pi-lens/dist/clients/sgconfig.js +1 -5
  247. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +1 -1
  248. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +108 -0
  249. package/node_modules/pi-lens/dist/clients/smells-rollup.js +2 -2
  250. package/node_modules/pi-lens/dist/clients/startup-scan.js +2 -10
  251. package/node_modules/pi-lens/dist/clients/test-runner-client.js +291 -76
  252. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +77 -72
  253. package/node_modules/pi-lens/dist/clients/tool-config.js +199 -0
  254. package/node_modules/pi-lens/dist/clients/tool-cwd.js +299 -0
  255. package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
  256. package/node_modules/pi-lens/dist/clients/tool-probe.js +53 -0
  257. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +42 -0
  258. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +1 -52
  259. package/node_modules/pi-lens/dist/clients/trivy-client.js +9 -1
  260. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -4
  261. package/node_modules/pi-lens/dist/clients/turn-context.js +52 -0
  262. package/node_modules/pi-lens/dist/clients/user-notify.js +0 -4
  263. package/node_modules/pi-lens/dist/clients/widget-state.js +4 -3
  264. package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -7
  265. package/node_modules/pi-lens/dist/clients/word-index-store.js +3 -7
  266. package/node_modules/pi-lens/dist/clients/word-index.js +9 -16
  267. package/node_modules/pi-lens/dist/clients/zizmor-config.js +1 -1
  268. package/node_modules/pi-lens/dist/index.js +19969 -17481
  269. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -2
  270. package/node_modules/pi-lens/dist/mcp/build-staleness.js +1 -1
  271. package/node_modules/pi-lens/dist/mcp/server.js +135 -159
  272. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +1 -1
  273. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +1 -1
  274. package/node_modules/pi-lens/dist/tools/activate-tools.js +10 -7
  275. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +2 -13
  276. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +8 -13
  277. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +83 -39
  278. package/node_modules/pi-lens/dist/tools/effective-config.js +4 -3
  279. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +1 -10
  280. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +354 -86
  281. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +54 -112
  282. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +20 -44
  283. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +3 -3
  284. package/node_modules/pi-lens/dist/tools/module-report.js +5 -9
  285. package/node_modules/pi-lens/dist/tools/project-report.js +9 -17
  286. package/node_modules/pi-lens/dist/tools/render-compact.js +281 -2
  287. package/node_modules/pi-lens/dist/tools/symbol-search.js +3 -4
  288. package/node_modules/pi-lens/docs/agent-guide.md +3 -4
  289. package/node_modules/pi-lens/docs/agent-tools.md +46 -22
  290. package/node_modules/pi-lens/docs/configuration.md +9 -0
  291. package/node_modules/pi-lens/docs/environment-variables.md +0 -5
  292. package/node_modules/pi-lens/docs/features.md +5 -5
  293. package/node_modules/pi-lens/docs/globalconfig.md +31 -1
  294. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +9 -1
  295. package/node_modules/pi-lens/docs/mcp.md +13 -1
  296. package/node_modules/pi-lens/docs/pi-lens-fixer.md +71 -0
  297. package/node_modules/pi-lens/docs/pi-lens-investigator.md +15 -0
  298. package/node_modules/pi-lens/docs/pi-lens-monitor.md +88 -0
  299. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +41 -0
  300. package/node_modules/pi-lens/docs/pi-lens-subagent.md +21 -2
  301. package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
  302. package/node_modules/pi-lens/docs/real-harness.md +46 -0
  303. package/node_modules/pi-lens/docs/release-qa-baseline.md +17 -5
  304. package/node_modules/pi-lens/docs/servercapabilities.md +2 -3
  305. package/node_modules/pi-lens/docs/settings.md +37 -2
  306. package/node_modules/pi-lens/docs/tools_improvement2.md +4 -4
  307. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  308. package/node_modules/pi-lens/docs/usage.md +13 -3
  309. package/node_modules/pi-lens/docs/word-index.md +1 -1
  310. package/node_modules/pi-lens/package.json +15 -15
  311. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +7 -3
  312. package/node_modules/pi-lens/rules/typos/_typos.toml +4 -2
  313. package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +1 -1
  314. package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +1 -1
  315. package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +4 -2
  316. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +8 -6
  317. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +19 -9
  318. package/node_modules/pi-web-access/CHANGELOG.md +29 -0
  319. package/node_modules/pi-web-access/README.md +33 -22
  320. package/node_modules/pi-web-access/abortable.ts +17 -0
  321. package/node_modules/pi-web-access/crawl4ai.ts +204 -0
  322. package/node_modules/pi-web-access/credential-source.ts +1 -0
  323. package/node_modules/pi-web-access/curator-page.ts +20 -2
  324. package/node_modules/pi-web-access/curator-run.ts +44 -0
  325. package/node_modules/pi-web-access/curator-server.ts +3 -1
  326. package/node_modules/pi-web-access/duckduckgo.ts +1 -1
  327. package/node_modules/pi-web-access/extract.ts +57 -12
  328. package/node_modules/pi-web-access/fetch-params.ts +1 -1
  329. package/node_modules/pi-web-access/gemini-search.ts +9 -5
  330. package/node_modules/pi-web-access/index.ts +25 -18
  331. package/node_modules/pi-web-access/openai-search.ts +35 -13
  332. package/node_modules/pi-web-access/package.json +2 -2
  333. package/node_modules/pi-web-access/page-query.ts +5 -2
  334. package/node_modules/pi-web-access/query-rewrite.ts +5 -2
  335. package/node_modules/pi-web-access/serpapi.ts +220 -0
  336. package/node_modules/pi-web-access/ssrf-protection.ts +5 -1
  337. package/node_modules/pi-web-access/storage.ts +10 -0
  338. package/node_modules/pi-web-access/summary-review.ts +34 -21
  339. package/node_modules/pi-web-access/utils.ts +6 -4
  340. package/package.json +7 -4
  341. package/node_modules/pi-lens/dist/tools/ast-dump.js +0 -103
@@ -51,15 +51,32 @@ function isTypeStub(content) {
51
51
  */
52
52
  export function detectFileRole(filePath, content) {
53
53
  const windowsShaped = isWindowsPath(filePath);
54
- const base = (windowsShaped ? win32.basename(filePath) : basename(filePath)).toLowerCase();
54
+ const rawBase = windowsShaped ? win32.basename(filePath) : basename(filePath);
55
+ const base = rawBase.toLowerCase();
55
56
  const dir = (windowsShaped ? win32.dirname(filePath) : dirname(filePath))
56
57
  .replace(/\\/g, "/")
57
58
  .toLowerCase();
58
59
  // --- Test ---
60
+ // Suffix conventions informed by the test-runner table
61
+ // (SOURCE_TO_TEST_PATTERNS + RUNNERS kinds in
62
+ // clients/test-runner-client.ts); hand-written, not iterated from it —
63
+ // see #2928 for the single-classifier fold:
64
+ // - `_test.` infix: Go (`*_test.go`), pytest (`*_test.py`), ExUnit
65
+ // (`*_test.exs`), Dart (`*_test.dart`). The underscore anchor keeps
66
+ // this precise: `contest.py` and `latest.ts` do not match.
67
+ // - `_spec.` infix: RSpec (`*_spec.rb`).
68
+ // - CamelCase `*Test(s).<ext>` suffix, matched case-SENSITIVELY on the
69
+ // raw basename: JUnit/Surefire (`*Test.java`, `*TestCase.java`),
70
+ // Kotlin (`*Test.kt`), dotnet (`*Test.cs`/`*Tests.cs`), PHPUnit
71
+ // (`*Test.php`). Case matters: Surefire's `*Test.java` does not match
72
+ // `contest.java`, so neither do we.
59
73
  if (base.includes(".test.") ||
60
74
  base.includes(".spec.") ||
75
+ base.includes("_test.") ||
76
+ base.includes("_spec.") ||
61
77
  base.startsWith("test_") ||
62
78
  base.startsWith("spec_") ||
79
+ /(Test|Tests|TestCase)\.(java|kt|kts|cs|fs|php)$/.test(rawBase) ||
63
80
  dir.includes("/__tests__/") ||
64
81
  dir.includes("/test/") ||
65
82
  dir.includes("/tests/") ||
@@ -119,7 +119,7 @@ export class FileTime {
119
119
  }
120
120
  }
121
121
  // --- Error Type ---
122
- export class FileTimeError extends Error {
122
+ class FileTimeError extends Error {
123
123
  filePath;
124
124
  reason;
125
125
  constructor(message, filePath, reason) {
@@ -302,7 +302,7 @@ function matchesGitignorePattern(pattern, relativePath, isDirectory, matchExpand
302
302
  return candidates.some((value) => matchExpanded(value, expanded));
303
303
  });
304
304
  }
305
- export function readGitignorePatterns(rootDir, layer = "gitignore") {
305
+ function readGitignorePatterns(rootDir, layer = "gitignore") {
306
306
  const gitignorePath = path.join(rootDir, ".gitignore");
307
307
  try {
308
308
  return parseGitignoreContent(fs.readFileSync(gitignorePath, "utf-8"), layer);
@@ -762,7 +762,11 @@ export function readGitignoreDirs(rootDir) {
762
762
  .map((entry) => entry.pattern);
763
763
  }
764
764
  function globToRegExp(glob) {
765
+ // Directory names use the same `*`-only dialect as read-guard exemptions.
766
+ // Collapse adjacent stars before compiling to avoid nullable-group
767
+ // backtracking on a non-matching name (#2622).
765
768
  const escaped = glob
769
+ .replace(/\*+/g, "*")
766
770
  .replace(/[.+^${}()|[\]\\]/g, "\\$&")
767
771
  .replace(/\*/g, ".*")
768
772
  .replace(/\?/g, ".");
@@ -139,6 +139,10 @@
139
139
  * agent-facing surface is EITHER gated OR explicitly labeled — never neither.
140
140
  */
141
141
  const MS_PER_MINUTE = 60_000;
142
+ /** Mark retained findings whose replacement result could not be reconciled. */
143
+ export function markUnreconciledFindings(findings) {
144
+ return findings.map((finding) => ({ ...finding, stale: true }));
145
+ }
142
146
  /**
143
147
  * Render a short, honest age suffix for a store that cannot be freshness-
144
148
  * gated per-path (see module doc). `undefined`/unparseable `scannedAt`
@@ -202,4 +202,3 @@ export function clearFormatServiceAndFileState() {
202
202
  resetFormatService();
203
203
  }
204
204
  // Re-export for convenience
205
- export { clearAllSessions } from "./file-time.js";
@@ -11,10 +11,12 @@
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
17
  import { createGenerationSource } from "./generation-guard.js";
17
18
  import { normalizeMapKey } from "./path-utils.js";
19
+ import { resolveToolCwd } from "./tool-cwd.js";
18
20
  import { resolveCargoPackageEdition } from "./cargo-manifest.js";
19
21
  import { resolveKtfmtGradleStyle } from "./gradle-ktfmt-style.js";
20
22
  import { resolvePhpCsFixerConfig } from "./php-cs-fixer-config.js";
@@ -26,8 +28,10 @@ import { compareOrdinal } from "./string-utils.js";
26
28
  import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
27
29
  import { findGlobalBinary, findLocalBinUpwards, VENDOR_BIN_DIRS, VENV_BIN_DIRS, } from "./package-manager.js";
28
30
  import { safeSpawnAsync } from "./safe-spawn.js";
31
+ import { probeToolAsync } from "./tool-probe.js";
29
32
  import { assertInstallAllowed } from "./project-trust.js";
30
33
  import { tryLazyInstallForFormatter } from "./dispatch/runners/utils/lazy-installer.js";
34
+ import { getToolPath } from "./installer/index.js";
31
35
  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";
32
36
  /**
33
37
  * Lazy-install a formatter's tool, through the shared seam (#1537).
@@ -379,7 +383,7 @@ async function resolveGoFmtBinary() {
379
383
  const inPath = await which("gofmt");
380
384
  if (inPath)
381
385
  return inPath;
382
- const goCheck = await safeSpawnAsync("go", ["env", "GOROOT"], {
386
+ const goCheck = await probeToolAsync("go", ["env", "GOROOT"], {
383
387
  timeout: 5000,
384
388
  });
385
389
  if (goCheck.error || goCheck.status !== 0)
@@ -467,6 +471,22 @@ async function resolveManagedSmartDefaultCommand(formatterName, filePath, args)
467
471
  return null;
468
472
  return [installed, ...args, filePath];
469
473
  }
474
+ /** Resolve an explicitly selected formatter from the shared managed-tool seam. */
475
+ async function resolveManagedFormatterCommand(toolId, filePath, args, findLocal, cwd) {
476
+ const local = findLocal && cwd ? await findLocal(toolId, cwd) : null;
477
+ const installed = local ?? (await which(toolId)) ?? (await getToolPath(toolId));
478
+ return installed ? [installed, ...args, filePath] : FORMATTER_UNAVAILABLE;
479
+ }
480
+ function managedFormatterResolver(toolId, args, findLocal) {
481
+ return (filePath, cwd) => resolveManagedFormatterCommand(toolId, filePath, args, findLocal, cwd);
482
+ }
483
+ function managedToolDetect(toolId, configDetect, nativeDetect) {
484
+ return async (cwd) => {
485
+ const available = (nativeDetect && (await nativeDetect(cwd))) ||
486
+ Boolean(await getToolPath(toolId));
487
+ return available && (configDetect ? await configDetect(cwd) : true);
488
+ };
489
+ }
470
490
  /**
471
491
  * One entry per formatter that can be selected via explicit project config
472
492
  * (the `formatterPolicy` "explicit-config" branch of `getFormattersForFile`).
@@ -602,6 +622,20 @@ async function indentationArgs(filePath, tool, cwd) {
602
622
  String(indentation.width),
603
623
  ];
604
624
  }
625
+ /**
626
+ * Resolve the cwd for the formatter child, capped at the user's home
627
+ * directory. Files outside a project retain the historical file-directory
628
+ * cwd. The optional homeDir is only for the ceiling regression test.
629
+ */
630
+ export function resolveFormatterCwd(absolutePath, formatterName, homeDir = os.homedir()) {
631
+ return resolveToolCwd("formatter", formatterName ?? "unknown", absolutePath, {
632
+ // Formatter discovery historically walks above the file directory. The
633
+ // filesystem root is the neutral dispatch boundary; the seam's `$HOME`
634
+ // ceiling prevents it from escaping the user's workspace.
635
+ cwd: path.parse(path.resolve(absolutePath)).root,
636
+ homeDir,
637
+ });
638
+ }
605
639
  /**
606
640
  * Every biome invocation must carry this. Biome exits 1 with "No files were
607
641
  * processed in the specified paths" when the path is ignored by the repo's own
@@ -783,11 +817,7 @@ export const oxfmtFormatter = {
783
817
  const found = await which("oxfmt");
784
818
  if (found)
785
819
  return [found, OXFMT_NO_ERROR_ON_UNMATCHED, filePath];
786
- // #2413: neither node_modules/.bin nor PATH has oxfmt, and `detect()` is
787
- // config-only (it never probes the binary), so selection can reach here
788
- // with nothing installed. The static command is bare `oxfmt` — spawning it
789
- // only reproduces the reported `spawn oxfmt ENOENT`. Prove it unavailable.
790
- return FORMATTER_UNAVAILABLE;
820
+ return ((await resolveManagedFormatterCommand("oxfmt", filePath, [OXFMT_NO_ERROR_ON_UNMATCHED], findInNodeModules, cwd)) ?? FORMATTER_UNAVAILABLE);
791
821
  },
792
822
  // Single source of truth: OXFMT_SUPPORTED_EXTENSIONS in tool-policy.ts.
793
823
  // Do not hand-maintain a second copy of this list (#1134 — previously two
@@ -836,21 +866,14 @@ export const ruffFormatter = {
836
866
  return true;
837
867
  // No-config fallback: if Ruff is already available, allow formatter usage.
838
868
  // This keeps Python default behavior consistent with startup defaults.
839
- const { getToolPath } = await import("./installer/index.js");
840
- const installed = await getToolPath("ruff");
841
- return Boolean(installed);
869
+ return Boolean(await getToolPath("ruff"));
842
870
  },
843
871
  };
844
872
  export const blackFormatter = {
845
873
  name: "black",
846
874
  command: ["black", "$FILE"],
847
875
  extensions: [".py", ".pyi"],
848
- async resolveCommand(filePath, cwd) {
849
- const venv = await findInVenv("black", cwd);
850
- if (venv)
851
- return [venv, filePath];
852
- return null;
853
- },
876
+ resolveCommand: managedFormatterResolver("black", [], findInVenv),
854
877
  async detect(cwd) {
855
878
  return hasBlackConfig(cwd);
856
879
  },
@@ -949,12 +972,7 @@ export const shfmtFormatter = {
949
972
  ...styleArgs,
950
973
  ]);
951
974
  },
952
- async detect(_cwd) {
953
- if ((await which("shfmt")) !== null)
954
- return true;
955
- const { getToolPath } = await import("./installer/index.js");
956
- return Boolean(await getToolPath("shfmt"));
957
- },
975
+ detect: managedToolDetect("shfmt", undefined, async () => (await which("shfmt")) !== null),
958
976
  };
959
977
  export const nixfmtFormatter = {
960
978
  name: "nixfmt",
@@ -1016,12 +1034,7 @@ export const ktlintFormatter = {
1016
1034
  return [inPath, "-F", filePath];
1017
1035
  return resolveManagedSmartDefaultCommand("ktlint", filePath, ["-F"]);
1018
1036
  },
1019
- async detect(_cwd) {
1020
- if ((await which("ktlint")) !== null)
1021
- return true;
1022
- const { getToolPath } = await import("./installer/index.js");
1023
- return Boolean(await getToolPath("ktlint"));
1024
- },
1037
+ detect: managedToolDetect("ktlint", undefined, async () => (await which("ktlint")) !== null),
1025
1038
  };
1026
1039
  export const ktfmtFormatter = {
1027
1040
  name: "ktfmt",
@@ -1151,36 +1164,25 @@ export const phpCsFixerFormatter = {
1151
1164
  const configPath = resolvePhpCsFixerConfig(filePath);
1152
1165
  const binary = (await findInVendorBin("php-cs-fixer", cwd)) ??
1153
1166
  (await which("php-cs-fixer"));
1167
+ const managed = await resolveManagedFormatterCommand("php-cs-fixer", filePath, [], findInVendorBin, cwd);
1168
+ if (managed === FORMATTER_UNAVAILABLE)
1169
+ return FORMATTER_UNAVAILABLE;
1170
+ const resolved = binary ?? managed[0];
1154
1171
  // #2413/#2472 review F4: both probes (vendor/bin, then PATH) have
1155
1172
  // PROVEN the binary is absent — returning `null` here would fall back
1156
1173
  // to the static `command` above, which is the SAME bare
1157
1174
  // `php-cs-fixer` this just failed to find, spawning it only to
1158
1175
  // re-observe the ENOENT already known. Report the proven-missing
1159
1176
  // state instead so `formatFile` skips the wasted spawn.
1160
- if (!binary)
1177
+ if (!resolved)
1161
1178
  return FORMATTER_UNAVAILABLE;
1162
1179
  return configPath
1163
- ? [binary, "fix", "--config", configPath, filePath]
1164
- : [binary, "fix", filePath];
1165
- },
1166
- async detect(cwd) {
1167
- const vendorBin = await findInVendorBin("php-cs-fixer", cwd);
1168
- const globalBin = await which("php-cs-fixer");
1169
- if (!vendorBin && !globalBin)
1170
- return false;
1171
- // Only run if project has explicit config. This is a presence-only
1172
- // climb from the project `cwd` (not necessarily the formatted file's
1173
- // own directory) via this file's own `findUp` — deliberately NOT
1174
- // merged with `resolvePhpCsFixerConfig` above (#2472 AC4): that
1175
- // resolver climbs from the FILE's directory and needs the exact
1176
- // winning path for `--config`, while this only needs a yes/no answer
1177
- // from whatever `cwd` the caller passed. `rustfmtFormatter.detect`
1178
- // keeps the same non-merged shape against `resolveCargoPackageEdition`
1179
- // for the identical reason.
1180
- const configs = [".php-cs-fixer.php", ".php-cs-fixer.dist.php"];
1181
- const found = await findUp(configs, cwd);
1182
- return found.length > 0;
1180
+ ? [resolved, "fix", "--config", configPath, filePath]
1181
+ : [resolved, "fix", filePath];
1183
1182
  },
1183
+ detect: managedToolDetect("php-cs-fixer", async (cwd) => (await findUp([".php-cs-fixer.php", ".php-cs-fixer.dist.php"], cwd))
1184
+ .length > 0, async (cwd) => Boolean((await findInVendorBin("php-cs-fixer", cwd)) ||
1185
+ (await which("php-cs-fixer")))),
1184
1186
  };
1185
1187
  export const csharpierFormatter = {
1186
1188
  name: "csharpier",
@@ -1196,9 +1198,7 @@ export const csharpierFormatter = {
1196
1198
  }
1197
1199
  // CSharpier 0.x: invoked through the dotnet driver.
1198
1200
  if ((await which("dotnet")) !== null) {
1199
- const legacy = await safeSpawnAsync("dotnet", ["csharpier", "--version"], {
1200
- timeout: 5000,
1201
- });
1201
+ const legacy = await probeToolAsync("dotnet", ["csharpier", "--version"], { timeout: 5000 });
1202
1202
  if (!legacy.error && legacy.status === 0) {
1203
1203
  return ["dotnet", "csharpier", filePath];
1204
1204
  }
@@ -1212,7 +1212,7 @@ export const csharpierFormatter = {
1212
1212
  // … or the legacy dotnet-driver form (CSharpier 0.x).
1213
1213
  if ((await which("dotnet")) === null)
1214
1214
  return false;
1215
- const result = await safeSpawnAsync("dotnet", ["csharpier", "--version"], {
1215
+ const result = await probeToolAsync("dotnet", ["csharpier", "--version"], {
1216
1216
  timeout: 5000,
1217
1217
  });
1218
1218
  return !result.error && result.status === 0;
@@ -1238,23 +1238,8 @@ export const styluaFormatter = {
1238
1238
  name: "stylua",
1239
1239
  command: ["stylua", "$FILE"],
1240
1240
  extensions: [".lua"],
1241
- async resolveCommand(filePath, cwd) {
1242
- // Project binary first (#1731, discipline B): stylua has no pi-lens
1243
- // managed install, so before this the ONLY resolution was a bare
1244
- // `stylua` PATH lookup — a project-local install via npm
1245
- // `@johnnymorganz/stylua` (`node_modules/.bin/stylua`) was invisible.
1246
- const local = findLocalBinUpwards("stylua", cwd);
1247
- return local ? [local, filePath] : null;
1248
- },
1249
- async detect(cwd) {
1250
- const local = findLocalBinUpwards("stylua", cwd);
1251
- if (!local && (await which("stylua")) === null)
1252
- return false;
1253
- // Prefer explicit config but also run if binary is present in a Lua project
1254
- const configs = ["stylua.toml", ".stylua.toml"];
1255
- const found = await findUp(configs, cwd);
1256
- return found.length > 0;
1257
- },
1241
+ resolveCommand: managedFormatterResolver("stylua", [], findLocalBinUpwards),
1242
+ detect: managedToolDetect("stylua", async (cwd) => (await findUp(["stylua.toml", ".stylua.toml"], cwd)).length > 0, async (cwd) => Boolean(findLocalBinUpwards("stylua", cwd) || (await which("stylua")))),
1258
1243
  };
1259
1244
  export const ormoluFormatter = {
1260
1245
  name: "ormolu",
@@ -1274,42 +1259,28 @@ export const taploFormatter = {
1274
1259
  return [inPath, "fmt", filePath];
1275
1260
  return resolveManagedSmartDefaultCommand("taplo", filePath, ["fmt"]);
1276
1261
  },
1277
- async detect(_cwd) {
1278
- if ((await which("taplo")) !== null)
1279
- return true;
1280
- const { getToolPath } = await import("./installer/index.js");
1281
- return Boolean(await getToolPath("taplo"));
1282
- },
1262
+ detect: managedToolDetect("taplo", undefined, async () => (await which("taplo")) !== null),
1283
1263
  };
1284
1264
  export const googleJavaFormatFormatter = {
1285
1265
  name: "google-java-format",
1286
1266
  command: ["google-java-format", "--replace", "$FILE"],
1287
1267
  extensions: [".java"],
1288
- async detect(cwd) {
1289
- if ((await which("google-java-format")) === null)
1290
- return false;
1291
- return hasGoogleJavaFormatConfig(cwd);
1292
- },
1268
+ resolveCommand: managedFormatterResolver("google-java-format", ["--replace"]),
1269
+ detect: managedToolDetect("google-java-format", hasGoogleJavaFormatConfig, async () => (await which("google-java-format")) !== null),
1293
1270
  };
1294
1271
  export const cljfmtFormatter = {
1295
1272
  name: "cljfmt",
1296
1273
  command: ["cljfmt", "fix", "$FILE"],
1297
1274
  extensions: [".clj", ".cljc", ".cljs"],
1298
- async detect(cwd) {
1299
- if ((await which("cljfmt")) === null)
1300
- return false;
1301
- return hasCljfmtConfig(cwd);
1302
- },
1275
+ resolveCommand: managedFormatterResolver("cljfmt", ["fix"]),
1276
+ detect: managedToolDetect("cljfmt", hasCljfmtConfig, async () => (await which("cljfmt")) !== null),
1303
1277
  };
1304
1278
  export const cmakeFormatFormatter = {
1305
1279
  name: "cmake-format",
1306
1280
  command: ["cmake-format", "-i", "$FILE"],
1307
1281
  extensions: [".cmake"],
1308
- async detect(cwd) {
1309
- if ((await which("cmake-format")) === null)
1310
- return false;
1311
- return hasCmakeFormatConfig(cwd);
1312
- },
1282
+ resolveCommand: managedFormatterResolver("cmake-format", ["-i"]),
1283
+ detect: managedToolDetect("cmake-format", hasCmakeFormatConfig, async () => (await which("cmake-format")) !== null),
1313
1284
  };
1314
1285
  export const cueFormatter = {
1315
1286
  name: "cue",
@@ -1382,7 +1353,7 @@ export const psscriptanalyzerFormatFormatter = {
1382
1353
  if (!pwsh)
1383
1354
  return false;
1384
1355
  // Check PSScriptAnalyzer module is available
1385
- const result = await safeSpawnAsync(pwsh, [
1356
+ const result = await probeToolAsync(pwsh, [
1386
1357
  "-NoProfile",
1387
1358
  "-Command",
1388
1359
  "Get-Module -ListAvailable PSScriptAnalyzer | Select-Object -First 1 -ExpandProperty Name",
@@ -1438,7 +1409,7 @@ const detectionCache = new BoundedLruCache(32);
1438
1409
  // The signature is immutable for a cache generation. This memo is separate
1439
1410
  // from detectionCache because a cwd can have several extension entries, and a
1440
1411
  // warm lookup must not repeat the ancestor walk or stat matched configs.
1441
- const formatterSignatureFlights = new Map();
1412
+ const formatterSignatureFlights = new BoundedLruCache(32);
1442
1413
  const formatterCacheGeneration = createGenerationSource("formatter-cache");
1443
1414
  // These are the formatter configuration files consulted by the policy helpers
1444
1415
  // above. Their metadata is captured by the cold detection signature. The
@@ -1810,6 +1781,7 @@ export function _getFormatterResetStateForTests() {
1810
1781
  whichTransientCommands,
1811
1782
  cooldownRecordedForRetryAtMs,
1812
1783
  detectionCache,
1784
+ formatterSignatureFlights,
1813
1785
  };
1814
1786
  }
1815
1787
  export function clearFormatterRuntimeState() {
@@ -1887,6 +1859,7 @@ export async function formatFile(filePath, formatter) {
1887
1859
  try {
1888
1860
  const absolutePath = path.resolve(filePath);
1889
1861
  const cwd = path.dirname(absolutePath);
1862
+ const formatterCwd = resolveFormatterCwd(absolutePath, formatter.name);
1890
1863
  const contentBefore = await fs.readFile(absolutePath, "utf-8");
1891
1864
  // Resolve command: prefer local (venv/vendor/node_modules) over global.
1892
1865
  // The shared seam must honor SKIP_FORMATTING before selecting the static
@@ -1912,7 +1885,7 @@ export async function formatFile(filePath, formatter) {
1912
1885
  // Run formatter without blocking the event loop.
1913
1886
  const result = await safeSpawnAsync(cmd[0], cmd.slice(1), {
1914
1887
  timeout: 15000,
1915
- cwd,
1888
+ cwd: formatterCwd,
1916
1889
  });
1917
1890
  // A resolver that could NOT prove absence (it never probed PATH — e.g.
1918
1891
  // black/sqlfluff, which only look in a venv) legitimately falls back to the
@@ -205,7 +205,7 @@ function hasGeneratedArtifactContent(content) {
205
205
  * file is not machine-emitted evidence), and an empty or whitespace-only
206
206
  * file yields a zero mean and stays source.
207
207
  */
208
- export const MACHINE_EMITTED_LINE_SHAPE_MIN_CONTENT = 2048;
208
+ const MACHINE_EMITTED_LINE_SHAPE_MIN_CONTENT = 2048;
209
209
  export const MACHINE_EMITTED_LINE_SHAPE_MEAN_THRESHOLD = 2500;
210
210
  export function classifyMachineEmittedLineShape(content) {
211
211
  const contentLength = content.length;
@@ -325,8 +325,7 @@ function analyzeFileHeader(filePath, maxBytes) {
325
325
  * function and short-circuit on a hit, so by the time this function runs for
326
326
  * those callers evidence (a) has already come back negative. Non-walk
327
327
  * callers with no upstream sibling probe (`file-role.ts`'s content-based
328
- * check, `project-scan-policy.ts`'s single-path `shouldSkipProjectPath`)
329
- * evaluate evidence (b) only — a documented, narrower guarantee than the
328
+ * check) evaluate evidence (b) only — a documented, narrower guarantee than the
330
329
  * walk path's.
331
330
  *
332
331
  * Tradeoff (documented per the issue): when NEITHER piece of evidence
@@ -387,7 +386,7 @@ export function classifyGeneratedOrArtifactDetailed(filePath, options = {}) {
387
386
  : { verdict: "clean" };
388
387
  }
389
388
  /** Verdict-only convenience wrapper over {@link classifyGeneratedOrArtifactDetailed}. */
390
- export function classifyGeneratedOrArtifact(filePath, options = {}) {
389
+ function classifyGeneratedOrArtifact(filePath, options = {}) {
391
390
  return classifyGeneratedOrArtifactDetailed(filePath, options).verdict;
392
391
  }
393
392
  export function isGeneratedOrArtifact(filePath, options = {}) {
@@ -186,6 +186,10 @@ export function createGenerationMap(name, options = {}) {
186
186
  invalidations += 1;
187
187
  stamps.delete(normalize(key));
188
188
  },
189
+ clear() {
190
+ invalidations += 1;
191
+ stamps.clear();
192
+ },
189
193
  size() {
190
194
  return stamps.size;
191
195
  },
@@ -141,6 +141,11 @@ function canonicalizeGuardCommand(command) {
141
141
  let pendingSpace = false;
142
142
  quote = undefined;
143
143
  for (const ch of result) {
144
+ if (!quote && ch === "\n") {
145
+ collapsed += ch;
146
+ pendingSpace = false;
147
+ continue;
148
+ }
144
149
  if (!quote && /\s/.test(ch)) {
145
150
  pendingSpace = collapsed.length > 0;
146
151
  continue;
@@ -45,11 +45,12 @@
45
45
  * module doc for why blanket gitignore-respect is wrong for a secrets scanner
46
46
  * (an untracked `.env` with a real credential is exactly the case gitleaks
47
47
  * exists to catch, and `.env` is commonly gitignored). `classifyAndFilterFindings`
48
- * backstops the config with a TS-side re-classification: a finding under the
49
- * secrets-lane scratch tier is DEMOTED (not deleted) to `severity: "info"`,
50
- * `semantic: "none"`, `pathStatus: "scratch"` — visible for an audit read,
51
- * but it can never surface as a blocking "leaked secret" alarm. Every other
52
- * finding gets `pathStatus` from git's tracked/ignored view.
48
+ * backstops the config with a TS-side re-classification. Findings under the
49
+ * secrets-lane scratch tier, in gitignored files, or inside nested repositories
50
+ * are DEMOTED (not deleted) to `severity: "info"` / `semantic: "none"` by the
51
+ * project-diagnostics adapter. They remain visible for an explicit audit but
52
+ * can never surface as blocking "leaked secret" alarms or automatic turn
53
+ * context. Tracked and ordinary untracked files remain blocking.
53
54
  *
54
55
  * Refs: #130, #1562
55
56
  */
@@ -333,8 +334,10 @@ export class GitleaksClient extends SecurityScanClient {
333
334
  }
334
335
  const rawFindings = parseGitleaksReport(fs.readFileSync(reportPath, "utf-8"));
335
336
  const findings = await classifyAndFilterFindings(rawFindings, cwd);
337
+ // #2154: the one gitleaks site that parsed a scan of this root.
336
338
  return {
337
339
  success: true,
340
+ analyzed: true,
338
341
  findings,
339
342
  scannedAt,
340
343
  };
@@ -375,7 +378,10 @@ export class GitleaksClient extends SecurityScanClient {
375
378
  * assert the config we hand it, so this backstop guarantees the demotion
376
379
  * even if the generated regex ever mismatches gitleaks's own path
377
380
  * normalization.
378
- * 2. Every OTHER finding gets `pathStatus` set from git's tracked/ignored
381
+ * 2. A finding below a nested git boundary is labelled
382
+ * `nested-repository`. Parent scans must not claim ownership of a
383
+ * submodule's or nested repository's files.
384
+ * 3. Every OTHER finding gets `pathStatus` set from git's tracked/ignored
379
385
  * view, so a future false-positive triage is a log read.
380
386
  *
381
387
  * `collectTrackedFiles`/`collectUntrackedIgnoredIds` degrade to `undefined`
@@ -383,6 +389,32 @@ export class GitleaksClient extends SecurityScanClient {
383
389
  * keep `pathStatus: undefined` rather than a guessed value (fail-open,
384
390
  * matching `git-tracked-ignore.ts`'s own contract). Exported for unit tests.
385
391
  */
392
+ function isInsideNestedGitRepository(file, cwd) {
393
+ const root = path.resolve(cwd);
394
+ const absoluteFile = path.isAbsolute(file)
395
+ ? path.resolve(file)
396
+ : path.resolve(root, file);
397
+ const relative = path.relative(root, absoluteFile);
398
+ if (relative === ".." ||
399
+ relative.startsWith(`..${path.sep}`) ||
400
+ path.isAbsolute(relative))
401
+ return false;
402
+ let directory = path.dirname(absoluteFile);
403
+ while (directory !== root) {
404
+ try {
405
+ if (fs.existsSync(path.join(directory, ".git")))
406
+ return true;
407
+ }
408
+ catch {
409
+ return false;
410
+ }
411
+ const parent = path.dirname(directory);
412
+ if (parent === directory)
413
+ return false;
414
+ directory = parent;
415
+ }
416
+ return false;
417
+ }
386
418
  export async function classifyAndFilterFindings(findings, cwd) {
387
419
  if (findings.length === 0)
388
420
  return findings;
@@ -399,6 +431,10 @@ export async function classifyAndFilterFindings(findings, cwd) {
399
431
  const absPath = path.isAbsolute(finding.file)
400
432
  ? finding.file
401
433
  : path.resolve(cwd, finding.file);
434
+ if (isInsideNestedGitRepository(absPath, cwd)) {
435
+ classified.push({ ...finding, pathStatus: "nested-repository" });
436
+ continue;
437
+ }
402
438
  let pathStatus;
403
439
  if (trackedIds?.has(normalizeEphemeralMapKey(absPath))) {
404
440
  pathStatus = "tracked";
@@ -20,6 +20,8 @@ import * as fs from "node:fs";
20
20
  import * as os from "node:os";
21
21
  import * as path from "node:path";
22
22
  import { safeSpawnAsync } from "./safe-spawn.js";
23
+ import { probeToolAsync } from "./tool-probe.js";
24
+ import { firstOutputLine, spawnFailedWithNoOutput, } from "./dispatch/runners/utils/spawn-outcome.js";
23
25
  import { assertInstallAllowed } from "./project-trust.js";
24
26
  import { SecurityScanClient } from "./security-scan-client.js";
25
27
  import { classifyProbeFailure, describeProbeEvidence, INSTALL_TRANSIENT_MAX_ATTEMPTS, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
@@ -86,7 +88,7 @@ export class GovulncheckClient extends SecurityScanClient {
86
88
  let goOnPath;
87
89
  let goHostStallMs;
88
90
  try {
89
- goOnPath = await safeSpawnAsync("go", ["version"], {
91
+ goOnPath = await probeToolAsync("go", ["version"], {
90
92
  timeout: 5000,
91
93
  });
92
94
  }
@@ -219,7 +221,7 @@ export class GovulncheckClient extends SecurityScanClient {
219
221
  let reprobe;
220
222
  let reprobeHostStallMs;
221
223
  try {
222
- reprobe = await safeSpawnAsync("govulncheck", ["-version"], {
224
+ reprobe = await probeToolAsync("govulncheck", ["-version"], {
223
225
  timeout: 5000,
224
226
  });
225
227
  }
@@ -366,21 +368,60 @@ export class GovulncheckClient extends SecurityScanClient {
366
368
  const bin = this.binaryPath ?? "govulncheck";
367
369
  try {
368
370
  const result = await safeSpawnAsync(bin, ["-mode=source", "-format=json", "./..."], { cwd, timeout: SCAN_TIMEOUT_MS });
371
+ // #2154 v4 F1: this was the one runner client with no `result.error`
372
+ // check before its parse site. `safeSpawnAsync` reports a timeout or
373
+ // an ambient abort as `{status: null, error, stdout: <partial>}`, and
374
+ // the old guard (`!stdout.trim() && status !== 0 && status !== 3`)
375
+ // let that partial stream through as an authoritative scan. Same
376
+ // order as the seven sibling clients: the process first, its output
377
+ // second.
378
+ if (result.error) {
379
+ this.log(`Scan error: ${result.error.message}`);
380
+ return {
381
+ ...EMPTY_RESULT,
382
+ scannedAt,
383
+ summary: result.error.message.slice(0, 200),
384
+ };
385
+ }
369
386
  // govulncheck exits non-zero (status 3) when vulnerabilities are
370
- // found — that's success from our perspective. Genuine failures
371
- // produce empty stdout + a stderr message.
387
+ // found — that's success from our perspective. Any other exit code
388
+ // means the scan was not analysed, even if its config preamble or
389
+ // partial stream reached stdout before the failure.
372
390
  const rawStdout = result.stdout ?? "";
373
- if (!rawStdout.trim() && result.status !== 0 && result.status !== 3) {
374
- this.log(`Scan failed: ${(result.stderr ?? "").slice(0, 200)}`);
391
+ if (result.status !== 0 && result.status !== 3) {
392
+ return {
393
+ ...EMPTY_RESULT,
394
+ scannedAt,
395
+ summary: firstOutputLine(result.stderr) || "scan failed",
396
+ };
397
+ }
398
+ if (!rawStdout.trim()) {
399
+ // Nothing to parse. `govulncheck -format=json` always writes a
400
+ // stream (a `config` record at minimum), so an empty one is never
401
+ // evidence of "no vulnerabilities" — it must not retire a retained
402
+ // CVE finding, whatever the exit code. `spawnFailedWithNoOutput`
403
+ // is the same shared discriminator knip and dead-code use, in
404
+ // place of this file's hand-rolled status test.
405
+ if (spawnFailedWithNoOutput(result, rawStdout)) {
406
+ this.log(`Scan failed: ${(result.stderr ?? "").slice(0, 200)}`);
407
+ return {
408
+ ...EMPTY_RESULT,
409
+ scannedAt,
410
+ summary: firstOutputLine(result.stderr) || "scan failed",
411
+ };
412
+ }
375
413
  return {
376
414
  ...EMPTY_RESULT,
415
+ success: true,
377
416
  scannedAt,
378
- summary: (result.stderr ?? "").trim().split("\n")[0] || "scan failed",
417
+ summary: "govulncheck produced no output; nothing parsed",
379
418
  };
380
419
  }
381
420
  const findings = parseGovulncheckJson(rawStdout);
421
+ // #2154: the one govulncheck site that parsed a scan of this root.
382
422
  return {
383
423
  success: true,
424
+ analyzed: true,
384
425
  findings,
385
426
  scannedAt,
386
427
  };