pi-lens 4.2.0 โ†’ 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +543 -0
  2. package/README.md +45 -2
  3. package/dist/clients/actionable-warnings.js +44 -16
  4. package/dist/clients/advisory-provenance.js +187 -51
  5. package/dist/clients/cache-manager.js +64 -5
  6. package/dist/clients/cascade-format.js +24 -0
  7. package/dist/clients/child-unref.js +17 -8
  8. package/dist/clients/config-diagnostic-codes.js +2 -0
  9. package/dist/clients/config-locations.js +237 -9
  10. package/dist/clients/config-resolve.js +7 -1
  11. package/dist/clients/degradation-ledger.js +33 -3
  12. package/dist/clients/dependency-checker.js +87 -64
  13. package/dist/clients/diagnostic-dispositions.js +39 -3
  14. package/dist/clients/dispatch/finding-policy.js +118 -1
  15. package/dist/clients/dispatch/indent-detect.js +8 -2
  16. package/dist/clients/dispatch/inline-suppressions.js +23 -4
  17. package/dist/clients/dispatch/rule-ignores.js +14 -7
  18. package/dist/clients/dispatch/runners/actionlint.js +26 -28
  19. package/dist/clients/dispatch/runners/biome-check.js +38 -25
  20. package/dist/clients/dispatch/runners/cpp-check.js +32 -49
  21. package/dist/clients/dispatch/runners/credo.js +16 -14
  22. package/dist/clients/dispatch/runners/cue-vet.js +25 -7
  23. package/dist/clients/dispatch/runners/dart-analyze.js +28 -47
  24. package/dist/clients/dispatch/runners/detekt.js +11 -4
  25. package/dist/clients/dispatch/runners/dotnet-build.js +27 -22
  26. package/dist/clients/dispatch/runners/elixir-check.js +31 -46
  27. package/dist/clients/dispatch/runners/eslint.js +29 -46
  28. package/dist/clients/dispatch/runners/gleam-check.js +91 -11
  29. package/dist/clients/dispatch/runners/go-vet.js +16 -2
  30. package/dist/clients/dispatch/runners/golangci-lint.js +38 -23
  31. package/dist/clients/dispatch/runners/hadolint.js +27 -15
  32. package/dist/clients/dispatch/runners/htmlhint.js +17 -8
  33. package/dist/clients/dispatch/runners/javac.js +31 -21
  34. package/dist/clients/dispatch/runners/ktlint.js +34 -60
  35. package/dist/clients/dispatch/runners/lsp.js +6 -5
  36. package/dist/clients/dispatch/runners/markdownlint.js +12 -5
  37. package/dist/clients/dispatch/runners/mypy.js +2 -1
  38. package/dist/clients/dispatch/runners/oxlint.js +22 -6
  39. package/dist/clients/dispatch/runners/php-lint.js +39 -16
  40. package/dist/clients/dispatch/runners/phpstan.js +1 -1
  41. package/dist/clients/dispatch/runners/rubocop.js +27 -18
  42. package/dist/clients/dispatch/runners/ruff.js +18 -19
  43. package/dist/clients/dispatch/runners/rust-clippy.js +6 -1
  44. package/dist/clients/dispatch/runners/shellcheck.js +6 -2
  45. package/dist/clients/dispatch/runners/spellcheck.js +10 -3
  46. package/dist/clients/dispatch/runners/sqlfluff.js +11 -2
  47. package/dist/clients/dispatch/runners/stylelint.js +10 -2
  48. package/dist/clients/dispatch/runners/swiftlint.js +11 -2
  49. package/dist/clients/dispatch/runners/taplo.js +12 -6
  50. package/dist/clients/dispatch/runners/tflint.js +30 -15
  51. package/dist/clients/dispatch/runners/tree-sitter.js +23 -0
  52. package/dist/clients/dispatch/runners/trivy-config.js +8 -2
  53. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
  54. package/dist/clients/dispatch/runners/vale.js +15 -3
  55. package/dist/clients/dispatch/runners/yamllint.js +12 -2
  56. package/dist/clients/dispatch/runners/zig-check.js +27 -39
  57. package/dist/clients/dispatch/suppress-writer.js +13 -3
  58. package/dist/clients/effective-config.js +3 -1
  59. package/dist/clients/extension-log.js +48 -1
  60. package/dist/clients/finding-delivery-gate.js +53 -23
  61. package/dist/clients/formatters.js +49 -11
  62. package/dist/clients/git-guard.js +247 -34
  63. package/dist/clients/gradle-ktfmt-style.js +121 -25
  64. package/dist/clients/inline-blocker-dispositions.js +186 -0
  65. package/dist/clients/install-diagnostics.js +19 -6
  66. package/dist/clients/installer/index.js +281 -32
  67. package/dist/clients/jscpd-client.js +8 -0
  68. package/dist/clients/latency-logger.js +8 -0
  69. package/dist/clients/lens-config.js +66 -8
  70. package/dist/clients/lsp/cascade-tier.js +3 -0
  71. package/dist/clients/lsp/client.js +139 -11
  72. package/dist/clients/lsp/config.js +9 -7
  73. package/dist/clients/lsp/content-limits.js +41 -0
  74. package/dist/clients/lsp/index.js +42 -8
  75. package/dist/clients/lsp/launch.js +23 -2
  76. package/dist/clients/lsp/server.js +106 -25
  77. package/dist/clients/lsp/sync-kind.js +14 -0
  78. package/dist/clients/lsp/wait-policy/strategies.js +105 -16
  79. package/dist/clients/mcp/analyze.js +9 -2
  80. package/dist/clients/mcp/ipc.js +232 -44
  81. package/dist/clients/mcp/review.js +53 -7
  82. package/dist/clients/opengrep-client.js +11 -6
  83. package/dist/clients/path-utils.js +51 -9
  84. package/dist/clients/persistent-reverify.js +271 -0
  85. package/dist/clients/pipeline.js +136 -67
  86. package/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
  87. package/dist/clients/project-lens-config.js +11 -3
  88. package/dist/clients/review-graph/builder.js +34 -26
  89. package/dist/clients/review-graph/git-identity.js +16 -10
  90. package/dist/clients/ruff-client.js +31 -4
  91. package/dist/clients/runtime-coordinator.js +38 -4
  92. package/dist/clients/runtime-tool-call.js +16 -5
  93. package/dist/clients/runtime-tool-result.js +27 -12
  94. package/dist/clients/runtime-turn.js +571 -287
  95. package/dist/clients/safe-spawn.js +256 -65
  96. package/dist/clients/skills-resolver.js +38 -2
  97. package/dist/clients/spawn-output-cap.js +84 -1
  98. package/dist/clients/tree-sitter-client.js +119 -58
  99. package/dist/clients/turn-end/lane.js +51 -0
  100. package/dist/clients/turn-end/lanes/govulncheck.js +112 -0
  101. package/dist/clients/turn-end/lanes/secrets.js +204 -0
  102. package/dist/clients/warm-attach.js +59 -0
  103. package/dist/clients/widget-state.js +294 -17
  104. package/dist/clients/workspace-topology.js +57 -3
  105. package/dist/clients/zizmor-config.js +26 -13
  106. package/dist/index.js +6342 -3995
  107. package/dist/mcp/analyze-cli.js +4 -0
  108. package/dist/mcp/server.js +33 -21
  109. package/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  110. package/dist/tools/lens-diagnostic-mark.js +14 -2
  111. package/dist/tools/lens-diagnostics.js +127 -33
  112. package/dist/tools/lsp-diagnostics.js +150 -12
  113. package/docs/adr/0001-stale-advisory-live-arm.md +29 -0
  114. package/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
  115. package/docs/adr/0003-git-guard-latch-writer.md +203 -0
  116. package/docs/adr/0004-disposition-policy-seam.md +38 -0
  117. package/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
  118. package/docs/adr/0006-derived-state-benchmark-first.md +31 -0
  119. package/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
  120. package/docs/adr/0008-turn-end-lane-interface.md +165 -0
  121. package/docs/adr/0009-reported-path-attribution.md +114 -0
  122. package/docs/adr/README.md +26 -0
  123. package/docs/agent-tools.md +18 -1
  124. package/docs/ast-grep_rules_catalog.md +4 -3
  125. package/docs/configuration.md +55 -1
  126. package/docs/engineering-principles.md +202 -0
  127. package/docs/environment-variables.md +61 -4
  128. package/docs/features.md +2 -2
  129. package/docs/globalconfig.md +6 -1
  130. package/docs/language-coverage.md +17 -0
  131. package/docs/lsp-capability-matrix.md +98 -31
  132. package/docs/pi-lens-fixer.md +57 -3
  133. package/docs/pi-lens-investigator.md +9 -0
  134. package/docs/pi-lens-reviewer.md +16 -0
  135. package/docs/pi-lens-subagent.md +9 -0
  136. package/docs/public-api-stability.md +1 -0
  137. package/docs/release-qa-baseline.md +2 -0
  138. package/docs/servercapabilities.md +21 -11
  139. package/docs/settings.md +12 -3
  140. package/docs/subagent-compat.md +1 -1
  141. package/package.json +5 -3
  142. package/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
  143. package/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
  144. package/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
  145. package/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
  146. package/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
  147. package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
  148. package/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
  149. package/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
  150. package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
  151. package/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
  152. package/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
  153. package/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
  154. package/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
  155. package/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
  156. package/scripts/install-selftest.mjs +21 -17
  157. package/scripts/lib/web-tree-sitter-dir.mjs +123 -0
