@herbertgao/pi-extensions 2026.9.2 → 2026.9.4

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 (255) hide show
  1. package/README.md +4 -2
  2. package/THIRD_PARTY_NOTICES.md +49 -0
  3. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +3 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +16 -11
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +20 -6
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +12 -7
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +98 -0
  8. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  9. package/node_modules/@herbertgao/sol-pi/LICENSE +19 -0
  10. package/node_modules/@herbertgao/sol-pi/README.md +159 -0
  11. package/node_modules/@herbertgao/sol-pi/SECURITY.md +26 -0
  12. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +19 -0
  13. package/node_modules/@herbertgao/sol-pi/agents-install.md +150 -0
  14. package/node_modules/@herbertgao/sol-pi/assets/sol-pi-hero.png +0 -0
  15. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +69 -0
  16. package/node_modules/@herbertgao/sol-pi/docs/configuration.md +75 -0
  17. package/node_modules/@herbertgao/sol-pi/package.json +76 -0
  18. package/node_modules/@herbertgao/sol-pi/scripts/check-pi-compat.mjs +32 -0
  19. package/node_modules/@herbertgao/sol-pi/scripts/check-sol-pi-config.mjs +120 -0
  20. package/node_modules/@herbertgao/sol-pi/sol-pi.example.json +10 -0
  21. package/node_modules/@herbertgao/sol-pi/src/sol-pi/config.ts +135 -0
  22. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/file-queue.ts +74 -0
  23. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +185 -0
  24. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/then-run.ts +128 -0
  25. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/archive.ts +53 -0
  26. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/candidate.ts +101 -0
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/config.ts +71 -0
  28. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/index.ts +220 -0
  29. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/journal.ts +25 -0
  30. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/provider.ts +164 -0
  31. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/receipt.ts +177 -0
  32. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/index.ts +227 -0
  33. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/ledger.ts +20 -0
  34. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/observation.ts +252 -0
  35. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +237 -0
  36. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +455 -0
  37. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/index.ts +49 -0
  38. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/plan.ts +79 -0
  39. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +208 -0
  40. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/tools.ts +100 -0
  41. package/node_modules/@herbertgao/sol-pi/src/sol-pi/index.ts +42 -0
  42. package/node_modules/@herbertgao/sol-pi/src/sol-pi/runtime-paths.ts +17 -0
  43. package/node_modules/@herbertgao/sol-pi/src/sol-pi/tui.ts +71 -0
  44. package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -2
  45. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
  46. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  47. package/node_modules/@narumitw/pi-btw/src/btw.ts +13 -2
  48. package/node_modules/pi-antigravity/LICENSE +21 -0
  49. package/node_modules/pi-antigravity/README.md +194 -0
  50. package/node_modules/pi-antigravity/package.json +67 -0
  51. package/node_modules/pi-antigravity/src/auth/index.ts +14 -0
  52. package/node_modules/pi-antigravity/src/auth/oauth.ts +442 -0
  53. package/node_modules/pi-antigravity/src/client/client.ts +561 -0
  54. package/node_modules/pi-antigravity/src/client/index.ts +1 -0
  55. package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
  56. package/node_modules/pi-antigravity/src/diagnostics/index.ts +1 -0
  57. package/node_modules/pi-antigravity/src/image/image.ts +336 -0
  58. package/node_modules/pi-antigravity/src/image/index.ts +1 -0
  59. package/node_modules/pi-antigravity/src/index.ts +280 -0
  60. package/node_modules/pi-antigravity/src/models/discovery.ts +154 -0
  61. package/node_modules/pi-antigravity/src/models/grouping.ts +424 -0
  62. package/node_modules/pi-antigravity/src/models/index.ts +3 -0
  63. package/node_modules/pi-antigravity/src/models/models.ts +500 -0
  64. package/node_modules/pi-antigravity/src/stream/index.ts +1 -0
  65. package/node_modules/pi-antigravity/src/stream/stream.ts +1460 -0
  66. package/node_modules/pi-antigravity/src/types/enums.ts +42 -0
  67. package/node_modules/pi-antigravity/src/types/index.ts +2 -0
  68. package/node_modules/pi-antigravity/src/types/types.ts +292 -0
  69. package/node_modules/pi-antigravity/src/usage/index.ts +1 -0
  70. package/node_modules/pi-antigravity/src/usage/usage.ts +371 -0
  71. package/node_modules/pi-antigravity/src/utils/http.ts +91 -0
  72. package/node_modules/pi-antigravity/src/utils/index.ts +3 -0
  73. package/node_modules/pi-antigravity/src/utils/security.ts +73 -0
  74. package/node_modules/pi-antigravity/src/utils/util.ts +132 -0
  75. package/node_modules/pi-antigravity/tsconfig.json +21 -0
  76. package/node_modules/pi-lens/CHANGELOG.md +176 -0
  77. package/node_modules/pi-lens/README.md +13 -8
  78. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -1
  79. package/node_modules/pi-lens/dist/clients/analysed-root.js +1 -0
  80. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +1 -1
  81. package/node_modules/pi-lens/dist/clients/biome-client.js +13 -2
  82. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +2 -0
  83. package/node_modules/pi-lens/dist/clients/cache-observability.js +122 -1
  84. package/node_modules/pi-lens/dist/clients/complexity-client.js +5 -0
  85. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
  86. package/node_modules/pi-lens/dist/clients/config-resolve.js +6 -3
  87. package/node_modules/pi-lens/dist/clients/config-schema.js +17 -0
  88. package/node_modules/pi-lens/dist/clients/config-warn.js +2 -2
  89. package/node_modules/pi-lens/dist/clients/dead-code-client.js +8 -1
  90. package/node_modules/pi-lens/dist/clients/dependency-checker.js +5 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +38 -15
  92. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +1 -1
  93. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +5 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +2 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +2 -3
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +4 -3
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +2 -1
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +2 -1
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +2 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +2 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +2 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +2 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +2 -1
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +2 -1
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +2 -1
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +2 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +2 -1
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +3 -1
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +2 -1
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +2 -1
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +2 -1
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +2 -1
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +19 -1
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +3 -2
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +2 -1
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +2 -1
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +2 -1
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +2 -1
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +12 -12
  122. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +2 -1
  123. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +2 -1
  124. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +2 -2
  125. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +12 -11
  126. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -1
  127. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -1
  128. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +2 -2
  129. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  130. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +3 -2
  131. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +3 -2
  132. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +2 -1
  133. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  134. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +2 -1
  135. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +2 -1
  136. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +2 -1
  137. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
  138. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
  139. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
  140. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +2 -1
  141. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +4 -3
  142. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +2 -1
  143. package/node_modules/pi-lens/dist/clients/effective-config.js +12 -2
  144. package/node_modules/pi-lens/dist/clients/extension-log.js +2 -0
  145. package/node_modules/pi-lens/dist/clients/file-role.js +18 -1
  146. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +4 -0
  147. package/node_modules/pi-lens/dist/clients/formatters.js +54 -188
  148. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -0
  149. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +42 -6
  150. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +48 -7
  151. package/node_modules/pi-lens/dist/clients/installer/index.js +196 -52
  152. package/node_modules/pi-lens/dist/clients/jscpd-client.js +9 -1
  153. package/node_modules/pi-lens/dist/clients/knip-client.js +9 -2
  154. package/node_modules/pi-lens/dist/clients/language-profile.js +12 -2
  155. package/node_modules/pi-lens/dist/clients/latency-logger.js +2 -0
  156. package/node_modules/pi-lens/dist/clients/lens-config.js +4 -0
  157. package/node_modules/pi-lens/dist/clients/lsp/config.js +36 -7
  158. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +24 -1
  159. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +78 -0
  160. package/node_modules/pi-lens/dist/clients/lsp/index.js +281 -45
  161. package/node_modules/pi-lens/dist/clients/lsp/server.js +62 -15
  162. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +37 -4
  163. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +3 -0
  164. package/node_modules/pi-lens/dist/clients/mcp/session.js +2 -0
  165. package/node_modules/pi-lens/dist/clients/opengrep-client.js +2 -0
  166. package/node_modules/pi-lens/dist/clients/package-manager.js +2 -1
  167. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +8 -3
  168. package/node_modules/pi-lens/dist/clients/pipeline.js +3 -2
  169. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +38 -10
  170. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
  171. package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
  172. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +2 -0
  173. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +2 -0
  174. package/node_modules/pi-lens/dist/clients/ruff-client.js +18 -9
  175. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +9 -0
  176. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +5 -0
  177. package/node_modules/pi-lens/dist/clients/runtime-session.js +13 -4
  178. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +4 -1
  179. package/node_modules/pi-lens/dist/clients/runtime-turn.js +48 -1
  180. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -2
  181. package/node_modules/pi-lens/dist/clients/session-event-guard.js +69 -1
  182. package/node_modules/pi-lens/dist/clients/sg-runner.js +2 -1
  183. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +108 -0
  184. package/node_modules/pi-lens/dist/clients/test-runner-client.js +291 -76
  185. package/node_modules/pi-lens/dist/clients/tool-config.js +199 -0
  186. package/node_modules/pi-lens/dist/clients/tool-cwd.js +299 -0
  187. package/node_modules/pi-lens/dist/clients/tool-probe.js +53 -0
  188. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +42 -0
  189. package/node_modules/pi-lens/dist/clients/trivy-client.js +9 -1
  190. package/node_modules/pi-lens/dist/clients/turn-context.js +52 -0
  191. package/node_modules/pi-lens/dist/clients/widget-state.js +4 -3
  192. package/node_modules/pi-lens/dist/index.js +21969 -20005
  193. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -2
  194. package/node_modules/pi-lens/dist/mcp/server.js +135 -159
  195. package/node_modules/pi-lens/dist/tools/activate-tools.js +10 -7
  196. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +2 -13
  197. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +8 -13
  198. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +83 -39
  199. package/node_modules/pi-lens/dist/tools/effective-config.js +4 -3
  200. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +1 -10
  201. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +353 -85
  202. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +21 -110
  203. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +20 -44
  204. package/node_modules/pi-lens/dist/tools/module-report.js +5 -9
  205. package/node_modules/pi-lens/dist/tools/project-report.js +9 -17
  206. package/node_modules/pi-lens/dist/tools/render-compact.js +280 -1
  207. package/node_modules/pi-lens/dist/tools/symbol-search.js +2 -2
  208. package/node_modules/pi-lens/docs/agent-guide.md +3 -4
  209. package/node_modules/pi-lens/docs/agent-tools.md +46 -22
  210. package/node_modules/pi-lens/docs/configuration.md +9 -0
  211. package/node_modules/pi-lens/docs/environment-variables.md +0 -5
  212. package/node_modules/pi-lens/docs/features.md +5 -5
  213. package/node_modules/pi-lens/docs/globalconfig.md +11 -1
  214. package/node_modules/pi-lens/docs/mcp.md +13 -1
  215. package/node_modules/pi-lens/docs/pi-lens-fixer.md +71 -0
  216. package/node_modules/pi-lens/docs/pi-lens-investigator.md +15 -0
  217. package/node_modules/pi-lens/docs/pi-lens-monitor.md +88 -0
  218. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +35 -0
  219. package/node_modules/pi-lens/docs/pi-lens-subagent.md +12 -4
  220. package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
  221. package/node_modules/pi-lens/docs/real-harness.md +46 -0
  222. package/node_modules/pi-lens/docs/release-qa-baseline.md +5 -1
  223. package/node_modules/pi-lens/docs/servercapabilities.md +1 -6
  224. package/node_modules/pi-lens/docs/settings.md +37 -2
  225. package/node_modules/pi-lens/docs/tools_improvement2.md +4 -4
  226. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  227. package/node_modules/pi-lens/docs/usage.md +13 -3
  228. package/node_modules/pi-lens/package.json +10 -1
  229. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +7 -3
  230. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +8 -6
  231. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +19 -9
  232. package/node_modules/pi-web-access/CHANGELOG.md +29 -0
  233. package/node_modules/pi-web-access/README.md +33 -22
  234. package/node_modules/pi-web-access/abortable.ts +17 -0
  235. package/node_modules/pi-web-access/crawl4ai.ts +204 -0
  236. package/node_modules/pi-web-access/credential-source.ts +1 -0
  237. package/node_modules/pi-web-access/curator-page.ts +20 -2
  238. package/node_modules/pi-web-access/curator-run.ts +44 -0
  239. package/node_modules/pi-web-access/curator-server.ts +3 -1
  240. package/node_modules/pi-web-access/duckduckgo.ts +1 -1
  241. package/node_modules/pi-web-access/extract.ts +57 -12
  242. package/node_modules/pi-web-access/fetch-params.ts +1 -1
  243. package/node_modules/pi-web-access/gemini-search.ts +9 -5
  244. package/node_modules/pi-web-access/index.ts +25 -18
  245. package/node_modules/pi-web-access/openai-search.ts +35 -13
  246. package/node_modules/pi-web-access/package.json +2 -2
  247. package/node_modules/pi-web-access/page-query.ts +5 -2
  248. package/node_modules/pi-web-access/query-rewrite.ts +5 -2
  249. package/node_modules/pi-web-access/serpapi.ts +220 -0
  250. package/node_modules/pi-web-access/ssrf-protection.ts +5 -1
  251. package/node_modules/pi-web-access/storage.ts +10 -0
  252. package/node_modules/pi-web-access/summary-review.ts +34 -21
  253. package/node_modules/pi-web-access/utils.ts +6 -4
  254. package/package.json +11 -5
  255. package/node_modules/pi-lens/dist/tools/ast-dump.js +0 -103
