pi-lens 4.1.5 → 4.2.0

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 (219) hide show
  1. package/CHANGELOG.md +415 -0
  2. package/README.md +13 -8
  3. package/config/dependency-cruiser-eager-allowlist.json +6 -1
  4. package/dist/clients/actionable-warnings.js +30 -4
  5. package/dist/clients/agent-behavior-client.js +2 -0
  6. package/dist/clients/analysed-root.js +1 -0
  7. package/dist/clients/ast-grep-tool-logger.js +1 -1
  8. package/dist/clients/bash-file-access.js +26 -0
  9. package/dist/clients/biome-client.js +13 -2
  10. package/dist/clients/blocker-freshness.js +403 -35
  11. package/dist/clients/bootstrap.js +6 -1
  12. package/dist/clients/bounded-telemetry.js +2 -0
  13. package/dist/clients/cache-observability.js +122 -1
  14. package/dist/clients/cascade-format.js +200 -4
  15. package/dist/clients/code-quality-warnings.js +10 -2
  16. package/dist/clients/complexity-client.js +5 -0
  17. package/dist/clients/config-diagnostic-codes.js +2 -0
  18. package/dist/clients/config-resolve.js +6 -3
  19. package/dist/clients/config-schema.js +17 -0
  20. package/dist/clients/config-warn.js +2 -2
  21. package/dist/clients/dead-code-client.js +8 -1
  22. package/dist/clients/degradation-ledger.js +16 -1
  23. package/dist/clients/dependency-checker.js +5 -1
  24. package/dist/clients/diagnostic-dispositions.js +12 -1
  25. package/dist/clients/dispatch/ast-grep-catalog.js +65 -0
  26. package/dist/clients/dispatch/auxiliary-lsp.js +28 -0
  27. package/dist/clients/dispatch/dispatcher.js +47 -17
  28. package/dist/clients/dispatch/facts/function-facts.js +1 -1
  29. package/dist/clients/dispatch/facts/import-facts.js +5 -1
  30. package/dist/clients/dispatch/finding-policy.js +222 -0
  31. package/dist/clients/dispatch/indent-detect.js +389 -13
  32. package/dist/clients/dispatch/integration.js +175 -25
  33. package/dist/clients/dispatch/rule-ignores.js +62 -0
  34. package/dist/clients/dispatch/runners/actionlint.js +2 -1
  35. package/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
  36. package/dist/clients/dispatch/runners/biome-check.js +8 -4
  37. package/dist/clients/dispatch/runners/cpp-check.js +4 -3
  38. package/dist/clients/dispatch/runners/credo.js +2 -1
  39. package/dist/clients/dispatch/runners/cue-vet.js +2 -1
  40. package/dist/clients/dispatch/runners/dart-analyze.js +2 -1
  41. package/dist/clients/dispatch/runners/detekt.js +2 -1
  42. package/dist/clients/dispatch/runners/dotnet-build.js +2 -1
  43. package/dist/clients/dispatch/runners/elixir-check.js +2 -1
  44. package/dist/clients/dispatch/runners/eslint.js +2 -1
  45. package/dist/clients/dispatch/runners/fish-indent.js +2 -1
  46. package/dist/clients/dispatch/runners/gleam-check.js +2 -1
  47. package/dist/clients/dispatch/runners/go-vet.js +2 -1
  48. package/dist/clients/dispatch/runners/golangci-lint.js +2 -1
  49. package/dist/clients/dispatch/runners/hadolint.js +2 -1
  50. package/dist/clients/dispatch/runners/helm-lint.js +3 -1
  51. package/dist/clients/dispatch/runners/helm-render.js +2 -1
  52. package/dist/clients/dispatch/runners/htmlhint.js +2 -1
  53. package/dist/clients/dispatch/runners/javac.js +2 -1
  54. package/dist/clients/dispatch/runners/ktlint.js +2 -1
  55. package/dist/clients/dispatch/runners/lsp.js +19 -1
  56. package/dist/clients/dispatch/runners/markdownlint.js +3 -2
  57. package/dist/clients/dispatch/runners/mypy.js +2 -1
  58. package/dist/clients/dispatch/runners/oxlint.js +2 -1
  59. package/dist/clients/dispatch/runners/php-lint.js +2 -1
  60. package/dist/clients/dispatch/runners/phpstan.js +2 -1
  61. package/dist/clients/dispatch/runners/prisma-validate.js +2 -1
  62. package/dist/clients/dispatch/runners/psscriptanalyzer.js +12 -12
  63. package/dist/clients/dispatch/runners/pyright.js +2 -1
  64. package/dist/clients/dispatch/runners/rubocop.js +2 -1
  65. package/dist/clients/dispatch/runners/ruff.js +2 -2
  66. package/dist/clients/dispatch/runners/rust-clippy.js +12 -11
  67. package/dist/clients/dispatch/runners/shellcheck.js +2 -1
  68. package/dist/clients/dispatch/runners/shfmt.js +4 -13
  69. package/dist/clients/dispatch/runners/spellcheck.js +2 -2
  70. package/dist/clients/dispatch/runners/spotbugs.js +2 -1
  71. package/dist/clients/dispatch/runners/sqlfluff.js +3 -2
  72. package/dist/clients/dispatch/runners/stylelint.js +3 -2
  73. package/dist/clients/dispatch/runners/swiftlint.js +2 -1
  74. package/dist/clients/dispatch/runners/taplo.js +2 -1
  75. package/dist/clients/dispatch/runners/terragrunt.js +2 -1
  76. package/dist/clients/dispatch/runners/tflint.js +2 -1
  77. package/dist/clients/dispatch/runners/tree-sitter.js +2 -21
  78. package/dist/clients/dispatch/runners/trivy-config.js +2 -1
  79. package/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
  80. package/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
  81. package/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
  82. package/dist/clients/dispatch/runners/vale.js +2 -1
  83. package/dist/clients/dispatch/runners/yamllint.js +4 -3
  84. package/dist/clients/dispatch/runners/zig-check.js +2 -1
  85. package/dist/clients/dispatch/types.js +4 -1
  86. package/dist/clients/disposition-publish.js +1 -0
  87. package/dist/clients/effective-config.js +12 -2
  88. package/dist/clients/extension-log.js +2 -0
  89. package/dist/clients/file-kinds.js +26 -1
  90. package/dist/clients/file-role.js +23 -6
  91. package/dist/clients/file-utils.js +156 -10
  92. package/dist/clients/finding-delivery-gate.js +60 -9
  93. package/dist/clients/format-service.js +39 -21
  94. package/dist/clients/formatters.js +113 -200
  95. package/dist/clients/generation-guard.js +6 -2
  96. package/dist/clients/git-guard.js +11 -3
  97. package/dist/clients/gitleaks-client.js +42 -6
  98. package/dist/clients/govulncheck-client.js +48 -7
  99. package/dist/clients/indent-retarget.js +78 -8
  100. package/dist/clients/installer/index.js +524 -161
  101. package/dist/clients/instance-reaper-state.js +7 -0
  102. package/dist/clients/instance-reaper.js +20 -10
  103. package/dist/clients/jscpd-client.js +9 -1
  104. package/dist/clients/knip-client.js +9 -2
  105. package/dist/clients/language-profile.js +35 -3
  106. package/dist/clients/language-registry.js +5 -1
  107. package/dist/clients/latency-logger.js +6 -1
  108. package/dist/clients/lens-config.js +4 -0
  109. package/dist/clients/lens-engine.js +12 -3
  110. package/dist/clients/lens-events.js +2 -1
  111. package/dist/clients/lens-flag-registry.js +79 -0
  112. package/dist/clients/live-bus-emitter.js +2 -1
  113. package/dist/clients/lsp/client.js +25 -21
  114. package/dist/clients/lsp/config.js +36 -7
  115. package/dist/clients/lsp/diagnostic-binding.js +24 -1
  116. package/dist/clients/lsp/document-drift.js +78 -0
  117. package/dist/clients/lsp/index.js +288 -45
  118. package/dist/clients/lsp/launch.js +16 -4
  119. package/dist/clients/lsp/server.js +79 -15
  120. package/dist/clients/lsp/workspace-diagnostics-cache.js +39 -5
  121. package/dist/clients/lsp-mutation.js +5 -3
  122. package/dist/clients/map-with-concurrency.js +12 -8
  123. package/dist/clients/mcp/analyze.js +3 -0
  124. package/dist/clients/mcp/session.js +38 -4
  125. package/dist/clients/ndjson-logger.js +24 -7
  126. package/dist/clients/observed-mutation.js +40 -2
  127. package/dist/clients/opaque-mutation-scan.js +23 -3
  128. package/dist/clients/opengrep-client.js +111 -14
  129. package/dist/clients/package-manager.js +2 -1
  130. package/dist/clients/path-utils.js +123 -3
  131. package/dist/clients/performance-report.js +5 -1
  132. package/dist/clients/php-cs-fixer-config.js +8 -3
  133. package/dist/clients/pipeline.js +77 -7
  134. package/dist/clients/project-diagnostics/cache.js +37 -5
  135. package/dist/clients/project-diagnostics/fresh-fetch.js +173 -18
  136. package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
  137. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
  138. package/dist/clients/project-diagnostics/scanner.js +59 -3
  139. package/dist/clients/project-lens-config.js +48 -2
  140. package/dist/clients/read-guard-logger.js +2 -0
  141. package/dist/clients/read-guard.js +70 -11
  142. package/dist/clients/resource-sampler.js +147 -9
  143. package/dist/clients/review-graph-logger.js +2 -0
  144. package/dist/clients/ruff-client.js +18 -9
  145. package/dist/clients/runtime-agent-end.js +42 -5
  146. package/dist/clients/runtime-context.js +6 -2
  147. package/dist/clients/runtime-coordinator.js +82 -7
  148. package/dist/clients/runtime-session.js +33 -5
  149. package/dist/clients/runtime-tool-call.js +43 -12
  150. package/dist/clients/runtime-tool-result.js +334 -86
  151. package/dist/clients/runtime-turn.js +195 -18
  152. package/dist/clients/safe-spawn.js +258 -5
  153. package/dist/clients/scratch-tree-policy.js +6 -0
  154. package/dist/clients/security-scan-client.js +2 -2
  155. package/dist/clients/session-event-guard.js +119 -6
  156. package/dist/clients/sg-runner.js +25 -12
  157. package/dist/clients/sgconfig.js +79 -2
  158. package/dist/clients/situational-tool-telemetry.js +108 -0
  159. package/dist/clients/test-runner-client.js +258 -78
  160. package/dist/clients/test-runner-delivery.js +43 -0
  161. package/dist/clients/tool-agreement.js +423 -0
  162. package/dist/clients/tool-config.js +249 -0
  163. package/dist/clients/tool-cwd.js +270 -0
  164. package/dist/clients/tool-policy.js +163 -7
  165. package/dist/clients/tool-probe.js +53 -0
  166. package/dist/clients/tool-set-policy.js +42 -0
  167. package/dist/clients/tree-sitter-client.js +120 -0
  168. package/dist/clients/tree-sitter-query-loader.js +127 -189
  169. package/dist/clients/trivy-client.js +9 -1
  170. package/dist/clients/turn-context.js +52 -0
  171. package/dist/clients/widget-state.js +102 -3
  172. package/dist/clients/word-index.js +5 -2
  173. package/dist/clients/write-ordering-guard.js +7 -0
  174. package/dist/index.js +26968 -21741
  175. package/dist/mcp/analyze-cli.js +3 -2
  176. package/dist/mcp/server.js +140 -161
  177. package/dist/tools/activate-tools.js +10 -7
  178. package/dist/tools/ast-grep-outline.js +2 -13
  179. package/dist/tools/ast-grep-replace.js +8 -13
  180. package/dist/tools/ast-grep-search.js +83 -39
  181. package/dist/tools/effective-config.js +4 -3
  182. package/dist/tools/lens-diagnostic-mark.js +8 -11
  183. package/dist/tools/lens-diagnostics.js +516 -133
  184. package/dist/tools/lsp-diagnostics.js +265 -182
  185. package/dist/tools/lsp-navigation.js +20 -44
  186. package/dist/tools/module-report.js +5 -9
  187. package/dist/tools/project-report.js +9 -17
  188. package/dist/tools/render-compact.js +280 -1
  189. package/dist/tools/symbol-search.js +2 -2
  190. package/docs/agent-guide.md +3 -4
  191. package/docs/agent-tools.md +46 -22
  192. package/docs/audit1.md +5 -5
  193. package/docs/configuration.md +9 -0
  194. package/docs/dependencies.md +11 -0
  195. package/docs/dispositions.md +52 -0
  196. package/docs/environment-variables.md +2 -7
  197. package/docs/features.md +7 -7
  198. package/docs/globalconfig.md +14 -2
  199. package/docs/language-coverage.md +1 -0
  200. package/docs/mcp.md +13 -1
  201. package/docs/pi-lens-fixer.md +48 -18
  202. package/docs/pi-lens-investigator.md +38 -18
  203. package/docs/pi-lens-monitor.md +88 -0
  204. package/docs/pi-lens-retro.md +97 -0
  205. package/docs/pi-lens-reviewer.md +53 -31
  206. package/docs/pi-lens-subagent.md +12 -4
  207. package/docs/public-api-stability.md +1 -0
  208. package/docs/real-harness.md +46 -0
  209. package/docs/release-qa-baseline.md +27 -1
  210. package/docs/servercapabilities.md +1 -6
  211. package/docs/settings.md +44 -6
  212. package/docs/tools_improvement2.md +4 -4
  213. package/docs/usage.md +15 -3
  214. package/package.json +10 -1
  215. package/rules/tree-sitter-queries/typescript/sql-injection.yml +23 -7
  216. package/scripts/analyze-pi-lens-logs.mjs +40 -0
  217. package/skills/pi-lens-ast-grep/SKILL.md +23 -6
  218. package/skills/pi-lens-lsp-navigation/SKILL.md +26 -9
  219. package/dist/tools/ast-dump.js +0 -103
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import { safeSpawnAsync } from "../../safe-spawn.js";
22
22
  import { PRIORITY } from "../priorities.js";
