@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
@@ -165,6 +165,10 @@ function formatTurnEnd(response) {
165
165
  */
166
166
  const TURN_END_SKIP_REASONS = {
167
167
  "ipc-error": "no warm pi-lens MCP server answered for this workspace, or its build is stale — start the MCP server, or run `npm run build` if you have edited pi-lens sources",
168
+ // #3255: split out of `ipc-error` because its remedy is different and no
169
+ // rebuild reaches it. A server that was already running when pi-lens was
170
+ // upgraded still owns the endpoint name the previous build derived.
171
+ "no-listener": "nothing is listening on this workspace's warm endpoint — start the MCP server; if pi-lens was upgraded while a server was running, that server still owns the endpoint name of the previous build, so restart it",
168
172
  timeout: "the warm pi-lens MCP server did not finish the pass within the hook budget — the turn-end findings are kept and will be delivered on a later Stop",
169
173
  "schema-mismatch": "the warm pi-lens MCP server speaks a different turn-end schema than this hook binary — rebuild and restart the server so both halves match",
170
174
  "stale-answer": "the warm pi-lens MCP server replied about a superseded state — the findings are kept for a later Stop",
@@ -321,8 +321,16 @@ function startIpcServer() {
321
321
  socket.end(`${JSON.stringify({ error: String(err) })}\n`);
322
322
  }
323
323
  });
324
+ }, {
325
+ label: "mcp-warm-server",
326
+ // #3383: a peer that never terminates its request line gets the same
327
+ // error reply every other unusable request gets, instead of this
328
+ // process holding its bytes until the client's timeout.
329
+ onOverflow: () => socket.end(`${JSON.stringify({ error: "warm request line exceeded the framing limit" })}\n`),
324
330
  }));
325
- socket.on("error", () => socket.destroy());
331
+ // The stream is already destroyed when this handler runs; the listener
332
+ // remains the surviving teardown witness, while destroy() was a no-op.
333
+ socket.on("error", () => { });
326
334
  });