@@ -18,6 +18,7 @@ import { DEPENDENCY_DRIFT_MAX_DELIVERIES } from "../clients/blocker-freshness.js
18
18
  import { freshnessFromMtime } from "../clients/freshness.js";
19
19
  import { applyInlineSuppressions } from "../clients/dispatch/inline-suppressions.js";
20
20
  import { gateFindingsByPathFreshness } from "../clients/advisory-provenance.js";
21
+ import { markUnreconciledFindings } from "../clients/finding-delivery-gate.js";
21
22
  import { normalizeRuleId } from "../clients/dispatch/rule-id-normalize.js";
22
23
  import { applyRulePolicy, rulePolicyMapFromConfig, } from "../clients/dispatch/rule-policy.js";
23
24
  import { loadPiLensProjectConfig } from "../clients/project-lens-config.js";
@@ -26,7 +27,8 @@ import { combineAbortSignals } from "../clients/deadline-utils.js";
26
27
  import { getProjectIgnoreMatcher } from "../clients/file-utils.js";
27
28
  import { isAtOrAboveHomeDir, normalizeEphemeralMapKey, normalizeFilePath, } from "../clients/path-utils.js";
28
29
  import { getLSPService } from "../clients/lsp/index.js";
29
- import { primaryServerId } from "../clients/lsp/config.js";
30
+ import { retireInlineBlockerAndResyncGuard } from "../clients/git-guard.js";
31
+ import { primaryServerId, resolveLspCwdForFile, } from "../clients/lsp/config.js";
30
32
  import { getFullScanWallClockMs } from "../clients/lsp/workspace-sweep-hold.js";
31
33
  import { demoteInferredProjectSweepResults } from "../clients/lsp/inferred-project.js";
32
34
  import { hashDiagnosticContent, } from "../clients/lsp/diagnostic-binding.js";
