pi-lens 4.1.0 → 4.1.2

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/CHANGELOG.md +185 -0
  2. package/README.md +106 -0
  3. package/dist/clients/actionable-warnings-logger.js +2 -1
  4. package/dist/clients/actionable-warnings.js +14 -5
  5. package/dist/clients/advisory-provenance.js +48 -18
  6. package/dist/clients/ast-grep-client.js +1 -3
  7. package/dist/clients/atomic-write-staging.js +16 -4
  8. package/dist/clients/bash-file-access.js +99 -6
  9. package/dist/clients/biome-client.js +1 -3
  10. package/dist/clients/blocker-freshness.js +21 -8
  11. package/dist/clients/blocker-past-eof.js +16 -6
  12. package/dist/clients/bounded-cache.js +24 -8
  13. package/dist/clients/bounded-pid-file-lock.js +4 -3
  14. package/dist/clients/bounded-telemetry.js +14 -0
  15. package/dist/clients/cache-manager.js +6 -7
  16. package/dist/clients/cache-observability.js +782 -110
  17. package/dist/clients/call-graph.js +238 -63
  18. package/dist/clients/codebase-model.js +13 -5
  19. package/dist/clients/complexity-client.js +1 -3
  20. package/dist/clients/dead-code-client.js +19 -8
  21. package/dist/clients/dead-code-logger.js +1 -2
  22. package/dist/clients/deadline-utils.js +3 -1
  23. package/dist/clients/debug-heap.js +8 -1
  24. package/dist/clients/degradation-ledger.js +22 -1
  25. package/dist/clients/demoted-finding-render.js +155 -0
  26. package/dist/clients/dependency-checker.js +11 -7
  27. package/dist/clients/deps/pi-tui.js +8 -2
  28. package/dist/clients/deps/typebox.js +8 -3
  29. package/dist/clients/diagnostic-dispositions.js +39 -2
  30. package/dist/clients/diagnostic-line-freshness.js +5 -1
  31. package/dist/clients/dispatch/auxiliary-lsp.js +2 -3
  32. package/dist/clients/dispatch/dispatcher.js +13 -1
  33. package/dist/clients/dispatch/facts/function-facts.js +3 -1
  34. package/dist/clients/dispatch/facts/import-facts.js +2 -1
  35. package/dist/clients/dispatch/facts/tree-sitter-facts.js +1 -1
  36. package/dist/clients/dispatch/integration.js +9 -4
  37. package/dist/clients/dispatch/rules/error-swallowing.js +3 -1
  38. package/dist/clients/dispatch/rules/pass-through-wrappers.js +6 -2
  39. package/dist/clients/dispatch/rules/unsafe-boundary.js +31 -7
  40. package/dist/clients/dispatch/runners/actionlint.js +5 -2
  41. package/dist/clients/dispatch/runners/biome-check.js +10 -14
  42. package/dist/clients/dispatch/runners/cpp-check.js +5 -1
  43. package/dist/clients/dispatch/runners/detekt.js +9 -6
  44. package/dist/clients/dispatch/runners/dotnet-build.js +1 -1
  45. package/dist/clients/dispatch/runners/elixir-check.js +3 -1
  46. package/dist/clients/dispatch/runners/eslint.js +25 -6
  47. package/dist/clients/dispatch/runners/fish-indent.js +1 -1
  48. package/dist/clients/dispatch/runners/gleam-check.js +1 -1
  49. package/dist/clients/dispatch/runners/golangci-lint.js +3 -1
  50. package/dist/clients/dispatch/runners/hadolint.js +22 -15
  51. package/dist/clients/dispatch/runners/helm-lint.js +4 -1
  52. package/dist/clients/dispatch/runners/htmlhint.js +33 -16
  53. package/dist/clients/dispatch/runners/javac.js +1 -1
  54. package/dist/clients/dispatch/runners/ktlint.js +1 -1
  55. package/dist/clients/dispatch/runners/lsp.js +1 -1
  56. package/dist/clients/dispatch/runners/markdownlint.js +34 -12
  57. package/dist/clients/dispatch/runners/mypy.js +15 -17
  58. package/dist/clients/dispatch/runners/oxlint.js +231 -15
  59. package/dist/clients/dispatch/runners/php-lint.js +1 -1
  60. package/dist/clients/dispatch/runners/phpstan.js +52 -15
  61. package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -6
  62. package/dist/clients/dispatch/runners/pyright.js +23 -14
  63. package/dist/clients/dispatch/runners/rust-clippy.js +1 -1
  64. package/dist/clients/dispatch/runners/shellcheck.js +7 -9
  65. package/dist/clients/dispatch/runners/shfmt.js +5 -2
  66. package/dist/clients/dispatch/runners/spellcheck.js +10 -15
  67. package/dist/clients/dispatch/runners/sqlfluff.js +25 -17
  68. package/dist/clients/dispatch/runners/stylelint.js +40 -20
  69. package/dist/clients/dispatch/runners/swiftlint.js +8 -8
  70. package/dist/clients/dispatch/runners/taplo.js +110 -23
  71. package/dist/clients/dispatch/runners/terragrunt.js +13 -8
  72. package/dist/clients/dispatch/runners/tflint.js +25 -15
  73. package/dist/clients/dispatch/runners/tree-sitter.js +17 -3
  74. package/dist/clients/dispatch/runners/utils/candidate-probe.js +7 -1
  75. package/dist/clients/dispatch/runners/utils/runner-helpers.js +24 -1
  76. package/dist/clients/dispatch/runners/utils/tool-failure.js +152 -1
  77. package/dist/clients/dispatch/runners/vale.js +16 -14
  78. package/dist/clients/dispatch/runners/yaml-rule-parser.js +1 -3
  79. package/dist/clients/dispatch/runners/yamllint.js +13 -16
  80. package/dist/clients/dispatch/runners/zig-check.js +1 -1
  81. package/dist/clients/dispatch/suppress-writer.js +2 -1
  82. package/dist/clients/dispatch/types.js +6 -1
  83. package/dist/clients/dispatch/utils/lsp-diagnostics.js +6 -2
  84. package/dist/clients/extension-log.js +5 -3
  85. package/dist/clients/feature-hints.js +8 -1
  86. package/dist/clients/file-kinds.js +34 -129
  87. package/dist/clients/file-utils.js +17 -1
  88. package/dist/clients/finding-delivery-gate.js +79 -2
  89. package/dist/clients/formatters.js +144 -27
  90. package/dist/clients/freshness.js +38 -0
  91. package/dist/clients/git-guard.js +43 -13
  92. package/dist/clients/go-client.js +1 -3
  93. package/dist/clients/grammar-source.js +1 -1
  94. package/dist/clients/gzip-stage-write.js +30 -4
  95. package/dist/clients/host-edit-normalize.js +1 -3
  96. package/dist/clients/install-diagnostics.js +4 -2
  97. package/dist/clients/installer/index.js +86 -77
  98. package/dist/clients/instance-reaper.js +5 -2
  99. package/dist/clients/instance-registry.js +4 -2
  100. package/dist/clients/jscpd-client.js +11 -3
  101. package/dist/clients/knip-client.js +29 -15
  102. package/dist/clients/language-profile.js +11 -3
  103. package/dist/clients/latency-logger.js +13 -2
  104. package/dist/clients/lens-config.js +1 -1
  105. package/dist/clients/lens-map.js +8 -3
  106. package/dist/clients/live-bus-emitter.js +1 -1
  107. package/dist/clients/lsp/cascade-tier.js +27 -2
  108. package/dist/clients/lsp/client.js +144 -5
  109. package/dist/clients/lsp/config.js +4 -6
  110. package/dist/clients/lsp/edits.js +119 -32
  111. package/dist/clients/lsp/index.js +245 -41
  112. package/dist/clients/lsp/inferred-project.js +2 -1
  113. package/dist/clients/lsp/jvm-runtime.js +4 -1
  114. package/dist/clients/lsp/launch.js +11 -3
  115. package/dist/clients/lsp/pending-aux-coverage.js +152 -0
  116. package/dist/clients/lsp/server.js +19 -7
  117. package/dist/clients/lsp/tsserver-sync.js +10 -2
  118. package/dist/clients/lsp/workspace-diagnostics-cache.js +9 -4
  119. package/dist/clients/lsp-document-symbols.js +1 -3
  120. package/dist/clients/lsp-mutation.js +10 -20
  121. package/dist/clients/mcp/host-shim.js +1 -1
  122. package/dist/clients/memory-sampler.js +73 -7
  123. package/dist/clients/metrics-client.js +1 -3
  124. package/dist/clients/middle-man-analysis.js +8 -3
  125. package/dist/clients/module-report-lsp.js +7 -1
  126. package/dist/clients/module-report.js +28 -18
  127. package/dist/clients/ndjson-logger.js +117 -28
  128. package/dist/clients/opaque-mutation-scan.js +220 -0
  129. package/dist/clients/package-manager.js +6 -1
  130. package/dist/clients/pipeline.js +57 -9
  131. package/dist/clients/project-conventions.js +2 -8
  132. package/dist/clients/project-diagnostics/cache.js +10 -7
  133. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +2 -1
  134. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +3 -1
  135. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +13 -9
  136. package/dist/clients/project-diagnostics/runner-adapters/trivy.js +3 -1
  137. package/dist/clients/project-diagnostics/scanner.js +22 -10
  138. package/dist/clients/project-lens-config.js +12 -5
  139. package/dist/clients/project-report.js +62 -27
  140. package/dist/clients/project-snapshot-fingerprint.js +48 -0
  141. package/dist/clients/project-snapshot-persist-worker.js +15 -1
  142. package/dist/clients/project-snapshot.js +472 -66
  143. package/dist/clients/read-expansion.js +12 -2
  144. package/dist/clients/read-guard-logger.js +12 -6
  145. package/dist/clients/read-guard-tool-lines.js +5 -2
  146. package/dist/clients/read-guard.js +88 -19
  147. package/dist/clients/resource-sampler.js +1 -1
  148. package/dist/clients/reverse-deps.js +6 -5
  149. package/dist/clients/review-graph/builder.js +236 -48
  150. package/dist/clients/review-graph/git-identity.js +8 -2
  151. package/dist/clients/review-graph/import-resolvers.js +10 -1
  152. package/dist/clients/review-graph/query.js +2 -1
  153. package/dist/clients/review-graph/revision-drift.js +21 -0
  154. package/dist/clients/review-graph/tsconfig-paths.js +4 -2
  155. package/dist/clients/review-graph/workspace-modules.js +8 -5
  156. package/dist/clients/ruff-client.js +6 -5
  157. package/dist/clients/runtime-agent-end.js +63 -31
  158. package/dist/clients/runtime-context.js +12 -4
  159. package/dist/clients/runtime-coordinator.js +127 -4
  160. package/dist/clients/runtime-session.js +55 -0
  161. package/dist/clients/runtime-tool-call.js +59 -3
  162. package/dist/clients/runtime-tool-result.js +148 -34
  163. package/dist/clients/runtime-turn.js +194 -8
  164. package/dist/clients/rust-client.js +1 -3
  165. package/dist/clients/safe-spawn.js +150 -11
  166. package/dist/clients/security-scan-client.js +1 -3
  167. package/dist/clients/session-event-guard.js +184 -0
  168. package/dist/clients/session-summary.js +3 -1
  169. package/dist/clients/sg-runner.js +1 -3
  170. package/dist/clients/source-filter.js +16 -8
  171. package/dist/clients/source-walker.js +1 -1
  172. package/dist/clients/spawn-timeout-cooldown.js +96 -0
  173. package/dist/clients/startup-scan.js +9 -3
  174. package/dist/clients/tool-definition.js +7 -0
  175. package/dist/clients/tool-policy.js +26 -11
  176. package/dist/clients/tool-render.js +2 -1
  177. package/dist/clients/tree-sitter-client.js +10 -4
  178. package/dist/clients/tree-sitter-logger.js +2 -1
  179. package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
  180. package/dist/clients/warm-attach.js +12 -5
  181. package/dist/clients/widget-state.js +27 -11
  182. package/dist/clients/word-index.js +3 -1
  183. package/dist/clients/workspace-topology.js +8 -2
  184. package/dist/clients/zizmor-config.js +9 -1
  185. package/dist/i18n.js +13 -5
  186. package/dist/index.js +15811 -12810
  187. package/dist/mcp/server.js +3 -1
  188. package/dist/tools/ast-grep-replace.js +68 -12
  189. package/dist/tools/lens-diagnostic-mark.js +21 -7
  190. package/dist/tools/lens-diagnostics.js +174 -38
  191. package/dist/tools/lsp-diagnostics.js +24 -7
  192. package/dist/tools/lsp-navigation.js +3 -1
  193. package/dist/tools/module-report.js +1 -1
  194. package/dist/tools/project-report.js +1 -1
  195. package/dist/tools/symbol-search.js +5 -2
  196. package/docs/durable-store-audit-1202.md +1 -1
  197. package/docs/servercapabilities.md +1 -1
  198. package/package.json +160 -143
  199. package/rules/typos/_typos.toml +2 -2
  200. package/scripts/analyze-pi-lens-logs.mjs +31 -13
  201. package/scripts/download-grammars.js +9 -3
  202. package/scripts/install-selftest.mjs +99 -11
  203. package/scripts/lib/host-provided-deps.mjs +75 -0
  204. package/scripts/lib/warm-loader-cache.mjs +285 -0
  205. package/scripts/rpc-load-check.mjs +30 -9
  206. package/scripts/warm-loader-cache.mjs +233 -0
