@herbertgao/pi-extensions 2026.9.15 → 2026.9.17

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 (197) hide show
  1. package/README.md +5 -0
  2. package/THIRD_PARTY_NOTICES.md +26 -0
  3. package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
  4. package/node_modules/@herbertgao/sol-pi/package.json +1 -1
  5. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +11 -53
  6. package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -6
  7. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  8. package/node_modules/@narumitw/pi-btw/package.json +4 -4
  9. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +2 -0
  10. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +16 -4
  11. package/node_modules/pi-lens/CHANGELOG.md +498 -0
  12. package/node_modules/pi-lens/README.md +45 -2
  13. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +23 -7
  14. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +187 -51
  15. package/node_modules/pi-lens/dist/clients/cache-manager.js +64 -5
  16. package/node_modules/pi-lens/dist/clients/child-unref.js +17 -8
  17. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
  18. package/node_modules/pi-lens/dist/clients/config-locations.js +237 -9
  19. package/node_modules/pi-lens/dist/clients/config-resolve.js +7 -1
  20. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +33 -3
  21. package/node_modules/pi-lens/dist/clients/dependency-checker.js +87 -64
  22. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +39 -3
  23. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +118 -1
  24. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +8 -2
  25. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +23 -4
  26. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +14 -7
  27. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +26 -28
  28. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +38 -25
  29. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +32 -49
  30. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +16 -14
  31. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +25 -7
  32. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +28 -47
  33. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +11 -4
  34. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +27 -22
  35. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +31 -46
  36. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +29 -46
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +91 -11
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +16 -2
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +38 -23
  40. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +27 -15
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +17 -8
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +31 -21
  43. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +34 -60
  44. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +6 -5
  45. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +12 -5
  46. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
  47. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +22 -6
  48. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +39 -16
  49. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +1 -1
  50. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +27 -18
  51. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +18 -19
  52. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +6 -1
  53. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +6 -2
  54. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -3
  55. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +11 -2
  56. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +10 -2
  57. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +11 -2
  58. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +12 -6
  59. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +30 -15
  60. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +23 -0
  61. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +8 -2
  62. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
  63. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +15 -3
  64. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +12 -2
  65. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +27 -39
  66. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +13 -3
  67. package/node_modules/pi-lens/dist/clients/effective-config.js +3 -1
  68. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +42 -11
  69. package/node_modules/pi-lens/dist/clients/formatters.js +49 -11
  70. package/node_modules/pi-lens/dist/clients/git-guard.js +247 -34
  71. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +121 -25
  72. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +186 -0
  73. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +19 -6
  74. package/node_modules/pi-lens/dist/clients/installer/index.js +281 -32
  75. package/node_modules/pi-lens/dist/clients/jscpd-client.js +8 -0
  76. package/node_modules/pi-lens/dist/clients/latency-logger.js +8 -0
  77. package/node_modules/pi-lens/dist/clients/lens-config.js +66 -8
  78. package/node_modules/pi-lens/dist/clients/lsp/client.js +139 -11
  79. package/node_modules/pi-lens/dist/clients/lsp/config.js +9 -7
  80. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +41 -0
  81. package/node_modules/pi-lens/dist/clients/lsp/index.js +1 -1
  82. package/node_modules/pi-lens/dist/clients/lsp/launch.js +23 -2
  83. package/node_modules/pi-lens/dist/clients/lsp/server.js +106 -25
  84. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +14 -0
  85. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +105 -16
  86. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +232 -44
  87. package/node_modules/pi-lens/dist/clients/mcp/review.js +53 -7
  88. package/node_modules/pi-lens/dist/clients/opengrep-client.js +11 -6
  89. package/node_modules/pi-lens/dist/clients/pipeline.js +121 -66
  90. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
  91. package/node_modules/pi-lens/dist/clients/project-lens-config.js +11 -3
  92. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +34 -26
  93. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +16 -10
  94. package/node_modules/pi-lens/dist/clients/ruff-client.js +31 -4
  95. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +38 -4
  96. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +16 -5
  97. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +27 -12
  98. package/node_modules/pi-lens/dist/clients/runtime-turn.js +447 -280
  99. package/node_modules/pi-lens/dist/clients/safe-spawn.js +256 -65
  100. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +84 -1
  101. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +119 -58
  102. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +51 -0
  103. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +112 -0
  104. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +204 -0
  105. package/node_modules/pi-lens/dist/clients/warm-attach.js +59 -0
  106. package/node_modules/pi-lens/dist/clients/widget-state.js +145 -14
  107. package/node_modules/pi-lens/dist/clients/workspace-topology.js +57 -3
  108. package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -13
  109. package/node_modules/pi-lens/dist/index.js +5695 -3733
  110. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +4 -0
  111. package/node_modules/pi-lens/dist/mcp/server.js +33 -21
  112. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  113. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +43 -16
  114. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +118 -7
  115. package/node_modules/pi-lens/docs/adr/0001-stale-advisory-live-arm.md +29 -0
  116. package/node_modules/pi-lens/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
  117. package/node_modules/pi-lens/docs/adr/0003-git-guard-latch-writer.md +203 -0
  118. package/node_modules/pi-lens/docs/adr/0004-disposition-policy-seam.md +38 -0
  119. package/node_modules/pi-lens/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
  120. package/node_modules/pi-lens/docs/adr/0006-derived-state-benchmark-first.md +31 -0
  121. package/node_modules/pi-lens/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
  122. package/node_modules/pi-lens/docs/adr/0008-turn-end-lane-interface.md +165 -0
  123. package/node_modules/pi-lens/docs/adr/0009-reported-path-attribution.md +114 -0
  124. package/node_modules/pi-lens/docs/adr/README.md +26 -0
  125. package/node_modules/pi-lens/docs/agent-tools.md +18 -1
  126. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +4 -3
  127. package/node_modules/pi-lens/docs/configuration.md +55 -1
  128. package/node_modules/pi-lens/docs/engineering-principles.md +202 -0
  129. package/node_modules/pi-lens/docs/environment-variables.md +61 -4
  130. package/node_modules/pi-lens/docs/features.md +2 -2
  131. package/node_modules/pi-lens/docs/globalconfig.md +6 -1
  132. package/node_modules/pi-lens/docs/language-coverage.md +17 -0
  133. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +98 -31
  134. package/node_modules/pi-lens/docs/pi-lens-fixer.md +57 -3
  135. package/node_modules/pi-lens/docs/pi-lens-investigator.md +9 -0
  136. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +16 -0
  137. package/node_modules/pi-lens/docs/pi-lens-subagent.md +9 -0
  138. package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
  139. package/node_modules/pi-lens/docs/release-qa-baseline.md +2 -0
  140. package/node_modules/pi-lens/docs/servercapabilities.md +21 -11
  141. package/node_modules/pi-lens/docs/settings.md +12 -3
  142. package/node_modules/pi-lens/docs/subagent-compat.md +1 -1
  143. package/node_modules/pi-lens/package.json +5 -3
  144. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
  145. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
  146. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
  147. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
  148. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
  149. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
  150. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
  151. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
  152. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
  153. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
  154. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
  155. package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
  156. package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
  157. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
  158. package/node_modules/pi-lens/scripts/install-selftest.mjs +21 -17
  159. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  160. package/node_modules/pi-mcp-adapter/CHANGELOG.md +29 -0
  161. package/node_modules/pi-mcp-adapter/OAUTH.md +369 -0
  162. package/node_modules/pi-mcp-adapter/README.md +12 -5
  163. package/node_modules/pi-mcp-adapter/config.ts +31 -8
  164. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +1 -1
  165. package/node_modules/pi-mcp-adapter/dist/config.js +33 -8
  166. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  167. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +15 -2
  168. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -1
  169. package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +2 -0
  170. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +27 -2
  171. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -1
  172. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +1 -1
  173. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  174. package/node_modules/pi-mcp-adapter/dist/server-manager.js +6 -4
  175. package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -1
  176. package/node_modules/pi-mcp-adapter/dist/types.d.ts +2 -2
  177. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +2 -0
  178. package/node_modules/pi-mcp-adapter/dist/utils.js +10 -3
  179. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  180. package/node_modules/pi-mcp-adapter/index.ts +37 -20
  181. package/node_modules/pi-mcp-adapter/init.ts +14 -15
  182. package/node_modules/pi-mcp-adapter/json-schema-validator.ts +16 -2
  183. package/node_modules/pi-mcp-adapter/mcp-auth.ts +27 -2
  184. package/node_modules/pi-mcp-adapter/metadata-cache.ts +1 -1
  185. package/node_modules/pi-mcp-adapter/package.json +3 -2
  186. package/node_modules/pi-mcp-adapter/proxy-modes.ts +1 -1
  187. package/node_modules/pi-mcp-adapter/server-manager.ts +6 -3
  188. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +99 -6
  189. package/node_modules/pi-mcp-adapter/types.ts +2 -2
  190. package/node_modules/pi-mcp-adapter/ui-session.ts +60 -27
  191. package/node_modules/pi-mcp-adapter/utils.ts +11 -3
  192. package/node_modules/pi-next-cue/.github/workflows/publish.yml +20 -0
  193. package/node_modules/pi-next-cue/LICENSE +21 -0
  194. package/node_modules/pi-next-cue/README.md +81 -0
  195. package/node_modules/pi-next-cue/index.ts +358 -0
  196. package/node_modules/pi-next-cue/package.json +28 -0
  197. package/package.json +9 -6