@@ -38,11 +40,14 @@ import { generatedSkipNotice, scanTruncationNotice, } from "../clients/lens-engi
38
40
  import { scanProjectDiagnostics } from "../clients/project-diagnostics/scanner.js";
39
41
  import { classifyDiagnosticTier, getFileDiagnosticSummaries, reconcileCorrelatedScanDiagnostics, reconcileScanDiagnostics, reconcileStaleWidgetDependencyBlockers, reconcileStaleWidgetFiles, widgetDiagnosticUri, } from "../clients/widget-state.js";
40
42
  import { logLatency } from "../clients/latency-logger.js";
43
+ import { logExtension } from "../clients/extension-log.js";
44
+ import { recordDegradationOnce } from "../clients/degradation-ledger.js";
41
45
  import { convertLspDiagnostics } from "../clients/dispatch/utils/lsp-diagnostics.js";
42
46
  import { retagAuxiliaryDiagnostics } from "../clients/dispatch/auxiliary-lsp.js";
43
47
  import { detectFileRole } from "../clients/file-role.js";
44
48
  import { STALE_LINE_MARKER } from "../clients/stale-marker.js";
45
49
  import { makeProgressReporter, scanningSummaryLine } from "./scan-progress.js";
50
+ import { createLspDiagnosticsTool, LSP_SEVERITY_FILTERS, MAX_BATCH_FILES, } from "./lsp-diagnostics.js";
46
51
  import { demotePastEofDiagnostics, PAST_EOF_STALE_MARKER, resyncDocumentOnPastEof, } from "../clients/diagnostic-line-freshness.js";
47
52
  // The widget state exposes the full per-file diagnostic set; this is the tool's
48
53
  // own generous display budget per file (independent of the TUI's 12 cap), to
@@ -53,7 +58,7 @@ const MAX_DIAGNOSTICS_PER_FILE = 50;
53
58
  // narrow, not paginate. Erroring (rather than silently truncating) means a
54
59
  // caller can never believe it checked files it didn't (issue's stated
55
60
  // invariant).
56
- const MAX_PATHS_ENTRIES = 200;
61
+ const MAX_PATHS_ENTRIES = MAX_BATCH_FILES;
57
62
  // #1623: the reason rendered for every heavyweight-analyzer lane (gitleaks,
58
63
  // trivy, govulncheck, dead-code, knip, jscpd, madge, opengrep, test-runner)
59
64
  // when mode=full is called without refreshRunners=cheap/all/cached — the
@@ -135,35 +140,75 @@ nextWriteIndex,
135
140
  // #798/#338: capture host UI methods synchronously from the active tool event.
136
141
  // The returned closure is safe for the later async sweep to invoke because it
137
142
  // never dereferences the session-guarded ctx.ui getter.
