@herbertgao/pi-extensions 2026.9.2 → 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 (221) 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/dist/index.ts +12 -2
  39. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
  40. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  41. package/node_modules/@narumitw/pi-btw/src/btw.ts +13 -2
  42. package/node_modules/pi-lens/CHANGELOG.md +176 -0
  43. package/node_modules/pi-lens/README.md +13 -8
  44. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -1
  45. package/node_modules/pi-lens/dist/clients/analysed-root.js +1 -0
  46. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +1 -1
  47. package/node_modules/pi-lens/dist/clients/biome-client.js +13 -2
  48. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +2 -0
  49. package/node_modules/pi-lens/dist/clients/cache-observability.js +122 -1
  50. package/node_modules/pi-lens/dist/clients/complexity-client.js +5 -0
  51. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
  52. package/node_modules/pi-lens/dist/clients/config-resolve.js +6 -3
  53. package/node_modules/pi-lens/dist/clients/config-schema.js +17 -0
  54. package/node_modules/pi-lens/dist/clients/config-warn.js +2 -2
  55. package/node_modules/pi-lens/dist/clients/dead-code-client.js +8 -1
  56. package/node_modules/pi-lens/dist/clients/dependency-checker.js +5 -1
  57. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +38 -15
  58. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +1 -1
  59. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +5 -1
  60. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +2 -1
  61. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +2 -3
  62. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +4 -3
  63. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +2 -1
  64. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +2 -1
  65. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +2 -1
  66. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +2 -1
  67. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +2 -1
  68. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +2 -1
  69. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +2 -1
  70. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +2 -1
  71. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +2 -1
  72. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +2 -1
  73. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +2 -1
  74. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  75. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +3 -1
  76. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +2 -1
  77. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +2 -1
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +2 -1
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +2 -1
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +19 -1
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +3 -2
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +2 -1
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +2 -1
  85. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +2 -1
  86. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +2 -1
  87. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +12 -12
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +2 -1
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +2 -1
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +2 -2
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +12 -11
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -1
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +2 -2
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +3 -2
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +3 -2
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +2 -1
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +2 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +2 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +2 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +2 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +4 -3
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +2 -1
  109. package/node_modules/pi-lens/dist/clients/effective-config.js +12 -2
  110. package/node_modules/pi-lens/dist/clients/extension-log.js +2 -0
  111. package/node_modules/pi-lens/dist/clients/file-role.js +18 -1
  112. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +4 -0
  113. package/node_modules/pi-lens/dist/clients/formatters.js +54 -188
  114. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -0
  115. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +42 -6
  116. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +48 -7
  117. package/node_modules/pi-lens/dist/clients/installer/index.js +196 -52
  118. package/node_modules/pi-lens/dist/clients/jscpd-client.js +9 -1
  119. package/node_modules/pi-lens/dist/clients/knip-client.js +9 -2
  120. package/node_modules/pi-lens/dist/clients/language-profile.js +12 -2
  121. package/node_modules/pi-lens/dist/clients/latency-logger.js +2 -0
  122. package/node_modules/pi-lens/dist/clients/lens-config.js +4 -0
  123. package/node_modules/pi-lens/dist/clients/lsp/config.js +36 -7
  124. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +24 -1
  125. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +78 -0
  126. package/node_modules/pi-lens/dist/clients/lsp/index.js +281 -45
  127. package/node_modules/pi-lens/dist/clients/lsp/server.js +62 -15
  128. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +37 -4
  129. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +3 -0
  130. package/node_modules/pi-lens/dist/clients/mcp/session.js +2 -0
  131. package/node_modules/pi-lens/dist/clients/opengrep-client.js +2 -0
  132. package/node_modules/pi-lens/dist/clients/package-manager.js +2 -1
  133. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +8 -3
  134. package/node_modules/pi-lens/dist/clients/pipeline.js +3 -2
  135. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +38 -10
  136. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
  137. package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
  138. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +2 -0
  139. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +2 -0
  140. package/node_modules/pi-lens/dist/clients/ruff-client.js +18 -9
  141. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +9 -0
  142. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +5 -0
  143. package/node_modules/pi-lens/dist/clients/runtime-session.js +13 -4
  144. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +4 -1
  145. package/node_modules/pi-lens/dist/clients/runtime-turn.js +48 -1
  146. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -2
  147. package/node_modules/pi-lens/dist/clients/session-event-guard.js +69 -1
  148. package/node_modules/pi-lens/dist/clients/sg-runner.js +2 -1
  149. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +108 -0
  150. package/node_modules/pi-lens/dist/clients/test-runner-client.js +291 -76
  151. package/node_modules/pi-lens/dist/clients/tool-config.js +199 -0
  152. package/node_modules/pi-lens/dist/clients/tool-cwd.js +299 -0
  153. package/node_modules/pi-lens/dist/clients/tool-probe.js +53 -0
  154. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +42 -0
  155. package/node_modules/pi-lens/dist/clients/trivy-client.js +9 -1
  156. package/node_modules/pi-lens/dist/clients/turn-context.js +52 -0
  157. package/node_modules/pi-lens/dist/clients/widget-state.js +4 -3
  158. package/node_modules/pi-lens/dist/index.js +21969 -20005
  159. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -2
  160. package/node_modules/pi-lens/dist/mcp/server.js +135 -159
  161. package/node_modules/pi-lens/dist/tools/activate-tools.js +10 -7
  162. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +2 -13
  163. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +8 -13
  164. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +83 -39
  165. package/node_modules/pi-lens/dist/tools/effective-config.js +4 -3
  166. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +1 -10
  167. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +353 -85
  168. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +21 -110
  169. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +20 -44
  170. package/node_modules/pi-lens/dist/tools/module-report.js +5 -9
  171. package/node_modules/pi-lens/dist/tools/project-report.js +9 -17
  172. package/node_modules/pi-lens/dist/tools/render-compact.js +280 -1
  173. package/node_modules/pi-lens/dist/tools/symbol-search.js +2 -2
  174. package/node_modules/pi-lens/docs/agent-guide.md +3 -4
  175. package/node_modules/pi-lens/docs/agent-tools.md +46 -22
  176. package/node_modules/pi-lens/docs/configuration.md +9 -0
  177. package/node_modules/pi-lens/docs/environment-variables.md +0 -5
  178. package/node_modules/pi-lens/docs/features.md +5 -5
  179. package/node_modules/pi-lens/docs/globalconfig.md +11 -1
  180. package/node_modules/pi-lens/docs/mcp.md +13 -1
  181. package/node_modules/pi-lens/docs/pi-lens-fixer.md +71 -0
  182. package/node_modules/pi-lens/docs/pi-lens-investigator.md +15 -0
  183. package/node_modules/pi-lens/docs/pi-lens-monitor.md +88 -0
  184. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +35 -0
  185. package/node_modules/pi-lens/docs/pi-lens-subagent.md +12 -4
  186. package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
  187. package/node_modules/pi-lens/docs/real-harness.md +46 -0
  188. package/node_modules/pi-lens/docs/release-qa-baseline.md +5 -1
  189. package/node_modules/pi-lens/docs/servercapabilities.md +1 -6
  190. package/node_modules/pi-lens/docs/settings.md +37 -2
  191. package/node_modules/pi-lens/docs/tools_improvement2.md +4 -4
  192. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  193. package/node_modules/pi-lens/docs/usage.md +13 -3
  194. package/node_modules/pi-lens/package.json +10 -1
  195. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +7 -3
  196. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +8 -6
  197. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +19 -9
  198. package/node_modules/pi-web-access/CHANGELOG.md +29 -0
  199. package/node_modules/pi-web-access/README.md +33 -22
  200. package/node_modules/pi-web-access/abortable.ts +17 -0
  201. package/node_modules/pi-web-access/crawl4ai.ts +204 -0
  202. package/node_modules/pi-web-access/credential-source.ts +1 -0
  203. package/node_modules/pi-web-access/curator-page.ts +20 -2
  204. package/node_modules/pi-web-access/curator-run.ts +44 -0
  205. package/node_modules/pi-web-access/curator-server.ts +3 -1
  206. package/node_modules/pi-web-access/duckduckgo.ts +1 -1
  207. package/node_modules/pi-web-access/extract.ts +57 -12
  208. package/node_modules/pi-web-access/fetch-params.ts +1 -1
  209. package/node_modules/pi-web-access/gemini-search.ts +9 -5
  210. package/node_modules/pi-web-access/index.ts +25 -18
  211. package/node_modules/pi-web-access/openai-search.ts +35 -13
  212. package/node_modules/pi-web-access/package.json +2 -2
  213. package/node_modules/pi-web-access/page-query.ts +5 -2
  214. package/node_modules/pi-web-access/query-rewrite.ts +5 -2
  215. package/node_modules/pi-web-access/serpapi.ts +220 -0
  216. package/node_modules/pi-web-access/ssrf-protection.ts +5 -1
  217. package/node_modules/pi-web-access/storage.ts +10 -0
  218. package/node_modules/pi-web-access/summary-review.ts +34 -21
  219. package/node_modules/pi-web-access/utils.ts +6 -4
  220. package/package.json +7 -4
  221. package/node_modules/pi-lens/dist/tools/ast-dump.js +0 -103
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
4
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
4
5
  import { PRIORITY } from "../priorities.js";