@@ -29,7 +29,9 @@ export function formatToolFailure(input) {
29
29
  ? "did not run (spawn failure)"
30
30
  : `exited ${input.status}`;
31
31
  const missing = input.reportMissing ? "no report file" : "no output";
32
- const detail = firstOutputLine(input.stderr) || firstOutputLine(input.stdout) || "no stderr";
32
+ const detail = firstOutputLine(input.stderr) ||
33
+ firstOutputLine(input.stdout) ||
34
+ "no stderr";
33
35
  return truncateForLedger(`${identity} ${what} with ${missing}: ${detail}`);
34
36
  }
35
37
  /**
@@ -70,3 +72,152 @@ export function skipUnlessToolRan(tool, input, fields) {
70
72
  });
71
73
  return { status: "skipped", diagnostics: [], semantic: "none" };
72
74
  }
75
+ /**
76
+ * The record a runner leaves when its parser read NOTHING out of output the
77
+ * tool did produce (#1948).
78
+ *
79
+ * `skipUnlessToolRan` covers the adjacent case: the tool produced no output at
80
+ * all. The hole it leaves is the one that hid five parser bugs for months —
81
+ * vale (#1933), taplo, stylelint, phpstan (#1946), sqlfluff. Each received a
82
+ * real report, extracted zero diagnostics from it, and reported "succeeded, 0
83
+ * diagnostics", which is byte-for-byte what a genuinely clean file records.
84
+ * A reader could not answer "is this file clean, or did the parser fail?".
85
+ *
86
+ * THE RULE: record only when the exit status is NONZERO, the output handed to
87
+ * the parser is non-empty, and the parse yielded zero diagnostics. A nonzero
88
+ * exit is the tool asserting something is wrong; extracting nothing from that
89
+ * assertion means our reader disagrees with the tool, and one of the two is
90
+ * broken.
91
+ *
92
+ * Exit 0 with zero parsed is deliberately NOT recorded, even with non-empty
93
+ * output: that is the overwhelmingly common clean save. Linters print summary
94
+ * banners, empty JSON arrays, and progress noise on a clean run, so a row per
95
+ * clean save would drown the ledger and bury the real signal.
96
+ *
97
+ * FALSE NEGATIVES this rule accepts, stated plainly:
98
+ * - A tool that reports findings under exit 0. `swiftlint` is the live
99
+ * example among the adopters: a warning-only run exits 0 and still writes
100
+ * a full JSON array, so a swiftlint parser break that never trips a
101
+ * nonzero exit stays invisible here. (`vale` also exits 0 on findings by
102
+ * default, but its actual #1933 break exited 1, so it is not the example.)
103
+ * This gap is deliberate and is NOT closed by adding an output-length
104
+ * threshold for exit 0. That class belongs to the captured-fixture lane —
105
+ * `tests/clients/dispatch/runners/captured-real-output.test.ts` pins each
106
+ * parser against real recorded binary output, which catches a broken
107
+ * parser directly instead of guessing from output size. A length
108
+ * threshold here would fire on every clean run of every tool that prints
109
+ * a summary banner.
110
+ * - A tool whose parser breaks only for a SUBSET of findings still reports
111
+ * the ones it can read, so `parsedCount` is nonzero and nothing records.
112
+ *
113
+ * FALSE POSITIVES it accepts: a runner that legitimately parses zero
114
+ * diagnostics out of a failing run — `go-vet` exits nonzero for a SIBLING
115
+ * file's problem and then filters to the edited file, and `terragrunt` does
116
+ * the same across a unit directory. Such runners stay off this helper rather
117
+ * than being special-cased inside it; see the runner-family registries in
118
+ * `tests/clients/dispatch/runners/parsed-nothing-sweep.test.ts`.
119
+ *
120
+ * Bounded by `incrementDegradationCount`: one latest-reason entry per
121
+ * (kind, subject) with an exact tally, so a permanently broken parser costs
122
+ * ONE ledger row and not one per dispatched file. `subject` is the tool id
123
+ * because a parser break is a property of the tool and our reader of it, not
124
+ * of the project it ran in; aggregating on the tool keeps exactly the identity
125
+ * a reader needs ("which tool is stuck") while staying bounded across every
126
+ * workspace in the session.
127
+ *
128
+ * @returns true when a row was recorded.
129
+ */
130
+ export function recordParsedNothing(input) {
131
+ const { tool, status, output, parsedCount } = input;
132
+ // Three conjuncts, one per clause of the rule above. Each is separately
133
+ // load-bearing and separately tested: `parsedCount` keeps a run that DID
134
+ // yield findings silent, `status` keeps the clean-save case silent (0 and a
135
+ // never-arrived null alike — neither is a tool asserting a problem), and
136
+ // `output` keeps "the tool said nothing at all" with `runner-empty-result`
137
+ // rather than double-recording it here.
138
+ if (parsedCount > 0)
139
+ return false;
140
+ if (!status)
141
+ return false;
142
+ const text = output ?? "";
143
+ if (!text.trim())
144
+ return false;
145
+ const entries = Object.entries(input.fields ?? {}).filter(([, value]) => value !== undefined && value !== "");
146
+ const identity = entries.length
147
+ ? `${tool} (${entries.map(([key, value]) => `${key}=${value}`).join(", ")})`
148
+ : tool;
149
+ incrementDegradationCount({
150
+ kind: "runner-parsed-nothing",
151
+ subject: tool,
152
+ reason: truncateForLedger(`${identity} exited ${status} with ${text.length} chars of output but the parser read 0 diagnostics: ${firstOutputLine(text) || "no first line"}`),
153
+ });
154
+ return true;
155
+ }
156
+ /**
157
+ * The one seam a runner uses to go from a spawn result to diagnostics (#1948).
158
+ *
159
+ * It composes the two gates that must BOTH hold before a runner may claim a
160
+ * clean file: `skipUnlessToolRan` (the tool produced nothing — #1816) and
161
+ * `recordParsedNothing` (the tool produced something the parser could not
162
+ * read). Runners call this instead of calling `skipUnlessToolRan` and then
163
+ * parsing on their own, so the second gate cannot be forgotten by whoever
164
+ * writes runner 53. `tests/clients/dispatch/runners/parsed-nothing-sweep.test.ts`
165
+ * enforces that.
166
+ */
167
+ export function parseToolRun(tool, input, parse, options = {}) {
168
+ const skipped = skipUnlessToolRan(tool, input, options.fields);
169
+ if (skipped)
170
+ return { skipped, diagnostics: [], parsedNothing: false };
171
+ const output = options.parseOutput ?? input.output ?? input.result.stdout ?? "";
172
+ const diagnostics = [...parse(output)];
173
+ const parsedNothing = recordParsedNothing({
174
+ tool,
175
+ status: input.result.status ?? null,
176
+ output,
177
+ parsedCount: diagnostics.length,
178
+ fields: options.fields,
179
+ });
180
+ if (parsedNothing && options.skipWhenParsedNothing) {
181
+ return {
182
+ skipped: { status: "skipped", diagnostics: [], semantic: "none" },
183
+ diagnostics: [],
184
+ parsedNothing,
185
+ };
186
+ }
187
+ return { skipped: null, diagnostics, parsedNothing };
188
+ }
189
+ export function finishParsedRun(input) {
190
+ const raw = `${input.result.stdout ?? ""}${input.result.stderr ?? ""}`;
191
+ const status = input.result.status ?? null;
192
+ if (input.diagnostics.length === 0) {
193
+ if (status !== 0 && raw.trim().length > 0) {
194
+ return {
195
+ status: "failed",
196
+ diagnostics: [
197
+ {
198
+ id: `${input.tool}:parse-error:1`,
199
+ message: `${input.tool} exited ${status} but its output could not be parsed`,
200
+ filePath: input.ctx.filePath,
201
+ line: 1,
202
+ column: 1,
203
+ severity: "warning",
204
+ semantic: "warning",
205
+ tool: input.tool,
206
+ },
207
+ ],
208
+ semantic: "warning",
209
+ };
210
+ }
211
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
212
+ }
213
+ const mapped = input.classify?.(input.diagnostics);
214
+ if (mapped) {
215
+ return { ...mapped, diagnostics: input.diagnostics };
216
+ }
217
+ const hasBlocking = input.diagnostics.some((d) => d.semantic === "blocking");
218
+ return {
219
+ status: hasBlocking ? "failed" : "succeeded",
220
+ diagnostics: input.diagnostics,
221
+ semantic: hasBlocking ? "blocking" : "warning",
222
+ };
223
+ }
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
4
4
  import { PRIORITY } from "../priorities.js";
