@herbertgao/pi-extensions 2026.9.14 → 2026.9.16

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 (206) hide show
  1. package/README.md +8 -3
  2. package/THIRD_PARTY_NOTICES.md +28 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
  4. package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -6
  5. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  6. package/node_modules/@narumitw/pi-btw/package.json +4 -4
  7. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +2 -0
  8. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +16 -4
  9. package/node_modules/pi-jev-auto-mode/CHANGELOG.md +8 -0
  10. package/node_modules/pi-jev-auto-mode/README.md +7 -0
  11. package/node_modules/pi-jev-auto-mode/package.json +1 -1
  12. package/node_modules/pi-jev-auto-mode/src/extension.ts +21 -2
  13. package/node_modules/pi-jev-auto-mode/src/records.ts +68 -2
  14. package/node_modules/pi-jev-auto-mode/src/settings.ts +22 -0
  15. package/node_modules/pi-jev-auto-mode/src/ui.ts +10 -0
  16. package/node_modules/pi-lens/CHANGELOG.md +498 -0
  17. package/node_modules/pi-lens/README.md +45 -2
  18. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +23 -7
  19. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +187 -51
  20. package/node_modules/pi-lens/dist/clients/cache-manager.js +64 -5
  21. package/node_modules/pi-lens/dist/clients/child-unref.js +17 -8
  22. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
  23. package/node_modules/pi-lens/dist/clients/config-locations.js +237 -9
  24. package/node_modules/pi-lens/dist/clients/config-resolve.js +7 -1
  25. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +33 -3
  26. package/node_modules/pi-lens/dist/clients/dependency-checker.js +87 -64
  27. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +39 -3
  28. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +118 -1
  29. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +8 -2
  30. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +23 -4
  31. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +14 -7
  32. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +26 -28
  33. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +38 -25
  34. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +32 -49
  35. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +16 -14
  36. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +25 -7
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +28 -47
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +11 -4
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +27 -22
  40. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +31 -46
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +29 -46
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +91 -11
  43. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +16 -2
  44. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +38 -23
  45. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +27 -15
  46. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +17 -8
  47. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +31 -21
  48. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +34 -60
  49. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +6 -5
  50. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +12 -5
  51. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
  52. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +22 -6
  53. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +39 -16
  54. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +1 -1
  55. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +27 -18
  56. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +18 -19
  57. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +6 -1
  58. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +6 -2
  59. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -3
  60. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +11 -2
  61. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +10 -2
  62. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +11 -2
  63. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +12 -6
  64. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +30 -15
  65. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +23 -0
  66. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +8 -2
  67. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
  68. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +15 -3
  69. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +12 -2
  70. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +27 -39
  71. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +13 -3
  72. package/node_modules/pi-lens/dist/clients/effective-config.js +3 -1
  73. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +42 -11
  74. package/node_modules/pi-lens/dist/clients/formatters.js +49 -11
  75. package/node_modules/pi-lens/dist/clients/git-guard.js +247 -34
  76. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +121 -25
  77. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +186 -0
  78. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +19 -6
  79. package/node_modules/pi-lens/dist/clients/installer/index.js +281 -32
  80. package/node_modules/pi-lens/dist/clients/jscpd-client.js +8 -0
  81. package/node_modules/pi-lens/dist/clients/latency-logger.js +8 -0
  82. package/node_modules/pi-lens/dist/clients/lens-config.js +66 -8
  83. package/node_modules/pi-lens/dist/clients/lsp/client.js +139 -11
  84. package/node_modules/pi-lens/dist/clients/lsp/config.js +9 -7
  85. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +41 -0
  86. package/node_modules/pi-lens/dist/clients/lsp/index.js +1 -1
  87. package/node_modules/pi-lens/dist/clients/lsp/launch.js +23 -2
  88. package/node_modules/pi-lens/dist/clients/lsp/server.js +106 -25
  89. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +14 -0
  90. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +105 -16
  91. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +232 -44
  92. package/node_modules/pi-lens/dist/clients/mcp/review.js +53 -7
  93. package/node_modules/pi-lens/dist/clients/opengrep-client.js +11 -6
  94. package/node_modules/pi-lens/dist/clients/pipeline.js +121 -66
  95. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
  96. package/node_modules/pi-lens/dist/clients/project-lens-config.js +11 -3
  97. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +34 -26
  98. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +16 -10
  99. package/node_modules/pi-lens/dist/clients/ruff-client.js +31 -4
  100. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +38 -4
  101. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +16 -5
  102. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +27 -12
  103. package/node_modules/pi-lens/dist/clients/runtime-turn.js +447 -280
  104. package/node_modules/pi-lens/dist/clients/safe-spawn.js +256 -65
  105. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +84 -1
  106. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +119 -58
  107. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +51 -0
  108. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +112 -0
  109. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +204 -0
  110. package/node_modules/pi-lens/dist/clients/warm-attach.js +59 -0
  111. package/node_modules/pi-lens/dist/clients/widget-state.js +145 -14
  112. package/node_modules/pi-lens/dist/clients/workspace-topology.js +57 -3
  113. package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -13
  114. package/node_modules/pi-lens/dist/index.js +5695 -3733
  115. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +4 -0
  116. package/node_modules/pi-lens/dist/mcp/server.js +33 -21
  117. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  118. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +43 -16
  119. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +118 -7
  120. package/node_modules/pi-lens/docs/adr/0001-stale-advisory-live-arm.md +29 -0
  121. package/node_modules/pi-lens/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
  122. package/node_modules/pi-lens/docs/adr/0003-git-guard-latch-writer.md +203 -0
  123. package/node_modules/pi-lens/docs/adr/0004-disposition-policy-seam.md +38 -0
  124. package/node_modules/pi-lens/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
  125. package/node_modules/pi-lens/docs/adr/0006-derived-state-benchmark-first.md +31 -0
  126. package/node_modules/pi-lens/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
  127. package/node_modules/pi-lens/docs/adr/0008-turn-end-lane-interface.md +165 -0
  128. package/node_modules/pi-lens/docs/adr/0009-reported-path-attribution.md +114 -0
  129. package/node_modules/pi-lens/docs/adr/README.md +26 -0
  130. package/node_modules/pi-lens/docs/agent-tools.md +18 -1
  131. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +4 -3
  132. package/node_modules/pi-lens/docs/configuration.md +55 -1
  133. package/node_modules/pi-lens/docs/engineering-principles.md +202 -0
  134. package/node_modules/pi-lens/docs/environment-variables.md +61 -4
  135. package/node_modules/pi-lens/docs/features.md +2 -2
  136. package/node_modules/pi-lens/docs/globalconfig.md +6 -1
  137. package/node_modules/pi-lens/docs/language-coverage.md +17 -0
  138. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +98 -31
  139. package/node_modules/pi-lens/docs/pi-lens-fixer.md +57 -3
  140. package/node_modules/pi-lens/docs/pi-lens-investigator.md +9 -0
  141. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +16 -0
  142. package/node_modules/pi-lens/docs/pi-lens-subagent.md +9 -0
  143. package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
  144. package/node_modules/pi-lens/docs/release-qa-baseline.md +2 -0
  145. package/node_modules/pi-lens/docs/servercapabilities.md +21 -11
  146. package/node_modules/pi-lens/docs/settings.md +12 -3
  147. package/node_modules/pi-lens/docs/subagent-compat.md +1 -1
  148. package/node_modules/pi-lens/package.json +5 -3
  149. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
  150. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
  151. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
  152. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
  153. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
  154. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
  155. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
  156. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
  157. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
  158. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
  159. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
  160. package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
  161. package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
  162. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
  163. package/node_modules/pi-lens/scripts/install-selftest.mjs +21 -17
  164. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  165. package/node_modules/pi-mcp-adapter/CHANGELOG.md +29 -0
  166. package/node_modules/pi-mcp-adapter/OAUTH.md +369 -0
  167. package/node_modules/pi-mcp-adapter/README.md +12 -5
  168. package/node_modules/pi-mcp-adapter/config.ts +31 -8
  169. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +1 -1
  170. package/node_modules/pi-mcp-adapter/dist/config.js +33 -8
  171. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  172. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +15 -2
  173. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -1
  174. package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +2 -0
  175. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +27 -2
  176. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -1
  177. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +1 -1
  178. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  179. package/node_modules/pi-mcp-adapter/dist/server-manager.js +6 -4
  180. package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -1
  181. package/node_modules/pi-mcp-adapter/dist/types.d.ts +2 -2
  182. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +2 -0
  183. package/node_modules/pi-mcp-adapter/dist/utils.js +10 -3
  184. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  185. package/node_modules/pi-mcp-adapter/index.ts +37 -20
  186. package/node_modules/pi-mcp-adapter/init.ts +14 -15
  187. package/node_modules/pi-mcp-adapter/json-schema-validator.ts +16 -2
  188. package/node_modules/pi-mcp-adapter/mcp-auth.ts +27 -2
  189. package/node_modules/pi-mcp-adapter/metadata-cache.ts +1 -1
  190. package/node_modules/pi-mcp-adapter/package.json +3 -2
  191. package/node_modules/pi-mcp-adapter/proxy-modes.ts +1 -1
  192. package/node_modules/pi-mcp-adapter/server-manager.ts +6 -3
  193. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +99 -6
  194. package/node_modules/pi-mcp-adapter/types.ts +2 -2
  195. package/node_modules/pi-mcp-adapter/ui-session.ts +60 -27
  196. package/node_modules/pi-mcp-adapter/utils.ts +11 -3
  197. package/node_modules/pi-multi-account/CHANGELOG.md +6 -0
  198. package/node_modules/pi-multi-account/README.md +15 -1
  199. package/node_modules/pi-multi-account/index.ts +295 -41
  200. package/node_modules/pi-multi-account/package.json +1 -1
  201. package/node_modules/pi-next-cue/.github/workflows/publish.yml +20 -0
  202. package/node_modules/pi-next-cue/LICENSE +21 -0
  203. package/node_modules/pi-next-cue/README.md +81 -0
  204. package/node_modules/pi-next-cue/index.ts +358 -0
  205. package/node_modules/pi-next-cue/package.json +28 -0
  206. package/package.json +10 -7