5
6
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
6
7
  import { parseToolRun } from "./utils/tool-failure.js";
@@ -72,7 +73,7 @@ const valeRunner = {
72
73
  priority: PRIORITY.DOC_QUALITY,
73
74
  skipTestFiles: false,
74
75
  async run(ctx) {
75
- const cwd = ctx.cwd || process.cwd();
76
+ const cwd = resolveRunnerCwd(ctx, "vale");
76
77
  // Config-gated: skip unless a .vale.ini is found
77
78
  if (!findValeConfig(cwd)) {
78
79
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -1,7 +1,8 @@
1
1
  import { safeSpawnAsync } from "../../safe-spawn.js";
2
+ import { logRunnerAdvisoryOnce } from "../../tool-cwd.js";
2
3
  import { getLinterPolicyForCwd, hasYamllintConfig } from "../../tool-policy.js";
3
4
  import { PRIORITY } from "../priorities.js";
4
- import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
5
+ import { createAvailabilityChecker, resolveRunnerCwd, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
5
6
  import { parseToolRun } from "./utils/tool-failure.js";
6
7
  import { finishParsedRun } from "./utils/tool-failure.js";
7
8
  const yamllint = createAvailabilityChecker("yamllint", ".exe");
@@ -35,14 +36,14 @@ const yamllintRunner = {
35
36
  priority: PRIORITY.YAML_LINT,
36
37
  skipTestFiles: false,
37
38
  async run(ctx) {
38
- const cwd = ctx.cwd || process.cwd();
39
+ const cwd = resolveRunnerCwd(ctx, "yamllint");
39
40
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
40
41
  if (policy && !policy.preferredRunners.includes("yamllint")) {
41
42
  return { status: "skipped", diagnostics: [], semantic: "none" };
42
43
  }
43
44
  const hasConfig = hasYamllintConfig(cwd);
44
45
  if (!hasConfig) {
45
- ctx.log("yamllint: no config detected, running with default rules");
46
+ logRunnerAdvisoryOnce(ctx, "yamllint", cwd, "yamllint: no config detected, running with default rules");
46
47
  }
47
48
  let cmd = null;
48
49
  if (await yamllint.isAvailableAsync(cwd)) {
@@ -1,5 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
3
4
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
4
5
  import { PRIORITY } from "../priorities.js";
5
6
  // zig rejects `--version`; the version subcommand is `zig version`. Using the
@@ -46,7 +47,7 @@ const zigCheckRunner = {
46
47
  priority: PRIORITY.GENERAL_ANALYSIS,
47
48
  skipTestFiles: false,
48
49
  async run(ctx) {
49
- const cwd = ctx.cwd || process.cwd();
50
+ const cwd = resolveRunnerCwd(ctx, "zig-check");
50
51
  if (!(await zig.isAvailableAsync(cwd))) {
51
52
  return { status: "skipped", diagnostics: [], semantic: "none" };
52
53
  }
@@ -47,7 +47,9 @@ import { explainServersForFile, lspConfigOf, registerLSPConfig, } from "./lsp/co
47
47
  // apart on a Windows-shaped path (AGENTS.md defect shape 2).
48
48
  import { isSameOrWithin } from "./lsp/server.js";
49
49
  import { homeRelativePath } from "./path-utils.js";
50
+ import { resolveToolCwd } from "./tool-cwd.js";
50
51
  import { compareOrdinal } from "./string-utils.js";
52
+ import { LENS_TOOL_NAMES, resolveLensToolEnabled } from "./tool-config.js";
51
53
  /** True when a `file` half is the rejection shape, not a resolved view. */
52
54
  export function isEffectiveFileViewError(file) {
53
55
  return "error" in file;
@@ -289,6 +291,10 @@ export async function effectiveConfig(options = {}) {
289
291
  provenance: provenanceView(resolved, homeDir).entries,
290
292
  provenanceCounts: summary.countsByTier,
291
293
  recordCounts: countBy(resolution.records, (record) => record.code),
294
+ tools: LENS_TOOL_NAMES.map((name) => ({
295
+ name,
296
+ enabled: resolveLensToolEnabled(name, resolved.value, undefined, options.noTools),
297
+ })),
292
298
  ...(fileOutsideCwd
293
299
  ? {
294
300
  file: {
@@ -299,7 +305,7 @@ export async function effectiveConfig(options = {}) {
299
305
  : absolute === undefined
300
306
  ? {}
301
307
  : {
302
- file: await fileView(absolute, resolved, homeDir,
308
+ file: await fileView(absolute, resolved, homeDir, cwd,
303
309
  // The gates read the LSP slice of that SAME resolution, through
304
310
  // the same `registerLSPConfig` conversion `initLSPConfig` uses —
305
311
  // no session-root registration, no per-root config-store write
@@ -325,7 +331,7 @@ function toolReason(selected, kind) {
325
331
  return "selected";
326
332
  return kind === undefined ? "no-dispatch-plan" : "not-registered-for-kind";
327
333
  }
328
- async function fileView(absolute, resolved, homeDir, lspConfig) {
334
+ async function fileView(absolute, resolved, homeDir, workspaceCwd, lspConfig) {
329
335
  const language = resolveLanguage(absolute);
330
336
  const kind = detectFileKind(absolute);
331
337
  const section = lspSectionOf(resolved.value);
@@ -392,6 +398,10 @@ async function fileView(absolute, resolved, homeDir, lspConfig) {
392
398
  id,
393
399
  selected: available.has(id),
394
400
  reason: toolReason(available.has(id), kind),
401
+ resolvedCwd: homeRelativePath(resolveToolCwd("runner", id, absolute, {
402
+ cwd: workspaceCwd,
403
+ suppressTelemetry: true,
404
+ }), homeDir),
395
405
  }));
396
406
  return {
397
407
  path: homeRelativePath(absolute, homeDir),
@@ -26,6 +26,7 @@ import { isTestMode } from "./env-utils.js";
26
26
  import { getGlobalPiLensLogDir } from "./probe-home-state.js";
27
27
  import { getMaxLogSizeMB } from "./log-cleanup.js";
28
28
  import { createNdjsonLogger } from "./ndjson-logger.js";
29
+ import { getTurnId } from "./turn-context.js";
29
30
  export const EXTENSION_LOG_FILE = path.join(getGlobalPiLensLogDir(), "extension.log");
30
31
  const writer = createNdjsonLogger({
31
32
  filePath: EXTENSION_LOG_FILE,
@@ -37,6 +38,7 @@ export function logExtension(entry) {
37
38
  writer.log({
38
39
  ts: new Date().toISOString(),
39
40
  pid: process.pid,
41
+ turnId: getTurnId(),
40
42
  level: entry.level ?? "error",
41
43
  subsystem: entry.subsystem,
42
44
  message: entry.message,
@@ -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/") ||
@@ -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`
@@ -15,7 +15,8 @@ import * as os from "node:os";
15
15
  import * as path from "node:path";
16
16
  import { BoundedLruCache } from "./bounded-cache.js";
17
17
  import { createGenerationSource } from "./generation-guard.js";
18
- import { findNearestMarkerRoot, isRealGitMarker, normalizeMapKey, } from "./path-utils.js";
18
+ import { normalizeMapKey } from "./path-utils.js";
19
+ import { resolveToolCwd } from "./tool-cwd.js";
19
20
  import { resolveCargoPackageEdition } from "./cargo-manifest.js";
20
21
  import { resolveKtfmtGradleStyle } from "./gradle-ktfmt-style.js";
21
22
  import { resolvePhpCsFixerConfig } from "./php-cs-fixer-config.js";
@@ -27,8 +28,10 @@ import { compareOrdinal } from "./string-utils.js";
27
28
  import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
28
29
  import { findGlobalBinary, findLocalBinUpwards, VENDOR_BIN_DIRS, VENV_BIN_DIRS, } from "./package-manager.js";
29
30
  import { safeSpawnAsync } from "./safe-spawn.js";
31
+ import { probeToolAsync } from "./tool-probe.js";
30
32
  import { assertInstallAllowed } from "./project-trust.js";
31
33
  import { tryLazyInstallForFormatter } from "./dispatch/runners/utils/lazy-installer.js";
34
+ import { getToolPath } from "./installer/index.js";
32
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";
33
36
  /**
34
37
  * Lazy-install a formatter's tool, through the shared seam (#1537).
@@ -380,7 +383,7 @@ async function resolveGoFmtBinary() {
380
383
  const inPath = await which("gofmt");
381
384
  if (inPath)
382
385
  return inPath;
383
- const goCheck = await safeSpawnAsync("go", ["env", "GOROOT"], {
386
+ const goCheck = await probeToolAsync("go", ["env", "GOROOT"], {
384
387
  timeout: 5000,
385
388
  });
386
389
  if (goCheck.error || goCheck.status !== 0)
@@ -468,6 +471,22 @@ async function resolveManagedSmartDefaultCommand(formatterName, filePath, args)
468
471
  return null;
469
472
  return [installed, ...args, filePath];
470
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
+ }
471
490
  /**
472
491
  * One entry per formatter that can be selected via explicit project config
473
492
  * (the `formatterPolicy` "explicit-config" branch of `getFormattersForFile`).
@@ -603,109 +622,19 @@ async function indentationArgs(filePath, tool, cwd) {
603
622
  String(indentation.width),
604
623
  ];
605
624
  }
606
- const FORMATTER_MARKERS_BY_NAME = new Map([
607
- ["biome", ["biome.json", "biome.jsonc", "package.json"]],
608
- [
609
- "prettier",
610
- [
611
- ".prettierrc",
612
- ".prettierrc.json",
613
- ".prettierrc.yaml",
614
- ".prettierrc.yml",
615
- ".prettierrc.js",
616
- ".prettierrc.cjs",
617
- ".prettierrc.mjs",
618
- "prettier.config.js",
619
- "prettier.config.cjs",
620
- "prettier.config.mjs",
621
- ".prettierignore",
622
- "package.json",
623
- ],
624
- ],
625
- [
626
- "oxfmt",
627
- ["oxfmt.toml", ".oxfmtrc.json", "vite-plus.json", "package.json"],
628
- ],
629
- ["ruff", ["pyproject.toml", "ruff.toml", ".ruff.toml"]],
630
- ["black", ["pyproject.toml", "black.toml", ".black"]],
631
- ["sqlfluff", [".sqlfluff", "pyproject.toml", "setup.cfg"]],
632
- ["rustfmt", ["rustfmt.toml", ".rustfmt.toml", "Cargo.toml"]],
633
- ["rubocop", [".rubocop.yml", ".rubocop.yaml"]],
634
- ["standardrb", [".standard.yml", ".standard.yaml"]],
635
- ["clang-format", [".clang-format", "_clang-format"]],
636
- ["php-cs-fixer", [".php-cs-fixer.php", ".php-cs-fixer.dist.php"]],
637
- ["stylua", ["stylua.toml", ".stylua.toml"]],
638
- ["ocamlformat", [".ocamlformat"]],
639
- ["google-java-format", [".google-java-format", ".editorconfig"]],
640
- ["cljfmt", [".cljfmt.edn", "cljfmt.edn", ".cljfmt"]],
641
- [
642
- "cmake-format",
643
- [
644
- ".cmake-format",
645
- ".cmake-format.yaml",
646
- ".cmake-format.yml",
647
- ".cmake-format.json",
648
- ".cmake-format.py",
649
- "cmake-format.yaml",
650
- "cmake-format.yml",
651
- ".editorconfig",
652
- ],
653
- ],
654
- [
655
- "psscriptanalyzer-format",
656
- ["PSScriptAnalyzerSettings.psd1", "ScriptAnalyzerSettings.psd1"],
657
- ],
658
- [
659
- "csharpier",
660
- [
661
- ".csharpierrc",
662
- ".csharpierrc.json",
663
- ".csharpierrc.yaml",
664
- ".csharpierrc.yml",
665
- ],
666
- ],
667
- ["ormolu", [".ormolu"]],
668
- ["taplo", ["taplo.toml", ".taplo.toml"]],
669
- ["terraform", [".terraform.lock.hcl"]],
670
- ["swiftformat", [".swiftformat"]],
671
- ["fantomas", [".fantomasignore", ".editorconfig"]],
672
- ["mix", [".formatter.exs"]],
673
- ["shfmt", [".editorconfig"]],
674
- ["ktlint", [".editorconfig"]],
675
- [
676
- "ktfmt",
677
- [
678
- ".editorconfig",
679
- ".ktfmt",
680
- ".ktfmt.kts",
681
- "build.gradle",
682
- "build.gradle.kts",
683
- "settings.gradle",
684
- "settings.gradle.kts",
685
- ],
686
- ],
687
- ]);
688
625
  /**
689
626
  * Resolve the cwd for the formatter child, capped at the user's home
690
627
  * directory. Files outside a project retain the historical file-directory
691
628
  * cwd. The optional homeDir is only for the ceiling regression test.
692
629
  */
693
630
  export function resolveFormatterCwd(absolutePath, formatterName, homeDir = os.homedir()) {
694
- const fileDir = path.dirname(path.resolve(absolutePath));
695
- const effectiveHome = homeDir || undefined;
696
- const markers = formatterName
697
- ? [...(FORMATTER_MARKERS_BY_NAME.get(formatterName) ?? []), ".gitignore"]
698
- : [".gitignore"];
699
- const root = findNearestMarkerRoot(fileDir, markers, {
700
- homeDir: effectiveHome,
701
- });
702
- if (root)
703
- return root;
704
- const gitRoot = findNearestMarkerRoot(fileDir, [".git"], {
705
- homeDir: effectiveHome,
706
- markerPredicate: isRealGitMarker,
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,
707
637
  });
708
- return gitRoot ?? fileDir;
709
638
  }
710
639
  /**
711
640
  * Every biome invocation must carry this. Biome exits 1 with "No files were
@@ -888,11 +817,7 @@ export const oxfmtFormatter = {
888
817
  const found = await which("oxfmt");
889
818
  if (found)
890
819
  return [found, OXFMT_NO_ERROR_ON_UNMATCHED, filePath];
891
- // #2413: neither node_modules/.bin nor PATH has oxfmt, and `detect()` is
892
- // config-only (it never probes the binary), so selection can reach here
893
- // with nothing installed. The static command is bare `oxfmt` — spawning it
894
- // only reproduces the reported `spawn oxfmt ENOENT`. Prove it unavailable.
895
- return FORMATTER_UNAVAILABLE;
820
+ return ((await resolveManagedFormatterCommand("oxfmt", filePath, [OXFMT_NO_ERROR_ON_UNMATCHED], findInNodeModules, cwd)) ?? FORMATTER_UNAVAILABLE);
896
821
  },
897
822
  // Single source of truth: OXFMT_SUPPORTED_EXTENSIONS in tool-policy.ts.
898
823
  // Do not hand-maintain a second copy of this list (#1134 — previously two
@@ -941,21 +866,14 @@ export const ruffFormatter = {
941
866
  return true;
942
867
  // No-config fallback: if Ruff is already available, allow formatter usage.
943
868
  // This keeps Python default behavior consistent with startup defaults.
944
- const { getToolPath } = await import("./installer/index.js");
945
- const installed = await getToolPath("ruff");
946
- return Boolean(installed);
869
+ return Boolean(await getToolPath("ruff"));
947
870
  },
948
871
  };
949
872
  export const blackFormatter = {
950
873
  name: "black",
951
874
  command: ["black", "$FILE"],
952
875
  extensions: [".py", ".pyi"],
953
- async resolveCommand(filePath, cwd) {
954
- const venv = await findInVenv("black", cwd);
955
- if (venv)
956
- return [venv, filePath];
957
- return null;
958
- },
876
+ resolveCommand: managedFormatterResolver("black", [], findInVenv),
959
877
  async detect(cwd) {
960
878
  return hasBlackConfig(cwd);
961
879
  },
@@ -1054,12 +972,7 @@ export const shfmtFormatter = {
1054
972
  ...styleArgs,
1055
973
  ]);
1056
974
  },
1057
- async detect(_cwd) {
1058
- if ((await which("shfmt")) !== null)
1059
- return true;
1060
- const { getToolPath } = await import("./installer/index.js");
1061
- return Boolean(await getToolPath("shfmt"));
1062
- },
975
+ detect: managedToolDetect("shfmt", undefined, async () => (await which("shfmt")) !== null),
1063
976
  };
1064
977
  export const nixfmtFormatter = {
1065
978
  name: "nixfmt",
@@ -1121,12 +1034,7 @@ export const ktlintFormatter = {
1121
1034
  return [inPath, "-F", filePath];
1122
1035
  return resolveManagedSmartDefaultCommand("ktlint", filePath, ["-F"]);
1123
1036
  },
1124
- async detect(_cwd) {
1125
- if ((await which("ktlint")) !== null)
1126
- return true;
1127
- const { getToolPath } = await import("./installer/index.js");
1128
- return Boolean(await getToolPath("ktlint"));
1129
- },
1037
+ detect: managedToolDetect("ktlint", undefined, async () => (await which("ktlint")) !== null),
1130
1038
  };
1131
1039
  export const ktfmtFormatter = {
1132
1040
  name: "ktfmt",
@@ -1256,36 +1164,25 @@ export const phpCsFixerFormatter = {
1256
1164
  const configPath = resolvePhpCsFixerConfig(filePath);
1257
1165
  const binary = (await findInVendorBin("php-cs-fixer", cwd)) ??
1258
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];
1259
1171
  // #2413/#2472 review F4: both probes (vendor/bin, then PATH) have
1260
1172
  // PROVEN the binary is absent — returning `null` here would fall back
1261
1173
  // to the static `command` above, which is the SAME bare
1262
1174
  // `php-cs-fixer` this just failed to find, spawning it only to
1263
1175
  // re-observe the ENOENT already known. Report the proven-missing
1264
1176
  // state instead so `formatFile` skips the wasted spawn.
1265
- if (!binary)
1177
+ if (!resolved)
1266
1178
  return FORMATTER_UNAVAILABLE;
1267
1179
  return configPath
1268
- ? [binary, "fix", "--config", configPath, filePath]
1269
- : [binary, "fix", filePath];
1270
- },
1271
- async detect(cwd) {
1272
- const vendorBin = await findInVendorBin("php-cs-fixer", cwd);
1273
- const globalBin = await which("php-cs-fixer");
1274
- if (!vendorBin && !globalBin)
1275
- return false;
1276
- // Only run if project has explicit config. This is a presence-only
1277
- // climb from the project `cwd` (not necessarily the formatted file's
1278
- // own directory) via this file's own `findUp` — deliberately NOT
1279
- // merged with `resolvePhpCsFixerConfig` above (#2472 AC4): that
1280
- // resolver climbs from the FILE's directory and needs the exact
1281
- // winning path for `--config`, while this only needs a yes/no answer
1282
- // from whatever `cwd` the caller passed. `rustfmtFormatter.detect`
1283
- // keeps the same non-merged shape against `resolveCargoPackageEdition`
1284
- // for the identical reason.
1285
- const configs = [".php-cs-fixer.php", ".php-cs-fixer.dist.php"];
1286
- const found = await findUp(configs, cwd);
1287
- return found.length > 0;
1180
+ ? [resolved, "fix", "--config", configPath, filePath]
1181
+ : [resolved, "fix", filePath];
1288
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")))),
1289
1186
  };
1290
1187
  export const csharpierFormatter = {
1291
1188
  name: "csharpier",
@@ -1301,9 +1198,7 @@ export const csharpierFormatter = {
1301
1198
  }
1302
1199
  // CSharpier 0.x: invoked through the dotnet driver.
1303
1200
  if ((await which("dotnet")) !== null) {
1304
- const legacy = await safeSpawnAsync("dotnet", ["csharpier", "--version"], {
1305
- timeout: 5000,
1306
- });
1201
+ const legacy = await probeToolAsync("dotnet", ["csharpier", "--version"], { timeout: 5000 });
1307
1202
  if (!legacy.error && legacy.status === 0) {
1308
1203
  return ["dotnet", "csharpier", filePath];
1309
1204
  }
@@ -1317,7 +1212,7 @@ export const csharpierFormatter = {
1317
1212
  // … or the legacy dotnet-driver form (CSharpier 0.x).
1318
1213
  if ((await which("dotnet")) === null)
1319
1214
  return false;
1320
- const result = await safeSpawnAsync("dotnet", ["csharpier", "--version"], {
1215
+ const result = await probeToolAsync("dotnet", ["csharpier", "--version"], {
1321
1216
  timeout: 5000,
1322
1217
  });
1323
1218
  return !result.error && result.status === 0;
@@ -1343,23 +1238,8 @@ export const styluaFormatter = {
1343
1238
  name: "stylua",
1344
1239
  command: ["stylua", "$FILE"],
1345
1240
  extensions: [".lua"],
1346
- async resolveCommand(filePath, cwd) {
1347
- // Project binary first (#1731, discipline B): stylua has no pi-lens
1348
- // managed install, so before this the ONLY resolution was a bare
1349
- // `stylua` PATH lookup — a project-local install via npm
1350
- // `@johnnymorganz/stylua` (`node_modules/.bin/stylua`) was invisible.
1351
- const local = findLocalBinUpwards("stylua", cwd);
1352
- return local ? [local, filePath] : null;
1353
- },
1354
- async detect(cwd) {
1355
- const local = findLocalBinUpwards("stylua", cwd);
1356
- if (!local && (await which("stylua")) === null)
1357
- return false;
1358
- // Prefer explicit config but also run if binary is present in a Lua project
1359
- const configs = ["stylua.toml", ".stylua.toml"];
1360
- const found = await findUp(configs, cwd);
1361
- return found.length > 0;
1362
- },
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")))),
1363
1243
  };
1364
1244
  export const ormoluFormatter = {
1365
1245
  name: "ormolu",
@@ -1379,42 +1259,28 @@ export const taploFormatter = {
1379
1259
  return [inPath, "fmt", filePath];
1380
1260
  return resolveManagedSmartDefaultCommand("taplo", filePath, ["fmt"]);
1381
1261
  },
1382
- async detect(_cwd) {
1383
- if ((await which("taplo")) !== null)
1384
- return true;
1385
- const { getToolPath } = await import("./installer/index.js");
1386
- return Boolean(await getToolPath("taplo"));
1387
- },
1262
+ detect: managedToolDetect("taplo", undefined, async () => (await which("taplo")) !== null),
1388
1263
  };
1389
1264
  export const googleJavaFormatFormatter = {
1390
1265
  name: "google-java-format",
1391
1266
  command: ["google-java-format", "--replace", "$FILE"],
1392
1267
  extensions: [".java"],
1393
- async detect(cwd) {
1394
- if ((await which("google-java-format")) === null)
1395
- return false;
1396
- return hasGoogleJavaFormatConfig(cwd);
1397
- },
1268
+ resolveCommand: managedFormatterResolver("google-java-format", ["--replace"]),
1269
+ detect: managedToolDetect("google-java-format", hasGoogleJavaFormatConfig, async () => (await which("google-java-format")) !== null),
1398
1270
  };
1399
1271
  export const cljfmtFormatter = {
1400
1272
  name: "cljfmt",
1401
1273
  command: ["cljfmt", "fix", "$FILE"],
1402
1274
  extensions: [".clj", ".cljc", ".cljs"],
1403
- async detect(cwd) {
1404
- if ((await which("cljfmt")) === null)
1405
- return false;
1406
- return hasCljfmtConfig(cwd);
1407
- },
1275
+ resolveCommand: managedFormatterResolver("cljfmt", ["fix"]),
1276
+ detect: managedToolDetect("cljfmt", hasCljfmtConfig, async () => (await which("cljfmt")) !== null),
1408
1277
  };
1409
1278
  export const cmakeFormatFormatter = {
1410
1279
  name: "cmake-format",
1411
1280
  command: ["cmake-format", "-i", "$FILE"],
1412
1281
  extensions: [".cmake"],
1413
- async detect(cwd) {
1414
- if ((await which("cmake-format")) === null)
1415
- return false;
1416
- return hasCmakeFormatConfig(cwd);
1417
- },
1282
+ resolveCommand: managedFormatterResolver("cmake-format", ["-i"]),
1283
+ detect: managedToolDetect("cmake-format", hasCmakeFormatConfig, async () => (await which("cmake-format")) !== null),
1418
1284
  };
1419
1285
  export const cueFormatter = {
1420
1286
  name: "cue",
@@ -1487,7 +1353,7 @@ export const psscriptanalyzerFormatFormatter = {
1487
1353
  if (!pwsh)
1488
1354
  return false;
1489
1355
  // Check PSScriptAnalyzer module is available
1490
- const result = await safeSpawnAsync(pwsh, [
1356
+ const result = await probeToolAsync(pwsh, [
1491
1357
  "-NoProfile",
1492
1358
  "-Command",
1493
1359
  "Get-Module -ListAvailable PSScriptAnalyzer | Select-Object -First 1 -ExpandProperty Name",
@@ -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
  },
@@ -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";