@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
@@ -1,28 +1,108 @@
1
+ import * as path from "node:path";
2
+ import { pathsEqual } from "../../path-utils.js";
1
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
4
+ import { stripAnsi } from "../../sanitize.js";
2
5
  import { resolveRunnerCwd } from "../../tool-cwd.js";
3
6
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
4
7
  import { PRIORITY } from "../priorities.js";
5
8
  const gleam = createAvailabilityChecker("gleam", ".exe");
6
- function parseGleamOutput(raw, filePath) {
9
+ /**
10
+ * gleam renders every located diagnostic through
11
+ * `codespan_reporting::term::emit` (v1.18.1
12
+ * `compiler-core/src/diagnostic.rs:92-133`, unchanged since v1.6.3), so its
13
+ * location line is codespan's LOCUS line, not a bare `path:line:col`:
14
+ *
15
+ * ```text
16
+ * error: Type mismatch
17
+ * ┌─ /abs/proj/src/app.gleam:1:8
18
+ * ```
19
+ *
20
+ * `<outer gutter spaces> ┌─ <name>:<line>:<column>` —
21
+ * codespan-reporting 0.13.1 `src/term/renderer.rs:379-396` (the gutter, then
22
+ * `chars().snippet_start`, then one space, then the locus) and `:869-878`
23
+ * (`{name}:{line_number}:{column_number}`), with `snippet_start` = `┌─` in the
24
+ * default box-drawing set (`src/term/config.rs:419-429`, reached because gleam
25
+ * passes `Config::default()`, whose display style is Rich).
26
+ *
27
+ * `name` is `location.path` verbatim (`diagnostic.rs:92-94`,
28
+ * `files.add(main_location_path, …)`), and the CLI builds that path by walking
29
+ * UP from the absolute `std::env::current_dir()` to the nearest `gleam.toml`
30
+ * (`compiler-cli/src/fs.rs:32-62`) and joining `src`/`test`
31
+ * (`compiler-core/src/paths.rs:42-48`) before enumerating the directory — so it
32
+ * is ABSOLUTE, with host-native separators. Resolving it against the cwd the
33
+ * tool ran in is therefore a no-op today and the ADR 0009 shape for the
34
+ * hypothetical relative spelling.
35
+ *
36
+ * Capturing the path REQUIRES matching the gutter: the pre-#3285 regex
37
+ * `/^(.+?):(\d+):(\d+)$/` captured ` ┌─ /abs/proj/src/app.gleam`, gutter
38
+ * included, and only an `endsWith` compare tolerated that. An equality
39
+ * predicate (the seam every other runner now uses) drops every gleam diagnostic
40
+ * unless the gutter is out of the capture — which is why PR #3284 reverted its
41
+ * own fold here and filed #3285.
42
+ */
43
+ const GLEAM_LOCUS = /^\s*┌─\s+(.+?):(\d+):(\d+)$/;
44
+ const GLEAM_HEADER = /^\s*(error|warning):\s*(.+?)\s*$/;
45
+ const GLEAM_LABEL = /^\s*│\s*\^+\s*(.*?)\s*$/;
46
+ /**
47
+ * codespan writes ONE title line per diagnostic and then everything that
48
+ * belongs to it — locus, borders, snippet, labels, notes — before the next
49
+ * title (`codespan-reporting` 0.13.1 `src/term/renderer.rs:141-210`). Splitting
50
+ * on the title is therefore the diagnostic boundary, and it is what keeps an
51
+ * adjacent diagnostic's label out of its predecessor's message. Lines before
52
+ * the first title are gleam's own progress output (`Compiling demo`), which
53
+ * belongs to no diagnostic.
54
+ */
55
+ function gleamBlocks(lines) {
56
+ const blocks = [];
57
+ for (const line of lines) {
58
+ const header = line.match(GLEAM_HEADER);
59
+ if (header)
60
+ blocks.push({ header, body: [] });
61
+ else
62
+ blocks.at(-1)?.body.push(line);
63
+ }
64
+ return blocks;
65
+ }
66
+ function gleamMessage(title, body) {
67
+ const labels = body.flatMap((line) => line.match(GLEAM_LABEL)?.[1]?.trim() || []);
68
+ return labels.length > 0 ? `${title} — ${labels.join(" ")}` : title;
69
+ }
70
+ function parseGleamOutput(raw, filePath, cwd) {
7
71
  const diagnostics = [];
8
- const lines = raw.split(/\r?\n/);
9
- for (let i = 0; i < lines.length; i++) {
10
- const location = lines[i].match(/^(.+?):(\d+):(\d+)$/);
72
+ const absTarget = path.resolve(cwd, filePath);
73
+ // `stripAnsi` (the seam `ruff.ts`, `go-vet.ts` and `rust-clippy.ts` already
74
+ // use): gleam colours the gutter whenever `FORCE_COLOR` is set non-empty,
75
+ // whatever stderr is (`compiler-cli/src/cli.rs:194-207` — `ColorChoice::Never`
76
+ // only when it is neither forced nor a terminal), and the SGR bytes sit
77
+ // between the line start and `┌─`. The old suffix compare never saw the
78
+ // prefix; an anchored capture would drop every diagnostic in that
79
+ // environment.
80
+ const lines = stripAnsi(raw).split(/\r?\n/);
81
+ for (const { header, body } of gleamBlocks(lines)) {
82
+ const severity = header[1] === "warning" ? "warning" : "error";
83
+ const title = `${header[1]}: ${header[2]}`;
84
+ // A Gleam diagnostic with NO locus describes the whole project: `check`
85
+ // receives no file argument (`Diagnostic::write_title`, no codespan emit).
86
+ // Leave it for the nonzero-without-diagnostics fallback instead of
87
+ // charging it to whichever file happened to trigger dispatch — ADR 0009:
88
+ // without a reported path there is no evidence it is about this file.
89
+ const location = body
90
+ .find((line) => GLEAM_LOCUS.test(line))
91
+ ?.match(GLEAM_LOCUS);
11
92
  if (!location)
12
93
  continue;
13
94
  const [, sourcePath, lineStr, colStr] = location;
14
- if (!sourcePath.replace(/\\/g, "/").endsWith(filePath.replace(/\\/g, "/"))) {
95
+ // #3278: one seam for reported-path attribution — see javac.ts.
96
+ if (!pathsEqual(path.resolve(cwd, sourcePath.trim()), absTarget))
15
97
  continue;
16
- }
17
- const message = lines.slice(i + 1).find((line) => line.trim().length > 0);
18
98
  diagnostics.push({
19
99
  id: `gleam-check-${lineStr}-${colStr}`,
20
- message: message?.trim() || "gleam check reported an error",
100
+ message: gleamMessage(title, body),
21
101
  filePath,
22
102
  line: Number.parseInt(lineStr, 10) || 1,
23
103
  column: Number.parseInt(colStr, 10) || 1,
24
- severity: "error",
25
- semantic: "blocking",
104
+ severity,
105
+ semantic: severity === "error" ? "blocking" : "warning",
26
106
  tool: "gleam",
27
107
  rule: "gleam-check",
28
108
  fixable: false,
@@ -54,7 +134,7 @@ const gleamCheckRunner = {
54
134
  if (result.error && !result.stdout && !result.stderr) {
55
135
  return { status: "skipped", diagnostics: [], semantic: "none" };
56
136
  }
57
- const diagnostics = parseGleamOutput(`${result.stderr || ""}\n${result.stdout || ""}`, ctx.filePath);
137
+ const diagnostics = parseGleamOutput(`${result.stderr || ""}\n${result.stdout || ""}`, ctx.filePath, cwd);
58
138
  if (diagnostics.length === 0) {
59
139
  if (result.status && result.status !== 0) {
60
140
  return {
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { relative, resolve, sep, posix } from "node:path";
7
7
  import { goClient } from "../../go-client.js";
8
+ import { pathsEqual } from "../../path-utils.js";
8
9
  import { safeSpawnAsync } from "../../safe-spawn.js";
9
10
  import { resolveRunnerCwd } from "../../tool-cwd.js";
10
11
  import { stripAnsi } from "../../sanitize.js";
@@ -71,15 +72,28 @@ const goVetRunner = {
71
72
  // cwd before comparing, so go's path FORM (a leading `./` for the
72
73
  // module-root package, an absolute path, `.` segments) can't cause the
73
74
  // edited file's OWN diagnostics to be silently dropped.
75
+ //
76
+ // #3277: resolving is not enough — the two sides are SPELLED by
77
+ // different actors. `ctx.filePath` is `normalizeMapKey`-canonical
78
+ // (#2016) while go prints its own spelling: `cmd/go` rewrites the
79
+ // package-dir prefix to a cwd-relative one only when the rewrite
80
+ // matches byte-for-byte (`replacePrefix`), even though the check that
81
+ // authorized it (`filepath.Rel`) compares case-insensitively on
82
+ // Windows, so a differently-cased ABSOLUTE path reaches this filter.
83
+ // A bare `===` then drops every line for the edited file and the run
84
+ // is reported clean — the #209 defect. `pathsEqual` asks the
85
+ // filesystem instead of a platform list, so it is also right on a
86
+ // case-folding POSIX mount. It does NOT resolve symlinks on POSIX
87
+ // (`clients/path-utils.ts:181`); nothing here relies on that.
74
88
  const absTarget = resolve(ctx.filePath);
75
89
  const relevant = raw
76
90
  .split("\n")
77
91
  .filter((line) => {
78
92
  const m = line.match(/^(.+?):(\d+):(\d+):\s*(.+)/);
79
- return m != null && resolve(cwd, m[1].trim()) === absTarget;
93
+ return m != null && pathsEqual(resolve(cwd, m[1].trim()), absTarget);
80
94
  })
81
95
  .join("\n");
82
- const diagnostics = parseGoVetOutput(relevant, ctx.filePath);
96
+ const diagnostics = parseGoVetOutput(relevant, ctx.filePath, cwd);
83
97
  // Edited file clean → succeeded: a sibling-file error no longer flags
84
98
  // the edited file's turn (it surfaces when that file is itself edited).
85
99
  return diagnostics.length > 0
@@ -11,10 +11,12 @@
11
11
  */
12
12
  import * as path from "node:path";
13
13
  import { safeSpawnAsync } from "../../safe-spawn.js";
14
+ import { pathsEqual } from "../../path-utils.js";
14
15
  import { resolveRunnerCwd } from "../../tool-cwd.js";
15
16
  import { getLinterPolicyForCwd, hasGolangciConfig } from "../../tool-policy.js";
16
17
  import { PRIORITY } from "../priorities.js";
17
18
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
19
+ import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
18
20
  const golangci = createAvailabilityChecker("golangci-lint", ".exe");
19
21
  /**
20
22
  * Summarize a golangci-lint Replacement into a single-line fixSuggestion.
@@ -39,13 +41,21 @@ function describeReplacement(replacement) {
39
41
  }
40
42
  return "Apply golangci-lint suggested fix";
41
43
  }
42
- function parseGolangciJson(raw, filePath) {
44
+ function parseGolangciJson(raw, filePath, cwd) {
43
45
  try {
44
46
  const output = JSON.parse(raw);
45
47
  if (!output.Issues)
46
48
  return [];
47
- const absFile = path.resolve(filePath);
48
- return output.Issues.filter((issue) => path.resolve(issue.Pos.Filename) === absFile).map((issue) => {
49
+ const absFile = path.resolve(cwd, filePath);
50
+ // #3278: one seam for reported-path attribution — see javac.ts. The base
51
+ // matters here more than anywhere else in the family: golangci-lint's
52
+ // `PathPrettifier` OVERWRITES `Pos.Filename` with `filepath.Rel(basePath,
53
+ // …)` before the JSON printer sees it (v1.64.8,
54
+ // `pkg/result/processors/path_prettifier.go:31` +
55
+ // `path_relativity.go:43`), so the spelling is relative to the child's
56
+ // base path — the `cwd` we spawned it in — and `path.resolve` with no
57
+ // base resolved it against the EXTENSION's cwd instead.
58
+ return output.Issues.filter((issue) => pathsEqual(path.resolve(cwd, issue.Pos.Filename), absFile)).map((issue) => {
49
59
  const severity = issue.Severity === "error" ? "error" : "warning";
50
60
  // golangci-lint's --out-format=json emits a Replacement object per
51
61
  // issue when `golangci-lint run --fix` would deterministically
@@ -95,26 +105,31 @@ const golangciRunner = {
95
105
  return { status: "skipped", diagnostics: [], semantic: "none" };
96
106
  // Run on the specific file. golangci-lint accepts file paths directly.
97
107
  const result = await safeSpawnAsync(cmd, ["run", "--out-format=json", ctx.filePath], { timeout: 60000, cwd });
98
- if (result.status === 0) {
99
- return { status: "succeeded", diagnostics: [], semantic: "none" };
100
- }
101
- const diagnostics = parseGolangciJson(result.stdout, ctx.filePath);
102
- let semantic = "none";
103
- if (diagnostics.some((d) => d.semantic === "blocking")) {
104
- semantic = "blocking";
105
- }
106
- else if (diagnostics.length > 0) {
107
- semantic = "warning";
108
- }
109
- if (semantic === "none") {
110
- // Non-zero exit but no parseable issues — likely a config/tool error
111
- return { status: "skipped", diagnostics: [], semantic };
112
- }
113
- return {
114
- status: semantic === "blocking" ? "failed" : "succeeded",
115
- diagnostics,
116
- semantic,
117
- };
108
+ // Exit table: 0 is clean-or-findings; 1 means issues, 3 failure,
109
+ // 4 timeout, and 5 missing config. Every nonzero code is a ran outcome
110
+ // whose parser decides: JSON issues remain findings, while emitted
111
+ // non-JSON text becomes a parse-error diagnostic and no output stays
112
+ // skipped. Keep stderr as the parse input when stdout is absent so tool
113
+ // errors cannot be mistaken for a clean file.
114
+ const raw = (result.stdout ?? "").length > 0
115
+ ? (result.stdout ?? "")
116
+ : result.status !== 0
117
+ ? result.stderr || ""
118
+ : "";
119
+ const parsed = parseToolRun("golangci-lint", {
120
+ result,
121
+ output: raw,
122
+ // EXIT TABLE (golangci-lint 1.60 docs https://golangci-lint.run/docs/welcome/quick-start/): 0 clean; 1 findings; 2 error; 3 error; 4 error; 5 error; other nonzero rejected.
123
+ exitCodes: { ran: [1, 2, 3, 4, 5] },
124
+ }, (raw) => parseGolangciJson(raw, ctx.filePath, cwd));
125
+ if (parsed.skipped)
126
+ return parsed.skipped;
127
+ return finishParsedRun({
128
+ tool: "golangci-lint",
129
+ ctx,
130
+ result,
131
+ diagnostics: parsed.diagnostics,
132
+ });
118
133
  },
119
134
  };
120
135
  export default golangciRunner;
@@ -1,4 +1,5 @@
1
1
  import * as path from "node:path";
2
+ import { pathsEqual } from "../../path-utils.js";
2
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
4
  import { resolveRunnerCwd } from "../../tool-cwd.js";
4
5
  import { getLinterPolicyForCwd } from "../../tool-policy.js";
@@ -7,25 +8,32 @@ import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } fro
7
8
  import { parseToolRun } from "./utils/tool-failure.js";
8
9
  import { finishParsedRun } from "./utils/tool-failure.js";
9
10
  const hadolint = createAvailabilityChecker("hadolint", ".exe");
10
- function parseHadolintOutput(raw, filePath) {
11
+ function parseHadolintOutput(raw, filePath, cwd) {
11
12
  try {
12
13
  const parsed = JSON.parse(raw);
13
14
  if (!Array.isArray(parsed))
14
15
  return [];
15
- return parsed.map((item) => {
16
+ const absTarget = path.resolve(cwd, filePath);
17
+ return parsed.flatMap((item) => {
18
+ // #3295: hadolint takes a LIST of Dockerfiles and names each finding's
19
+ // own `file`.
20
+ if (item.file && !pathsEqual(path.resolve(cwd, item.file), absTarget))
21
+ return [];
16
22
  const severity = item.level === "error" ? "error" : "warning";
17
- return {
18
- id: `hadolint-${item.code}-${item.line}`,
19
- message: `[${item.code}] ${item.message}`,
20
- filePath,
21
- line: item.line,
22
- column: item.column ?? 1,
23
- severity,
24
- semantic: severity === "error" ? "blocking" : "warning",
25
- tool: "hadolint",
26
- rule: item.code,
27
- fixable: false,
28
- };
23
+ return [
24
+ {
25
+ id: `hadolint-${item.code}-${item.line}`,
26
+ message: `[${item.code}] ${item.message}`,
27
+ filePath,
28
+ line: item.line,
29
+ column: item.column ?? 1,
30
+ severity,
31
+ semantic: severity === "error" ? "blocking" : "warning",
32
+ tool: "hadolint",
33
+ rule: item.code,
34
+ fixable: false,
35
+ },
36
+ ];
29
37
  });
30
38
  }
31
39
  catch {
@@ -57,7 +65,11 @@ const hadolintRunner = {
57
65
  // #1948: hadolint runs with `--no-fail`, so it exits 0 even when it finds
58
66
  // something. A nonzero exit therefore means hadolint itself failed, and
59
67
  // zero parsed diagnostics out of whatever it printed is a parser break.
60
- const run = parseToolRun("hadolint", { result }, (out) => parseHadolintOutput(out, ctx.filePath));
68
+ const run = parseToolRun("hadolint", {
69
+ result,
70
+ // EXIT TABLE (Hadolint 2.12 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
71
+ exitCodes: { ran: [1, 2] },
72
+ }, (out) => parseHadolintOutput(out, ctx.filePath, cwd));
61
73
  if (run.skipped)
62
74
  return run.skipped;
63
75
  const diagnostics = run.diagnostics;
@@ -1,4 +1,5 @@
1
1
  import * as path from "node:path";
2
+ import { pathsEqual } from "../../path-utils.js";
2
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
4
  import { resolveRunnerCwd } from "../../tool-cwd.js";
4
5
  import { getLinterPolicyForCwd } from "../../tool-policy.js";
@@ -15,19 +16,22 @@ const HTMLHINT_RULES = {
15
16
  "spec-char-escape": true,
16
17
  "id-unique": true,
17
18
  };
18
- function parseHtmlhintOutput(raw, filePath) {
19
+ function parseHtmlhintOutput(raw, filePath, cwd) {
19
20
  const diagnostics = [];
21
+ const absTarget = path.resolve(cwd, filePath);
20
22
  // unix format: "file:line:col: message [severity/rule]"
21
- const lineRe = /^.+?:(\d+):(\d+): (.+?) \[(error|warning)\/([^\]]+)\]/;
23
+ const lineRe = /^(.+?):(\d+):(\d+): (.+?) \[(error|warning)\/([^\]]+)\]/;
22
24
  for (const line of raw.split("\n")) {
23
25
  const match = line.match(lineRe);
24
26
  if (!match)
25
27
  continue;
26
- const lineNum = parseInt(match[1], 10);
27
- const col = parseInt(match[2], 10);
28
- const message = match[3].trim();
29
- const level = match[4];
30
- const rule = match[5].trim();
28
+ if (!pathsEqual(path.resolve(cwd, match[1]), absTarget))
29
+ continue;
30
+ const lineNum = parseInt(match[2], 10);
31
+ const col = parseInt(match[3], 10);
32
+ const message = match[4].trim();
33
+ const level = match[5];
34
+ const rule = match[6].trim();
31
35
  const severity = level === "error" ? "error" : "warning";
32
36
  diagnostics.push({
33
37
  id: `htmlhint-${rule}-${lineNum}`,
@@ -86,7 +90,12 @@ const htmlhintRunner = {
86
90
  // #1948: htmlhint exits 1 when it finds errors and prints them in `unix`
87
91
  // format on stdout. Zero parsed out of a nonzero exit is a parser break.
88
92
  const output = result.stdout || result.stderr || "";
89
- const run = parseToolRun("htmlhint", { result, output }, (out) => parseHtmlhintOutput(out, ctx.filePath));
93
+ const run = parseToolRun("htmlhint", {
94
+ result,
95
+ output,
96
+ // EXIT TABLE (HTMLHint 1.1 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
97
+ exitCodes: { ran: [1, 2] },
98
+ }, (out) => parseHtmlhintOutput(out, ctx.filePath, cwd));
90
99
  if (run.skipped)
91
100
  return run.skipped;
92
101
  const diagnostics = run.diagnostics;
@@ -1,21 +1,29 @@
1
1
  import * as path from "node:path";
2
+ import { pathsEqual } from "../../path-utils.js";
2
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
4
  import { resolveRunnerCwd } from "../../tool-cwd.js";
4
5
  import { hasJavaBuildDescriptor } from "../../tool-policy.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 javac = createAvailabilityChecker("javac", ".exe");
8
- function parseJavacOutput(raw, filePath) {
10
+ function parseJavacOutput(raw, filePath, cwd) {
9
11
  const diagnostics = [];
10
12
  const lines = raw.split(/\r?\n/);
13
+ const absTarget = path.resolve(cwd, filePath);
11
14
  for (const line of lines) {
12
15
  const match = line.match(/^(.*?\.java):(\d+):\s+(error|warning):\s+(.+)$/i);
13
16
  if (!match)
14
17
  continue;
15
18
  const [, reportedFile, lineStr, severityLabel, message] = match;
16
- const resolvedReported = path.resolve(reportedFile.trim());
17
- const resolvedTarget = path.resolve(filePath);
18
- if (resolvedReported !== resolvedTarget)
19
+ // Is this reported diagnostic about the file we dispatched for? ONE seam
20
+ // answers that for every runner (#3278): resolve the tool's spelling against
21
+ // the cwd the tool RAN in — never `process.cwd()`, which is the extension's,
22
+ // not the runner's — and compare through `pathsEqual`, the repo's on-disk
23
+ // identity predicate. A bare `===` treats a spelling that differs only in
24
+ // case (the same file on Windows and on a case-folding POSIX mount) as a
25
+ // different file and drops every finding for the edited file (#209, #3277).
26
+ if (!pathsEqual(path.resolve(cwd, reportedFile.trim()), absTarget))
19
27
  continue;
20
28
  const severity = severityLabel.toLowerCase() === "error" ? "error" : "warning";
21
29
  const lineNum = Number.parseInt(lineStr, 10) || 1;
@@ -66,24 +74,26 @@ const javacRunner = {
66
74
  timeout: 30000,
67
75
  });
68
76
  const raw = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
69
- if (result.status === 0 && !raw) {
70
- return { status: "succeeded", diagnostics: [], semantic: "none" };
71
- }
72
- const diagnostics = parseJavacOutput(raw, ctx.filePath);
73
- if (diagnostics.length === 0) {
74
- return {
75
- status: result.status === 0 ? "succeeded" : "failed",
76
- diagnostics: [],
77
+ const parsed = parseToolRun("javac", {
78
+ result,
79
+ output: raw,
80
+ // EXIT TABLE (javac 21 docs https://docs.oracle.com/en/java/javase/21/docs/specs/man/javac.html): 0 clean; 1 findings; 2 error; other nonzero rejected.
81
+ exitCodes: { ran: [1, 2] },
82
+ }, (output) => parseJavacOutput(output, ctx.filePath, cwd));
83
+ if (parsed.skipped)
84
+ return parsed.skipped;
85
+ return finishParsedRun({
86
+ tool: "javac",
87
+ ctx,
88
+ result,
89
+ diagnostics: parsed.diagnostics,
90
+ classify: (diagnostics) => ({
91
+ status: diagnostics.some((d) => d.severity === "error")
92
+ ? "failed"
93
+ : "succeeded",
77
94
  semantic: "warning",
78
- rawOutput: raw,
79
- };
80
- }
81
- const hasErrors = diagnostics.some((d) => d.severity === "error");
82
- return {
83
- status: hasErrors ? "failed" : "succeeded",
84
- diagnostics,
85
- semantic: "warning",
86
- };
95
+ }),
96
+ });
87
97
  },
88
98
  };
89
99
  export default javacRunner;
@@ -1,10 +1,21 @@
1
1
  import * as path from "node:path";
2
+ import { pathsEqual } from "../../path-utils.js";
2
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
4
  import { resolveRunnerCwd } from "../../tool-cwd.js";
4
5
  import { getAutofixCapability, getLinterPolicyForCwd, } from "../../tool-policy.js";
5
6
  import { PRIORITY } from "../priorities.js";
6
7
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
8
+ import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
7
9
  const ktlint = createAvailabilityChecker("ktlint", ".exe");
10
+ // KtLint's exit contract, declared HERE rather than in the shared classifier:
11
+ // an admission is a property of this tool, and #3291 round 2 shipped it through
12
+ // a shared `parseToolRun` option that silently erased every OTHER runner's
13
+ // table. KtLint 1.8.0's CLI page documents the JSON reporter and a nonzero exit
14
+ // on violations, but does not enumerate numeric classes; 1, 2 and 3 are the
15
+ // statuses this runner observes carrying a valid reporter payload, and a valid
16
+ // payload is the evidence that analysis was reached. Any other nonzero status
17
+ // stays a rejected invocation.
18
+ const KTLINT_EXIT_CODES = { ran: [1, 2, 3] };
8
19
  function normalizeKtlintResults(parsed) {
9
20
  if (Array.isArray(parsed)) {
10
21
  return parsed;
@@ -16,14 +27,19 @@ function normalizeKtlintResults(parsed) {
16
27
  }
17
28
  return null;
18
29
  }
19
- function parseKtlintOutput(raw, filePath) {
30
+ function parseKtlintOutput(raw, filePath, cwd) {
20
31
  try {
21
32
  const parsed = normalizeKtlintResults(JSON.parse(raw));
22
33
  if (!parsed)
23
34
  return null;
24
35
  const autofix = getAutofixCapability("ktlint");
25
36
  const diagnostics = [];
37
+ const absTarget = path.resolve(cwd, filePath);
26
38
  for (const result of parsed) {
39
+ // #3295: ktlint's JSON reporter is an array of FILES; `.editorconfig`
40
+ // globs put more than the argv in it.
41
+ if (result.file && !pathsEqual(path.resolve(cwd, result.file), absTarget))
42
+ continue;
27
43
  for (const err of result.errors ?? []) {
28
44
  diagnostics.push({
29
45
  id: `ktlint-${err.ruleId}-${err.line}-${err.col}`,
@@ -47,12 +63,6 @@ function parseKtlintOutput(raw, filePath) {
47
63
  return null;
48
64
  }
49
65
  }
50
- function firstOutputLine(result) {
51
- return (result.stderr || result.stdout || "")
52
- .trim()
53
- .split(/\r?\n/, 1)[0]
54
- .slice(0, 200);
55
- }
56
66
  const ktlintRunner = {
57
67
  id: "ktlint",
58
68
  appliesTo: ["kotlin"],
@@ -78,59 +88,23 @@ const ktlintRunner = {
78
88
  cwd,
79
89
  timeout: 30000,
80
90
  });
81
- // Ktlint exits non-zero when issues are found, so only treat a total lack
82
- // of output as a hard skip. Any non-empty but unparseable output should
83
- // surface as runner failure instead of a false clean result.
84
- if (result.error && !result.stdout) {
85
- return { status: "skipped", diagnostics: [], semantic: "none" };
86
- }
87
- const diagnostics = parseKtlintOutput(result.stdout || "", ctx.filePath);
88
- if (diagnostics === null) {
89
- const detail = firstOutputLine(result) || "Unknown ktlint output";
90
- return {
91
- status: "failed",
92
- diagnostics: [
93
- {
94
- id: "ktlint-output-unparseable",
95
- message: `Unable to parse ktlint output: ${detail}`,
96
- filePath: ctx.filePath,
97
- severity: "warning",
98
- semantic: "warning",
99
- tool: "ktlint",
100
- fixable: false,
101
- autoFixAvailable: false,
102
- },
103
- ],
104
- semantic: "warning",
105
- };
106
- }
107
- if (diagnostics.length === 0) {
108
- if (result.status && result.status !== 0) {
109
- return {
110
- status: "failed",
111
- diagnostics: [
112
- {
113
- id: "ktlint-nonzero-no-diagnostics",
114
- message: firstOutputLine(result) ||
115
- "ktlint exited non-zero without JSON diagnostics",
116
- filePath: ctx.filePath,
117
- severity: "warning",
118
- semantic: "warning",
119
- tool: "ktlint",
120
- fixable: false,
121
- autoFixAvailable: false,
122
- },
123
- ],
124
- semantic: "warning",
125
- };
126
- }
127
- return { status: "succeeded", diagnostics: [], semantic: "none" };
128
- }
129
- return {
130
- status: result.status && result.status !== 0 ? "failed" : "succeeded",
131
- diagnostics,
132
- semantic: "warning",
133
- };
91
+ // `output` is what the classifier judges AND what the parser reads: ktlint
92
+ // writes its reporter payload to stdout and its errors to stderr, so both
93
+ // are one wire here. The exit table rides in this same per-runner input,
94
+ // never in a shared option (#3291 r3).
95
+ const run = parseToolRun("ktlint", {
96
+ result,
97
+ output: `${result.stdout ?? ""}\n${result.stderr ?? ""}`,
98
+ exitCodes: KTLINT_EXIT_CODES,
99
+ }, (output) => parseKtlintOutput(output, ctx.filePath, cwd) ?? []);
100
+ if (run.skipped)
101
+ return run.skipped;
102
+ return finishParsedRun({
103
+ tool: "ktlint",
104
+ ctx,
105
+ result,
106
+ diagnostics: run.diagnostics,
107
+ });
134
108
  },
135
109
  };
136
110
  export default ktlintRunner;
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { logExtension } from "../../extension-log.js";
15
15
  import { getLspCapableKinds } from "../../language-policy.js";
16
+ import { exceedsLspSyncLimits } from "../../lsp/content-limits.js";
16
17
  import { touchCoverageGap } from "../../lsp/diagnostic-binding.js";
17
18
  import { getLSPService } from "../../lsp/index.js";
18
19
  import { LSP_SERVERS } from "../../lsp/server.js";
@@ -25,8 +26,6 @@ import { enabledAuxiliaryLspServerIds, retagAuxiliaryDiagnostics, } from "../aux
25
26
  import { readFileContent } from "./utils.js";
26
27
  import { tryWarmAttachedCodeActions, tryWarmAttachedDiagnostics, } from "../../warm-attach.js";
27
28
  import { contentHash, WARM_CODE_ACTION_LOOKUP_LIMIT } from "../../mcp/ipc.js";
28
- const LSP_MAX_FILE_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
29
- const LSP_MAX_FILE_LINES = RUNTIME_CONFIG.pipeline.lspMaxFileLines;
30
29
  const LSP_SPAWN_BUDGET_MS = RUNTIME_CONFIG.pipeline.lspSpawnBudgetMs;
31
30
  // Diagnostics-wait cap for the dispatch lsp-runner. Bounded so a slow LSP
32
31
  // (typescript-language-server on large monorepos has been observed >7 s)
@@ -133,9 +132,11 @@ const lspRunner = {
133
132
  if (!content) {
134
133
  return { status: "skipped", diagnostics: [], semantic: "none" };
135
134
  }
136
- const sizeBytes = Buffer.byteLength(content, "utf-8");
137
- const lineCount = content.split("\n").length;
138
- if (sizeBytes > LSP_MAX_FILE_BYTES || lineCount > LSP_MAX_FILE_LINES) {
135
+ // #3405 r2: was an inline second copy of the pipeline's byte/line
136
+ // predicate. One module owns the bound now (M3406-1's root cause was that
137
+ // a third writer could be added with neither copy); the SKIP is still this
138
+ // runner's own policy.
139
+ if (exceedsLspSyncLimits(content).tooLarge) {
139
140
  return { status: "skipped", diagnostics: [], semantic: "none" };
140
141
  }
141
142
  // Cross-cutting auxiliary scanners (opengrep, …) attach alongside the