@@ -104,6 +104,7 @@
104
104
  import * as fs from "node:fs";
105
105
  import * as path from "node:path";
106
106
  import { safeSpawnAsync } from "../../safe-spawn.js";
107
+ import { pathsEqual } from "../../path-utils.js";
107
108
  import { resolveRunnerCwd } from "../../tool-cwd.js";
108
109
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
109
110
  import { spawnFailedWithNoOutput } from "./utils/spawn-outcome.js";
@@ -205,10 +206,24 @@ export function hasPackageClause(content) {
205
206
  }
206
207
  return false;
207
208
  }
208
- /** `.\bad.cue`, `./bad.cue`, and `bad.cue` all name the same file. */
209
- function locationMatchesFile(location, fileName) {
210
- const normalized = location.file.replace(/\\/g, "/").replace(/^\.\//, "");
211
- return path.posix.basename(normalized) === fileName;
209
+ /**
210
+ * Is this reported location about the file we dispatched for? ONE seam answers
211
+ * that for every runner (#3278): resolve the tool's spelling against the cwd
212
+ * the tool RAN in and compare through `pathsEqual`.
213
+ *
214
+ * `cue` prints every position relative to its own cwd and prefixes a `./` when
215
+ * the result does not already start with a dot (`cue/errors/errors.go:586-596`
216
+ * at v0.11.0: `filepath.Rel(cfg.Cwd, s)`), which is why `.\bad.cue`,
217
+ * `./bad.cue` and `bad.cue` all name the vetted file — `pathsEqual` folds the
218
+ * separator and the `.` segment, so all three still match.
219
+ *
220
+ * What the basename compare this replaces could not do is tell those apart
221
+ * from `./sub/bad.cue` or `../lib/bad.cue`: an imported package's file that
222
+ * merely SHARES the touched file's basename was reported as the touched file's
223
+ * own failure (the over-merge direction, #3278 sub-shape C).
224
+ */
225
+ function locationMatchesFile(location, absTarget, vetCwd) {
226
+ return pathsEqual(path.resolve(vetCwd, location.file), absTarget);
212
227
  }
213
228
  function toDiagnostic(id, message, fileName, line, column) {
214
229
  return {
@@ -252,16 +267,19 @@ function toDiagnostic(id, message, fileName, line, column) {
252
267
  * whole-output fallback message is used instead of one row per parsed
253
268
  * header.
254
269
  */
255
- export function filterToTouchedFile(errors, fileName) {
270
+ export function filterToTouchedFile(errors, fileName, vetCwd) {
256
271
  if (errors.length === 0)
257
272
  return undefined;
273
+ // `cue vet` always runs in the touched file's own directory, so the vet cwd
274
+ // joined with the bare file name IS the dispatched file.
275
+ const absTarget = path.resolve(vetCwd, fileName);
258
276
  const withLocation = errors.filter((error) => error.locations.length > 0);
259
277
  const unattributable = errors.filter((error) => error.locations.length === 0);
260
278
  if (withLocation.length === 0)
261
279
  return undefined;
262
280
  const diagnostics = [];
263
281
  for (const error of withLocation) {
264
- const match = error.locations.find((location) => locationMatchesFile(location, fileName));
282
+ const match = error.locations.find((location) => locationMatchesFile(location, absTarget, vetCwd));
265
283
  if (!match)
266
284
  continue; // sibling-only finding — surfaces when that file is touched
267
285
  diagnostics.push(toDiagnostic(`cue-vet-${diagnostics.length + 1}-${match.line}`, error.message, fileName, match.line, match.column));
@@ -364,7 +382,7 @@ const cueVetRunner = {
364
382
  // the directory-scoped path — one code path, and a single-file run
365
383
  // that somehow fails with no location at all still gets the same
366
384
  // never-silently-clean `undefined` fallback as the directory path.
367
- const filtered = filterToTouchedFile(errors, fileName);
385
+ const filtered = filterToTouchedFile(errors, fileName, fileDir);
368
386
  if (filtered === undefined) {
369
387
  // Nonzero exit, some output, but nothing in it could be attributed to
370
388
  // ANY file — a real failure must not present as zero findings.
@@ -1,7 +1,9 @@
1
1
  import * as path from "node:path";
2
2
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
+ import { pathsEqual } from "../../path-utils.js";
3
4
  import { resolveRunnerCwd } from "../../tool-cwd.js";
4
5
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
6
+ import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
5
7
  import { PRIORITY } from "../priorities.js";
6
8
  const dart = createAvailabilityChecker("dart", ".exe");
7
9
  const flutter = createAvailabilityChecker("flutter", ".bat");
@@ -94,8 +96,9 @@ const DART_FIXABLE_RULES = new Set([
94
96
  ]);
95
97
  // dart analyze --format=machine output:
96
98
  // severity|type|code|file|line|col|length|message
97
- function parseDartMachineOutput(raw, filePath) {
99
+ function parseDartMachineOutput(raw, filePath, cwd) {
98
100
  const diagnostics = [];
101
+ const absTarget = path.resolve(cwd, filePath);
99
102
  for (const line of raw.split(/\r?\n/)) {
100
103
  if (!line.trim())
101
104
  continue;
@@ -106,14 +109,16 @@ function parseDartMachineOutput(raw, filePath) {
106
109
  const message = messageParts.join("|").trim();
107
110
  const lineNum = parseInt(lineStr, 10);
108
111
  const colNum = parseInt(colStr, 10);
109
- // Only include diagnostics for the target file
110
- if (file &&
111
- !path.resolve(file).endsWith(path.resolve(filePath).replace(/\\/g, "/"))) {
112
- const resolvedFile = path.resolve(file.trim());
113
- const resolvedTarget = path.resolve(filePath);
114
- if (resolvedFile !== resolvedTarget)
115
- continue;
116
- }
112
+ // #3278: one seam for reported-path attribution — see javac.ts.
113
+ // The `endsWith` outer arm this replaces was inert on the target (a
114
+ // string ends with itself) and an over-merge risk everywhere else: any
115
+ // reported path whose TAIL spelled the absolute target attached to it.
116
+ // `file` is `parts[3]`, which `tsconfig.strict-indexed.json`
117
+ // (`noUncheckedIndexedAccess`) types as `string | undefined` even behind the
118
+ // `parts.length < 8` guard above — the strictness ratchet pins that, so the
119
+ // optional chain stays.
120
+ if (!pathsEqual(path.resolve(cwd, file?.trim() ?? ""), absTarget))
121
+ continue;
117
122
  const severity = severityStr?.trim().toLowerCase() === "error" ? "error" : "warning";
118
123
  const ruleId = code?.trim() ?? "dart";
119
124
  const fixable = DART_FIXABLE_RULES.has(ruleId);
@@ -136,12 +141,6 @@ function parseDartMachineOutput(raw, filePath) {
136
141
  }
137
142
  return diagnostics;
138
143
  }
139
- function firstOutputLine(result) {
140
- return `${result.stderr || ""}\n${result.stdout || ""}`
141
- .trim()
142
- .split(/\r?\n/, 1)[0]
143
- .slice(0, 200);
144
- }
145
144
  const dartAnalyzeRunner = {
146
145
  id: "dart-analyze",
147
146
  appliesTo: ["dart"],
@@ -162,40 +161,22 @@ const dartAnalyzeRunner = {
162
161
  ? ["analyze", "--format=machine", absPath]
163
162
  : ["analyze", "--machine", absPath];
164
163
  const result = await safeSpawnAsync(cmd, args, { cwd, timeout: 30000 });
165
- if (result.error && !result.stdout && !result.stderr) {
166
- return { status: "skipped", diagnostics: [], semantic: "none" };
167
- }
168
164
  // dart analyze writes diagnostics to stderr in machine format
169
165
  const raw = (result.stderr || "") + (result.stdout || "");
170
- const diagnostics = parseDartMachineOutput(raw, ctx.filePath);
171
- if (diagnostics.length === 0) {
172
- if (result.status && result.status !== 0) {
173
- return {
174
- status: "failed",
175
- diagnostics: [
176
- {
177
- id: "dart-analyze-nonzero-no-diagnostics",
178
- message: firstOutputLine(result) ||
179
- "dart analyze exited non-zero without machine diagnostics",
180
- filePath: ctx.filePath,
181
- severity: "warning",
182
- semantic: "warning",
183
- tool: "dart",
184
- rule: "dart-analyze",
185
- fixable: false,
186
- },
187
- ],
188
- semantic: "warning",
189
- };
190
- }
191
- return { status: "succeeded", diagnostics: [], semantic: "none" };
192
- }
193
- const hasErrors = diagnostics.some((d) => d.severity === "error");
194
- return {
195
- status: hasErrors ? "failed" : "succeeded",
196
- diagnostics,
197
- semantic: hasErrors ? "blocking" : "warning",
198
- };
166
+ const parsed = parseToolRun("dart-analyze", {
167
+ result,
168
+ output: raw,
169
+ // EXIT TABLE (Dart 3.5 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
170
+ exitCodes: { ran: [1, 2] },
171
+ }, (out) => parseDartMachineOutput(out, ctx.filePath, cwd));
172
+ if (parsed.skipped)
173
+ return parsed.skipped;
174
+ return finishParsedRun({
175
+ tool: "dart-analyze",
176
+ ctx,
177
+ result,
178
+ diagnostics: parsed.diagnostics,
179
+ });
199
180
  },
200
181
  };
201
182
  export default dartAnalyzeRunner;
@@ -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 { pathsEqual } from "../../path-utils.js";
4
5
  import { resolveRunnerCwd } from "../../tool-cwd.js";
5
6
  import { getLinterPolicyForCwd } from "../../tool-policy.js";
6
7
  import { PRIORITY } from "../priorities.js";
@@ -91,13 +92,14 @@ export function findDetektConfig(cwd) {
91
92
  return undefined;
92
93
  }
93
94
  // detekt text output: /path/file.kt:10:5: error: Message [RuleId]
94
- function parseDetektOutput(raw, filePath) {
95
+ function parseDetektOutput(raw, filePath, cwd) {
95
96
  const diagnostics = [];
96
97
  const pattern = /^(.+?):(\d+):(\d+): (error|warning): (.+?)(?:\s+\[([^\]]+)\])?$/gm;
97
- const absTarget = path.resolve(filePath);
98
+ const absTarget = path.resolve(cwd, filePath);
98
99
  for (const match of raw.matchAll(pattern)) {
99
100
  const [, file, lineStr, colStr, level, message, rule] = match;
100
- if (path.resolve(file.trim()) !== absTarget)
101
+ // #3278: one seam for reported-path attribution — see javac.ts.
102
+ if (!pathsEqual(path.resolve(cwd, file.trim()), absTarget))
101
103
  continue;
102
104
  const severity = level === "error" ? "error" : "warning";
103
105
  const lineNum = Number.parseInt(lineStr, 10);
@@ -150,7 +152,12 @@ const detektRunner = {
150
152
  // read the same concatenated string. A nonzero exit that yields zero
151
153
  // findings out of real output is a parser break, not clean Kotlin.
152
154
  const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
153
- const run = parseToolRun("detekt", { result, output: raw }, (out) => parseDetektOutput(out, ctx.filePath));
155
+ const run = parseToolRun("detekt", {
156
+ result,
157
+ output: raw,
158
+ // EXIT TABLE (detekt 1.23 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
159
+ exitCodes: { ran: [1, 2] },
160
+ }, (out) => parseDetektOutput(out, ctx.filePath, cwd));
154
161
  if (run.skipped)
155
162
  return run.skipped;
156
163
  const diagnostics = run.diagnostics;
@@ -1,9 +1,11 @@
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 { pathsEqual } from "../../path-utils.js";
4
5
  import { resolveRunnerCwd } from "../../tool-cwd.js";
5
6
  import { PRIORITY } from "../priorities.js";
6
7
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
8
+ import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
7
9
  const dotnet = createAvailabilityChecker("dotnet", ".exe");
8
10
  function findProjectTarget(cwd) {
9
11
  const entries = fs.readdirSync(cwd);
@@ -83,16 +85,16 @@ function parseDotnetDiagnosticLine(line) {
83
85
  }
84
86
  return { reportedFile, lineStr, colStr, severityLabel, rule, message };
85
87
  }
86
- function parseDotnetBuildOutput(raw, filePath) {
88
+ function parseDotnetBuildOutput(raw, filePath, cwd) {
87
89
  const diagnostics = [];
90
+ const absTarget = path.resolve(cwd, filePath);
88
91
  for (const line of raw.split(/\r?\n/)) {
89
92
  const parsed = parseDotnetDiagnosticLine(line);
90
93
  if (!parsed)
91
94
  continue;
92
95
  const { reportedFile, lineStr, colStr, severityLabel, rule, message } = parsed;
93
- const resolvedReported = path.resolve(reportedFile);
94
- const resolvedTarget = path.resolve(filePath);
95
- if (resolvedReported !== resolvedTarget)
96
+ // #3278: one seam for reported-path attribution — see javac.ts.
97
+ if (!pathsEqual(path.resolve(cwd, reportedFile), absTarget))
96
98
  continue;
97
99
  const severity = severityLabel.toLowerCase() === "error" ? "error" : "warning";
98
100
  const lineNum = Number.parseInt(lineStr, 10) || 1;
@@ -136,24 +138,27 @@ const dotnetBuildRunner = {
136
138
  timeout: 60000,
137
139
  });
138
140
  const raw = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
139
- if (result.status === 0 && !raw) {
140
- return { status: "succeeded", diagnostics: [], semantic: "none" };
141
- }
142
- const diagnostics = parseDotnetBuildOutput(raw, ctx.filePath);
143
- if (diagnostics.length === 0) {
144
- return {
145
- status: result.status === 0 ? "succeeded" : "failed",
146
- diagnostics: [],
147
- semantic: "warning",
148
- rawOutput: raw,
149
- };
150
- }
151
- const hasErrors = diagnostics.some((d) => d.severity === "error");
152
- return {
153
- status: hasErrors ? "failed" : "succeeded",
154
- diagnostics,
155
- semantic: hasErrors ? "blocking" : "warning",
156
- };
141
+ const parsed = parseToolRun("dotnet-build", {
142
+ result,
143
+ output: raw,
144
+ // EXIT TABLE (.NET SDK 8 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
145
+ exitCodes: { ran: [1, 2] },
146
+ }, (output) => parseDotnetBuildOutput(output, ctx.filePath, cwd));
147
+ if (parsed.skipped)
148
+ return parsed.skipped;
149
+ return finishParsedRun({
150
+ tool: "dotnet-build",
151
+ ctx,
152
+ result,
153
+ diagnostics: parsed.diagnostics,
154
+ classify: (diagnostics) => {
155
+ const hasErrors = diagnostics.some((d) => d.severity === "error");
156
+ return {
157
+ status: hasErrors ? "failed" : "succeeded",
158
+ semantic: hasErrors ? "blocking" : "warning",
159
+ };
160
+ },
161
+ });
157
162
  },
158
163
  };
159
164
  export default dotnetBuildRunner;
@@ -1,10 +1,12 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { getProjectDataDir } from "../../file-utils.js";
4
+ import { pathsEqual } from "../../path-utils.js";
4
5
  import { safeSpawnAsync } from "../../safe-spawn.js";
5
6
  import { resolveRunnerCwd } from "../../tool-cwd.js";
6
7
  import { PRIORITY } from "../priorities.js";
7
8
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
9
+ import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
8
10
  // Per-cwd cached `--version` probes (#120). Before this, each dispatch
9
11
  // invocation fired a fresh `safeSpawnAsync` per command — once per Elixir
10
12
  // file save.
@@ -34,15 +36,15 @@ function parseElixirOutput(raw, filePath, cwd = process.cwd()) {
34
36
  const lines = raw.split(/\r?\n/);
35
37
  // elixirc reports paths RELATIVE to its cwd (e.g. `bad.ex`, not the absolute
36
38
  // path we passed), so resolve the reported path against the runner cwd — not
37
- // process.cwd(). Elixir 1.16+ also normalizes to a lowercase drive letter and
38
- // forward slashes (`c:/...`), which never string-equals `C:\...` on Windows,
39
- // so compare case-insensitively there.
40
- const matchesTarget = (sourcePath) => {
41
- const resolved = path.resolve(cwd, sourcePath.trim());
42
- return process.platform === "win32"
43
- ? resolved.toLowerCase() === resolvedTarget.toLowerCase()
44
- : resolved === resolvedTarget;
45
- };
39
+ // process.cwd(). Two resolved spellings of ONE file still differ in ways
40
+ // only the filesystem can settle — Elixir 1.16+ normalizes to a lowercase
41
+ // drive letter and forward slashes (`c:/...`), which never string-equals
42
+ // `C:\...` on Windows — so ask `pathsEqual`, the repo's on-disk identity
43
+ // seam, instead of hand-rolling a win32 case fold here. It folds case
44
+ // exactly where the filesystem does and nowhere else, which a
45
+ // `process.platform` test cannot express; `terragrunt.ts:90` asks the same
46
+ // question the same way (#1193).
47
+ const matchesTarget = (sourcePath) => pathsEqual(path.resolve(cwd, sourcePath.trim()), resolvedTarget);
46
48
  for (let index = 0; index < lines.length; index++) {
47
49
  const line = lines[index];
48
50
  // Defense-in-depth: cap line length before regex matching. The input is
@@ -130,12 +132,6 @@ function parseElixirOutput(raw, filePath, cwd = process.cwd()) {
130
132
  }
131
133
  return diagnostics;
132
134
  }
133
- function firstOutputLine(result) {
134
- return `${result.stderr || ""}\n${result.stdout || ""}`
135
- .trim()
136
- .split(/\r?\n/, 1)[0]
137
- .slice(0, 200);
138
- }
139
135
  const elixirCheckRunner = {
140
136
  id: "elixir-check",
141
137
  appliesTo: ["elixir"],
@@ -163,47 +159,36 @@ const elixirCheckRunner = {
163
159
  cwd,
164
160
  timeout: 30000,
165
161
  });
166
- if (result.error && !result.stdout && !result.stderr) {
167
- return { status: "skipped", diagnostics: [], semantic: "none" };
168
- }
169
162
  const raw = `${result.stderr || ""}\n${result.stdout || ""}`;
170
163
  const hasProjectContext = command === "mix";
171
- const diagnostics = parseElixirOutput(raw, ctx.filePath, cwd).map((d) => hasProjectContext
164
+ const parsed = parseToolRun("elixir-check", {
165
+ result,
166
+ output: raw,
167
+ // EXIT TABLE (Elixir 1.16 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
168
+ exitCodes: { ran: [1, 2] },
169
+ }, (out) => parseElixirOutput(out, ctx.filePath, cwd).map((d) => hasProjectContext
172
170
  ? d
173
171
  : {
174
172
  ...d,
175
173
  // A direct elixirc invocation cannot resolve Mix dependencies.
176
174
  // Standalone findings inform the agent, but cannot block it.
177
175
  semantic: "warning",
178
- });
179
- if (diagnostics.length === 0) {
180
- if (result.status && result.status !== 0) {
176
+ }));
177
+ if (parsed.skipped)
178
+ return parsed.skipped;
179
+ return finishParsedRun({
180
+ tool: "elixir-check",
181
+ ctx,
182
+ result,
183
+ diagnostics: parsed.diagnostics,
184
+ classify: (diagnostics) => {
185
+ const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
181
186
  return {
182
- status: "failed",
183
- diagnostics: [
184
- {
185
- id: "elixir-check-nonzero-no-diagnostics",
186
- message: firstOutputLine(result) ||
187
- `${command} exited non-zero without structured diagnostics`,
188
- filePath: ctx.filePath,
189
- severity: "error",
190
- semantic: hasProjectContext ? "blocking" : "warning",
191
- tool: "elixir-check",
192
- rule: command,
193
- fixable: false,
194
- },
195
- ],
196
- semantic: hasProjectContext ? "blocking" : "warning",
187
+ status: hasBlocking ? "failed" : "succeeded",
188
+ semantic: hasBlocking ? "blocking" : "warning",
197
189
  };
198
- }
199
- return { status: "succeeded", diagnostics: [], semantic: "none" };
200
- }
201
- const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
202
- return {
203
- status: hasBlocking ? "failed" : "succeeded",
204
- diagnostics,
205
- semantic: hasBlocking ? "blocking" : "warning",
206
- };
190
+ },
191
+ });
207
192
  },
208
193
  };
209
194
  export default elixirCheckRunner;
@@ -6,11 +6,14 @@
6
6
  *
7
7
  * Gate: skips when no ESLint config is detected (project uses Biome/OxLint instead).
8
8
  */
9
+ import * as path from "node:path";
10
+ import { pathsEqual } from "../../path-utils.js";
9
11
  import { safeSpawnAsync } from "../../safe-spawn.js";
10
12
  import { resolveRunnerCwd } from "../../tool-cwd.js";
11
13
  import { getAutofixCapability, hasEslintConfig } from "../../tool-policy.js";
12
14
  import { PRIORITY } from "../priorities.js";
13
15
  import { createCwdCachedProbe, resolveToolCommand, } from "./utils/runner-helpers.js";
16
+ import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
14
17
  const ESLINT_PROBE_BUDGET_MS = 5000;
15
18
  // Per-cwd cached eslint `--version` verification (#120). Before this, every
16
19
  // dispatch invocation ran a fresh `safeSpawnAsync(cmd, ["--version"])` after
@@ -40,12 +43,18 @@ function getEslintProbe(cmd) {
40
43
  eslintAvailabilityByCmd.set(cmd, created);
41
44
  return created;
42
45
  }
43
- function parseEslintJson(raw, filePath) {
46
+ function parseEslintJson(raw, filePath, cwd) {
44
47
  try {
45
48
  const results = JSON.parse(raw);
46
49
  const autofix = getAutofixCapability("eslint");
47
50
  const diagnostics = [];
51
+ const absTarget = path.resolve(cwd, filePath);
48
52
  for (const fileResult of results) {
53
+ // #3295: flat config `files`/`ignores` and a directory argv both put a
54
+ // SECOND result in this array; each names its own `filePath`.
55
+ if (fileResult.filePath &&
56
+ !pathsEqual(path.resolve(cwd, fileResult.filePath), absTarget))
57
+ continue;
49
58
  for (const msg of fileResult.messages) {
50
59
  const severity = msg.severity === 2 ? "error" : "warning";
51
60
  diagnostics.push({
@@ -92,11 +101,11 @@ const eslintRunner = {
92
101
  return { status: "skipped", diagnostics: [], semantic: "none" };
93
102
  }
94
103
  const result = await safeSpawnAsync(cmd, ["--format", "json", "--no-error-on-unmatched-pattern", ctx.filePath], { timeout: 30000, cwd });
95
- // ESLint exits 2 on fatal/config errors — nothing was linted, so this
96
- // is an unavailable run, not a clean or failed one.
97
- if (result.status === 2) {
98
- return { status: "skipped", diagnostics: [], semantic: "none" };
99
- }
104
+ // Exit table: 0 is clean-or-findings, and 1/2 are ran outcomes whose
105
+ // JSON parser decides whether findings or a parse/config error reached the
106
+ // user. In particular, status 2 carries file-local fatal parse messages
107
+ // as JSON and must not be discarded before parsing.
108
+ //
100
109
  // ESLint exits 0 whenever nothing reached ERROR severity — that
101
110
  // includes a run that found only warnings (#1954), which also prints a
102
111
  // full JSON report. So parse stdout unconditionally and branch on the
@@ -109,46 +118,20 @@ const eslintRunner = {
109
118
  if (raw.length === 0 && result.status !== 0) {
110
119
  raw = result.stderr || "";
111
120
  }
112
- const parsed = parseEslintJson(raw, ctx.filePath);
113
- if (parsed.parseError && raw.trim().length > 0) {
114
- const preview = raw.replace(/\s+/g, " ").slice(0, 160);
115
- return {
116
- status: "failed",
117
- diagnostics: [
118
- {
119
- id: "eslint:parse-error:1",
120
- message: "ESLint JSON parse failed: " +
121
- parsed.parseError +
122
- (preview ? " (output preview: " + preview + ")" : ""),
123
- filePath: ctx.filePath,
124
- line: 1,
125
- column: 1,
126
- severity: "warning",
127
- semantic: "warning",
128
- tool: "eslint",
129
- },
130
- ],
131
- semantic: "warning",
132
- };
133
- }
134
- const diagnostics = parsed.diagnostics;
135
- if (diagnostics.length === 0) {
136
- return { status: "succeeded", diagnostics: [], semantic: "none" };
137
- }
138
- const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
139
- // A warning-only result on exit 0 is ESLint's normal outcome, not a
140
- // failure: exit 0 means nothing hit ERROR severity. Keying status off
141
- // blocking severity plus exit code (the sibling convention from oxlint,
142
- // biome-check, golangci-lint, rubocop) keeps plan.ts's fallback group
143
- // ["eslint", "oxlint", "biome-check-json"] stopping at eslint instead
144
- // of re-running oxlint and biome-check-json on every warning-only save.
145
- // The findings reach delivery regardless of status — dispatcher.ts
146
- // buckets by each diagnostic's own `semantic`.
147
- return {
148
- status: !hasErrors && result.status === 0 ? "succeeded" : "failed",
149
- diagnostics,
150
- semantic: hasErrors ? "blocking" : "warning",
151
- };
121
+ const parsed = parseToolRun("eslint", {
122
+ result,
123
+ output: raw,
124
+ // EXIT TABLE (ESLint 9.10 docs https://eslint.org/docs/latest/use/command-line-interface): 0 clean; 1 findings; 2 fatal findings/error; other nonzero rejected.
125
+ exitCodes: { ran: [1, 2] },
126
+ }, (rawOutput) => parseEslintJson(rawOutput, ctx.filePath, cwd).diagnostics);
127
+ if (parsed.skipped)
128
+ return parsed.skipped;
129
+ return finishParsedRun({
130
+ tool: "eslint",
131
+ ctx,
132
+ result,
133
+ diagnostics: parsed.diagnostics,
134
+ });
152
135
  },
153
136
  };
154
137
  export default eslintRunner;