23
- import { createAvailabilityChecker } from "./utils/runner-helpers.js";
23
+ import { createAvailabilityChecker, resolveRunnerCwd, } from "./utils/runner-helpers.js";
24
24
  import { parseToolRun } from "./utils/tool-failure.js";
25
25
  const typos = createAvailabilityChecker("typos", ".exe");
26
26
  // typos-cli exit codes: 0 = no typos, 2 = typos found, 1 = an error that
@@ -82,7 +82,7 @@ const spellcheckRunner = {
82
82
  priority: PRIORITY.DOC_QUALITY,
83
83
  skipTestFiles: false, // Check docs in test files too
84
84
  async run(ctx) {
85
- const cwd = ctx.cwd || process.cwd();
85
+ const cwd = resolveRunnerCwd(ctx, "spellcheck/typos");
86
86
  // Skip if typos-cli is not installed
87
87
  if (!(await typos.isAvailableAsync(cwd))) {
88
88
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -3,6 +3,7 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { BoundedLruCache } from "../../bounded-cache.js";
5
5
  import { safeSpawnAsync } from "../../safe-spawn.js";
6
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
6
7
  import { findCompiledClassesDir } from "../../tool-policy.js";
7
8
  import { PRIORITY } from "../priorities.js";
8
9
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
@@ -191,7 +192,7 @@ const spotbugsRunner = {
191
192
  appliesTo: ["java", "kotlin"],
192
193
  priority: PRIORITY.DEEP_LANGUAGE_ANALYSIS,
193
194
  async run(ctx) {
194
- const cwd = ctx.cwd || process.cwd();
195
+ const cwd = resolveRunnerCwd(ctx, "spotbugs");
195
196
  const classesDir = findCompiledClassesDir(cwd);
196
197
  if (!classesDir) {
197
198
  ctx.log?.("spotbugs: Java project detected but no compiled .class files found — run `mvn compile` / `gradle build` first");
@@ -1,4 +1,5 @@
1
1
  import { safeSpawnAsync } from "../../safe-spawn.js";
2
+ import { logRunnerAdvisoryOnce, resolveRunnerCwd } from "../../tool-cwd.js";
2
3
  import { getLinterPolicyForCwd, hasSqlfluffConfig } from "../../tool-policy.js";
3
4
  import { PRIORITY } from "../priorities.js";
4
5
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
@@ -117,14 +118,14 @@ const sqlfluffRunner = {
117
118
  priority: PRIORITY.SQL_LINT,
118
119
  skipTestFiles: false,
119
120
  async run(ctx) {
120
- const cwd = ctx.cwd || process.cwd();
121
+ const cwd = resolveRunnerCwd(ctx, "sqlfluff");
121
122
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
122
123
  if (policy && !policy.preferredRunners.includes("sqlfluff")) {
123
124
  return { status: "skipped", diagnostics: [], semantic: "none" };
124
125
  }
125
126
  const hasConfig = hasSqlfluffConfig(cwd);
126
127
  if (!hasConfig) {
127
- ctx.log("sqlfluff: no config detected, using ANSI dialect defaults");
128
+ logRunnerAdvisoryOnce(ctx, "sqlfluff", cwd, "sqlfluff: no config detected, using ANSI dialect defaults");
128
129
  }
129
130
  let cmd = null;
130
131
  if (await sqlfluff.isAvailableAsync(cwd)) {
@@ -1,5 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
+ import { logRunnerAdvisoryOnce, resolveRunnerCwd } from "../../tool-cwd.js";
3
4
  import { getLinterPolicyForCwd, hasStylelintConfig, } from "../../tool-policy.js";
4
5
  import { PRIORITY } from "../priorities.js";
5
6
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
@@ -116,7 +117,7 @@ const stylelintRunner = {
116
117
  priority: PRIORITY.FORMAT_AND_LINT_PRIMARY,
117
118
  skipTestFiles: false,
118
119
  async run(ctx) {
119
- const cwd = ctx.cwd || process.cwd();
120
+ const cwd = resolveRunnerCwd(ctx, "stylelint");
120
121
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
121
122
  if (policy && !policy.preferredRunners.includes("stylelint")) {
122
123
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -124,7 +125,7 @@ const stylelintRunner = {
124
125
  const fileDir = path.dirname(path.resolve(cwd, ctx.filePath));
125
126
  const hasConfig = hasStylelintConfig(fileDir) || hasStylelintConfig(cwd);
126
127
  if (!hasConfig) {
127
- ctx.log("stylelint: no config detected, running with default rules");
128
+ logRunnerAdvisoryOnce(ctx, "stylelint", cwd, "stylelint: no config detected, running with default rules");
128
129
  }
129
130
  let cmd = null;
130
131
  if (await stylelint.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 { PRIORITY } from "../priorities.js";
4
5
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
5
6
  import { parseToolRun } from "./utils/tool-failure.js";
@@ -115,7 +116,7 @@ const swiftlintRunner = {
115
116
  priority: PRIORITY.GENERAL_ANALYSIS,
116
117
  skipTestFiles: false,
117
118
  async run(ctx) {
118
- const cwd = ctx.cwd || process.cwd();
119
+ const cwd = resolveRunnerCwd(ctx, "swiftlint");
119
120
  let cmd = null;
120
121
  if (await swiftlint.isAvailableAsync(cwd)) {
121
122
  cmd = swiftlint.getCommand(cwd);
@@ -1,6 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import { findLocalBinUpwards } from "../../package-manager.js";
3
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
4
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
4
5
  import { getLinterPolicyForCwd } from "../../tool-policy.js";
5
6
  import { PRIORITY } from "../priorities.js";
6
7
  import { createAvailabilityChecker, lspPrimaryCoversFile, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
@@ -106,7 +107,7 @@ const taploRunner = {
106
107
  priority: PRIORITY.FORMAT_AND_LINT_PRIMARY,
107
108
  skipTestFiles: false,
108
109
  async run(ctx) {
109
- const cwd = ctx.cwd || process.cwd();
110
+ const cwd = resolveRunnerCwd(ctx, "taplo");
110
111
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
111
112
  if (policy && !policy.preferredRunners.includes("taplo")) {
112
113
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -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 { pathsEqual } from "../../path-utils.js";
4
5
  import { getLinterPolicyForCwd } from "../../tool-policy.js";
5
6
  import { PRIORITY } from "../priorities.js";
@@ -128,7 +129,7 @@ const terragruntRunner = {
128
129
  priority: PRIORITY.GENERAL_ANALYSIS,
129
130
  skipTestFiles: false,
130
131
  async run(ctx) {
131
- const cwd = ctx.cwd || process.cwd();
132
+ const cwd = resolveRunnerCwd(ctx, "terragrunt");
132
133
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
133
134
  if (policy && !policy.preferredRunners.includes("terragrunt"))
134
135
  return SKIPPED;
@@ -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 { getLinterPolicyForCwd } from "../../tool-policy.js";
4
5
  import { findNearestDirWithAnyBasename } from "../../workspace-topology.js";
5
6
  import { PRIORITY } from "../priorities.js";
@@ -52,7 +53,7 @@ const tflintRunner = {
52
53
  priority: PRIORITY.GENERAL_ANALYSIS,
53
54
  skipTestFiles: false,
54
55
  async run(ctx) {
55
- const cwd = ctx.cwd || process.cwd();
56
+ const cwd = resolveRunnerCwd(ctx, "tflint");
56
57
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
57
58
  if (policy && !policy.preferredRunners.includes("tflint")) {
58
59
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import * as path from "node:path";
8
8
  import { RuleCache } from "../../cache/rule-cache.js";
9
- import { minimatch } from "../../deps/minimatch.js";
9
+ import { isRuleIgnoredForPath } from "../rule-ignores.js";
10
10
  import { isTestFile } from "../../file-utils.js";
11
11
  import { buildOrUpdateGraph, computeImpactCascade, recordEntitySnapshotDiff, } from "../../review-graph/service.js";
12
12
  import { getSharedTreeSitterClient, isTreeSitterWasmAborted, markTreeSitterWasmAborted, resolveTreeSitterLanguage, } from "../../tree-sitter-shared.js";
@@ -292,25 +292,6 @@ function getTotalLinesChanged(ranges) {
292
292
  }
293
293
  /** Threshold: skip entity extraction for changes under 5 lines */
294
294
  const ENTITY_EXTRACTION_LINE_THRESHOLD = 5;
295
- /**
296
- * `filePath` relative to `root`, forward-slashed, for matching a rule's
297
- * `ignore_paths` globs. Falls back to the absolute (slash-normalized) path
298
- * when `filePath` isn't under `root` (e.g. an out-of-tree temp file), so a
299
- * glob like `scripts/**` simply never matches rather than throwing.
300
- */
301
- function relativeForIgnoreGlob(filePath, root) {
302
- const rel = path.relative(root, filePath);
303
- const normalized = (rel.startsWith("..") ? filePath : rel)
304
- .split(path.sep)
305
- .join("/");
306
- return normalized;
307
- }
308
- function matchesIgnorePaths(filePath, root, patterns) {
309
- if (!patterns || patterns.length === 0)
310
- return false;
311
- const rel = relativeForIgnoreGlob(filePath, root);
312
- return patterns.some((pattern) => minimatch(rel, pattern, { dot: true }));
313
- }
314
295
  const treeSitterRunner = {
315
296
  id: "tree-sitter",
316
297
  appliesTo: [
@@ -447,7 +428,7 @@ const treeSitterRunner = {
447
428
  const effectiveQueries = (ctx.blockingOnly
448
429
  ? languageQueries.filter((q) => q.inline_tier !== "review")
449
430
  : languageQueries).filter((q) => !(fileIsTest && q.skip_test_files) &&
450
- !matchesIgnorePaths(filePath, ignoreRoot, q.ignore_paths));
431
+ !isRuleIgnoredForPath(filePath, ignoreRoot, q.ignore_paths));
451
432
  logTreeSitter({
452
433
  phase: "queries_loaded",
453
434
  filePath,
@@ -40,6 +40,7 @@ import * as fs from "node:fs";
40
40
  import * as path from "node:path";
41
41
  import { incrementDegradationCount } from "../../degradation-ledger.js";
42
42
  import { safeSpawnAsync } from "../../safe-spawn.js";
43
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
43
44
  import { isTrivyEnabled, resolveSeverityFloor, } from "../../trivy-client.js";
44
45
  import { PRIORITY } from "../priorities.js";
45
46
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
@@ -148,7 +149,7 @@ const trivyConfigRunner = {
148
149
  priority: PRIORITY.GENERAL_ANALYSIS,
149
150
  skipTestFiles: false,
150
151
  async run(ctx) {
151
- const cwd = ctx.cwd || process.cwd();
152
+ const cwd = resolveRunnerCwd(ctx, "trivy-config");
152
153
  // Single opt-in switch — trivy is opt-in across all modes.
153
154
  if (!isTrivyEnabled(cwd)) {
154
155
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -75,6 +75,11 @@ export function describeInstallAttempt(attempt, options = {}) {
75
75
  return { install: "succeeded", ...(reason && { installReason: reason }) };
76
76
  case "failed":
77
77
  return { install: "failed", ...(reason && { installReason: reason }) };
78
+ case "unavailable":
79
+ return {
80
+ install: "unavailable",
81
+ ...(reason && { installReason: reason }),
82
+ };
78
83
  default:
79
84
  return {
80
85
  install: "not-attempted",
@@ -11,7 +11,7 @@
11
11
  * Complexity limit. This is that sweep, written once.
12
12
  */
13
13
  import * as fs from "node:fs";
14
- import { safeSpawnAsync } from "../../../safe-spawn.js";
14
+ import { probeToolAsync } from "../../../tool-probe.js";
15
15
  import { classifyProbeFailure, startHostStallSampler, } from "./availability-policy.js";
16
16
  /**
17
17
  * Walk `candidates` in order. A candidate containing a path separator is
@@ -45,7 +45,7 @@ export async function probeAvailabilityCandidates(candidates, probeArgs, timeout
45
45
  let result;
46
46
  let stallMs;
47
47
  try {
48
- result = await safeSpawnAsync(candidate, [...probeArgs], {
48
+ result = await probeToolAsync(candidate, probeArgs, {
49
49
  timeout: timeoutMs,
50
50
  });
51
51
  }
@@ -20,9 +20,11 @@ import { getServersForFileWithConfig, isServerDisabled, } from "../../../lsp/con
20
20
  import { findGlobalBinary, findLocalBinAt, findLocalBinsUpwards, findLocalBinUpwards, VENDOR_BIN_DIRS, VENV_BIN_DIRS, } from "../../../package-manager.js";
21
21
  import { logLatency } from "../../../latency-logger.js";
22
22
  import { safeSpawnAsync } from "../../../safe-spawn.js";
23
+ import { probeToolAsync } from "../../../tool-probe.js";
23
24
  import { compareOrdinal } from "../../../string-utils.js";
24
25
  import { getToolCommandSpec, shouldAutoInstallTool, } from "../../../tool-policy.js";
25
26
  import { isInSpawnTimeoutCooldown } from "../../../spawn-timeout-cooldown.js";
27
+ import { resolveToolCwd } from "../../../tool-cwd.js";
26
28
  import { createAvailabilityProbeFlight } from "../../../availability-probe-flight.js";
27
29
  import { classifyProbeFailure, createAvailabilityLatch, describeInstallAttempt, describeProbeEvidence, isLatchingOutcome, logAvailabilityDecision, startHostStallSampler, transientRetryDelayMs, } from "./availability-policy.js";
28
30
  export { createAvailabilityLatch, classifyProbeFailure, describeProbeEvidence, describeUnavailability, isTransientDecision, logAvailabilityDecision, startHostStallSampler, } from "./availability-policy.js";
@@ -1390,7 +1392,7 @@ async function probeAstGrepCommandAsync(cmd, argsPrefix = []) {
1390
1392
  let check;
1391
1393
  let hostStallMs;
1392
1394
  try {
1393
- check = await safeSpawnAsync(cmd, [...argsPrefix, "--version"], {
1395
+ check = await probeToolAsync(cmd, [...argsPrefix, "--version"], {
1394
1396
  timeout: 5000,
1395
1397
  });
1396
1398
  }
@@ -1674,7 +1676,7 @@ export async function resolveLocalFirstAsync(toolName, cwd, windowsExt = ".cmd")
1674
1676
  if (globalBin)
1675
1677
  return { cmd: globalBin, args: [] };
1676
1678
  // 3. Global PATH (already installed system-wide, on PATH)
1677
- const globalCheck = await safeSpawnAsync(toolName, ["--version"], {
1679
+ const globalCheck = await probeToolAsync(toolName, ["--version"], {
1678
1680
  timeout: 3000,
1679
1681
  });
1680
1682
  if (!globalCheck.error && globalCheck.status === 0) {
@@ -1693,3 +1695,7 @@ export const sg = {
1693
1695
  isAvailableAsync: isSgAvailableAsync,
1694
1696
  getCommand: getSgCommand,
1695
1697
  };
1698
+ /** Shared cwd seam for runner probes and analysis children (#2777). */
1699
+ export function resolveRunnerCwd(ctx, tool) {
1700
+ return resolveToolCwd("runner", tool, ctx.filePath, ctx).cwd;
1701
+ }
@@ -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
  }
@@ -11,7 +11,10 @@
11
11
  * The dispatcher must handle these semantics consistently.
12
12
  */
13
13
  /** Closed telemetry taxonomy for expected runner skips. */
14
- export const RUNNER_SKIP_REASONS = ["no-files-matched"];
14
+ export const RUNNER_SKIP_REASONS = [
15
+ "no-files-matched",
16
+ "configured-non-biome-linter",
17
+ ];
15
18
  /** Runtime guard for untyped/plugin-provided runner results. */
16
19
  export function isRunnerSkipReason(value) {
17
20
  return RUNNER_SKIP_REASONS.some((reason) => reason === value);
@@ -97,6 +97,7 @@ export function publishDisposition(args) {
97
97
  line: args.line,
98
98
  anchor: args.anchor,
99
99
  reason: args.reason,
100
+ message: args.message,
100
101
  };
101
102
  busEmit(BUS_DISPOSITION_EVENT, payload);
102
103
  hasLoggedFailure = false;
@@ -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
+ }).cwd, 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,
@@ -4,7 +4,10 @@
4
4
  * Centralized file type detection to avoid duplication across clients.
5
5
  * Maps file extensions and paths to semantic file kinds.
6
6
  */
7
- import { basename, extname } from "node:path";
7
+ import { basename, extname, win32 } from "node:path";
8
+ import * as path from "node:path";
9
+ import { isWindowsPath } from "./path-utils.js";
10
+ import { findNearestDirWithMarker } from "./workspace-topology.js";
8
11
  // --- Extension Maps ---
9
12
  export const KIND_EXTENSIONS = {
10
13
  clojure: [".clj", ".cljc", ".cljs", ".edn"],
@@ -40,6 +43,9 @@ export const KIND_EXTENSIONS = {
40
43
  ".cu",
41
44
  // HIP
42
45
  ".hip",
46
+ // CUDA/HIP header convention; clang's driver table omits `.cuh`, but
47
+ // projects use it for C++ headers that must remain in the cxx kind (#2986).
48
+ ".cuh",
43
49
  // Objective-C
44
50
  ".m",
45
51
  ".mm",
@@ -179,6 +185,22 @@ export const SPECIAL_FILENAMES = [
179
185
  })),
180
186
  ];
181
187
  // --- Detection Functions ---
188
+ /**
189
+ * Helm renders YAML-looking files as Go templates before they become YAML.
190
+ * Require the shared chart-root marker before promoting a YAML template, so
191
+ * ordinary repositories can use a `templates/` directory without entering
192
+ * Helm dispatch. `.tpl` remains extension-classified below.
193
+ */
194
+ export function isHelmYamlTemplatePath(filePath) {
195
+ const templatesSegment = isWindowsPath(filePath)
196
+ ? /[\\/]templates[\\/]/i
197
+ : /\/templates\//i;
198
+ if (!templatesSegment.test(filePath) || !/\.ya?ml$/i.test(filePath)) {
199
+ return false;
200
+ }
201
+ const pathApi = isWindowsPath(filePath) ? win32 : path;
202
+ return Boolean(findNearestDirWithMarker(pathApi.dirname(filePath), "chartYamlPath"));
203
+ }
182
204
  /**
183
205
  * Detect the file kind from a file path.
184
206
  * Returns the semantic file kind or undefined if unknown.
@@ -187,6 +209,9 @@ export function detectFileKind(filePath) {
187
209
  if (!filePath || typeof filePath !== "string") {
188
210
  return undefined;
189
211
  }
212
+ if (isHelmYamlTemplatePath(filePath)) {
213
+ return "helm-template";
214
+ }
190
215
  // Check special filenames first
191
216
  const base = basename(filePath);
192
217
  for (const { pattern, kind } of SPECIAL_FILENAMES) {
@@ -34,6 +34,24 @@ function isTypeStub(content) {
34
34
  /^export\s+type\s+\{/.test(l));
35
35
  return typeOnlyLines.length / lines.length >= 0.75;
36
36
  }
37
+ // Shared filename predicate (#2928). The parity test iterates the runner's
38
+ // discovery table; additional conventions are pinned in file-role.test.ts.
39
+ // Keep the case-preserved basename for CamelCase suffixes (contest.java is source).
40
+ function testFileNameMatches(rawBase, base) {
41
+ return (base.includes(".test.") ||
42
+ base.includes(".spec.") ||
43
+ base.includes("_test.") ||
44
+ base.includes("_spec.") ||
45
+ base.startsWith("test_") ||
46
+ base.startsWith("spec_") ||
47
+ /(Test|Tests|TestCase)\.(java|kt|kts|cs|fs|php)$/.test(rawBase));
48
+ }
49
+ /** Filename-only classification for consumers with their own directory policy. */
50
+ export function isTestFileName(filePath) {
51
+ const windowsShaped = isWindowsPath(filePath);
52
+ const rawBase = windowsShaped ? win32.basename(filePath) : basename(filePath);
53
+ return testFileNameMatches(rawBase, rawBase.toLowerCase());
54
+ }
37
55
  /**
38
56
  * Classify the structural role of a file. Pass `content` for higher
39
57
  * accuracy (enables generated-code detection, barrel detection, etc.).
@@ -51,16 +69,15 @@ function isTypeStub(content) {
51
69
  */
52
70
  export function detectFileRole(filePath, content) {
53
71
  const windowsShaped = isWindowsPath(filePath);
54
- const base = (windowsShaped ? win32.basename(filePath) : basename(filePath)).toLowerCase();
72
+ const rawBase = windowsShaped ? win32.basename(filePath) : basename(filePath);
73
+ const base = rawBase.toLowerCase();
55
74
  const dir = (windowsShaped ? win32.dirname(filePath) : dirname(filePath))
56
75
  .replace(/\\/g, "/")
57
76
  .toLowerCase();
58
77
  // --- Test ---
59
- if (base.includes(".test.") ||
60
- base.includes(".spec.") ||
61
- base.startsWith("test_") ||
62
- base.startsWith("spec_") ||
63
- dir.includes("/__tests__/") ||
78
+ // #2928: dirname has no trailing slash; match direct __tests__ children too.
79
+ if (testFileNameMatches(rawBase, base) ||
80
+ /(^|[\\/])__tests__([\\/]|$)/.test(dir) ||
64
81
  dir.includes("/test/") ||
65
82
  dir.includes("/tests/") ||
66
83
  dir.includes("/spec/") ||