@@ -25,7 +25,8 @@ import { loadWebTreeSitter } from "./deps/web-tree-sitter.js";
25
25
  import { transientRetryDelayMs } from "./dispatch/runners/utils/availability-policy.js";
26
26
  import { getProjectIgnoreMatcher, isExcludedDirName } from "./file-utils.js";
27
27
  import { downloadGrammarDetailed, fileHasWasmMagic, grammarBlockReason, grammarFileSha256, isVendoredGrammar, LANGUAGE_TO_GRAMMAR, pinnedGrammarHash, vendoredGrammarRefusal, vendoredGrammarsDir, } from "./grammar-source.js";
28
- import { resolvePackagePath } from "./package-root.js";
28
+ import { getPackageRoot, resolvePackagePath } from "./package-root.js";
29
+ import { resolveWebTreeSitterPackageDir } from "../scripts/lib/web-tree-sitter-dir.mjs";
29
30
  import { assertInstallAllowed, getProjectTrustGeneration, } from "./project-trust.js";
30
31
  import { escapeRegExp } from "./string-utils.js";
31
32
  import { logTreeSitterDiagnostic } from "./tree-sitter-logger.js";
@@ -73,6 +74,27 @@ function createParserCounters() {
73
74
  parserFailures: 0,
74
75
  };
75
76
  }