@@ -7,6 +7,7 @@
7
7
  * vice versa.
8
8
  */
9
9
  import * as path from "node:path";
10
+ import { parseInlineSuppressionRuleIds, splitInlineSuppressionPayload, } from "./inline-suppressions.js";
10
11
  const HASH_COMMENT_EXTENSIONS = new Set([
11
12
  ".py",
12
13
  ".rb",
@@ -50,14 +51,23 @@ export function insertSuppressComment(content, filePath, line, rule) {
50
51
  const suppressRe = /((?:\/\/|#)\s*pi-lens-ignore:\s*)(.+)$/;
51
52
  const match = existingAbove !== undefined ? suppressRe.exec(existingAbove) : null;
52
53
  if (match && existingAbove !== undefined) {
53
- const rules = match[2]
54
+ const prefix = match[1];
55
+ const payload = match[2];
56
+ if (prefix === undefined || payload === undefined)
57
+ return content;
58
+ const { ruleList, trailingReason } = splitInlineSuppressionPayload(payload);
59
+ const rules = ruleList
54
60
  .split(",")
55
61
  .map((r) => r.trim())
56
62
  .filter(Boolean);
57
- if (!rules.includes(rule))
63
+ const ruleIds = parseInlineSuppressionRuleIds(ruleList);
64
+ if (!ruleIds.includes(rule))
58
65
  rules.push(rule);
59
66
  lines[aboveIdx] =
60
- existingAbove.slice(0, match.index) + match[1] + rules.join(", ");
67
+ existingAbove.slice(0, match.index) +
68
+ prefix +
69
+ rules.join(", ") +
70
+ trailingReason;
61
71
  return lines.join("\n");
62
72
  }
63
73
  // Match the indentation of the flagged line so the inserted comment lines
@@ -229,7 +229,9 @@ export async function effectiveConfig(options = {}) {
229
229
  const resolveAt = (dir) => resolvePiLensConfig({
230
230
  cwd: dir,
231
231
  globalDir: getGlobalPiLensDir(),
232
- globalConfigPath: getPiLensGlobalConfigPath(homeDir),
232
+ // Production resolution for the global tier โ€” see loadLSPConfig's
233
+ // comment (global-config-location PR, refs #2457).
234
+ globalConfigPath: getPiLensGlobalConfigPath(),
233
235
  homeDir,
234
236
  });
235
237
  // The queried path resolves HERE, beside the resolution root it selects, so
@@ -211,6 +211,48 @@ function inCaptureWindow(fn) {
211
211
  function isCaptureSeam(prop) {
212
212
  return (prop === "on" || (typeof prop === "string" && prop.startsWith("register")));
213
213
  }
214
+ /**
215
+ * True for a host SCHEMA value that happens to be callable.
216
+ *
217
+ * A tool definition's `parameters` is DATA the host reads, not a pi-lens
218
+ * callback โ€” but the schema builder pi-lens imports is host-provided
219
+ * (`clients/deps/typebox.ts`: the host resolves the bare `typebox`
220
+ * specifier), and on a host whose builders are ArkType-style the schema is a
221
+ * FUNCTION. `@oh-my-pi/pi-coding-agent` rewrites an extension's `typebox`
222
+ * import to its own `src/extensibility/legacy-typebox.ts`, which re-exports
223
+ * `@oh-my-pi/omptype`'s builders; those return a callable whose prototype
224
+ * carries `toJsonSchema`/`assert`. `inCaptureWindow` returns a plain rest
225
+ * wrapper with neither, so the host falls off its ark branch, the wire schema
226
+ * degrades to `undefined`, and every xd:// device renders `type Args =
227
+ * unknown` and rejects every call with `root: schema must be an object or
228
+ * boolean` (#3195).
229
+ *
230
+ * The predicate IS the host's own, copied from `@oh-my-pi/pi-ai@18.2.4`
231
+ * `src/utils/schema/wire.ts:21-27` (`isArkSchema`) โ€” the same test the host's
232
+ * `applyToolProxy` (`@oh-my-pi/pi-coding-agent@18.2.4`
233
+ * `src/extensibility/tool-proxy.ts:26`) uses to leave a callable schema
234
+ * unbound for exactly this hazard ("`bind()` โ€ฆ drops the schema surface โ€ฆ
235
+ * so a bound schema later stringifies to `undefined`"). Matching it means
236
+ * pi-lens declines to wrap precisely the values the host reads as schemas,
237
+ * and still wraps everything else.
238
+ * `tests/clients/pi-host-callable-schema-contract.test.ts` asserts against
239
+ * that upstream predicate (and against a fixture measured from a real
240
+ * `@oh-my-pi/omptype@18.2.4` schema), not against this copy of it.
241
+ *
242
+ * Deliberately NOT an allow-list of handler keys (`execute`, `renderResult`,
243
+ * `handler` โ€” the measured set today): that is `isCaptureSeam`'s #1434 S1a
244
+ * mistake one level down (defect shape 34, spelling enumerator). The host's
245
+ * own `ToolDefinition` already declares `onSession`, `renderCall` and
246
+ * `shellEnv`, and `registerCommand` takes `getArgumentCompletions`; the day
247
+ * pi-lens adopts one of those, an allow-list drops its console capture
248
+ * SILENTLY โ€” the #1333 regression this seam exists to prevent โ€” while this
249
+ * predicate keeps wrapping it.
250
+ */
251
+ function isCallableSchema(value) {
252
+ return (typeof value === "function" &&
253
+ typeof value.toJsonSchema === "function" &&
254
+ typeof value.assert === "function");
255
+ }
214
256
  /**
215
257
  * Wrap every function value found in `value`, in place. Handles the two
216
258
  * shapes a `register*`/`on` call takes today: a function passed directly
@@ -218,6 +260,11 @@ function isCaptureSeam(prop) {
218
260
  * function ONE level inside a plain options/definition object
219
261
  * (`options.handler`, `tool.execute`).
220
262
  *
263
+ * Only the property walk screens for a callable schema (#3195): every
264
+ * function pi-lens passes as a register argument DIRECTLY is a handler
265
+ * (`on`, `registerMessageRenderer`), measured across the whole activation,
266
+ * so a schema can only ever arrive as a property of a definition object.
267
+ *
221
268
  * Deliberately NOT recursive past that one level (#1434 perf review): a tool
222
269
  * definition's `parameters`/`schema` is a large, deeply nested TypeBox object
223
270
  * that holds no functions, but walking its full tree on every `registerTool`
@@ -249,7 +296,7 @@ function wrapFunctionsInPlace(value) {
249
296
  const obj = value;
250
297
  for (const key of Object.keys(obj)) {
251
298
  const propValue = obj[key];
252
- if (typeof propValue === "function") {
299
+ if (typeof propValue === "function" && !isCallableSchema(propValue)) {
253
300
  assignWrapped(obj, key, propValue);
254
301
  }
255
302
  }
@@ -30,7 +30,11 @@
30
30
  * deliver unchanged. A missed demotion is noise; a wrong drop can hide a
31
31
  * live credential or CVE.
32
32
  * The turn-end secrets (gitleaks/trivy-secrets) and govulncheck advisory both
33
- * route through this gate in `clients/runtime-turn.ts`.
33
+ * route through this gate in `clients/runtime-turn.ts`, in ONE shared call
34
+ * (#3264) whose arms the secrets LANE (`clients/turn-end/lanes/secrets.ts`,
35
+ * #1892) renders. A lane declares its sources and never gates itself: the
36
+ * pass, its stat budget and its bounded records are per DELIVERY, not per
37
+ * lane.
34
38
  *
35
39
  * The SAME freshness family, one level up, covers cross-file drift: a cached
36
40
  * inline blocker is a verdict about its file *and everything that file
@@ -222,9 +226,25 @@ function labeled(file, description, reason, ageSource, evidence = [], extra = {}
222
226
  const RUNTIME_TURN_FILE = "clients/runtime-turn.ts";
223
227
  const LENS_DIAGNOSTICS_FILE = "tools/lens-diagnostics.ts";
224
228
  export const DELIVERY_SURFACES = {
225
- "runtime-turn:secrets-gitleaks": gated(RUNTIME_TURN_FILE, "Turn-end ๐Ÿ”ด secrets blocker, gitleaks cache.", ["gateFindingsByPathFreshness"], ['store: "gitleaks"']),
226
- "runtime-turn:secrets-trivy": gated(RUNTIME_TURN_FILE, "Turn-end ๐Ÿ”ด secrets blocker, trivy secrets cache.", ["gateFindingsByPathFreshness"], ['store: "trivy-secrets"']),
227
- "runtime-turn:govulncheck-advisory": gated(RUNTIME_TURN_FILE, "Turn-end ๐Ÿ›ก๏ธ Go CVE advisory (call-site line only).", ["gateFindingsByPathFreshness"], ['store: "govulncheck"']),
229
+ // #1892 (lane extraction): both secrets tiers are rendered by the secrets
230
+ // LANE (`clients/turn-end/lanes/secrets.ts`), so `clients/runtime-turn.ts`
231
+ // no longer reads `.live`/`.stale` itself โ€” the arm it can still be pinned
232
+ // to is the whole gated store object it hands the lane. That is WEAKER than
233
+ // the previous `gitleaksGate.live,` pin (it no longer says which partition
234
+ // feeds which tier) and the loss is deliberate: the live/stale split is a
235
+ // lane rule now, pinned behaviourally instead, by
236
+ // `tests/clients/runtime-turn-finding-freshness.test.ts` ("keeps an
237
+ // unmodified file's finding as a full-severity blocker" / "DEMOTES a
238
+ // finding whose file was edited after the scan โ€” kept, no line") and the
239
+ // committed witness goldens under `tests/fixtures/witness/`. What the pin
240
+ // still proves is what this registry exists for: the rows the tier renders
241
+ // came out of a real `gateFindingsByPathFreshness` call and not out of a
242
+ // hand-built object (the R2 binding chain in
243
+ // `tests/clients/finding-delivery-gate.test.ts` walks
244
+ // `gitleaksGate` โ†’ `scannerGates` โ†’ the call).
245
+ "runtime-turn:secrets-gitleaks": gated(RUNTIME_TURN_FILE, "Turn-end ๐Ÿ”ด secrets blocker, gitleaks cache.", ["gateFindingsByPathFreshness"], ["gitleaksGate,"]),
246
+ "runtime-turn:secrets-trivy": gated(RUNTIME_TURN_FILE, "Turn-end ๐Ÿ”ด secrets blocker, trivy secrets cache.", ["gateFindingsByPathFreshness"], ["trivySecretsGate,"]),
247
+ "runtime-turn:govulncheck-advisory": gated(RUNTIME_TURN_FILE, "Turn-end ๐Ÿ›ก๏ธ Go CVE advisory (call-site line only).", ["gateFindingsByPathFreshness"], ["scannerGates.govulncheck"]),
228
248
  // Two tagged seams (the stale and live render branches below the same
229
249
  // `sweepInlineBlockerFreshness` call) legitimately share one evidence
230
250
  // occurrence โ€” evidenceMin: 2 tells the exclusive-assignment check both
@@ -234,9 +254,11 @@ export const DELIVERY_SURFACES = {
234
254
  // third argument, so the literal is the call's opening rather than the
235
255
  // whole single-line invocation.
236
256
  ["sweepInlineBlockerFreshness(runtime, cwd, {"], { evidenceMin: 2 }),
237
- // Same two gate calls as the live secrets tier โ€” this tier renders their
238
- // `.stale` arm, so it has no OWN gate call to point at.
239
- "runtime-turn:stale-secrets-tier": gated(RUNTIME_TURN_FILE, "Turn-end ๐Ÿ”‘ demoted-secrets tier (drifted since scan).", ["gateFindingsByPathFreshness"], ['store: "gitleaks"', 'store: "trivy-secrets"']),
257
+ // The same shared gate call and the same two stores as the live secrets
258
+ // tier โ€” one lane renders both tiers, so this surface has no gate call and
259
+ // no arm read of its own; see the note above the live tier for what the pin
260
+ // does and does not prove.
261
+ "runtime-turn:stale-secrets-tier": gated(RUNTIME_TURN_FILE, "Turn-end ๐Ÿ”‘ demoted-secrets tier (drifted since scan).", ["gateFindingsByPathFreshness"], ["gitleaksGate,", "trivySecretsGate,"]),
240
262
  // The evidence below is the literal header FRAGMENT including the
241
263
  // interpolation โ€” proves the label is actually rendered, not merely
242
264
  // computed and discarded (review round F1's "deleted the age
@@ -280,24 +302,32 @@ export const DELIVERY_SURFACES = {
280
302
  // nowhere else in this file: a bare `applyFindingPolicy` would also be
281
303
  // satisfied by an import line or an unrelated caller.
282
304
  "runtime-turn:late-auxiliary-findings": gated(RUNTIME_TURN_FILE, "Turn-end late-auxiliary LSP findings (collect-later probe of aux " +
283
- "client caches whose grace window expired).", ["gateFindingsByPathFreshness", "applyFindingPolicy"], ['store: "late-auxiliary-findings"', "applyFindingPolicy(gate.live, {"]),
284
- "runtime-turn:late-runner-findings": gated(RUNTIME_TURN_FILE, "Turn-end CLI runner findings collected after the post-write path.", ["gateFindingsByPathFreshness"], ['store: "late-runner-findings"'], { evidenceMin: 2 }),
305
+ "client caches whose grace window expired).",
306
+ // #3248: the open-coded `applyFindingPolicy(...)` here and the identical
307
+ // one the late-RUNNER lane needed are now one `applyPushedFindingPolicy`
308
+ // call, so the gate and the evidence name that call. Same stack, same
309
+ // identities โ€” only the spelling moved.
310
+ ["gateFindingsByPathFreshness", "applyPushedFindingPolicy"], ['"late-auxiliary-findings": {', "applyPushedFindingPolicy(gate.live, {"]),
311
+ "runtime-turn:late-runner-findings": gated(RUNTIME_TURN_FILE, "Turn-end CLI runner findings collected after the post-write path.",
312
+ // #3248: this lane was the last push surface with no disposition stage;
313
+ // it now routes through the same shared policy call as its
314
+ // late-auxiliary twin, which is what this gate pins.
315
+ ["gateFindingsByPathFreshness", "applyPushedFindingPolicy"], ['"late-runner-findings"'], { evidenceMin: 2 }),
285
316
  "runtime-turn:cascade-blocker": labeled(RUNTIME_TURN_FILE, "Turn-end ๐Ÿงช cascade neighbor blocker.", "Cascade results settle synchronously this turn where possible " +
286
317
  "(`settleCascadeRuns`), but an unsettled compute can carry over to a " +
287
- "later turn (bounded by a carry cap) โ€” this round does not freshness-" +
288
- "gate that carry-over window.", "live", [], {
289
- status: "partial",
290
- partialReason: "A carried-over cascade result (run.carriedTurns > 0) is rendered " +
291
- "without an age label. Follow-up: surface carriedTurns as an explicit " +
292
- "label when > 0, or route through formatCacheAgeLabel using the run's " +
293
- "own timestamp.",
294
- }),
318
+ "later turn (bounded by a carry cap) โ€” a carried-over result renders " +
319
+ "with an explicit `(carried N turns ยท scanned Xm ago)` label (#3167, " +
320
+ "#3168 F3), so the agent can tell it from a fresh observation.", "live", ["cascadeCarrySuffix("], { evidenceMin: 1 }),
295
321
  "runtime-turn:cascade-coverage-advisory": labeled(RUNTIME_TURN_FILE, "Turn-end cascade-coverage-gap advisories (graph/binding/budget).", "Explains what the cascade check could NOT confirm this turn โ€” not a " +
296
322
  "finding with a cited path, an absence-of-coverage disclosure computed " +
297
- "from this turn's own indeterminate-run list.", "live", [], {
298
- status: "partial",
299
- partialReason: "Same cascade carry-over caveat as runtime-turn:cascade-blocker.",
300
- }),
323
+ "from this turn's own indeterminate-run list. An advisory computed from " +
324
+ "a carried indeterminate run is labeled `(carried N turns ยท <age>)` " +
325
+ "(#3167, #3168 F4 โ€” dropped on mixed carried/fresh buckets). The age " +
326
+ "half reads `scan age unknown` on this surface today: no indeterminate-" +
327
+ "run producer stamps `observedAt` (only the resolved-found plumb does, " +
328
+ "`clients/cascade-format.ts`), and one unstamped carried run collapses " +
329
+ "the whole bucket's age rather than claiming the stamped runs' (#3168 " +
330
+ "F13). The carry COUNT is always real.", "live", ["withCarryLabel("], { evidenceMin: 3 }),
301
331
  // #3102: the cold-neighbour cascade run is BUILT here, in the quiet-window
302
332
  // reconcile (`onResolvedFound` in index.ts), a turn earlier than the
303
333
  // `runtime-turn:cascade-blocker` render that finally carries it โ€” so it is
@@ -360,9 +390,9 @@ export const DELIVERY_SURFACES = {
360
390
  "lens-diagnostics:mode-delta": gated(LENS_DIAGNOSTICS_FILE, "`lens_diagnostics mode=delta` report (the tool's DEFAULT mode) โ€” " +
361
391
  "re-serves the actionable-warnings/code-quality-warnings caches AND " +
362
392
  "the persisted project-diagnostics delta report.", ["gateFindingsByPathFreshness"], [
363
- 'store: "lens-diagnostics-delta"',
393
+ '"lens-diagnostics-delta": {',
364
394
  "applyDeltaFreshnessGate(",
365
- 'store: "lens-diagnostics-delta-project"',
395
+ '"lens-diagnostics-delta-project": {',
366
396
  ]),
367
397
  "widget-state:footer": gated("clients/widget-state.ts", "TUI footer blocking/error/warning tally.", ["reconcileStaleWidgetFiles", "isBlocking"], ["isBlocking(diagnostic)", "reconcileStaleWidgetFiles()"]),
368
398
  "agent-nudge:context-message": labeled("clients/agent-nudge.ts", "Post-edit file-touch nudge injected via `context`.", "Not a scanner finding store: no severity, no cited line, no cache โ€” the " +
@@ -1823,7 +1823,10 @@ export function clearFormatterRuntimeState() {
1823
1823
  // is `session_start`'s block in runtime-session.ts.
1824
1824
  }
1825
1825
  const BOX_DRAWING_GLOBAL = /[\u2500-\u257F]/g;
1826
- const HAS_BOX_DRAWING = /[\u2500-\u257F]/;
1826
+ /** Blank, or a rule made of box-drawing characters and spacing alone. */
1827
+ const DECORATION_RULE = /^[\u2500-\u257F\s]*$/;
1828
+ /** The renderer's section heading: one word, then box-drawing to end of line. */
1829
+ const DECORATION_HEADING = /^\S+[ \t]+[\u2500-\u257F][\u2500-\u257F\s]*$/;
1827
1830
  /**
1828
1831
  * First line of `text` that actually carries a diagnostic.
1829
1832
  *
@@ -1839,18 +1842,53 @@ const HAS_BOX_DRAWING = /[\u2500-\u257F]/;
1839
1842
  export function firstDiagnosticLine(text) {
1840
1843
  for (const raw of (text ?? "").split("\n")) {
1841
1844
  const line = stripAnsi(raw).trimEnd();
1842
- const stripped = line.replace(BOX_DRAWING_GLOBAL, "").trim();
1843
- if (!stripped)
1845
+ if (isDecorativeLine(line))
1844
1846
  continue;
1845
- // "format โ”โ”โ”โ”โ”โ”โ”โ”" is a section banner, not a diagnostic. Require a rule
1846
- // AND a short remainder so a real one-line error containing a box
1847
- // character is not discarded.
1848
- if (HAS_BOX_DRAWING.test(line) && stripped.length <= 24)
1849
- continue;
1850
- return stripped.slice(0, 300);
1847
+ return line.replace(BOX_DRAWING_GLOBAL, "").trim().slice(0, 300);
1851
1848
  }
1852
1849
  return undefined;
1853
1850
  }
1851
+ /**
1852
+ * The two decoration SHAPES a renderer emits, and nothing else: a rule made of
1853
+ * box-drawing characters alone (blank lines included), and a `<word> โ”โ”โ”โ”`
1854
+ * section heading whose remainder after the word is box-drawing only.
1855
+ *
1856
+ * The single normalization both readers above and below share, so the bounded
1857
+ * tail cannot surface what the first-line reader already rejects (#3312 review
1858
+ * R3-4: the tail put biome's `format โ”โ”โ”โ”` banner back in front of the strict
1859
+ * #1337 seam). Matching SHAPES rather than "contains a box character and is
1860
+ * short" is #3312 review R4-1: the length heuristic deleted
1861
+ * `โ” traceback source excerpt`, a diagnostic whose own text opens with a box
1862
+ * character. A line carrying any non-decorative text is never decoration.
1863
+ */
1864
+ function isDecorativeLine(line) {
1865
+ return DECORATION_RULE.test(line) || DECORATION_HEADING.test(line);
1866
+ }
1867
+ const FORMATTER_ERROR_TAIL_LINES = 20;
1868
+ /**
1869
+ * Preserve a bounded tail of a formatter's diagnostic output.
1870
+ *
1871
+ * A traceback's first line is not actionable on its own (#3312). Keep the
1872
+ * final lines, which include the exception and its message, while bounding
1873
+ * the model-facing error when a formatter emits an unexpectedly large log.
1874
+ *
1875
+ * Decorative lines are dropped FIRST, by the same predicate `firstDiagnosticLine`
1876
+ * uses: a tail that kept them would hand the strict #1337 seam the banner this
1877
+ * module already decided is not a diagnostic, and every banner line it kept
1878
+ * would evict a real traceback line from the bound.
1879
+ */
1880
+ export function diagnosticTail(text, maxLines = FORMATTER_ERROR_TAIL_LINES) {
1881
+ const lines = [];
1882
+ for (const raw of (text ?? "").split("\n")) {
1883
+ const line = stripAnsi(raw).trimEnd();
1884
+ if (isDecorativeLine(line))
1885
+ continue;
1886
+ lines.push(line.slice(0, 300));
1887
+ }
1888
+ if (lines.length === 0)
1889
+ return undefined;
1890
+ return lines.slice(-maxLines).join("\n");
1891
+ }
1854
1892
  /**
1855
1893
  * Resolve a formatter command without allowing the static command fallback to
1856
1894
  * bypass a resolver's style-preservation refusal (#1345). `null` means the
@@ -1963,11 +2001,11 @@ export async function formatFile(filePath, formatter) {
1963
2001
  changed: false,
1964
2002
  outcome: "failed",
1965
2003
  error: result.error?.message ||
1966
- firstDiagnosticLine(result.stderr) ||
2004
+ diagnosticTail(result.stderr) ||
1967
2005
  // biome, ktlint and `mix format` report on STDOUT; without this
1968
2006
  // their diagnostic is discarded and the user is told only that
1969
2007
  // the tool "exited with status 1".
1970
- firstDiagnosticLine(result.stdout) ||
2008
+ diagnosticTail(result.stdout) ||
1971
2009
  `${formatter.name} exited with status ${result.status}`,
1972
2010
  };
1973
2011
  }