138
- captureLspStatusRepaint, getRuntime) {
143
+ captureLspStatusRepaint, getRuntime, isLensGuardEnabled = () => true) {
144
+ const lspProbe = createLspDiagnosticsTool(nextWriteIndex, (info) => {
145
+ const runtime = getRuntime?.();
146
+ if (!runtime)
147
+ return;
148
+ const retired = retireInlineBlockerAndResyncGuard({
149
+ runtime,
150
+ cacheManager,
151
+ cwd: info.cwd,
152
+ filePath: info.filePath,
153
+ ...(info.writeIndex === undefined
154
+ ? {}
155
+ : { writeIndex: info.writeIndex }),
156
+ coveredSources: info.coveredSources,
157
+ lensGuardEnabled: isLensGuardEnabled(),
158
+ });
159
+ if (retired) {
160
+ logExtension({
161
+ subsystem: "git-guard",
162
+ level: "debug",
163
+ message: `inline_blocker: retired for ${info.filePath}`,
164
+ });
165
+ }
166
+ }, getLspService);
139
167
  return {
140
168
  name: "lens_diagnostics",
141
169
  label: "Project Diagnostics",
142
- description: "Query pi-lens's diagnostic state. mode=delta/all are cache-only and instant; " +
143
- "mode=full is an expensive active project-wide LSP scan merged with cached runner state.\n\n" +
144
- "IMPORTANT: unlike lsp_diagnostics (LSP only), this tool covers ALL dispatch " +
145
- "runners: LSP errors, tree-sitter structural rules, ast-grep security rules, " +
146
- "biome/ruff/eslint lint findings, complexity violations, and more.\n\n" +
147
- "mode=delta (default): all warnings for the current agent turn — fixable warnings " +
148
- "(actionable-warnings cache) AND code quality/style/complexity issues " +
149
- "(code-quality-warnings cache). Same scope as the turn-end advisory, current turn only.\n\n" +
150
- "mode=all: blocking errors and warnings — with the actual messages (line, rule, " +
151
- "text), not just counts — for every file the agent has " +
152
- "EDITED this session (files that went through the dispatch pipeline). " +
153
- "NOTE: unedited files with pre-existing errors do NOT appear here — this is " +
154
- "not a full project scan. Use before declaring work done; stale blocking " +
155
- "errors from earlier turns are visible even if they dropped from turn-end context.\n\n" +
156
- "mode=full: EXPENSIVE active scan. Runs project-wide LSP diagnostics for " +
157
- "all supported files (including unedited files), then merges/deduplicates " +
158
- "that with mode=all cached runner state. Optional refreshRunners=cheap/all/cached " +
159
- "folds in project-wide runner findings: the in-process scanners (tree-sitter + " +
160
- "fact-rules + ast-grep) plus a FRESH run of the heavyweight analyzers — knip, " +
161
- "jscpd (copy-paste), madge (circular deps), gitleaks (secrets), govulncheck/trivy " +
162
- "(CVEs), dead-code — rather than a possibly-stale session_start cache; each " +
163
- "analyzer de-dupes against a concurrent background run of itself, so this can't " +
164
- "double-spawn. Bounded by the slowest analyzer (trivy's own ~180s ceiling).",
165
- promptSnippet: "Use lens_diagnostics mode=all to verify no blocking errors remain; use mode=full for expensive project-wide checks",
170
+ description: 'Query pi-lens diagnostics from the session cache or an active LSP probe, at paths or workspace scope. Empty cache is not proof of clean; probe changed paths when findings are absent or stale. Example: `{source: "lsp", scope: "paths", paths: ["src/app.ts"]}`.',
171
+ promptSnippet: "lens_diagnostics source=session reads the session cache and an empty cache is not proof of a clean file; use source=lsp scope=paths for changed files when cached findings are absent or stale",
166
172
  renderResult: compactRenderResult(({ details, args, isError, text }) => {
173
+ if (details?.source === "lsp") {
174
+ const count = details.totalDiagnostics ?? 0;
175
+ const files = details.filesChecked ?? details.filesScanned ?? 0;
176
+ const noun = count === 1 ? "diagnostic" : "diagnostics";
177
+ const filePath = typeof details?.filePath === "string"
178
+ ? details.filePath
179
+ : Array.isArray(args?.paths) &&
180
+ args.paths.length === 1 &&
181
+ typeof args.paths[0] === "string"
182
+ ? args.paths[0]
183
+ : undefined;
184
+ const singleFile = (details?.mode === "file" || files === 1) &&
185
+ typeof filePath === "string"
186
+ ? ` ${path.basename(filePath)}`
187
+ : "";
188
+ const scope = files > 1 ? ` across ${files} files` : singleFile;
189
+ if (isError)
190
+ return `lens_diagnostics lsp — ${text.split("\n")[0] ?? "error"}`;
191
+ if ((details.navigationOnlyFiles ?? 0) > 0)
192
+ return `lens_diagnostics${scope} — ${count} ${noun} · ${details.cleanFiles ?? 0} clean · ${details.navigationOnlyFiles} navigation-only`;
193
+ if ((details.unconfirmedFiles ?? 0) > 0)
194
+ return `lens_diagnostics${scope} — ${count} ${noun} · ${details.cleanFiles ?? 0} clean · ${details.unconfirmedFiles} unconfirmed${details.timedOutFiles ? ` (${details.timedOutFiles} timed out)` : ""}`;
195
+ const outcomeCounts = details.outcomeCounts;
196
+ const notConfirmed = outcomeCounts
197
+ ? (outcomeCounts.inconclusive ?? 0) +
198
+ (outcomeCounts.unavailable ?? 0) +
199
+ (outcomeCounts.unsupported ?? 0) +
200
+ (outcomeCounts.failed ?? 0)
201
+ : 0;
202
+ if (notConfirmed > 0)
203
+ return `lens_diagnostics${scope} — ${count} ${noun} · ${notConfirmed} checks not confirmed`;
204
+ if ((details.incompleteFiles ?? 0) > 0)
205
+ return `lens_diagnostics${scope} — incomplete (${details.incompleteFiles} files not confirmed)`;
206
+ if (count === 0 && details.unconfirmed)
207
+ return details.timedOut
208
+ ? `lens_diagnostics${scope} — timed out (result may be incomplete)`
209
+ : `lens_diagnostics${scope} — unconfirmed (server cannot confirm clean)`;
210
+ return `lens_diagnostics${scope} — ${count} ${noun}`;
211
+ }
167
212
  // Streaming progress partials render the live bar (see scanningSummaryLine)
168
213
  // instead of the details-driven summary, which would show "0 diagnostics"
169
214
  // mid-scan.
@@ -222,54 +267,124 @@ captureLspStatusRepaint, getRuntime) {
222
267
  return `lens_diagnostics ${mode} — ${parts.join(" · ")} (${files} files)${coldSuffix}${failedSuffix}`;
223
268
  }),
224
269
  parameters: Type.Object({
270
+ source: Type.Optional(Type.String({
271
+ enum: ["session", "lsp"],
272
+ description: "Evidence source: session cache (empty cache is not proof of a " +
273
+ "clean file — use source=lsp for changed files) or an active LSP probe.",
274
+ })),
275
+ scope: Type.Optional(Type.String({
276
+ enum: ["paths", "workspace"],
277
+ description:
278
+ // #2860: no schema value maps to "current turn's delta" — the
279
+ // default (omitted scope) already gives that for
280
+ // source=session, and for source=lsp `delta` was byte-identical
281
+ // to `paths` (round-2 verify N/F4), so exposing it would only
282
+ // invite a caller to believe it does something distinct.
283
+ "Coverage scope: explicit paths, or workspace (default: current turn's delta for source=session).",
284
+ })),
225
285
  mode: Type.Optional(Type.String({
226
286
  enum: ["delta", "all", "full"],
227
- description: "delta = current turn's fixable warnings (default). " +
228
- "all = session diagnostics for edited/dispatched files. " +
229
- "full = expensive active project-wide LSP scan plus cached runner diagnostics.",
287
+ description: "delta = current turn; all = cache-only; full = active LSP scan of paths. Empty cache is not proof of clean.",
288
+ })),
289
+ path: Type.Optional(Type.String({
290
+ description: "One file or directory for source=lsp checks.",
291
+ })),
292
+ concurrency: Type.Optional(Type.Number({
293
+ description: "Source=lsp batch concurrency (maximum 16).",
294
+ })),
295
+ waitMs: Type.Optional(Type.Number({
296
+ description: "Source=lsp per-file wait budget in milliseconds.",
297
+ })),
298
+ serverScope: Type.Optional(Type.String({
299
+ enum: ["primary", "all"],
300
+ description: "Source=lsp server coverage: primary or all.",
230
301
  })),
231
302
  refreshRunners: Type.Optional(Type.Union([
232
303
  Type.Boolean(),
233
304
  Type.String({ enum: ["cached", "cheap", "all", "none"] }),
234
305
  ], {
235
- description: "mode=full only: false/none = LSP + widget state only. cached/cheap/all all now trigger a FRESH run (#585) of the heavyweight project analyzers (knip, jscpd, madge, gitleaks, govulncheck, trivy, dead-code) in parallel — bounded by the slowest one (trivy's own ~180s ceiling) — instead of reading a possibly-stale session_start cache; safe to relaunch since each analyzer de-dupes concurrent runs against the same project root. cheap/all additionally refresh the in-process runners (tree-sitter + fact-rules + ast-grep) first.",
306
+ description: "Analyzer refresh mode for full view.",
236
307
  })),
237
308
  maxProjectFiles: Type.Optional(Type.Number({
238
- description: "mode=full refreshRunners=cheap/all only: cap project files scanned by the cheap project runners (tree-sitter + fact-rules + ast-grep). Does NOT bound the LSP sweep — use maxLspFiles for that.",
309
+ description: "Project-file limit for cheap runners.",
239
310
  })),
240
311
  maxLspFiles: Type.Optional(Type.Number({
241
- description: "mode=full only: cap the number of files routed through the language server for the project-wide LSP sweep. On large projects (e.g. a Next.js app with thousands of source files) the uncapped sweep can take many minutes; set this to bound it. Default is generous (env PI_LENS_LSP_WORKSPACE_MAX_FILES, else 5000).",
312
+ description: "File limit for the LSP sweep.",
242
313
  })),
243
314
  includeGenerated: Type.Optional(Type.Boolean({
244
- description: "mode=full refreshRunners=cheap/all only (no effect with refreshRunners=cached/none, since no project scan runs to apply it to): scan WITHOUT the generated/artifact NAME-heuristic filter (lockfiles, gen.ts-style names, generated/ dirs, …). Default false. Use when a scan's 'excluded by generated-name heuristics' notice suggests a real file was skipped.",
315
+ description: "Include generated-name paths in full scans.",
245
316
  })),
246
317
  severity: Type.Optional(Type.String({
247
- enum: ["error", "warning", "all"],
248
- description: "Filter by severity (default: all).",
318
+ enum: [...LSP_SEVERITY_FILTERS],
319
+ description: "Filter by severity threshold (default: all): error shows errors; warning includes errors and warnings; information includes errors, warnings, and information; hint and all show every known tier.",
249
320
  })),
250
321
  paths: Type.Optional(Type.Array(Type.String(), {
251
322
  maxItems: MAX_PATHS_ENTRIES,
252
- description: `Restrict any mode to an explicit file/directory list (max ${MAX_PATHS_ENTRIES} entries; ` +
253
- "more errors instead of silently truncating). Entries may be relative " +
254
- "(resolved against cwd) or absolute, and a directory entry matches all " +
255
- 'files under it (e.g. "src/"). mode=delta/all are a pure post-filter ' +
256
- "of cached/session state — they can only show findings for files pi-lens " +
257
- "has already dispatched, so an unseen file shows nothing (use mode=full " +
258
- "for an active scan). mode=full actively scans exactly these paths (LSP " +
259
- "sweep + cheap in-process runners); cached heavyweight analyzers " +
260
- "(jscpd/madge/gitleaks/knip) and the project snapshot are still post-filtered " +
261
- "cache reads, never relaunched. Explicitly-listed files are NOT filtered " +
262
- "through the project ignore matcher (matching lsp_diagnostics' paths " +
263
- "semantics) — naming a file is assumed to mean it regardless of " +
264
- ".gitignore/.pi-lens.json; a directory entry's expansion still honors " +
265
- "ignore (and when the list mixes directories and files, mode=full scans " +
266
- "via the ignore-filtered walk, so an ignore-excluded file entry is only " +
267
- "guaranteed an active scan in a files-only list). Nonexistent entries " +
268
- "are skipped (mode=full notes them; useful for git-staged-file wrappers " +
269
- "where a deleted-but-staged path can appear).",
323
+ description: "Files or directories to filter.",
270
324
  })),
271
325
  }),
272
326
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
327
+ const requestedSource = params.source;
328
+ const requestedScope = params.scope;
329
+ const legacyMode = params.mode;
330
+ const source = requestedSource ?? "session";
331
+ const scope = requestedScope ??
332
+ (legacyMode === "delta" || legacyMode === undefined
333
+ ? source === "lsp"
334
+ ? "paths"
335
+ : "delta"
336
+ : "workspace");
337
+ const cwd = ctx.cwd ?? getCwd();
338
+ if (source === "lsp") {
339
+ const lspParams = { ...params };
340
+ delete lspParams.source;
341
+ delete lspParams.scope;
342
+ // #2860: explicit `path`/`paths` always win. `scope=workspace`
343
+ // only supplies a `cwd` default (a full workspace sweep) when the
344
+ // caller gave NEITHER — it must never override an explicit
345
+ // narrower request (the #2052 root-eviction hang and the
346
+ // SKILL.md "check a folder" recipe both depend on this: a
347
+ // directory `path` under scope=workspace scans that directory,
348
+ // not the whole project).
349
+ const hasExplicitPath = typeof lspParams.path === "string" && lspParams.path.length > 0;
350
+ const hasExplicitPaths = Array.isArray(lspParams.paths) && lspParams.paths.length > 0;
351
+ if (scope === "workspace" && !hasExplicitPath && !hasExplicitPaths) {
352
+ lspParams.path = cwd;
353
+ }
354
+ const lspStart = Date.now();
355
+ const result = (await lspProbe.execute(_toolCallId, lspParams, signal, onUpdate, ctx.signal ? { cwd, signal: ctx.signal } : { cwd }));
356
+ // #2860 F11: the folded route's own durable trace — without this,
357
+ // F1's class of defect (a real LSP result rendering as "clean")
358
+ // leaves no record in latency.log distinguishing source=lsp from
359
+ // source=session. One record per tool call (not per file), same
360
+ // convention as the mode=all `blocker_freshness_widget_gate`
361
+ // phase above.
362
+ logLatency({
363
+ type: "phase",
364
+ toolName: "lens_diagnostics",
365
+ filePath: cwd,
366
+ phase: "lens_diagnostics_lsp_route",
367
+ durationMs: Date.now() - lspStart,
368
+ metadata: {
369
+ scope,
370
+ isError: result.isError === true,
371
+ totalDiagnostics: result.details?.totalDiagnostics ?? 0,
372
+ cleanFiles: result.details?.cleanFiles ?? 0,
373
+ unconfirmedFiles: result.details?.unconfirmedFiles ?? 0,
374
+ timedOutFiles: result.details?.timedOutFiles ?? 0,
375
+ },
376
+ });
377
+ return {
378
+ ...result,
379
+ isError: result.isError === true,
380
+ details: { ...result.details, source, scope },
381
+ };
382
+ }
383
+ if (requestedSource !== undefined) {
384
+ const effectiveMode = legacyMode ??
385
+ (scope === "workspace" ? "all" : scope === "paths" ? "all" : "delta");
386
+ params = { ...params, mode: effectiveMode };
387
+ }
273
388
  const repaintLspStatus = captureLspStatusRepaint?.(ctx);
274
389
  const mode = params.mode ?? "delta";
275
390
  const severity = params.severity ?? "all";
@@ -280,7 +395,6 @@ captureLspStatusRepaint, getRuntime) {
280
395
  const maxProjectFiles = parsePositiveInt(params.maxProjectFiles);
281
396
  const maxLspFiles = parsePositiveInt(params.maxLspFiles);
282
397
  const includeGenerated = params.includeGenerated === true;
283
- const cwd = ctx.cwd ?? getCwd();
284
398
  let pathsScope;
285
399
  try {
286
400
  pathsScope = resolvePathsScope(cwd, params.paths);
@@ -633,8 +747,7 @@ function formatDeltaMode(cacheManager, cwd, severity, pathsScope) {
633
747
  // filtered findings). Weak-anchored like weak disposition — zero I/O.
634
748
  const policyMap = loadProjectRulePolicyMap(cwd);
635
749
  const projectDelta = filterDeltaReportDispositions(loadProjectDiagnosticsDeltaReport(cwd), cwd, policyMap);
636
- const ignoreFile = createCurrentIgnoreFilter(cwd);
637
- const includeFile = (filePath) => ignoreFile(filePath) && (!pathsScope || pathsScope.includeFile(filePath));
750
+ const includeFile = createScopedFileFilter(cwd, pathsScope);
638
751
  // #755: delta re-serves the actionable/quality caches verbatim, but those
639
752
  // were filtered at DISPATCH time — before any lens_diagnostic_mark. Re-apply
640
753
  // dispositions here so a mark converges immediately, not only on the next
@@ -652,10 +765,24 @@ function formatDeltaMode(cacheManager, cwd, severity, pathsScope) {
652
765
  .filter((file) => file.warnings.length > 0);
653
766
  const actionableFiles = applyDeltaFreshnessGate(visibleWarningFiles(actionable?.files), cwd, actionable?.generatedAt);
654
767
  const qualityFiles = applyDeltaFreshnessGate(visibleWarningFiles(quality?.files), cwd, quality?.generatedAt);
768
+ const matchingWarnings = (warnings) => warnings.filter((warning) => matchesRecordSeverity(warning.severity, severity));
769
+ const filteredActionableFiles = actionableFiles
770
+ .map((file) => ({
771
+ ...file,
772
+ warnings: matchingWarnings(file.warnings),
773
+ }))
774
+ .filter((file) => file.warnings.length > 0);
775
+ const filteredQualityFiles = qualityFiles
776
+ .map((file) => ({
777
+ ...file,
778
+ warnings: matchingWarnings(file.warnings),
779
+ }))
780
+ .filter((file) => file.warnings.length > 0);
655
781
  const lines = [];
656
- // Fixable warnings from actionable-warnings
657
- if (severity !== "error") {
658
- for (const file of actionableFiles) {
782
+ // Fixable warnings from actionable-warnings and quality cache entries retain
783
+ // their own severity tier. Apply the same threshold semantics as the LSP path.
784
+ if (filteredActionableFiles.length > 0) {
785
+ for (const file of filteredActionableFiles) {
659
786
  const rel = path.relative(cwd, file.filePath);
660
787
  lines.push(`${rel}`);
661
788
  for (const w of file.warnings) {
@@ -665,8 +792,8 @@ function formatDeltaMode(cacheManager, cwd, severity, pathsScope) {
665
792
  }
666
793
  }
667
794
  // Quality issues
668
- if (severity !== "error") {
669
- for (const file of qualityFiles) {
795
+ if (filteredQualityFiles.length > 0) {
796
+ for (const file of filteredQualityFiles) {
670
797
  const rel = path.relative(cwd, file.filePath);
671
798
  if (!lines.includes(rel))
672
799
  lines.push(rel);
@@ -677,8 +804,10 @@ function formatDeltaMode(cacheManager, cwd, severity, pathsScope) {
677
804
  }
678
805
  }
679
806
  const projectDeltaCount = appendProjectDiagnosticsDeltaLines(lines, cwd, projectDelta, severity, includeFile);
680
- const aw = actionableFiles.reduce((count, file) => count + file.warnings.length, 0);
681
- const cq = qualityFiles.reduce((count, file) => count + file.warnings.length, 0);
807
+ const selectedActionableFiles = filteredActionableFiles;
808
+ const selectedQualityFiles = filteredQualityFiles;
809
+ const aw = selectedActionableFiles.reduce((count, file) => count + file.warnings.length, 0);
810
+ const cq = selectedQualityFiles.reduce((count, file) => count + file.warnings.length, 0);
682
811
  if (lines.length === 0) {
683
812
  let text = `No ${severity === "all" ? "" : severity + " "}issues in the current turn delta.`;
684
813
  // Discoverability (#190): `delta` is current-turn-scoped, so it's empty
@@ -772,6 +901,7 @@ function resolvePathsScope(cwd, rawPaths) {
772
901
  }
773
902
  const fileKeys = new Set(fileEntries.map((f) => normalizeFilePath(f)));
774
903
  const dirPrefixes = dirEntries.map((d) => normalizeFilePath(d));
904
+ const includeExplicitFile = (filePath) => fileKeys.has(normalizeFilePath(path.resolve(filePath)));
775
905
  const includeFile = (filePath) => {
776
906
  const key = normalizeFilePath(path.resolve(filePath));
777
907
  if (fileKeys.has(key))
@@ -780,6 +910,7 @@ function resolvePathsScope(cwd, rawPaths) {
780
910
  };
781
911
  return {
782
912
  includeFile,
913
+ includeExplicitFile,
783
914
  missing,
784
915
  existingEntries,
785
916
  hasDirectoryEntries: dirEntries.length > 0,
@@ -803,6 +934,16 @@ function pathsScopeMissingNote(scope) {
803
934
  : "";
804
935
  return `\n\n⚠ Skipped ${scope.missing.length} path(s) not found on disk: ${shown.join(", ")}${more}`;
805
936
  }
937
+ function createScopedFileFilter(cwd, pathsScope) {
938
+ const ignoreFile = createCurrentIgnoreFilter(cwd);
939
+ return (filePath) => {
940
+ if (!pathsScope)
941
+ return ignoreFile(filePath);
942
+ if (!pathsScope.includeFile(filePath))
943
+ return false;
944
+ return pathsScope.includeExplicitFile(filePath) || ignoreFile(filePath);
945
+ };
946
+ }
806
947
  function filterProjectDiagnosticsSnapshot(snapshot, includeFile) {
807
948
  if (!snapshot)
808
949
  return undefined;
@@ -845,10 +986,28 @@ function isErrorLike(d) {
845
986
  return d.semantic === "blocking" || d.severity === "error";
846
987
  }
847
988
  function matchesSeverity(d, severity) {
848
- if (severity === "error")
849
- return isErrorLike(d);
850
- if (severity === "warning")
851
- return !isErrorLike(d);
989
+ return matchesRecordSeverity(isErrorLike(d) ? "error" : d.severity, severity);
990
+ }
991
+ function matchesRecordSeverity(recordSeverity, requested) {
992
+ if (requested === "all")
993
+ return true;
994
+ if (requested === "error")
995
+ return recordSeverity === "error";
996
+ if (requested === "warning")
997
+ return recordSeverity === "error" || recordSeverity === "warning";
998
+ if (requested === "information")
999
+ return (recordSeverity === "error" ||
1000
+ recordSeverity === "warning" ||
1001
+ recordSeverity === "info" ||
1002
+ recordSeverity === "note" ||
1003
+ recordSeverity === "help");
1004
+ if (requested === "hint")
1005
+ return (recordSeverity === "error" ||
1006
+ recordSeverity === "warning" ||
1007
+ recordSeverity === "info" ||
1008
+ recordSeverity === "note" ||
1009
+ recordSeverity === "help" ||
1010
+ recordSeverity === "hint");
852
1011
  return true;
853
1012
  }
854
1013
  /** Most-important first: blocking → error → warning/other, then by line. */
@@ -918,6 +1077,14 @@ function diagnosticDedupKey(filePath, diagnostic) {
918
1077
  const ruleId = normalizeRuleId(diagnostic.rule ?? diagnostic.tool ?? "");
919
1078
  return [path.resolve(filePath), diagnostic.line ?? "?", ruleId].join(":");
920
1079
  }
1080
+ /**
1081
+ * Which project runner produced a retained row (#2154). ONE derivation, used by
1082
+ * both the retirement filter and the record that counts what it retired — v2's
1083
+ * N2 was two verbatim copies of this expression drifting apart.
1084
+ */
1085
+ function runnerIdOf(diagnostic) {
1086
+ return diagnostic.tool ?? diagnostic.rule?.split(":", 1)[0] ?? "";
1087
+ }
921
1088
  function summarizeDiagnostics(filePath, diagnostics, hasFinalSnapshot) {
922
1089
  let blocking = 0;
923
1090
  let errors = 0;
@@ -1107,7 +1274,7 @@ function tallyLspPrimaryVsAuxiliary(results) {
1107
1274
  for (const result of results) {
1108
1275
  const primaryId = primaryServerId(result.filePath);
1109
1276
  for (const diagnostic of result.diagnostics ?? []) {
1110
- if (diagnostic.source === primaryId)
1277
+ if (diagnostic.serverId === primaryId)
1111
1278
  primary += 1;
1112
1279
  else
1113
1280
  auxiliary += 1;
@@ -1125,7 +1292,7 @@ function mergeDiagnosticsWithWidgetSummaries(widgetSummaries, lspResults, projec
1125
1292
  * additive behavior (fail-open: unconfirmed/timed-out sweeps never retire
1126
1293
  * widget state they did not actually re-check).
1127
1294
  */
1128
- authoritativeLspFiles) {
1295
+ authoritativeLspFiles, authoritativeRunnerIds) {
1129
1296
  const byFile = new Map();
1130
1297
  const seen = new Set();
1131
1298
  for (const summary of widgetSummaries) {
@@ -1138,8 +1305,18 @@ authoritativeLspFiles) {
1138
1305
  // suppression mechanism.
1139
1306
  if (authoritativeLspFiles?.has(filePath))
1140
1307
  continue;
1141
- const diagnostics = (summary.diagnostics ?? []).map((d) => ({ ...d }));
1142
- byFile.set(filePath, { ...summary, filePath, diagnostics });
1308
+ const retained = summary.diagnostics ?? [];
1309
+ const diagnostics = retained
1310
+ .filter((diagnostic) => !authoritativeRunnerIds?.has(runnerIdOf(diagnostic)))
1311
+ .map((d) => ({ ...d }));
1312
+ byFile.set(filePath,
1313
+ // #2154 v4: when the filter retires rows, the stored tallies describe
1314
+ // diagnostics that are no longer delivered — full mode rendered
1315
+ // `main.go 1W … 1 warning` with no row under it. Recompute from what
1316
+ // survived, and only then (an untouched file keeps its own counts).
1317
+ diagnostics.length === retained.length
1318
+ ? { ...summary, filePath, diagnostics }
1319
+ : summarizeDiagnostics(filePath, diagnostics, summary.hasFinalSnapshot ?? true));
1143
1320
  for (const diagnostic of diagnostics) {
1144
1321
  seen.add(diagnosticDedupKey(filePath, diagnostic));
1145
1322
  }
@@ -1302,8 +1479,7 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1302
1479
  };
1303
1480
  }
1304
1481
  const { signal, pathsScope, nextWriteIndex } = options;
1305
- const ignoreFile = createCurrentIgnoreFilter(cwd);
1306
- const includeFile = (filePath) => ignoreFile(filePath) && (!pathsScope || pathsScope.includeFile(filePath));
1482
+ const includeFile = createScopedFileFilter(cwd, pathsScope);
1307
1483
  // `paths` (#461): route the active scans at exactly the requested files
1308
1484
  // instead of walking the whole project. Three cases:
1309
1485
  // - files only → pass them as the explicit list (skips the walk). An EMPTY
@@ -1335,6 +1511,7 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1335
1511
  : Promise.resolve({
1336
1512
  diagnostics: [],
1337
1513
  runners: [],
1514
+ analyzed: [],
1338
1515
  // #1623: every heavyweight analyzer is ELIGIBLE for this project but
1339
1516
  // this call never asked for it (refreshRunners wasn't cheap/all/
1340
1517
  // cached) — the expensive fetch below deliberately never runs in
@@ -1393,6 +1570,8 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1393
1570
  const fullyCoveredLspResults = confirmedLspResults.filter((result) => (result.unconfirmedServerIds?.length ?? 0) === 0);
1394
1571
  const partiallyCoveredLspResults = confirmedLspResults.filter((result) => (result.unconfirmedServerIds?.length ?? 0) > 0);
1395
1572
  const unconfirmedLspResults = lspResults.filter((result) => result.timedOut || result.error || mismatchedLspResults.has(result));
1573
+ const authoritativeLspFiles = new Set();
1574
+ const unreconciledFiles = new Set();
1396
1575
  // #571: reconcile this scan's fresh, CONFIRMED per-file results into the
1397
1576
  // footer cache. A footer write is never allowed to fail the tool call, so
1398
1577
  // any unexpected throw is swallowed.
@@ -1413,7 +1592,7 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1413
1592
  // (`applyAuxiliarySuppressions`), so an empty content string here is a
1414
1593
  // safe no-op re-check rather than a behavior gap.
1415
1594
  const retagged = retagAuxiliaryDiagnostics(diagnostics, result.diagnostics, "", { cwd, fileRole: detectFileRole(result.filePath) });
1416
- reconcileScanDiagnostics(result.filePath, retagged, true,
1595
+ const retired = reconcileScanDiagnostics(result.filePath, retagged, true,
1417
1596
  // The service reserves this token when the file enters the scan. The
1418
1597
  // fallback preserves compatibility with older test doubles/services.
1419
1598
  result.writeIndex ?? nextWriteIndex?.(),
@@ -1424,9 +1603,31 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1424
1603
  // finding on screen by re-arming the mtime gate. Undefined for
1425
1604
  // freshly-touched results (observed now).
1426
1605
  result.observedAt);
1606
+ // A result rejected by the shared ordering guard is not authoritative
1607
+ // for delivery. Otherwise full mode hides the old widget row while
1608
+ // mode=all still serves it, creating a false clean/full disagreement.
1609
+ // Strict `=== true`: `undefined` (a double that predates the boolean
1610
+ // return) takes the unreconciled arm, never the retiring one.
1611
+ if (retired === true) {
1612
+ authoritativeLspFiles.add(path.resolve(result.filePath));
1613
+ }
1614
+ else {
1615
+ unreconciledFiles.add(path.resolve(result.filePath));
1616
+ recordDegradationOnce({
1617
+ kind: "diagnostic-retained-unreconciled",
1618
+ subject: `${path.resolve(result.filePath)}:lsp`,
1619
+ reason: "confirmed diagnostic result could not replace retained widget state",
1620
+ });
1621
+ }
1427
1622
  }
1428
1623
  catch {
1429
1624
  // Never let a footer-reconciliation hiccup fail the scan itself.
1625
+ unreconciledFiles.add(path.resolve(result.filePath));
1626
+ recordDegradationOnce({
1627
+ kind: "diagnostic-retained-unreconciled",
1628
+ subject: `${path.resolve(result.filePath)}:lsp`,
1629
+ reason: "confirmed diagnostic result failed during widget reconciliation",
1630
+ });
1430
1631
  }
1431
1632
  }
1432
1633
  // Project rule policy (`.pi-lens.json` `rules.<id>.disable`/`select`) —
@@ -1454,7 +1655,15 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1454
1655
  // computed above, in the SAME `Promise.all` as the LSP sweep (#613) — only
1455
1656
  // when the caller opted into project-runner state (otherwise it's the
1456
1657
  // `Promise.resolve({...})` stub from `analyzersPromise` above).
1457
- const projectSnapshot = applyProjectRulePolicy(foldExtraDiagnosticsIntoSnapshot(scannedSnapshot, extracted.diagnostics.filter((d) => includeFile(d.filePath)), extracted.runners, cwd), policyMap);
1658
+ // #2154: only ids whose client said it parsed a scan of this root this call
1659
+ // (`FreshProjectDiagnosticsResult.analyzed`). Every exclusion — a skipped
1660
+ // runner, a crash that still reported success, the cache-read test-runner
1661
+ // lane — is decided at the record site in fresh-fetch.ts, not re-derived
1662
+ // here; a second list of "which ids don't count" would be the mirror this
1663
+ // repo's single-source-of-truth rule forbids.
1664
+ const authoritativeRunnerIds = new Set(extracted.analyzed ?? []);
1665
+ const foldedProjectSnapshot = foldExtraDiagnosticsIntoSnapshot(scannedSnapshot, extracted.diagnostics.filter((d) => includeFile(d.filePath)), extracted.runners, cwd);
1666
+ const projectSnapshot = applyProjectRulePolicy(foldedProjectSnapshot, policyMap);
1458
1667
  const projectDelta = applyProjectRulePolicy(filterProjectDiagnosticsDeltaReport(loadProjectDiagnosticsDeltaReport(cwd), includeFile), policyMap);
1459
1668
  // #630: only the CONFIRMED LSP results contribute diagnostics to the merge
1460
1669
  // — an unconfirmed (timed-out/errored) file's placeholder `[]` must not be
@@ -1464,7 +1673,6 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1464
1673
  // #1993: files with a CONFIRMED, fully-covered LSP result are authoritative
1465
1674
  // - the fresh sweep replaces their widget-store state instead of merging
1466
1675
  // additively beside it.
1467
- const authoritativeLspFiles = new Set(fullyCoveredLspResults.map((result) => path.resolve(result.filePath)));
1468
1676
  // #1993 review: retirement must be observable - if a future regression
1469
1677
  // makes the set falsely authoritative, findings would vanish silently.
1470
1678
  const authoritativeRetiredCount = getFileDiagnosticSummaries().filter((summary) => includeFile(summary.filePath) &&
@@ -1479,7 +1687,32 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1479
1687
  metadata: { files: authoritativeRetiredCount },
1480
1688
  });
1481
1689
  }
1482
- const summaries = await applyInlineSuppressionsToSummaries(mergeDiagnosticsWithWidgetSummaries(getFileDiagnosticSummaries().filter((summary) => includeFile(summary.filePath)), confirmedLspResults, projectSnapshot, projectDelta, authoritativeLspFiles), cwd, policyMap);
1690
+ // #2154 v4 F2: the same rule for the runner arm, in the same shape as its
1691
+ // LSP sibling above — a retirement nobody can see is how a regression
1692
+ // deletes findings silently. Bounded by construction: at most one row per
1693
+ // mode=full call, and only when rows were actually retired.
1694
+ const runnerRetiredRows = getFileDiagnosticSummaries()
1695
+ .filter((summary) => includeFile(summary.filePath))
1696
+ .reduce((total, summary) => total +
1697
+ (summary.diagnostics ?? []).filter((diagnostic) => authoritativeRunnerIds.has(runnerIdOf(diagnostic))).length, 0);
1698
+ if (runnerRetiredRows > 0) {
1699
+ logLatency({
1700
+ type: "phase",
1701
+ phase: "runner_authoritative_widget_retire",
1702
+ filePath: "",
1703
+ durationMs: 0,
1704
+ metadata: {
1705
+ rows: runnerRetiredRows,
1706
+ runners: [...authoritativeRunnerIds].join(","),
1707
+ },
1708
+ });
1709
+ }
1710
+ let summaries = await applyInlineSuppressionsToSummaries(mergeDiagnosticsWithWidgetSummaries(getFileDiagnosticSummaries().filter((summary) => includeFile(summary.filePath)), confirmedLspResults, projectSnapshot, projectDelta, authoritativeLspFiles, authoritativeRunnerIds), cwd, policyMap);
1711
+ if (unreconciledFiles.size > 0) {
1712
+ summaries = summaries.map((summary) => unreconciledFiles.has(path.resolve(summary.filePath))
1713
+ ? summarizeDiagnostics(summary.filePath, markUnreconciledFindings(summary.diagnostics), summary.hasFinalSnapshot)
1714
+ : summary);
1715
+ }
1483
1716
  // #1888: the full-mode summary above is the first seam where every
1484
1717
  // producing lane is correlated. The earlier footer loop intentionally writes
1485
1718
  // only CONFIRMED LSP results, so an ast-grep backpressure failure left
@@ -1496,7 +1729,7 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1496
1729
  for (const summary of summaries) {
1497
1730
  reconcileCorrelatedScanDiagnostics(summary.filePath, summary.diagnostics, nextWriteIndex?.(), projectScanObservedAt);
1498
1731
  }
1499
- const result = formatAllMode(cwd, severity, summaries, {
1732
+ const result = await formatAllMode(cwd, severity, summaries, {
1500
1733
  mode: "full",
1501
1734
  lspFilesChecked: rawLspResults.length,
1502
1735
  partial: aborted,
@@ -1515,7 +1748,7 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1515
1748
  sources: projectDelta.sources,
1516
1749
  turnIndex: projectDelta.turnIndex,
1517
1750
  },
1518
- });
1751
+ }, 0, pathsScope);
1519
1752
  const missingNote = pathsScopeMissingNote(pathsScope);
1520
1753
  // #630: mirrors `tools/lsp-diagnostics.ts`'s `unconfirmedReasonClause`/
1521
1754
  // `tallyConfirmation` semantic guarantee (never silently render
@@ -1562,6 +1795,21 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1562
1795
  // so a page of ast-grep/opengrep/marksman noise never buries whether the
1563
1796
  // real language server itself found anything in this sweep.
1564
1797
  const lspPrimaryVsAuxiliary = tallyLspPrimaryVsAuxiliary(confirmedLspResults);
1798
+ logExtension({
1799
+ subsystem: "lsp-diagnostics",
1800
+ message: "lens_diagnostics verdict",
1801
+ metadata: {
1802
+ server: [
1803
+ ...new Set(confirmedLspResults.map((result) => primaryServerId(result.filePath) ?? "unknown")),
1804
+ ].join(","),
1805
+ primary: lspPrimaryVsAuxiliary.primary,
1806
+ auxiliary: lspPrimaryVsAuxiliary.auxiliary,
1807
+ total: lspPrimaryVsAuxiliary.primary + lspPrimaryVsAuxiliary.auxiliary,
1808
+ sources: [
1809
+ ...new Set(confirmedLspResults.flatMap((result) => (result.diagnostics ?? []).map((diagnostic) => diagnostic.source ?? "unknown"))),
1810
+ ].slice(0, 5),
1811
+ },
1812
+ });
1565
1813
  const lspPrimaryVsAuxiliaryNote = lspPrimaryVsAuxiliary.primary + lspPrimaryVsAuxiliary.auxiliary > 0
1566
1814
  ? `\n\nLSP sweep findings: ${lspPrimaryVsAuxiliary.primary} primary (language server), ` +
1567
1815
  `${lspPrimaryVsAuxiliary.auxiliary} auxiliary (ast-grep/opengrep/zizmor/typos/marksman/...).`
@@ -1878,8 +2126,20 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1878
2126
  }
1879
2127
  return resultWithCold;
1880
2128
  }
2129
+ async function projectResolvedCwd(summary, cwd) {
2130
+ // #2777 O1: the whole three-way lookup lives on the LSP seam now. A file
2131
+ // with no primary LSP server is `undefined` here by construction, and
2132
+ // formatAllMode renders that row without a cwd term (N1) — the literal
2133
+ // `cwd=undefined` cannot be produced.
2134
+ const resolvedCwd = await resolveLspCwdForFile(summary.filePath, cwd);
2135
+ return {
2136
+ ...summary,
2137
+ ...(resolvedCwd === undefined ? {} : { resolvedCwd }),
2138
+ diagnostics: summary.diagnostics ?? [],
2139
+ };
2140
+ }
1881
2141
  // @delivery-surface: lens-diagnostics:mode-all
1882
- function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(), detailOverrides = { mode: "all" }, staleDropped = 0, pathsScope, dependencyDemoted = 0) {
2142
+ async function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(), detailOverrides = { mode: "all" }, staleDropped = 0, pathsScope, dependencyDemoted = 0) {
1883
2143
  // #2275 review F2: the widget footer stops DRAWING a dependency-drift
1884
2144
  // demotion once it hits `DEPENDENCY_DRIFT_MAX_DELIVERIES` unconfirmed
1885
2145
  // deliveries, but the record stays here (dropping it would make an
@@ -1903,8 +2163,7 @@ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(),
1903
2163
  // result there IS a legitimate clean read — the cache-only note only applies
1904
2164
  // to delta/all, which is why this is gated on detailOverrides.mode !== "full".
1905
2165
  const isFullMode = detailOverrides.mode === "full";
1906
- const ignoreFile = createCurrentIgnoreFilter(cwd);
1907
- const includeFile = (filePath) => ignoreFile(filePath) && (!pathsScope || pathsScope.includeFile(filePath));
2166
+ const includeFile = createScopedFileFilter(cwd, pathsScope);
1908
2167
  // Cached summaries predate marks. Every cache-only surface re-applies
1909
2168
  // dispositions through the shared applyCachedDispositions seam — strict
1910
2169
  // false-positive anchors against current content when the file is
@@ -1955,7 +2214,7 @@ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(),
1955
2214
  // #2414: a "warning" filter must not admit hint/info — that is exactly
1956
2215
  // the "present hints as warnings" defect this issue exists to close.
1957
2216
  if (severity === "warning")
1958
- return s.warnings > 0;
2217
+ return s.blocking > 0 || s.errors > 0 || s.warnings > 0;
1959
2218
  // severity: "all" — a hint/info-only file (`advisories > 0`,
1960
2219
  // warnings === 0) must still surface here, or the #2414 fix that stops
1961
2220
  // hints inflating `warnings` would silently drop that file from the
@@ -1984,12 +2243,17 @@ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(),
1984
2243
  }
1985
2244
  // Sort: blocking first, then errors, then warnings
1986
2245
  const sorted = withIssues.sort((a, b) => b.blocking - a.blocking || b.errors - a.errors || b.warnings - a.warnings);
2246
+ // #2777 O2: only rendered rows resolve their root. The projection sits
2247
+ // below the withIssues filter, so a 500-file session pays the cwd
2248
+ // resolution for the handful of rows listed here, not for every summary
2249
+ // the cache holds.
2250
+ const rendered = await Promise.all(sorted.map((summary) => projectResolvedCwd(summary, cwd)));
1987
2251
  const lines = [];
1988
2252
  let totalBlocking = 0;
1989
2253
  let totalErrors = 0;
1990
2254
  let totalWarnings = 0;
1991
2255
  let totalAdvisories = 0;
1992
- for (const s of sorted) {
2256
+ for (const s of rendered) {
1993
2257
  const rel = path.relative(cwd, s.filePath);
1994
2258
  const parts = [];
1995
2259
  if (s.blocking > 0)
@@ -2009,6 +2273,10 @@ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(),
2009
2273
  if (staleCount > 0) {
2010
2274
  parts.push(`${staleCount} stale — re-run to confirm`);
2011
2275
  }
2276
+ // #2777 N1: files with no primary LSP server have no resolvedCwd —
2277
+ // omit the term rather than rendering the literal `cwd=undefined`.
2278
+ if (s.resolvedCwd !== undefined)
2279
+ parts.push(`cwd=${s.resolvedCwd}`);
2012
2280
  lines.push(`${rel} ${parts.join(" ")}`);
2013
2281
  // List the actual diagnostics (not just counts) so the agent can act on
2014
2282
  // them without re-running anything — same "L<line>: <message>" shape as the