327
335
  ipc.on("error", (err) => {
328
336
  // Listener failure must not take down the MCP server — warm channel is an
@@ -1623,28 +1631,32 @@ async function handleRequest(request) {
1623
1631
  }
1624
1632
  }
1625
1633
  // --- stdio read loop (newline-delimited JSON) --------------------------------
1626
- let buffer = "";
1627
1634
  process.stdin.setEncoding("utf8");
1628
- process.stdin.on("data", (chunk) => {
1629
- buffer += chunk;
1630
- let newlineIndex = buffer.indexOf("\n");
1631
- while (newlineIndex !== -1) {
1632
- const line = buffer.slice(0, newlineIndex).trim();
1633
- buffer = buffer.slice(newlineIndex + 1);
1634
- if (line.length > 0) {
1635
- let request;
1636
- try {
1637
- request = JSON.parse(line);
1638
- }
1639
- catch {
1640
- sendError(null, -32700, "Parse error");
1641
- }
1642
- if (request)
1643
- void handleRequest(request);
1644
- }
1645
- newlineIndex = buffer.indexOf("\n");
1635
+ // #3383: framed by the shared reader, so the partial line this loop holds
1636
+ // between chunks has a ceiling. It used to be `buffer += chunk` with none: a
1637
+ // host that never sent a newline grew one JS string until V8 refused the next
1638
+ // concatenation with `RangeError: Invalid string length` — raised inside this
1639
+ // `data` handler, where nothing can catch it, which is the ending #3375 fixed
1640
+ // for child output. `continuous` because this is a stream of requests, not one
1641
+ // request per connection like every socket reader.
1642
+ process.stdin.on("data", createWarmIpcLineReader((raw) => {
1643
+ const line = raw.trim();
1644
+ if (line.length === 0)
1645
+ return;
1646
+ let request;
1647
+ try {
1648
+ request = JSON.parse(line);
1646
1649
  }
1647
- });
1650
+ catch {
1651
+ sendError(null, -32700, "Parse error");
1652
+ }
1653
+ if (request)
1654
+ void handleRequest(request);
1655
+ }, {
1656
+ label: "mcp-stdio",
1657
+ continuous: true,
1658
+ onOverflow: () => sendError(null, -32700, "Parse error: request line exceeded the framing limit"),
1659
+ }));
1648
1660
  process.stdin.on("end", () => {
1649
1661
  endSituationalToolTelemetry();
1650
1662
  void flushExtensionLog().finally(() => process.exit(0));
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Where web-tree-sitter is installed — ONE ladder, for every caller that needs
3
+ * the answer (#3409).
4
+ *
5
+ * Callers: `clients/tree-sitter-client.ts` (the grammar read path and the
6
+ * lazy-fetch write target), `clients/install-diagnostics.ts` (the pasted
7
+ * environment fingerprint) and `scripts/install-selftest.mjs` (the installed
8
+ * host's own diagnostic). Each used to spell its own rungs, and the two that led
9
+ * with a BARE specifier were unconditionally broken on the host pi ships: inside
10
+ * a `bun build --compile` binary `require.resolve("web-tree-sitter")` throws
11
+ * MODULE_NOT_FOUND while `require.resolve("web-tree-sitter/tree-sitter.wasm")` —
12
+ * an explicit file subpath — still resolves.
13
+ *
14
+ * It lives in `scripts/lib/` rather than `clients/` because the installed
15
+ * selftest is packaged (`files[]`) without `clients/`, and this is the house
16
+ * pattern for logic a packaged .mjs script and the TypeScript runtime share
17
+ * (`scripts/lib/skills-predicate.mjs`, `scripts/lib/process-scan.mjs`). It
18
+ * imports nothing but `node:fs` and `node:path`, so it loads even on the hosts
19
+ * whose broken dependency graph the selftest exists to report.
20
+ */
21
+ import * as fs from "node:fs";
22
+ import * as path from "node:path";
23
+ /** The package this module is about. Also the name its manifest must carry. */
24
+ const WEB_TREE_SITTER = "web-tree-sitter";
25
+ /**
26
+ * The wasm the runtime loads, at the package root — where 0.25.10's `exports`
27
+ * map puts it (`"./tree-sitter.wasm": "./tree-sitter.wasm"`). If a later release
28
+ * moves it (#381's 0.26 migration), the two FALLBACK rungs below stop matching
29
+ * and the resolver rungs, which ask Node for that same subpath, keep working;
30
+ * this constant is then the one line to update.
31
+ */
32
+ const RUNTIME_WASM = "tree-sitter.wasm";
33
+ /**
34
+ * True when `dir` IS an installed web-tree-sitter package: its own manifest
35
+ * names it, and the wasm the runtime loads is in it.
36
+ *
37
+ * Round 1 review of #3409 (R3418-1): the constructed rungs used to accept any
38
+ * directory that merely EXISTED at `<base>/node_modules/web-tree-sitter`, so an
39
+ * empty or foreign directory of that name became the runtime fetch's write
40
+ * target and a downloaded grammar would land in an unrelated tree. Existence is
41
+ * not identity. The manifest name is the identity; the wasm is what makes the
42
+ * directory the right place to put grammars, since a grammar is only loadable by
43
+ * the web-tree-sitter runtime sitting next to it (the ABI drift #1564 is about).
44
+ */
45
+ export function isWebTreeSitterPackageDir(dir) {
46
+ try {
47
+ if (!fs.existsSync(path.join(dir, RUNTIME_WASM)))
48
+ return false;
49
+ const manifest = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
50
+ return manifest?.name === WEB_TREE_SITTER;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ /**
57
+ * The package directory owning `resolvedFile`: walk up until a directory passes
58
+ * the identity check, or the filesystem root is reached. The walk exists because
59
+ * an `exports` map may point into a subdirectory, so the file's own parent is
60
+ * not necessarily the package; the identity check is what stops the walk, so a
61
+ * resolver that answers outside any web-tree-sitter package yields nothing
62
+ * rather than an unrelated ancestor.
63
+ */
64
+ function packageDirOf(resolvedFile) {
65
+ let dir = path.dirname(resolvedFile);
66
+ for (;;) {
67
+ if (isWebTreeSitterPackageDir(dir))
68
+ return dir;
69
+ const parent = path.dirname(dir);
70
+ if (parent === dir)
71
+ return undefined;
72
+ dir = parent;
73
+ }
74
+ }
75
+ /**
76
+ * web-tree-sitter's installed package directory, or undefined.
77
+ *
78
+ * Rung order, most authoritative first:
79
+ * 1. the `tree-sitter.wasm` SUBPATH, which resolves on a compiled host as well
80
+ * as a plain one. This is the rung that makes the non-core grammars
81
+ * fetchable there at all.
82
+ * 2. the BARE specifier, for a future web-tree-sitter whose exports map no
83
+ * longer carries `./tree-sitter.wasm`.
84
+ * 3. pi-lens's own package root, for the temp-dir compile layout (#20) where
85
+ * the resolver's context is a temp directory but the package root still has
86
+ * the right `node_modules`.
87
+ * 4. the working directory, the pre-#3409 last resort.
88
+ *
89
+ * Rungs 1-2 are the resolver's answer and outrank the constructed guesses: the
90
+ * grammars must come from the SAME package whose wasm the runtime loaded. Every
91
+ * rung's candidate passes `isWebTreeSitterPackageDir` before it is returned.
92
+ *
93
+ * `resolve`, `packageRoot` and `cwd` are injected, never defaulted: the caller's
94
+ * resolver context and package root are what differ on a compiled host, so they
95
+ * must not be silently replaced by this module's — and a rung nothing can vary
96
+ * is a rung no test can drive.
97
+ */
98
+ export function resolveWebTreeSitterPackageDir(deps) {
99
+ for (const specifier of [
100
+ `${WEB_TREE_SITTER}/${RUNTIME_WASM}`,
101
+ WEB_TREE_SITTER,
102
+ ]) {
103
+ try {
104
+ const dir = packageDirOf(deps.resolve(specifier));
105
+ if (dir)
106
+ return dir;
107
+ }
108
+ catch {
109
+ /* next rung */
110
+ }
111
+ }
112
+ for (const base of [deps.packageRoot, deps.cwd]) {
113
+ try {
114
+ const candidate = path.join(base(), "node_modules", WEB_TREE_SITTER);
115
+ if (isWebTreeSitterPackageDir(candidate))
116
+ return candidate;
117
+ }
118
+ catch {
119
+ /* next rung */
120
+ }
121
+ }
122
+ return undefined;
123
+ }
@@ -47,7 +47,7 @@ import { findAuxiliaryProfileForSource, retagAuxiliaryDiagnostics, } from "../cl
47
47
  import { detectFileRole } from "../clients/file-role.js";
48
48
  import { STALE_LINE_MARKER } from "../clients/stale-marker.js";
49
49
  import { makeProgressReporter, scanningSummaryLine } from "./scan-progress.js";
50
- import { createLspDiagnosticsTool, LSP_SEVERITY_FILTERS, MAX_BATCH_FILES, } from "./lsp-diagnostics.js";
50
+ import { createLspDiagnosticsTool, LSP_SEVERITY_FILTERS, MAX_BATCH_FILES, renderBatchOutcomeLines, } from "./lsp-diagnostics.js";
51
51
  import { demotePastEofDiagnostics, PAST_EOF_STALE_MARKER, resyncDocumentOnPastEof, } from "../clients/diagnostic-line-freshness.js";
52
52
  // The widget state exposes the full per-file diagnostic set; this is the tool's
53
53
  // own generous display budget per file (independent of the TUI's 12 cap), to
@@ -197,11 +197,15 @@ captureLspStatusRepaint, getRuntime, isLensGuardEnabled = () => true) {
197
197
  if ((details.unconfirmedFiles ?? 0) > 0)
198
198
  return `lens_diagnostics${scope} — ${count} ${noun} · ${details.cleanFiles ?? 0} clean · ${details.unconfirmedFiles} unconfirmed${details.timedOutFiles ? ` (${details.timedOutFiles} timed out)` : ""}`;
199
199
  const outcomeCounts = details.outcomeCounts;
200
+ const tooLargeLines = renderBatchOutcomeLines(details.outcomes ?? []);
201
+ if (tooLargeLines.length > 0)
202
+ return `lens_diagnostics${scope} — too large: ${tooLargeLines.join("; ")}`;
200
203
  const notConfirmed = outcomeCounts
201
204
  ? (outcomeCounts.inconclusive ?? 0) +
202
205
  (outcomeCounts.unavailable ?? 0) +
203
206
  (outcomeCounts.unsupported ?? 0) +
204
- (outcomeCounts.failed ?? 0)
207
+ (outcomeCounts.failed ?? 0) +
208
+ (outcomeCounts.too_large ?? 0)
205
209
  : 0;
206
210
  if (notConfirmed > 0)
207
211
  return `lens_diagnostics${scope} — ${count} ${noun} · ${notConfirmed} checks not confirmed`;
@@ -511,13 +515,16 @@ function formatProjectDeltaDiagnostic(diagnostic, stale) {
511
515
  function appendProjectDiagnosticsDeltaLines(groups, cwd, report, severity, includeFile) {
512
516
  const scoped = (report?.diagnostics ?? []).filter((diagnostic) => includeFile(diagnostic.filePath) &&
513
517
  matchesSeverity(projectDiagnosticToWidget(diagnostic, diagnostic.filePath), severity));
514
- const gated = gateFindingsByPathFreshness({
515
- store: "lens-diagnostics-delta-project",
516
- findings: scoped,
518
+ const { "lens-diagnostics-delta-project": gated } = gateFindingsByPathFreshness({
517
519
  cwd,
518
- scannedAt: report?.generatedAt,
519
- citedPath: (d) => d.filePath,
520
- onMissing: "drop",
520
+ sources: {
521
+ "lens-diagnostics-delta-project": {
522
+ findings: scoped,
523
+ scannedAt: report?.generatedAt,
524
+ citedPath: (d) => d.filePath,
525
+ onMissing: "drop",
526
+ },
527
+ },
521
528
  });
522
529
  const staleSet = new Set(gated.stale);
523
530
  // Concatenating live-then-stale reorders a file's demoted rows to the end
@@ -702,13 +709,16 @@ function applyDeltaFreshnessGate(files, cwd, generatedAt) {
702
709
  }
703
710
  if (flat.length === 0)
704
711
  return files;
705
- const gated = gateFindingsByPathFreshness({
706
- store: "lens-diagnostics-delta",
707
- findings: flat,
712
+ const { "lens-diagnostics-delta": gated } = gateFindingsByPathFreshness({
708
713
  cwd,
709
- scannedAt: effectiveAt,
710
- citedPath: (f) => f.filePath,
711
- onMissing: "drop",
714
+ sources: {
715
+ "lens-diagnostics-delta": {
716
+ findings: flat,
717
+ scannedAt: effectiveAt,
718
+ citedPath: (f) => f.filePath,
719
+ onMissing: "drop",
720
+ },
721
+ },
712
722
  });
713
723
  // Two passes (live, then stale) reorder a file's demoted rows to the end
714
724
  // of its warnings array rather than the original report order — cosmetic
@@ -1160,11 +1170,20 @@ function runnerIdOf(diagnostic) {
1160
1170
  }
1161
1171
  /**
1162
1172
  * The one project-runner retirement decision. Coverage is authoritative when
1163
- * present for the runner; the older id-only arm is used only when absent.
1173
+ * present for the runner; the older id-only arm is used only when the runner
1174
+ * declared no coverage at all.
1175
+ *
1176
+ * #2962: an entry with an EMPTY file set is a declaration, not a missing one —
1177
+ * "this producer analysed the root and proved zero files". Selecting it out by
1178
+ * its own size (the pre-#2962 `entry.files.size > 0` filter) dropped it into the
1179
+ * whole-root id-only arm, so one complete opengrep report with
1180
+ * `paths.scanned: []` retired every retained opengrep finding in the tree even
1181
+ * though no file had been scanned. The id-only arm is unchanged for the eight
1182
+ * runners that declare no coverage.
1164
1183
  */
1165
1184
  export function runnerRetirementDecision(diagnostic, filePath, authoritativeRunnerIds, authoritativeCoverage) {
1166
1185
  const runnerId = runnerIdOf(diagnostic);
1167
- const coverage = (authoritativeCoverage ?? []).filter((entry) => entry.runnerId === runnerId && entry.files.size > 0);
1186
+ const coverage = (authoritativeCoverage ?? []).filter((entry) => entry.runnerId === runnerId);
1168
1187
  if (coverage.length === 0) {
1169
1188
  return authoritativeRunnerIds?.has(runnerId) ? "retire" : "keep";
1170
1189
  }
@@ -1859,6 +1878,14 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1859
1878
  });
1860
1879
  }
1861
1880
  for (const entry of authoritativeRunnerCoverage) {
1881
+ // #2962: a zero-file declaration retires nothing, so it must neither
1882
+ // write this row (a `runner_coverage_retired` row with count 0 would
1883
+ // report the opposite of what happened) nor BURN the once-per-session
1884
+ // claim — the session's first real coverage row would then be suppressed
1885
+ // by the call that proved no coverage. Its own observation is the
1886
+ // `runner-coverage-empty` ledger row raised at the fresh-fetch seam.
1887
+ if (entry.files.size === 0)
1888
+ continue;
1862
1889
  if (!claimPhaseOncePerSession("runner_coverage_retired", entry.runnerId)) {
1863
1890
  continue;
1864
1891
  }
@@ -28,6 +28,7 @@ import { countRetainedSuppressedRows, isBlocking, reconcileScanDiagnostics, } fr
28
28
  import { makeProgressReporter } from "./scan-progress.js";
29
29
  import { isWarmAttached, tryWarmAttachedDiagnostics, } from "../clients/warm-attach.js";
30
30
  import { extensionsForLanguage, SCAN_LANGUAGE_PRIORITY, } from "../clients/language-registry.js";
31
+ import { exceedsLspSyncLimits } from "../clients/lsp/content-limits.js";
31
32
  const MAX_FILES = 100;
32
33
  export const MAX_BATCH_FILES = 100;
33
34
  const MAX_DIAGNOSTICS = 200;
@@ -45,6 +46,30 @@ export const LSP_SEVERITY_FILTERS = [
45
46
  ...Object.values(SEVERITY_NAMES),
46
47
  "all",
47
48
  ];
49
+ function assertNeverBatchFileOutcome(value) {
50
+ throw new Error(`Unhandled batch file outcome: ${String(value)}`);
51
+ }
52
+ /** Render the bounded per-file outcomes shared by batch, directory, and fold surfaces. */
53
+ export function renderBatchOutcomeLines(outcomes, formatFile = (file) => file) {
54
+ const lines = [];
55
+ for (const outcome of outcomes) {
56
+ switch (outcome.outcome) {
57
+ case "too_large":
58
+ lines.push(`${formatFile(outcome.file)}: ${outcome.reason ?? "file too large for LSP diagnostics"}`);
59
+ break;
60
+ case "clean":
61
+ case "findings":
62
+ case "unsupported":
63
+ case "unavailable":
64
+ case "failed":
65
+ case "inconclusive":
66
+ break;
67
+ default:
68
+ assertNeverBatchFileOutcome(outcome.outcome);
69
+ }
70
+ }
71
+ return lines;
72
+ }
48
73
  function batchFileDeadlineMs() {
49
74
  const raw = Number(process.env.PI_LENS_LSP_BATCH_FILE_MS);
50
75
  return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_BATCH_FILE_DEADLINE_MS;
@@ -340,6 +365,24 @@ scanRoot, waitMs, serverScope = "all") {
340
365
  let touched;
341
366
  try {
342
367
  content = fs.readFileSync(absPath, "utf-8");
368
+ const contentLimit = exceedsLspSyncLimits(content);
369
+ if (contentLimit.tooLarge) {
370
+ const reason = `file too large for LSP diagnostics (${contentLimit.reason})`;
371
+ recordDegradationOnce({
372
+ kind: "lsp-diagnostics-file-too-large",
373
+ subject: absPath,
374
+ reason,
375
+ });
376
+ return {
377
+ diagnostics: [],
378
+ timedOut: false,
379
+ confirmedByTouch: false,
380
+ unconfirmedServerIds: [],
381
+ diagnosticsUnsupportedServerIds: [],
382
+ content,
383
+ tooLargeReason: reason,
384
+ };
385
+ }
343
386
  if (isWarmAttached()) {
344
387
  // The local sweep's default service wait is bounded to the same
345
388
  // 15-second per-file envelope used by the workspace sweep. An explicit
@@ -387,6 +430,12 @@ scanRoot, waitMs, serverScope = "all") {
387
430
  maxClientWaitMs: waitMs,
388
431
  source: "lsp_diagnostics",
389
432
  clientScope: serverScope,
433
+ // #3405: the caller asked whether THIS file is clean right now, and the
434
+ // content above was just read from disk — so it is the file's saved
435
+ // state. Without the save a save-triggered server answers zero
436
+ // diagnostics for every file in the query, which this path would report
437
+ // as clean.
438
+ saved: true,
390
439
  });
391
440
  timedOut = touched?.inconclusive === true;
392
441
  }
@@ -802,7 +851,10 @@ onConfirmedNoBlockers) {
802
851
  };
803
852
  }
804
853
  }
805
- const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, skipReason, } = await collectDiagnosticsForFile(file, lspService, cwd, waitMs, serverScope);
854
+ const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, tooLargeReason, skipReason, } = await collectDiagnosticsForFile(file, lspService, cwd, waitMs, serverScope);
855
+ if (tooLargeReason !== undefined) {
856
+ return { file, diagnostics: [], outcome: "too_large", tooLargeReason };
857
+ }
806
858
  const health = lspService.getDiagnosticsHealth?.(file);
807
859
  // #570: a timed-out priming check is never a confirmed "clean" — treat it
808
860
  // as unconfirmed without consulting the (unrelated) silent-tier
@@ -918,7 +970,22 @@ onConfirmedNoBlockers) {
918
970
  // Reserve the token before awaiting this file's LSP result. The direct-file
919
971
  // path performs its own confirmation/reconciliation below (#1198).
920
972
  const writeIndex = nextWriteIndex?.();
921
- const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, skipReason, } = await collectDiagnosticsForFile(absPath, lspService, cwd, waitMs, serverScope);
973
+ const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, tooLargeReason, skipReason, } = await collectDiagnosticsForFile(absPath, lspService, cwd, waitMs, serverScope);
974
+ if (tooLargeReason !== undefined) {
975
+ return {
976
+ content: [{ type: "text", text: tooLargeReason }],
977
+ details: {
978
+ filePath: absPath,
979
+ mode: "file",
980
+ severity,
981
+ serverScope,
982
+ outcome: "too_large",
983
+ tooLargeReason,
984
+ diagnostics: [],
985
+ totalDiagnostics: 0,
986
+ },
987
+ };
988
+ }
922
989
  const lspHealth = lspService.getDiagnosticsHealth?.(absPath);
923
990
  const unavailable = lspUnavailableMessage(absPath, lspHealth);
924
991
  // #533: an empty result needs a confirmed/unconfirmed verdict — a push-only,
@@ -1077,6 +1144,7 @@ onConfirmedNoBlockers) {
1077
1144
  mode: "file",
1078
1145
  severity,
1079
1146
  serverScope,
1147
+ ...(unavailable ? { unavailable } : {}),
1080
1148
  primaryServerId: primaryId,
1081
1149
  primaryDiagnosticsCount: primaryDiags.length,
1082
1150
  auxiliaryDiagnosticsCount: auxiliaryDiags.length,
@@ -1120,7 +1188,12 @@ function tallyConfirmation(results) {
1120
1188
  let unconfirmed = 0;
1121
1189
  let timedOut = 0;
1122
1190
  let navigationOnly = 0;
1191
+ let tooLarge = 0;
1123
1192
  for (const result of results) {
1193
+ if (result.tooLargeReason) {
1194
+ tooLarge += 1;
1195
+ continue;
1196
+ }
1124
1197
  if (result.diagnosticsUnsupported) {
1125
1198
  navigationOnly += 1;
1126
1199
  continue;
@@ -1138,11 +1211,13 @@ function tallyConfirmation(results) {
1138
1211
  clean += 1;
1139
1212
  }
1140
1213
  }
1141
- return { clean, unconfirmed, timedOut, navigationOnly };
1214
+ return { clean, unconfirmed, timedOut, navigationOnly, tooLarge };
1142
1215
  }
1143
1216
  function classifyBatchFileOutcome(result) {
1144
1217
  if (result.error)
1145
1218
  return "failed";
1219
+ if (result.tooLargeReason)
1220
+ return "too_large";
1146
1221
  if (result.diagnosticsUnsupported)
1147
1222
  return "unsupported";
1148
1223
  // A timed-out/unconfirmed answer may contain partial findings, but it cannot
@@ -1298,6 +1373,7 @@ async function collectBatchDiagnostics(files, severity, lspService, options) {
1298
1373
  "unsupported",
1299
1374
  "unavailable",
1300
1375
  "failed",
1376
+ "too_large",
1301
1377
  "inconclusive",
1302
1378
  ].map((outcome) => [
1303
1379
  outcome,
@@ -1353,6 +1429,7 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
1353
1429
  outcomeCounts.unavailable +
1354
1430
  outcomeCounts.unsupported +
1355
1431
  outcomeCounts.failed +
1432
+ outcomeCounts.too_large +
1356
1433
  incompleteFiles;
1357
1434
  if (notConfirmed > 0) {
1358
1435
  lines.push("", `Checks not confirmed: ${notConfirmed} (inconclusive/unavailable/unsupported/failed or not started).`);
@@ -1362,6 +1439,18 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
1362
1439
  }
1363
1440
  if (fileErrors.length > 0)
1364
1441
  lines.push("", "File errors:", ...fileErrors);
1442
+ const outcomeDetails = results.map((result) => ({
1443
+ file: result.file,
1444
+ outcome: result.outcome,
1445
+ reason: result.tooLargeReason ??
1446
+ result.inconclusiveReason ??
1447
+ result.error ??
1448
+ result.unavailable,
1449
+ }));
1450
+ const tooLargeLines = renderBatchOutcomeLines(outcomeDetails);
1451
+ if (tooLargeLines.length > 0) {
1452
+ lines.push("", "Files too large for LSP diagnostics:", ...tooLargeLines);
1453
+ }
1365
1454
  if (lspHealthWarnings.length > 0) {
1366
1455
  lines.push("", "LSP health warnings:", ...lspHealthWarnings.slice(0, 10));
1367
1456
  }
@@ -1376,7 +1465,7 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
1376
1465
  `${unconfirmedReasonClause(unconfirmed, timedOut)} NOT the same as 0 diagnostics.`);
1377
1466
  }
1378
1467
  if (display.length === 0) {
1379
- if (unconfirmed === 0) {
1468
+ if (unconfirmed === 0 && outcomeCounts.clean === results.length) {
1380
1469
  lines.push("", "No diagnostics found.");
1381
1470
  }
1382
1471
  }
@@ -1418,7 +1507,10 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
1418
1507
  outcomes: results.map((result) => ({
1419
1508
  file: result.file,
1420
1509
  outcome: result.outcome,
1421
- reason: result.inconclusiveReason ?? result.error ?? result.unavailable,
1510
+ reason: result.tooLargeReason ??
1511
+ result.inconclusiveReason ??
1512
+ result.error ??
1513
+ result.unavailable,
1422
1514
  primaryDiagnosticsCount: result.diagnostics.filter((diagnostic) => diagnostic.serverId === result.primaryServerId).length,
1423
1515
  auxiliaryDiagnosticsCount: result.diagnostics.filter((diagnostic) => diagnostic.serverId !== result.primaryServerId).length,
1424
1516
  })),
@@ -1482,11 +1574,20 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
1482
1574
  }
1483
1575
  const wasCapped = collectedFiles.length > MAX_FILES;
1484
1576
  const filesToProcess = collectedFiles.slice(0, MAX_FILES);
1485
- const { results, fileErrors, lspHealthWarnings, total, truncated, display, primaryDisplay, auxiliaryDisplay, clean, unconfirmed, timedOut, } = await collectBatchDiagnostics(filesToProcess, severity, lspService, options);
1577
+ const { results, fileErrors, lspHealthWarnings, total, truncated, display, primaryDisplay, auxiliaryDisplay, clean, unconfirmed, timedOut, outcomeCounts, } = await collectBatchDiagnostics(filesToProcess, severity, lspService, options);
1486
1578
  const navigationOnly = results.filter((result) => result.diagnosticsUnsupported).length;
1487
1579
  // #3088: the directory scan's own suppressed bucket — one summed line.
1488
1580
  const dispositionSuppressed = results.reduce((sum, result) => sum + (result.dispositionSuppressed ?? 0), 0);
1489
1581
  const suppressedLine = dispositionSuppressedLine(dispositionSuppressed);
1582
+ const outcomeDetails = results.map((result) => ({
1583
+ file: result.file,
1584
+ outcome: result.outcome,
1585
+ reason: result.tooLargeReason ??
1586
+ result.inconclusiveReason ??
1587
+ result.error ??
1588
+ result.unavailable,
1589
+ }));
1590
+ const tooLargeLines = renderBatchOutcomeLines(outcomeDetails, (file) => path.relative(absPath, file));
1490
1591
  let text;
1491
1592
  if (total === 0) {
1492
1593
  // #533/#570: an unconfirmed-containing directory result must never
@@ -1504,7 +1605,12 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
1504
1605
  "LSP unavailable for one or more files:",
1505
1606
  ...lspHealthWarnings.slice(0, 10),
1506
1607
  ]
1507
- : [cleanLine]),
1608
+ : unconfirmed > 0 || outcomeCounts.clean === filesToProcess.length
1609
+ ? [cleanLine]
1610
+ : []),
1611
+ ...(tooLargeLines.length > 0
1612
+ ? ["", "Files too large for LSP diagnostics:", ...tooLargeLines]
1613
+ : []),
1508
1614
  ...(suppressedLine ? ["", suppressedLine] : []),
1509
1615
  ].join("\n");
1510
1616
  }
@@ -1547,6 +1653,9 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
1547
1653
  }
1548
1654
  if (suppressedLine)
1549
1655
  lines.push("", suppressedLine);
1656
+ if (tooLargeLines.length > 0) {
1657
+ lines.push("", "Files too large for LSP diagnostics:", ...tooLargeLines);
1658
+ }
1550
1659
  text = lines.join("\n");
1551
1660
  }
1552
1661
  return {
@@ -1574,6 +1683,8 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
1574
1683
  truncated,
1575
1684
  cleanFiles: clean,
1576
1685
  unconfirmedFiles: unconfirmed,
1686
+ outcomes: outcomeDetails,
1687
+ outcomeCounts,
1577
1688
  navigationOnlyFiles: navigationOnly > 0 ? navigationOnly : undefined,
1578
1689
  timedOutFiles: timedOut > 0 ? timedOut : undefined,
1579
1690
  fileErrors: fileErrors.length > 0 ? fileErrors : undefined,
@@ -0,0 +1,29 @@
1
+ # ADR 0001: stale advisory skips disposition policy
2
+
3
+ ## Status
4
+
5
+ Accepted — 2026-09-23
6
+
7
+ ## Context
8
+
9
+ The #3247 review found an asymmetry: the live turn-end arm applies disposition
10
+ policy, but the freshness arm that demotes dependency-drift records does not.
11
+ The stale arm drives delivery-count commits and bounded retirement, so changing
12
+ it would change the freshness gate's accounting rather than merely filter text.
13
+
14
+ ## Decision
15
+
16
+ Apply disposition policy on the live arm only. Keep the stale advisory visible
17
+ with its stale marker and its existing bounded delivery accounting.
18
+
19
+ ## Consequences
20
+
21
+ Marked findings in a stale record may remain in the bounded stale advisory.
22
+ This is deliberate and does not recreate the unbounded replay fixed by #3247.
23
+
24
+ ## Links
25
+
26
+ - Catalog shape: `AGENTS.md` shape 10.
27
+ - Issue: #3246.
28
+ - PR: #3247, section “Known asymmetry: the stale arm does not run the policy”.
29
+
@@ -0,0 +1,30 @@
1
+ # ADR 0002: workspace hash is a rendezvous derivation
2
+
3
+ ## Status
4
+
5
+ Accepted — 2026-09-23
6
+
7
+ ## Context
8
+
9
+ `workspaceHash` names the warm IPC endpoint and turn-end status file. Separate
10
+ processes derive it at different times and must reproduce identical bytes.
11
+ Path-map normalizers can consult filesystem state, so they cannot be used for
12
+ this cross-process rendezvous.
13
+
14
+ ## Decision
15
+
16
+ Keep `workspaceHash` as pure string math. Neither the long-lived canonical path
17
+ key nor the process-local ephemeral path-key seam applies.
18
+
19
+ ## Consequences
20
+
21
+ The derivation remains independently reproducible and avoids filesystem-state
22
+ staleness. Its deliberate lowercase fold can collide for case-distinct POSIX
23
+ directories; changing those bytes requires a separate invalidation decision.
24
+
25
+ ## Links
26
+
27
+ - Catalog shapes: `AGENTS.md` shapes 1 and 2.
28
+ - Issue: #3255.
29
+ - PR: #3256; source record: `clients/mcp/ipc.ts:44-77`.
30
+