77
+ /**
78
+ * The errno that blocks writing into `dir`, or undefined when a write can be
79
+ * attempted (#3409). The directory itself need not exist yet — the lazy fetch
80
+ * creates it — so the nearest EXISTING ancestor is what gets probed.
81
+ */
82
+ function grammarWriteBlockedReason(dir) {
83
+ let probe = dir;
84
+ while (!fs.existsSync(probe)) {
85
+ const parent = path.dirname(probe);
86
+ if (parent === probe)
87
+ return "ENOENT";
88
+ probe = parent;
89
+ }
90
+ try {
91
+ fs.accessSync(probe, fs.constants.W_OK);
92
+ return undefined;
93
+ }
94
+ catch (err) {
95
+ return err?.code ?? "EACCES";
96
+ }
97
+ }
76
98
  /**
77
99
  * `size:mtimeMs` identity for a grammar candidate, or undefined when the path
78
100
  * is absent or not a regular file. Doubles as the existence check on the
@@ -347,6 +369,7 @@ export class TreeSitterClient {
347
369
  this.grammarDirResolutionDeps = grammarDirResolutionDeps ?? {
348
370
  resolveAsset: (asset) => this.resolveWebTreeSitterAsset(asset),
349
371
  resolvePackage: (specifier) => _require.resolve(specifier),
372
+ packageRoot: () => getPackageRoot(import.meta.url),
350
373
  cwd: () => process.cwd(),
351
374
  };
352
375
  this.grammarsDir = this.findGrammarsDir();
@@ -469,38 +492,42 @@ export class TreeSitterClient {
469
492
  }
470
493
  }
471
494
  /**
472
- * Resolve a web-tree-sitter asset path using multiple strategies:
473
- * 1. Node module resolution via createRequire (handles hoisted installs — issue #20)
474
- * 2. Package-root walk from import.meta.url (handles on-the-fly TS compilation by pi)
475
- * 3. process.cwd() fallback
495
+ * web-tree-sitter's installed package directory, through the one shared
496
+ * ladder in `scripts/lib/web-tree-sitter-dir.mjs` (#3409). Routed through
497
+ * `grammarDirResolutionDeps` so the resolver context — the thing that
498
+ * differs on a compiled host — stays the injected seam (#2112/#2136).
499
+ */
500
+ webTreeSitterPackageDir() {
501
+ return resolveWebTreeSitterPackageDir({
502
+ resolve: (specifier) => this.grammarDirResolutionDeps.resolvePackage(specifier),
503
+ packageRoot: () => this.grammarDirResolutionDeps.packageRoot(),
504
+ cwd: () => this.grammarDirResolutionDeps.cwd(),
505
+ });
506
+ }
507
+ /**
508
+ * Resolve a web-tree-sitter asset path:
509
+ * 1. the asset's own subpath, authoritative when the package `exports` it
510
+ * (`tree-sitter.wasm` is exported; `grammars` is NOT in 0.25.10, so this
511
+ * rung is dead for the grammars dir — #3409's second symptom);
512
+ * 2. the asset inside the package directory the shared ladder resolves. That
513
+ * ladder's own last two rungs are the package-root walk (#20, for pi's
514
+ * temp-dir compile) and the cwd fallback this method used to spell out.
476
515
  */
477
516
  resolveWebTreeSitterAsset(asset) {
478
- // Strategy 1: Node module resolution (hoisted installs, pnpm workspaces)
479
517
  try {
480
- const resolved = _require.resolve(`web-tree-sitter/${asset}`);
518
+ const resolved = this.grammarDirResolutionDeps.resolvePackage(`web-tree-sitter/${asset}`);
481
519
  if (fs.existsSync(resolved))
482
520
  return resolved;
483
521
  }
484
522
  catch {
485
523
  /* fall through */
486
524
  }
487
- // Strategy 2: Walk up from this module to find package.json, then into node_modules.
488
- // This is required when pi compiles TS on-the-fly to a temp directory —
489
- // createRequire(import.meta.url) resolves from the temp dir and can't find
490
- // web-tree-sitter, but the package root (where package.json lives) still has
491
- // the correct node_modules layout.
492
- try {
493
- const candidate = resolvePackagePath(import.meta.url, "node_modules", "web-tree-sitter", asset);
525
+ const pkgDir = this.webTreeSitterPackageDir();
526
+ if (pkgDir) {
527
+ const candidate = path.join(pkgDir, asset);
494
528
  if (fs.existsSync(candidate))
495
529
  return candidate;
496
530
  }
497
- catch {
498
- /* fall through */
499
- }
500
- // Strategy 3: cwd fallback
501
- const cwdCandidate = path.join(process.cwd(), "node_modules", "web-tree-sitter", asset);
502
- if (fs.existsSync(cwdCandidate))
503
- return cwdCandidate;
504
531
  return undefined;
505
532
  }
506
533
  /**
@@ -764,22 +791,16 @@ export class TreeSitterClient {
764
791
  * whether or not it exists yet — so we can create + populate it when the
765
792
  * postinstall download was skipped (pnpm/bun). Returns undefined if
766
793
  * web-tree-sitter itself can't be located.
794
+ *
795
+ * #3409: this used to resolve the BARE `web-tree-sitter` specifier and
796
+ * nothing else, which throws MODULE_NOT_FOUND inside the `bun
797
+ * build --compile` binary pi ships — so on every such host it returned
798
+ * undefined and NO non-core grammar could ever be fetched. It now shares the
799
+ * read path's ladder, whose first rung is a subpath that does resolve there.
767
800
  */
768
801
  grammarsWriteDir() {
769
- try {
770
- let dir = path.dirname(_require.resolve("web-tree-sitter"));
771
- while (path.basename(dir) !== "web-tree-sitter" &&
772
- dir !== path.dirname(dir)) {
773
- dir = path.dirname(dir);
774
- }
775
- if (path.basename(dir) === "web-tree-sitter") {
776
- return path.join(dir, "grammars");
777
- }
778
- }
779
- catch {
780
- /* fall through */
781
- }
782
- return undefined;
802
+ const pkgDir = this.webTreeSitterPackageDir();
803
+ return pkgDir ? path.join(pkgDir, "grammars") : undefined;
783
804
  }
784
805
  /**
785
806
  * Ensure a single grammar wasm is on disk, fetching it at runtime if the
@@ -847,7 +868,7 @@ export class TreeSitterClient {
847
868
  // refactor that lets an exception out cannot regress to it.
848
869
  const message = err instanceof Error ? err.message : String(err);
849
870
  this.recordGrammarFailure(grammarFile, `The runtime grammar fetch threw an unexpected error (${message}).`,
850
- /* retryable */ true);
871
+ /* retryable */ true, "download-failed");
851
872
  return false;
852
873
  }
853
874
  })();