5
5
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
6
- import { skipUnlessToolRan } from "./utils/tool-failure.js";
6
+ import { parseToolRun } from "./utils/tool-failure.js";
7
7
  const vale = createAvailabilityChecker("vale", ".exe");
8
8
  /**
9
9
  * Check for a .vale.ini config file in cwd or parent dirs.
@@ -24,19 +24,18 @@ function findValeConfig(cwd) {
24
24
  }
25
25
  return undefined;
26
26
  }
27
- function parseValeOutput(raw, filePath) {
27
+ export function parseValeOutput(raw, filePath) {
28
28
  const diagnostics = [];
29
29
  if (!raw.trim())
30
30
  return diagnostics;
31
31
  try {
32
32
  const parsed = JSON.parse(raw);
33
- const files = parsed?.Data?.Files;
34
- if (!files)
33
+ if (!parsed || typeof parsed !== "object")
35
34
  return diagnostics;
36
- for (const file of files) {
37
- if (!file.Alerts)
35
+ for (const alerts of Object.values(parsed)) {
36
+ if (!Array.isArray(alerts))
38
37
  continue;
39
- for (const alert of file.Alerts) {
38
+ for (const alert of alerts) {
40
39
  if (!alert.Message)
41
40
  continue;
42
41
  const severityMap = {
@@ -51,7 +50,7 @@ function parseValeOutput(raw, filePath) {
51
50
  message: `[${alert.Check ?? "vale"}] ${alert.Message}`,
52
51
  filePath,
53
52
  line: alert.Line ?? 1,
54
- column: alert.Column ?? 1,
53
+ column: alert.Span?.[0] ?? 1,
55
54
  severity,
56
55
  semantic: severity === "error" ? "blocking" : "warning",
57
56
  tool: "vale",
@@ -80,7 +79,7 @@ const valeRunner = {
80
79
  return { status: "skipped", diagnostics: [], semantic: "none" };
81
80
  }
82
81
  let cmd = null;
83
- if (await (vale.isAvailableAsync(cwd))) {
82
+ if (await vale.isAvailableAsync(cwd)) {
84
83
  cmd = vale.getCommand(cwd);
85
84
  }
86
85
  else {
@@ -95,11 +94,14 @@ const valeRunner = {
95
94
  // zero alerts, and reported clean prose. No exit-code table: Vale's
96
95
  // nonzero codes vary with --minAlertLevel, so the conservative
97
96
  // nothing-to-parse rule is the only safe discriminator here.
98
- const skipped = skipUnlessToolRan("vale", { result });
99
- if (skipped)
100
- return skipped;
101
- const raw = result.stdout || "";
102
- const diagnostics = parseValeOutput(raw, ctx.filePath);
97
+ //
98
+ // #1948: `parseToolRun` adds the second gate. Vale's `Data.Files`
99
+ // envelope bug produced exactly this shape — exit 1, a full JSON
100
+ // report on stdout, zero alerts parsed — and left no record.
101
+ const run = parseToolRun("vale", { result }, (raw) => parseValeOutput(raw, ctx.filePath));
102
+ if (run.skipped)
103
+ return run.skipped;
104
+ const diagnostics = run.diagnostics;
103
105
  if (diagnostics.length === 0) {
104
106
  return { status: "succeeded", diagnostics: [], semantic: "none" };
105
107
  }
@@ -106,9 +106,7 @@ export function loadYamlRulesFresh(ruleDir, severityFilter) {
106
106
  hash.update("\0");
107
107
  }
108
108
  const signature = hash.digest("hex");
109
- const cache = severityFilter === "error"
110
- ? contentBlockingRulesCache
111
- : contentRulesCache;
109
+ const cache = severityFilter === "error" ? contentBlockingRulesCache : contentRulesCache;
112
110
  const cached = cache.get(ruleDir);
113
111
  if (cached?.signature === signature)
114
112
  return cached.rules;
@@ -2,7 +2,8 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
2
2
  import { getLinterPolicyForCwd, hasYamllintConfig } from "../../tool-policy.js";
3
3
  import { PRIORITY } from "../priorities.js";
4
4
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
5
- import { skipUnlessToolRan } from "./utils/tool-failure.js";
5
+ import { parseToolRun } from "./utils/tool-failure.js";
6
+ import { finishParsedRun } from "./utils/tool-failure.js";
6
7
  const yamllint = createAvailabilityChecker("yamllint", ".exe");
7
8
  export { hasYamllintConfig };
8
9
  function parseYamllintParsable(raw, filePath) {
@@ -45,7 +46,7 @@ const yamllintRunner = {
45
46
  ctx.log("yamllint: no config detected, running with default rules");
46
47
  }
47
48
  let cmd = null;
48
- if (await (yamllint.isAvailableAsync(cwd))) {
49
+ if (await yamllint.isAvailableAsync(cwd)) {
49
50
  cmd = yamllint.getCommand(cwd);
50
51
  }
51
52
  else {
@@ -66,22 +67,18 @@ const yamllintRunner = {
66
67
  // and tracebacks. So the gate judges "nothing to parse" on stdout alone,
67
68
  // while the parser still reads both streams.
68
69
  const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
69
- const skipped = skipUnlessToolRan("yamllint", {
70
+ // #1948: classify on stdout as before, parse both streams as before, and
71
+ // record when a failing run yields nothing.
72
+ const run = parseToolRun("yamllint", { result, output: result.stdout }, (out) => parseYamllintParsable(out, ctx.filePath), { parseOutput: raw });
73
+ if (run.skipped)
74
+ return run.skipped;
75
+ const diagnostics = run.diagnostics;
76
+ return finishParsedRun({
77
+ tool: "yamllint",
78
+ ctx,
70
79
  result,
71
- output: result.stdout,
72
- });
73
- if (skipped)
74
- return skipped;
75
- const diagnostics = parseYamllintParsable(raw, ctx.filePath);
76
- if (diagnostics.length === 0) {
77
- return { status: "succeeded", diagnostics: [], semantic: "none" };
78
- }
79
- const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
80
- return {
81
- status: hasBlocking ? "failed" : "succeeded",
82
80
  diagnostics,
83
- semantic: hasBlocking ? "blocking" : "warning",
84
- };
81
+ });
85
82
  },
86
83
  };
87
84
  export default yamllintRunner;
@@ -48,7 +48,7 @@ const zigCheckRunner = {
48
48
  skipTestFiles: false,
49
49
  async run(ctx) {
50
50
  const cwd = ctx.cwd || process.cwd();
51
- if (!(await (zig.isAvailableAsync(cwd)))) {
51
+ if (!(await zig.isAvailableAsync(cwd))) {
52
52
  return { status: "skipped", diagnostics: [], semantic: "none" };
53
53
  }
54
54
  const cmd = zig.getCommand(cwd);
@@ -56,7 +56,8 @@ export function insertSuppressComment(content, filePath, line, rule) {
56
56
  .filter(Boolean);
57
57
  if (!rules.includes(rule))
58
58
  rules.push(rule);
59
- lines[aboveIdx] = existingAbove.slice(0, match.index) + match[1] + rules.join(", ");
59
+ lines[aboveIdx] =
60
+ existingAbove.slice(0, match.index) + match[1] + rules.join(", ");
60
61
  return lines.join("\n");
61
62
  }
62
63
  // Match the indentation of the flagged line so the inserted comment lines
@@ -10,4 +10,9 @@
10
10
  *
11
11
  * The dispatcher must handle these semantics consistently.
12
12
  */
13
- export {};
13
+ /** Closed telemetry taxonomy for expected runner skips. */
14
+ export const RUNNER_SKIP_REASONS = ["no-files-matched"];
15
+ /** Runtime guard for untyped/plugin-provided runner results. */
16
+ export function isRunnerSkipReason(value) {
17
+ return RUNNER_SKIP_REASONS.some((reason) => reason === value);
18
+ }
@@ -3,9 +3,13 @@ export function convertLspDiagnostics(diags, filePath, options = {}) {
3
3
  return diags
4
4
  .filter((d) => d.range?.start?.line !== undefined)
5
5
  .map((d, idx) => {
6
- const severityMap = { 1: "error", 2: "warning", 4: "hint" };
6
+ const severityMap = {
7
+ 1: "error",
8
+ 2: "warning",
9
+ 4: "hint",
10
+ };
7
11
  const severity = severityMap[d.severity] ?? "info";
8
- const semantic = d.severity === 1 ? "blocking" : (d.severity === 2 ? "warning" : "none");
12
+ const semantic = d.severity === 1 ? "blocking" : d.severity === 2 ? "warning" : "none";
9
13
  const code = String(d.code ?? "unknown");
10
14
  // #692: identity ALWAYS derives from the diagnostic's own source — never
11
15
  // from a caller-supplied scan label (see `scanOrigin`'s doc comment above).
@@ -137,7 +137,7 @@ let moduleLoadWindowOpen = false;
137
137
  * the same as an empty store, so it reads as "no window" rather than throwing.
138
138
  */
139
139
  export function isConsoleCaptureActive() {
140
- return (moduleLoadWindowOpen || consoleCaptureStorage?.getStore() === true);
140
+ return moduleLoadWindowOpen || consoleCaptureStorage?.getStore() === true;
141
141
  }
142
142
  /**
143
143
  * Open the module-evaluation window. Called by `clients/console-guard-install.ts`
@@ -207,7 +207,7 @@ function inCaptureWindow(fn) {
207
207
  * the terminal.
208
208
  */
209
209
  function isCaptureSeam(prop) {
210
- return prop === "on" || (typeof prop === "string" && prop.startsWith("register"));
210
+ return (prop === "on" || (typeof prop === "string" && prop.startsWith("register")));
211
211
  }
212
212
  /**
213
213
  * Wrap every function value found in `value`, in place. Handles the two
@@ -312,7 +312,9 @@ export function withConsoleCaptureWindows(api) {
312
312
  // value instead of tripping that invariant (S2a) — a frozen host API
313
313
  // loses the capture window for that member, but keeps working.
314
314
  const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
315
- if (descriptor && descriptor.writable === false && descriptor.configurable === false) {
315
+ if (descriptor &&
316
+ descriptor.writable === false &&
317
+ descriptor.configurable === false) {
316
318
  return descriptor.value;
317
319
  }
318
320
  const cached = wrapperCache.get(prop);
@@ -81,7 +81,14 @@ export function inferTrustBoundaries(nameOrPath) {
81
81
  boundaries.add("user-input");
82
82
  boundaries.add("process-exec");
83
83
  }
84
- if (hasHintToken(normalized, ["auth", "login", "token", "session", "oauth", "jwt"])) {
84
+ if (hasHintToken(normalized, [
85
+ "auth",
86
+ "login",
87
+ "token",
88
+ "session",
89
+ "oauth",
90
+ "jwt",
91
+ ])) {
85
92
  boundaries.add("auth");
86
93
  boundaries.add("user-input");
87
94
  }
@@ -7,24 +7,10 @@
7
7
  import { basename, extname } from "node:path";
8
8
  // --- Extension Maps ---
9
9
  export const KIND_EXTENSIONS = {
10
- clojure: [
11
- ".clj",
12
- ".cljc",
13
- ".cljs",
14
- ".edn",
15
- ],
16
- cmake: [
17
- ".cmake",
18
- ],
19
- csharp: [
20
- ".cs",
21
- ],
22
- css: [
23
- ".css",
24
- ".less",
25
- ".sass",
26
- ".scss",
27
- ],
10
+ clojure: [".clj", ".cljc", ".cljs", ".edn"],
11
+ cmake: [".cmake"],
12
+ csharp: [".cs"],
13
+ css: [".css", ".less", ".sass", ".scss"],
28
14
  cue: [".cue"],
29
15
  // From llvm-project/clang/lib/Driver/Types.cpp clang::driver::types::lookupTypeForExtension:
30
16
  cxx: [
@@ -61,49 +47,18 @@ export const KIND_EXTENSIONS = {
61
47
  ".cl",
62
48
  ".clcpp",
63
49
  ],
64
- dart: [
65
- ".dart",
66
- ],
67
- docker: [
68
- ".dockerfile",
69
- ],
70
- elixir: [
71
- ".ex",
72
- ".exs",
73
- ],
74
- fish: [
75
- ".fish",
76
- ],
77
- fsharp: [
78
- ".fs",
79
- ".fsi",
80
- ".fsx",
81
- ],
82
- gleam: [
83
- ".gleam",
84
- ],
85
- go: [
86
- ".go",
87
- ],
88
- haskell: [
89
- ".hs",
90
- ".lhs",
91
- ],
92
- "helm-template": [
93
- ".tpl",
94
- ],
95
- html: [
96
- ".htm",
97
- ".html",
98
- ],
99
- java: [
100
- ".java",
101
- ],
102
- json: [
103
- ".json",
104
- ".json5",
105
- ".jsonc",
106
- ],
50
+ dart: [".dart"],
51
+ docker: [".dockerfile"],
52
+ elixir: [".ex", ".exs"],
53
+ fish: [".fish"],
54
+ fsharp: [".fs", ".fsi", ".fsx"],
55
+ gleam: [".gleam"],
56
+ go: [".go"],
57
+ haskell: [".hs", ".lhs"],
58
+ "helm-template": [".tpl"],
59
+ html: [".htm", ".html"],
60
+ java: [".java"],
61
+ json: [".json", ".json5", ".jsonc"],
107
62
  jsts: [
108
63
  ".cjs",
109
64
  ".cts",
@@ -116,75 +71,25 @@ export const KIND_EXTENSIONS = {
116
71
  ".tsx",
117
72
  ".vue",
118
73
  ],
119
- kotlin: [
120
- ".kt",
121
- ".kts",
122
- ],
123
- lua: [
124
- ".lua",
125
- ],
126
- markdown: [
127
- ".md",
128
- ".mdx",
129
- ],
130
- nix: [
131
- ".nix",
132
- ],
133
- ocaml: [
134
- ".ml",
135
- ".mli",
136
- ],
137
- php: [
138
- ".php",
139
- ],
140
- powershell: [
141
- ".ps1",
142
- ".psm1",
143
- ".psd1",
144
- ],
145
- prisma: [
146
- ".prisma",
147
- ],
148
- python: [
149
- ".py",
150
- ".pyi",
151
- ],
152
- ruby: [
153
- ".gemspec",
154
- ".rake",
155
- ".rb",
156
- ".ru",
157
- ],
158
- rust: [
159
- ".rs",
160
- ],
161
- shell: [
162
- ".bash",
163
- ".sh",
164
- ".zsh",
165
- ],
166
- sql: [
167
- ".sql",
168
- ],
169
- swift: [
170
- ".swift",
171
- ],
172
- terraform: [
173
- ".tf",
174
- ".tfvars",
175
- ],
74
+ kotlin: [".kt", ".kts"],
75
+ lua: [".lua"],
76
+ markdown: [".md", ".mdx"],
77
+ nix: [".nix"],
78
+ ocaml: [".ml", ".mli"],
79
+ php: [".php"],
80
+ powershell: [".ps1", ".psm1", ".psd1"],
81
+ prisma: [".prisma"],
82
+ python: [".py", ".pyi"],
83
+ ruby: [".gemspec", ".rake", ".rb", ".ru"],
84
+ rust: [".rs"],
85
+ shell: [".bash", ".sh", ".zsh"],
86
+ sql: [".sql"],
87
+ swift: [".swift"],
88
+ terraform: [".tf", ".tfvars"],
176
89
  terragrunt: [],
177
- toml: [
178
- ".toml",
179
- ],
180
- yaml: [
181
- ".yaml",
182
- ".yml",
183
- ],
184
- zig: [
185
- ".zig",
186
- ".zon",
187
- ],
90
+ toml: [".toml"],
91
+ yaml: [".yaml", ".yml"],
92
+ zig: [".zig", ".zon"],
188
93
  };
189
94
  /** Return whether a path has an extension registered for the given file kind. */
190
95
  export function hasKindExtension(filePath, kind) {
@@ -5,7 +5,8 @@ import * as fs from "node:fs";
5
5
  import * as os from "node:os";
6
6
  import * as path from "node:path";
7
7
  import { minimatch } from "./deps/minimatch.js";
8
- import { collectTrackedFiles, getTrackedFilesSnapshot } from "./git-tracked-ignore.js";
8
+ import { isInSpawnTimeoutCooldown, noteSpawnTimeout, } from "./spawn-timeout-cooldown.js";
9
+ import { collectTrackedFiles, getTrackedFilesSnapshot, } from "./git-tracked-ignore.js";
9
10
  import { getGlobalIgnorePatterns, getPiLensGlobalConfigPath, } from "./lens-config.js";
10
11
  import { normalizeEphemeralMapKey, normalizeFilePath } from "./path-utils.js";
11
12
  import { findPiLensConfigInDir, findPiLensProjectConfig, loadPiLensConfigInDir, loadPiLensProjectConfig, } from "./project-lens-config.js";
@@ -577,12 +578,27 @@ export async function detectFileChangedAfterCommand(filePath, command, args, cwd
577
578
  catch {
578
579
  return 0;
579
580
  }
581
+ // #1995: a command cooling down after a spawn timeout must not be handed
582
+ // a second budget - autofix is one of the lanes that paid twice for a
583
+ // wedged .cmd shim. Return "no fix applied" (an honest not-checked).
584
+ if (isInSpawnTimeoutCooldown(command))
585
+ return 0;
580
586
  const result = await safeSpawnAsync(command, args, {
581
587
  timeout: 30000,
582
588
  cwd,
583
589
  });
584
590
  if (result.error)
585
591
  return 0;
592
+ if (result.failure === "timeout") {
593
+ noteSpawnTimeout({
594
+ tool: path.basename(command).replace(/\.[^.]+$/, ""),
595
+ command,
596
+ phase: "autofix",
597
+ durationMs: 30000,
598
+ teardown: result.timeoutTeardown,
599
+ });
600
+ return 0;
601
+ }
586
602
  if (result.status !== 0 && !ignoreStatuses.includes(result.status ?? -1)) {
587
603
  return 0;
588
604
  }