@@ -899,15 +920,31 @@ export class TreeSitterClient {
899
920
  ? this.grammarsDir
900
921
  : this.grammarsWriteDir();
901
922
  if (!dir) {
902
- // No writable grammars directory could be located (e.g. pi compiled
903
- // this module to a temp dir and web-tree-sitter isn't resolvable
904
- // from there yet). This is an environment condition, not a CDN
905
- // verdict — #1536 review F2: it must get the same cooldown +
906
- // notification treatment as a download failure, not a silent
907
- // unmemoized `false` that re-does the same failing resolution
908
- // sweep on every single demand.
909
- this.recordGrammarFailure(grammarFile, "No writable grammars directory could be located for the runtime fetch.",
910
- /* retryable */ true);
923
+ // Nothing to write into: web-tree-sitter is not locatable by ANY rung
924
+ // of the shared ladder (this runtime's resolver, pi-lens's own package
925
+ // root, the working directory), so the package is missing from this
926
+ // install. Still an environment condition, not a CDN verdict — #1536
927
+ // review F2: it gets the same cooldown + notification treatment as a
928
+ // download failure rather than a silent unmemoized `false` that re-does
929
+ // the same failing resolution sweep on every demand.
930
+ //
931
+ // #3409: before the shared ladder, this arm ALSO caught every host
932
+ // whose runtime cannot resolve a bare specifier — where the package was
933
+ // present and the wasm downloadable — and told the user to approve
934
+ // build scripts or restore network access. Hence the cause.
935
+ this.recordGrammarFailure(grammarFile, "No grammars directory could be resolved for the runtime fetch: the web-tree-sitter package is not locatable from this runtime, from pi-lens's own package root, or from the working directory.",
936
+ /* retryable */ true, "write-dir-unresolvable");
937
+ return false;
938
+ }
939
+ // A destination that cannot be written to is not a download problem
940
+ // either (#3409): a root-owned `node_modules` under a global install
941
+ // reaches `downloadGrammarDetailed`'s catch-all, which reports no reason
942
+ // at all, so the user was sent to inspect their package manager and
943
+ // network for an EACCES.
944
+ const writeBlock = grammarWriteBlockedReason(dir);
945
+ if (writeBlock) {
946
+ this.recordGrammarFailure(grammarFile, `The grammars directory ${dir} cannot be written to (${writeBlock}), so the runtime fetch has nowhere to land.`,
947
+ /* retryable */ true, "write-dir-unwritable");
911
948
  return false;
912
949
  }
913
950
  // Reuse the shared single-file downloader (same CDN/source as the
@@ -940,7 +977,7 @@ export class TreeSitterClient {
940
977
  // and one that sends the user to inspect their package manager while a
941
978
  // captive portal is the real cause.
942
979
  this.recordGrammarFailure(grammarFile, reason ??
943
- "The package manager skipped install scripts and the runtime download failed.", retryable);
980
+ "The package manager skipped install scripts and the runtime download failed.", retryable, "download-failed");
944
981
  }
945
982
  return ok;
946
983
  }
@@ -951,8 +988,14 @@ export class TreeSitterClient {
951
988
  * by the "no writable directory" and "download failed" arms of
952
989
  * `ensureGrammar` — both are failures of the SAME ensure attempt, just
953
990
  * with a different point of failure.
991
+ *
992
+ * `cause` is that point of failure (#3409). It picks the REMEDY the user is
993
+ * told and lands in the durable record, so `~/.pi-lens` forensics can tell
994
+ * "this host has nowhere to write" from "this download failed" — the two
995
+ * were one string, and the one it carried named build scripts and the
996
+ * network for a cause that is neither.
954
997
  */
955
- recordGrammarFailure(grammarFile, detail, retryable) {
998
+ recordGrammarFailure(grammarFile, detail, retryable, cause) {
956
999
  const attempts = (this.grammarFailureAttempts.get(grammarFile) ?? 0) + 1;
957
1000
  this.grammarFailureAttempts.set(grammarFile, attempts);
958
1001
  // A durable verdict (404/410) never expires -- matches the issue's "a
@@ -964,17 +1007,29 @@ export class TreeSitterClient {
964
1007
  this.grammarRetryAtMs.set(grammarFile, retryDelayMs === undefined
965
1008
  ? Number.POSITIVE_INFINITY
966
1009
  : Date.now() + retryDelayMs);
967
- const unavailable = retryable
968
- ? `tree-sitter grammar '${grammarFile}' is unavailable — symbol search, ` +
969
- `module reports and structural rules for this language will be degraded. ` +
970
- `${detail} pi-lens will retry automatically in ${Math.round(retryDelayMs / 1000)}s; ` +
971
- `if the problem persists, allow the package manager's build scripts ` +
972
- `(pnpm approve-builds / bun trustedDependencies) or restore network access.`
973
- : `tree-sitter grammar '${grammarFile}' is unavailable — symbol search, ` +
974
- `module reports and structural rules for this language will be degraded. ` +
975
- `${detail} The grammar source reports it does not exist (not a network problem), so this will not ` +
976
- `resolve on retry. Fix: reinstall with a manager that runs postinstall, allow its build scripts ` +
977
- `(pnpm approve-builds / bun trustedDependencies), or restore network access.`;
1010
+ // The remedy the user is given, keyed to WHERE the attempt failed. The
1011
+ // last two arms are the pre-existing strings, byte for byte; the first
1012
+ // two exist because sending someone to `pnpm approve-builds` and their
1013
+ // network for a resolution or permission fault is the #3409 complaint.
1014
+ const remedy = cause === "write-dir-unresolvable"
1015
+ ? `This is a packaging/module-resolution fault, not a download problem: reinstall pi-lens so its ` +
1016
+ `web-tree-sitter dependency is present, or drop the wasm into pi-lens's own grammars/ directory.`
1017
+ : cause === "write-dir-unwritable"
1018
+ ? `This is a filesystem-permission fault, not a download problem: make that directory writable ` +
1019
+ `for the user pi-lens runs as, or reinstall pi-lens as that user.`
1020
+ : retryable
1021
+ ? `if the problem persists, allow the package manager's build scripts ` +
1022
+ `(pnpm approve-builds / bun trustedDependencies) or restore network access.`
1023
+ : `Fix: reinstall with a manager that runs postinstall, allow its build scripts ` +
1024
+ `(pnpm approve-builds / bun trustedDependencies), or restore network access.`;
1025
+ const unavailable = `tree-sitter grammar '${grammarFile}' is unavailable — symbol search, ` +
1026
+ `module reports and structural rules for this language will be degraded. ` +
1027
+ `${detail} ` +
1028
+ (retryable
1029
+ ? `pi-lens will retry automatically in ${Math.round(retryDelayMs / 1000)}s; `
1030
+ : `The grammar source reports it does not exist (not a network problem), so this will not ` +
1031
+ `resolve on retry. `) +
1032
+ remedy;
978
1033
  logTreeSitterDiagnostic({
979
1034
  subsystem: "tree-sitter-client",
980
1035
  message: unavailable,
@@ -984,6 +1039,12 @@ export class TreeSitterClient {
984
1039
  retryable,
985
1040
  retryDelayMs,
986
1041
  attempts,
1042
+ // Omitted, not defaulted, for the arms that are neither a fetch
1043
+ // destination nor a download verdict (a vendored grammar is never
1044
+ // downloaded; a decode failure happened AFTER a successful one), so
1045
+ // those records keep their pre-#3409 shape rather than carrying a
1046
+ // cause that is not true of them.
1047
+ ...(cause ? { cause } : {}),
987
1048
  },
988
1049
  });
989
1050
  // #1536 review F1: incrementDegradationCount keeps ONE ring-buffer slot
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The ONE interface every turn-end delivery lane implements (#1892).
3
+ *
4
+ * `clients/runtime-turn.ts`'s `handleTurnEnd` grew fourteen delivery lanes
5
+ * inline: each read its own store, applied its own freshness/disposition
6
+ * policy, and formatted its own section into `blockerParts`/`advisoryParts`,
7
+ * so the composer had to know every lane's rules and no lane could be read,
8
+ * tested, or reasoned about on its own. The lanes converged on ONE shape
9
+ * anyway — read a store, gate the rows, render a tier — and that shape is
10
+ * this module. A lane is a deep module: three narrow calls over whatever its
11
+ * store's rules happen to be, and the composer states none of them.
12
+ *
13
+ * ── The three stages, and why they are three ──────────────────────────────
14
+ *
15
+ * 1. `collect(ctx)` — read this lane's stores and declare, per STORE, the
16
+ * findings plus the freshness policy that store's rows need. It returns
17
+ * structured rows keyed by store name (the source identity the shared
18
+ * pass and its bounded records carry), never a rendered string.
19
+ *
20
+ * It does NOT call the freshness gate. #3264 folded the turn-end scanner
21
+ * stores into ONE `gateFindingsByPathFreshness` call so a file two stores
22
+ * cite is stat'd once, spends one stat budget, and writes one bounded
23
+ * decision record per delivery instead of up to six. A lane that gated
24
+ * itself would re-split that pass the moment a second lane appeared, so
25
+ * the pass is the COMPOSER's (it is a cross-lane resource — a filesystem
26
+ * pass, a budget and a record, not a per-lane rule), and a lane only
27
+ * DECLARES its sources. `tests/config/turn-end-lane-boundaries.test.ts`
28
+ * pins that: a lane that calls the gate itself reds.
29
+ *
30
+ * 2. `gate(gates, ctx)` — apply the per-lane policy the shared pass cannot:
31
+ * dispositions (`filterFindingsByDisposition`, the seam in
32
+ * `clients/dispatch/finding-policy.ts`), and whatever existence or
33
+ * lifecycle contract the lane's own store carries. Takes the freshness
34
+ * partition the composer got for THIS lane's sources and returns the
35
+ * lane's own kept shape, which no other module reads.
36
+ *
37
+ * 3. `render(kept, ctx)` — format the tiers. A lane returns its sections; it
38
+ * never pushes into the turn's arrays. The push stays in the composer so
39
+ * the `@delivery-surface:` seam scan in
40
+ * `tests/clients/finding-delivery-gate.test.ts` — which reads
41
+ * `clients/runtime-turn.ts` and `tools/lens-diagnostics.ts` — keeps seeing
42
+ * every agent-facing render seam. A lane that pushed from its own file
43
+ * would be invisible to that scan; the boundary test reds on it.
44
+ *
45
+ * The composer still owns everything that is not one lane's rule: the order
46
+ * the tiers are assembled in, the display and message caps, the turn
47
+ * signature dedupe, the `turn-end-findings` cache write, the git-guard
48
+ * record, and the one-per-turn disposition-suppressed notice that every
49
+ * lane's counts fold into.
50
+ */
51
+ export {};
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The turn-end GOVULNCHECK lane (#1892): the session_start-cached Go CVE
3
+ * store, rendered as ONE advisory tier.
4
+ *
5
+ * One lane, one store, one tier — the straight second extraction against the
6
+ * `TurnEndLane` interface, and the first lane that renders an ADVISORY
7
+ * section (see `TurnEndLaneParts.advisoryParts` and ADR 0008's amendment for
8
+ * why that field arrives with this lane rather than with the interface).
9
+ *
10
+ * Registered surface (`clients/finding-delivery-gate.ts`):
11
+ * `runtime-turn:govulncheck-advisory` — mode `gated`, evidence
12
+ * `scannerGates.govulncheck` (the composer's own binding of the shared pass's
13
+ * result, unchanged by this extraction).
14
+ *
15
+ * History this file must not lose (each line below is a shipped defect or a
16
+ * review finding):
17
+ *
18
+ * - #1622 sibling sweep: govulncheck renders a call site as `file:line` off a
19
+ * session_start snapshot — the same stale-line shape as gitleaks, one tier
20
+ * lower. A file edited after the scan keeps its CVE and loses its line,
21
+ * plus the `STALE_LINE_MARKER`.
22
+ * - #1622 review H1: a CVE is pinned by `go.mod`, NOT by the call site, so a
23
+ * DELETED traced file may not drop it — `onMissing: "demote"` routes a
24
+ * vanished path into the same arm as an edited one. The first cut dropped
25
+ * it, and `citedPath` reads only the FIRST filename frame, so one deleted
26
+ * file in a long trace silently killed a CVE go.mod still pins.
27
+ * - #1694 F1: these findings pass through `filterFindingsByDisposition` with
28
+ * the SAME `ProjectDiagnostic` identity `lens_diagnostics mode=full`
29
+ * surfaces, over BOTH freshness arms — a mark made against what the agent
30
+ * was SHOWN is honoured here too.
31
+ * - #1627 + #1625 review: the post-gate row guard and the post-disposition
32
+ * row guard are the SAME guard — `render` tests the list it is about to
33
+ * print, never the raw cache length, or the header prints with zero rows
34
+ * beneath it whenever either filter empties the list.
35
+ */
36
+ import { filterFindingsByDisposition } from "../../dispatch/finding-policy.js";
37
+ import { toRunnerDisplayPath } from "../../dispatch/runner-context.js";
38
+ import { govulncheckFindingToProjectDiagnostic } from "../../project-diagnostics/runner-adapters/govulncheck.js";
39
+ import { STALE_LINE_MARKER } from "../../stale-marker.js";
40
+ /**
41
+ * #1622 H1: `onMissing: "demote"`, not the default `"drop"`. A Go CVE is
42
+ * pinned by `go.mod`; the traced call site is only where it is reachable
43
+ * FROM, so a deleted trace file means "the line is unusable", never "the
44
+ * vulnerability is gone". `citedPath` reads the first filename frame, which
45
+ * is also the frame `render` prints — the path the verdict is about is the
46
+ * path the agent is shown.
47
+ */
48
+ async function collect(ctx) {
49
+ const data = (await ctx.readScannerCache("govulncheck"))
50
+ ?.data;
51
+ return {
52
+ govulncheck: {
53
+ findings: data?.findings ?? [],
54
+ scannedAt: data?.scannedAt,
55
+ citedPath: (finding) => finding.trace.find((frame) => frame.filename)?.filename,
56
+ onMissing: "demote",
57
+ },
58
+ };
59
+ }
60
+ /**
61
+ * #1694 F1/#1625 review round: the freshness gate runs FIRST, so the
62
+ * disposition anchor is derived from each finding's post-demotion identity —
63
+ * this list is already the gate's live+stale partition, never the raw
64
+ * pre-gate cache.
65
+ */
66
+ function gate(gates, ctx) {
67
+ const stale = new Set(gates.govulncheck.stale);
68
+ const filtered = filterFindingsByDisposition([...gates.govulncheck.live, ...gates.govulncheck.stale], ctx.cwd, (f) => govulncheckFindingToProjectDiagnostic(ctx.cwd, f));
69
+ return {
70
+ findings: filtered.kept,
71
+ stale,
72
+ suppressed: filtered.suppressed,
73
+ };
74
+ }
75
+ function render(kept, ctx) {
76
+ // Lane-local accumulator, deliberately NOT named after the turn's tier:
77
+ // `advisoryParts` is the composer's array, and a lane that pushed into one
78
+ // would be a render seam the `@delivery-surface:` tag scan cannot see
79
+ // (`tests/config/turn-end-lane-boundaries.test.ts`).
80
+ const advisorySections = [];
81
+ // #1627/#1625: guard the list that is about to be printed, never the raw
82
+ // cache length — the header must not print with zero rows beneath it.
83
+ if (kept.findings.length) {
84
+ const findings = kept.findings.slice(0, 5);
85
+ let report = "🛡️ Go CVEs reachable from this code (govulncheck) — upgrade where possible:\n";
86
+ for (const f of findings) {
87
+ const callSite = f.trace.find((t) => t.filename);
88
+ const stale = kept.stale.has(f);
89
+ const where = callSite?.filename
90
+ ? `${toRunnerDisplayPath(ctx.cwd, callSite.filename)}${!stale && callSite.line ? `:${callSite.line}` : ""}${stale ? ` ${STALE_LINE_MARKER}` : ""}`
91
+ : (f.module ?? f.packageName ?? "(module)");
92
+ const fix = f.fixedVersion
93
+ ? ` — upgrade to ${f.fixedVersion} or later`
94
+ : " — no fix yet, track upstream";
95
+ report += ` ${f.osv} (${where})${fix}\n`;
96
+ }
97
+ if (kept.findings.length > findings.length) {
98
+ report += ` … and ${kept.findings.length - findings.length} more\n`;
99
+ }
100
+ advisorySections.push(report);
101
+ }
102
+ return {
103
+ advisoryParts: advisorySections,
104
+ dispositionSuppressed: { govulncheck: kept.suppressed },
105
+ };
106
+ }
107
+ /** The lane. Stateless — one object, no per-turn state to reset. */
108
+ export const govulncheckLane = {
109
+ collect,
110
+ gate,
111
+ render,
112
+ };
@@ -0,0 +1,204 @@
1
+ /**
2
+ * The turn-end SECRETS lane (#1892): the gitleaks and trivy-secrets stores,
3
+ * their live 🔴 blocker tier and their demoted 🔑 tier.
4
+ *
5
+ * One lane, two stores, on purpose. The two secret scanners do not have a
6
+ * section each: #131 Mode 3 collapses them (and ast-grep's hardcoded-secret
7
+ * rules) BY LOCATION into one report with combined provenance, because a
8
+ * credential flagged by two tools needs rotating once, not twice — and the
9
+ * demoted tier merges the same two stores for the same reason. So "gitleaks"
10
+ * is not a separable delivery lane: splitting it out would leave every
11
+ * rendering rule (the dedupe, the ast-grep enrichment, the cap, both
12
+ * preambles) in the composer, which is the opposite of the extraction. The
13
+ * lane is the delivery contract; the stores are its sources.
14
+ *
15
+ * Registered surfaces (`clients/finding-delivery-gate.ts`):
16
+ * `runtime-turn:secrets-gitleaks`, `runtime-turn:secrets-trivy`,
17
+ * `runtime-turn:stale-secrets-tier`.
18
+ *
19
+ * History this file must not lose (each line below is a shipped defect):
20
+ *
21
+ * - #1617: these findings never passed through `applyDispositions`, so an
22
+ * agent-marked false-positive re-reported as a 🔴 STOP blocker every turn.
23
+ * - #1625 review: the disposition filter runs AFTER the freshness gate, over
24
+ * BOTH arms — an fp-marked finding that later goes stale must not reappear
25
+ * in the demoted tier.
26
+ * - #1622 review M1: a demoted secret keeps its rule id and source; only the
27
+ * line is withheld. An agent triages `aws-access-token` differently from a
28
+ * low-confidence `generic-api-key`, and cannot do that from a bare path.
29
+ * - #1622 review M2: the demoted tier is its OWN tier, never `advisoryParts`
30
+ * ("no action required this turn" directly above copy telling the agent to
31
+ * re-scan).
32
+ */
33
+ import { recordDegradationOnce } from "../../degradation-ledger.js";
34
+ import { bounded } from "../../deadline-utils.js";
35
+ import { filterFindingsByDisposition } from "../../dispatch/finding-policy.js";
36
+ import { toRunnerDisplayPath } from "../../dispatch/runner-context.js";
37
+ import { classifyAndFilterFindings, } from "../../gitleaks-client.js";
38
+ import { HOOK_WALL_BUDGET_MS } from "../../hook-budgets.js";
39
+ import { gitleaksFindingToProjectDiagnostic } from "../../project-diagnostics/runner-adapters/gitleaks.js";
40
+ import { trivySecretFindingToProjectDiagnostic } from "../../project-diagnostics/runner-adapters/trivy.js";
41
+ import { dedupeSecretFindings, fromAstGrepWarnings, fromGitleaks, fromTrivySecrets, isSecretWarning, secretLocationKey, } from "../../secret-findings.js";
42
+ import { STALE_LINE_MARKER } from "../../stale-marker.js";
43
+ /**
44
+ * Gitleaks deliberately scans gitignored local files and nested repositories
45
+ * so an explicit security audit can still inspect them. The adapter is the
46
+ * source of truth for whether a finding belongs in a blocking delivery lane;
47
+ * filter here before freshness handling so demoted findings cannot leak into
48
+ * either the blocker or the stale-secret turn context.
49
+ */
50
+ async function blockingGitleaksFindings(data, ctx) {
51
+ const classification = await bounded(classifyAndFilterFindings(data?.findings ?? [], ctx.cwd), {
52
+ ms: HOOK_WALL_BUDGET_MS.turn_end,
53
+ signal: ctx.signal,
54
+ hook: "turn_end",
55
+ label: "classifyAndFilterFindings",
56
+ });
57
+ if (classification === undefined) {
58
+ recordDegradationOnce({
59
+ kind: "gitleaks_classification_timeout",
60
+ subject: ctx.cwd,
61
+ reason: "gitleaks classification exceeded the turn_end budget; retained raw findings to fail open",
62
+ });
63
+ }
64
+ return (classification ?? data?.findings ?? []).filter((finding) => gitleaksFindingToProjectDiagnostic(ctx.cwd, finding).semantic ===
65
+ "blocking");
66
+ }
67
+ /**
68
+ * #1460/#1622: the gitleaks cache is TTL-only, so a finding for a file deleted
69
+ * after the scan was served as a 🔴 blocker for the rest of the 30-minute
70
+ * window — 119 of 126 findings in pi-lens's own cache. This lane is the single
71
+ * agent-facing consumer of that store (session_start's read only decides
72
+ * whether to re-scan; the project-diagnostics path re-scans fresh and
73
+ * reconciles at load), so the drop belongs here, before the findings enter the
74
+ * shared secret pipeline. A cited file EDITED after the scan keeps its finding
75
+ * but loses its line number: the credential may still be there, just not where
76
+ * the snapshot says. Dropping instead would let any edit — malicious or
77
+ * accidental — mute a real secret. Both stores take the default
78
+ * `onMissing: "drop"`: the finding IS the deleted file's content.
79
+ */
80
+ async function collect(ctx) {
81
+ // One await for both stores: the two reads are independent, and the memo
82
+ // hands each of them the same promise the composer's own trivy read shares.
83
+ const [gitleaksEntry, trivyEntry] = await Promise.all([
84
+ ctx.readScannerCache("gitleaks"),
85
+ ctx.readScannerCache("trivy"),
86
+ ]);
87
+ const gitleaksData = gitleaksEntry?.data;
88
+ const trivyData = trivyEntry?.data;
89
+ return {
90
+ gitleaks: {
91
+ findings: await blockingGitleaksFindings(gitleaksData, ctx),
92
+ citedPath: (finding) => finding.file,
93
+ scannedAt: gitleaksData?.scannedAt,
94
+ },
95
+ "trivy-secrets": {
96
+ findings: trivyData?.secrets ?? [],
97
+ citedPath: (finding) => finding.file,
98
+ scannedAt: trivyData?.scannedAt,
99
+ },
100
+ };
101
+ }
102
+ /**
103
+ * #1617/#1625/#1628: filter through the SAME `ProjectDiagnostic` identity
104
+ * `lens_diagnostics mode=full` surfaces (tool="gitleaks",
105
+ * rule="gitleaks:<ruleId>" / tool="trivy", rule="trivy-secret:<ruleId>"), so a
106
+ * mark made against what the agent was SHOWN is honored here too — and over
107
+ * both arms, never live-only.
108
+ *
109
+ * ast-grep secret findings need no filtering here: they already went through
110
+ * dispatch's `applyDispositions` before reaching `peekActionableWarnings()`.
111
+ */
112
+ function gate(gates, ctx) {
113
+ const gitleaksLive = filterFindingsByDisposition(gates.gitleaks.live, ctx.cwd, (f) => gitleaksFindingToProjectDiagnostic(ctx.cwd, f));
114
+ const gitleaksStale = filterFindingsByDisposition(gates.gitleaks.stale, ctx.cwd, (f) => gitleaksFindingToProjectDiagnostic(ctx.cwd, f));
115
+ const trivyLive = filterFindingsByDisposition(gates["trivy-secrets"].live, ctx.cwd, (f) => trivySecretFindingToProjectDiagnostic(ctx.cwd, f));
116
+ const trivyStale = filterFindingsByDisposition(gates["trivy-secrets"].stale, ctx.cwd, (f) => trivySecretFindingToProjectDiagnostic(ctx.cwd, f));
117
+ return {
118
+ gitleaksLive: gitleaksLive.kept,
119
+ gitleaksStale: gitleaksStale.kept,
120
+ trivyLive: trivyLive.kept,
121
+ trivyStale: trivyStale.kept,
122
+ suppressed: {
123
+ gitleaks: gitleaksLive.suppressed + gitleaksStale.suppressed,
124
+ "trivy-secrets": trivyLive.suppressed + trivyStale.suppressed,
125
+ },
126
+ };
127
+ }
128
+ function render(kept, ctx) {
129
+ const sessionSecrets = dedupeSecretFindings([
130
+ ...fromGitleaks(kept.gitleaksLive),
131
+ ...fromTrivySecrets(kept.trivyLive),
132
+ ]);
133
+ // Locations already surfaced as session-scan secret blockers — used to
134
+ // enrich provenance where ast-grep agrees, and handed back so the
135
+ // actionable-warnings advisory can suppress its duplicate ast-grep copy.
136
+ const deliveredLocationKeys = new Set(sessionSecrets.map((f) => secretLocationKey(f.file, f.line)));
137
+ // Lane-local accumulators, deliberately NOT named after the turn's tiers:
138
+ // `blockerParts`/`staleSecretParts` are the composer's arrays, and a lane
139
+ // that pushed into one would be a render seam the `@delivery-surface:` tag
140
+ // scan cannot see (`tests/config/turn-end-lane-boundaries.test.ts`).
141
+ const blockerSections = [];
142
+ if (sessionSecrets.length) {
143
+ // Fold in ast-grep provenance ONLY where it coincides with a session
144
+ // secret — don't promote ast-grep-only findings out of their advisory
145
+ // tier.
146
+ const enriched = dedupeSecretFindings([
147
+ ...sessionSecrets,
148
+ ...fromAstGrepWarnings(ctx.peekActionableWarnings().filter(isSecretWarning)).filter((a) => deliveredLocationKeys.has(secretLocationKey(a.file, a.line))),
149
+ ]);
150
+ const shown = enriched.slice(0, 5);
151
+ let report = "🔴 STOP — hardcoded secrets detected. Rotate the credentials and remove them from source:\n";
152
+ for (const f of shown) {
153
+ const where = `${toRunnerDisplayPath(ctx.cwd, f.file)}:${f.line}`;
154
+ report += ` ${where} — ${f.rule} [${f.sources.join(" + ")}]${f.description ? `: ${f.description}` : ""}\n`;
155
+ }
156
+ if (enriched.length > shown.length) {
157
+ report += ` … and ${enriched.length - shown.length} more\n`;
158
+ }
159
+ blockerSections.push(report);
160
+ }
161
+ // Demoted secrets are addressed by FILE, never by line — the line is the one
162
+ // field the edit invalidated. Deduped on file+rule+source so a file with
163
+ // twenty stale hits of one rule is named once.
164
+ const staleSecretEntries = [
165
+ ...kept.gitleaksStale.map((f) => ({
166
+ file: toRunnerDisplayPath(ctx.cwd, f.file),
167
+ rule: f.ruleId,
168
+ source: "gitleaks",
169
+ })),
170
+ ...kept.trivyStale.map((f) => ({
171
+ file: toRunnerDisplayPath(ctx.cwd, f.file),
172
+ rule: f.ruleId,
173
+ source: "trivy",
174
+ })),
175
+ ];
176
+ const staleSecrets = [
177
+ ...new Map(staleSecretEntries.map((e) => [`${e.file}|${e.rule}|${e.source}`, e])).values(),
178
+ ];
179
+ const staleSections = [];
180
+ if (staleSecrets.length) {
181
+ const shown = staleSecrets.slice(0, 5);
182
+ let report = `🔑 ACTION NEEDED — secrets were flagged in files that changed after the scan. ${STALE_LINE_MARKER}\n` +
183
+ "The cached line numbers are no longer trustworthy, so they are withheld. Re-run a secrets scan to confirm or clear these:\n";
184
+ for (const entry of shown) {
185
+ report += ` ${entry.file} — ${entry.rule} [${entry.source}]\n`;
186
+ }
187
+ if (staleSecrets.length > shown.length) {
188
+ report += ` … and ${staleSecrets.length - shown.length} more\n`;
189
+ }
190
+ staleSections.push(report);
191
+ }
192
+ return {
193
+ blockerParts: blockerSections,
194
+ staleSecretParts: staleSections,
195
+ dispositionSuppressed: kept.suppressed,
196
+ deliveredLocationKeys,
197
+ };
198
+ }
199
+ /** The lane. Stateless — one object, no per-turn state to reset. */
200
+ export const secretsLane = {
201
+ collect,
202
+ gate,
203
+ render,
204
+ };