@herbertgao/pi-extensions 2026.8.6 → 2026.8.8

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 (385) hide show
  1. package/README.md +8 -6
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +19 -7
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +30 -9
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +320 -22
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +94 -77
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +79 -17
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +27 -0
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +66 -17
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +41 -2
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +75 -6
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +7 -3
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +64 -1
  15. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  16. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  17. package/node_modules/@herbertgao/pi-subagents/README.md +30 -22
  18. package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +5 -5
  19. package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +7 -1
  21. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +48 -19
  22. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +17 -13
  23. package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +26 -2
  24. package/node_modules/@herbertgao/pi-subagents/src/index.ts +219 -40
  25. package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +101 -3
  26. package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +18 -4
  27. package/node_modules/@herbertgao/pi-subagents/src/settings.ts +99 -0
  28. package/node_modules/@herbertgao/pi-subagents/src/types.ts +22 -3
  29. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +51 -3
  30. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +20 -2
  31. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +21 -6
  32. package/node_modules/@herbertgao/pi-subagents/src/usage.ts +129 -1
  33. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +20 -0
  34. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +5 -4
  35. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +174 -100
  36. package/node_modules/@juicesharp/rpiv-ask-user-question/config.ts +25 -0
  37. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/configuration.md +13 -4
  38. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +5 -1
  39. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
  40. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +10 -6
  41. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
  42. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +5 -2
  43. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/zh.json +2 -2
  44. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  45. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +10 -0
  46. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +140 -97
  47. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +63 -28
  48. package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
  49. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
  50. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
  51. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
  52. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
  53. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
  54. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
  55. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
  56. package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +86 -38
  57. package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +76 -9
  58. package/node_modules/@narumitw/pi-btw/README.md +120 -111
  59. package/node_modules/@narumitw/pi-btw/dist/index.ts +2898 -0
  60. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +7 -0
  61. package/node_modules/@narumitw/pi-btw/package.json +10 -6
  62. package/node_modules/@narumitw/pi-btw/src/btw.ts +36 -40
  63. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +55 -10
  64. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +390 -0
  65. package/node_modules/@narumitw/pi-btw/src/menu.ts +45 -2
  66. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
  67. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
  68. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  69. package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
  70. package/node_modules/pi-lens/CHANGELOG.md +999 -0
  71. package/node_modules/pi-lens/README.md +117 -3
  72. package/node_modules/pi-lens/config/ruff/core.toml +42 -0
  73. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +130 -27
  74. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +126 -13
  75. package/node_modules/pi-lens/dist/clients/agent-nudge.js +67 -11
  76. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +9 -3
  77. package/node_modules/pi-lens/dist/clients/atomic-write.js +9 -3
  78. package/node_modules/pi-lens/dist/clients/bash-file-access.js +381 -13
  79. package/node_modules/pi-lens/dist/clients/biome-client.js +61 -30
  80. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
  81. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +129 -0
  82. package/node_modules/pi-lens/dist/clients/bootstrap.js +12 -1
  83. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +12 -3
  84. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +206 -0
  85. package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
  86. package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
  87. package/node_modules/pi-lens/dist/clients/cascade-format.js +20 -1
  88. package/node_modules/pi-lens/dist/clients/cascade-types.js +9 -1
  89. package/node_modules/pi-lens/dist/clients/child-unref.js +54 -6
  90. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +76 -24
  91. package/node_modules/pi-lens/dist/clients/dead-code-client.js +123 -31
  92. package/node_modules/pi-lens/dist/clients/deadline-utils.js +8 -1
  93. package/node_modules/pi-lens/dist/clients/debug-handles.js +5 -0
  94. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +84 -14
  95. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
  96. package/node_modules/pi-lens/dist/clients/dependency-checker.js +25 -3
  97. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
  98. package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
  99. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +147 -33
  100. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
  101. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +52 -2
  102. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +256 -88
  103. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +7 -4
  104. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +5 -0
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +234 -18
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +187 -19
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +6 -3
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +11 -9
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +398 -0
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +11 -3
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +12 -7
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +18 -0
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +895 -0
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +4 -0
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +16 -3
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +60 -53
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -2
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +25 -2
  122. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
  123. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
  124. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +331 -55
  125. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +38 -5
  126. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +97 -19
  127. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +17 -8
  128. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +32 -4
  129. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +47 -3
  130. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +14 -4
  131. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +127 -28
  132. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
  133. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +73 -12
  134. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +275 -13
  135. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +21 -6
  136. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +260 -27
  137. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +734 -64
  138. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +86 -11
  139. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +188 -0
  140. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +9 -0
  141. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +21 -10
  142. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -2
  143. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
  144. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
  145. package/node_modules/pi-lens/dist/clients/extension-log.js +22 -1
  146. package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
  147. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +316 -0
  148. package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
  149. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +3 -2
  150. package/node_modules/pi-lens/dist/clients/formatters.js +500 -115
  151. package/node_modules/pi-lens/dist/clients/generation-guard.js +216 -0
  152. package/node_modules/pi-lens/dist/clients/git-guard.js +31 -0
  153. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
  154. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +109 -9
  155. package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
  156. package/node_modules/pi-lens/dist/clients/host-ports.js +8 -0
  157. package/node_modules/pi-lens/dist/clients/installer/index.js +1114 -100
  158. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +58 -0
  159. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +715 -0
  160. package/node_modules/pi-lens/dist/clients/instance-reaper.js +643 -97
  161. package/node_modules/pi-lens/dist/clients/instance-registry.js +87 -17
  162. package/node_modules/pi-lens/dist/clients/jscpd-client.js +87 -8
  163. package/node_modules/pi-lens/dist/clients/knip-client.js +310 -10
  164. package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
  165. package/node_modules/pi-lens/dist/clients/language-profile.js +9 -2
  166. package/node_modules/pi-lens/dist/clients/latency-logger.js +440 -11
  167. package/node_modules/pi-lens/dist/clients/lazy-import.js +21 -0
  168. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -0
  169. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  170. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +128 -8
  171. package/node_modules/pi-lens/dist/clients/lsp/client.js +1660 -164
  172. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +83 -1
  173. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
  174. package/node_modules/pi-lens/dist/clients/lsp/index.js +2423 -151
  175. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +215 -0
  176. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +146 -14
  177. package/node_modules/pi-lens/dist/clients/lsp/language.js +48 -4
  178. package/node_modules/pi-lens/dist/clients/lsp/server.js +277 -16
  179. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +22 -0
  180. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +43 -0
  181. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +79 -3
  182. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +43 -0
  183. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +422 -28
  184. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +30 -0
  185. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +175 -0
  186. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +3 -2
  187. package/node_modules/pi-lens/dist/clients/mcp/session.js +10 -2
  188. package/node_modules/pi-lens/dist/clients/module-report.js +22 -1
  189. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
  190. package/node_modules/pi-lens/dist/clients/opengrep-client.js +15 -1
  191. package/node_modules/pi-lens/dist/clients/package-manager.js +170 -14
  192. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +27 -0
  193. package/node_modules/pi-lens/dist/clients/path-utils.js +160 -0
  194. package/node_modules/pi-lens/dist/clients/pipeline.js +154 -33
  195. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +37 -0
  196. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +17 -3
  197. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +89 -22
  198. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +84 -14
  199. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +15 -3
  200. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +50 -2
  201. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +23 -9
  202. package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
  203. package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
  204. package/node_modules/pi-lens/dist/clients/project-snapshot.js +299 -0
  205. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +47 -0
  206. package/node_modules/pi-lens/dist/clients/quiet-window.js +11 -25
  207. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +8 -3
  208. package/node_modules/pi-lens/dist/clients/read-guard.js +253 -10
  209. package/node_modules/pi-lens/dist/clients/resource-sampler.js +49 -17
  210. package/node_modules/pi-lens/dist/clients/reverse-deps.js +50 -11
  211. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
  212. package/node_modules/pi-lens/dist/clients/review-graph/query.js +52 -4
  213. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
  214. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
  215. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +96 -7
  216. package/node_modules/pi-lens/dist/clients/runtime-context.js +8 -1
  217. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +352 -45
  218. package/node_modules/pi-lens/dist/clients/runtime-session.js +373 -9
  219. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
  220. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +212 -79
  221. package/node_modules/pi-lens/dist/clients/runtime-turn.js +620 -67
  222. package/node_modules/pi-lens/dist/clients/safe-spawn.js +220 -7
  223. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +161 -0
  224. package/node_modules/pi-lens/dist/clients/search-read-registration.js +34 -6
  225. package/node_modules/pi-lens/dist/clients/security-scan-client.js +157 -24
  226. package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
  227. package/node_modules/pi-lens/dist/clients/sg-runner.js +116 -22
  228. package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
  229. package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
  230. package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
  231. package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
  232. package/node_modules/pi-lens/dist/clients/startup-timing.js +19 -1
  233. package/node_modules/pi-lens/dist/clients/test-runner-client.js +151 -16
  234. package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
  235. package/node_modules/pi-lens/dist/clients/tool-policy.js +125 -20
  236. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +75 -1
  237. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +769 -64
  238. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
  239. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +11 -0
  240. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +90 -0
  241. package/node_modules/pi-lens/dist/clients/trivy-client.js +19 -1
  242. package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
  243. package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
  244. package/node_modules/pi-lens/dist/index.js +61407 -48577
  245. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +476 -57
  246. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +132 -43
  247. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
  248. package/node_modules/pi-lens/docs/agent-guide.md +6 -1
  249. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +515 -506
  250. package/node_modules/pi-lens/docs/dispositions.md +21 -0
  251. package/node_modules/pi-lens/docs/features.md +19 -5
  252. package/node_modules/pi-lens/docs/globalconfig.md +3 -0
  253. package/node_modules/pi-lens/docs/language-coverage.md +71 -5
  254. package/node_modules/pi-lens/docs/mcp.md +2 -2
  255. package/node_modules/pi-lens/docs/servercapabilities.md +4 -1
  256. package/node_modules/pi-lens/docs/settings.md +19 -1
  257. package/node_modules/pi-lens/docs/usage.md +1 -0
  258. package/node_modules/pi-lens/package.json +27 -10
  259. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-js-snapshot.yml +23 -0
  260. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-snapshot.yml +23 -0
  261. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/ts-void-zero-snapshot.yml +23 -0
  262. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-object-param-test.yml +16 -0
  263. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +15 -1
  264. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-compile-call-test.yml +29 -1
  265. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-dupe-class-members-test.yml +33 -0
  266. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-js-test.yml +7 -1
  267. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-test.yml +7 -1
  268. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-known-value-widening-test.yml +9 -0
  269. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-non-null-assertion-test.yml +216 -0
  270. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +2 -0
  271. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +2 -0
  272. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +2 -1
  273. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +2 -1
  274. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-return-value-in-generator-test.yml +12 -0
  275. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-runtime-typeof-test.yml +33 -0
  276. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-server-bind-wildcard-test.yml +9 -0
  277. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-shape-in-symbol-names-test.yml +13 -0
  278. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-js-test.yml +5 -1
  279. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-test.yml +5 -1
  280. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-parameters-test.yml +12 -0
  281. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-returns-test.yml +16 -0
  282. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-any-test.yml +10 -0
  283. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-unknown-test.yml +8 -0
  284. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/redundant-unsafe-function-test.yml +35 -0
  285. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/require-safety-comment-for-as-unknown-as-test.yml +177 -0
  286. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ts-void-zero-test.yml +1 -0
  287. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member-js.yml +13 -2
  288. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member.yml +13 -2
  289. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +2 -0
  290. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-object-param.yml +60 -0
  291. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +57 -12
  292. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-compile-call.yml +63 -5
  293. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-dupe-class-members.yml +52 -8
  294. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html-js.yml +27 -2
  295. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html.yml +27 -2
  296. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-known-value-widening.yml +71 -0
  297. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-non-null-assertion.yml +316 -0
  298. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +7 -0
  299. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +22 -2
  300. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +23 -2
  301. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +33 -5
  302. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +34 -8
  303. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-return-value-in-generator.yml +22 -5
  304. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-runtime-typeof.yml +121 -0
  305. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-server-bind-wildcard.yml +23 -7
  306. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-shape-in-symbol-names.yml +68 -0
  307. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +41 -2
  308. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code.yml +41 -2
  309. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +6 -1
  310. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-parameters.yml +67 -0
  311. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-returns.yml +106 -0
  312. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-any.yml +50 -0
  313. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-unknown.yml +56 -0
  314. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +2 -0
  315. package/node_modules/pi-lens/rules/ast-grep-rules/rules/redundant-unsafe-function.yml +32 -0
  316. package/node_modules/pi-lens/rules/ast-grep-rules/rules/require-safety-comment-for-as-unknown-as.yml +184 -0
  317. package/node_modules/pi-lens/rules/ast-grep-rules/rules/ts-void-zero.yml +5 -0
  318. package/node_modules/pi-lens/rules/ast-grep-rules/self-scan-baseline.json +5 -0
  319. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +53 -1
  320. package/node_modules/pi-lens/scripts/download-grammars.js +25 -0
  321. package/node_modules/pi-lens/scripts/grammars.lock.json +9 -0
  322. package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
  323. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
  324. package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
  325. package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
  326. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +2 -0
  327. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +80 -3
  328. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +41 -0
  329. package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
  330. package/node_modules/pi-mcp-adapter/CHANGELOG.md +36 -0
  331. package/node_modules/pi-mcp-adapter/README.md +47 -5
  332. package/node_modules/pi-mcp-adapter/cli.js +163 -1
  333. package/node_modules/pi-mcp-adapter/commands.ts +128 -22
  334. package/node_modules/pi-mcp-adapter/config.ts +25 -7
  335. package/node_modules/pi-mcp-adapter/direct-tools.ts +5 -4
  336. package/node_modules/pi-mcp-adapter/index.ts +219 -32
  337. package/node_modules/pi-mcp-adapter/init.ts +36 -4
  338. package/node_modules/pi-mcp-adapter/lifecycle.ts +315 -24
  339. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +39 -6
  340. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +387 -0
  341. package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +27 -3
  342. package/node_modules/pi-mcp-adapter/mcp-code.ts +4 -0
  343. package/node_modules/pi-mcp-adapter/mcp-panel.ts +42 -13
  344. package/node_modules/pi-mcp-adapter/metadata-cache.ts +32 -21
  345. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
  346. package/node_modules/pi-mcp-adapter/package.json +3 -1
  347. package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
  348. package/node_modules/pi-mcp-adapter/request-headers-command.ts +31 -37
  349. package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
  350. package/node_modules/pi-mcp-adapter/server-manager.ts +140 -2
  351. package/node_modules/pi-mcp-adapter/session-recovery.ts +7 -0
  352. package/node_modules/pi-mcp-adapter/tool-approval.ts +15 -2
  353. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
  354. package/node_modules/pi-mcp-adapter/types.ts +2 -0
  355. package/node_modules/pi-mcp-adapter/ui-server.ts +18 -16
  356. package/node_modules/pi-mcp-adapter/utils.ts +70 -0
  357. package/node_modules/pi-web-access/CHANGELOG.md +37 -0
  358. package/node_modules/pi-web-access/README.md +32 -13
  359. package/node_modules/pi-web-access/anysearch.ts +4 -2
  360. package/node_modules/pi-web-access/brave.ts +16 -4
  361. package/node_modules/pi-web-access/chrome-cookies.ts +86 -15
  362. package/node_modules/pi-web-access/curator-page.ts +6 -3
  363. package/node_modules/pi-web-access/curator-server.ts +4 -1
  364. package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
  365. package/node_modules/pi-web-access/exa.ts +18 -7
  366. package/node_modules/pi-web-access/extract.ts +40 -5
  367. package/node_modules/pi-web-access/firecrawl.ts +18 -2
  368. package/node_modules/pi-web-access/gemini-search.ts +16 -4
  369. package/node_modules/pi-web-access/github-extract.ts +65 -36
  370. package/node_modules/pi-web-access/index.ts +60 -61
  371. package/node_modules/pi-web-access/openai-search.ts +17 -6
  372. package/node_modules/pi-web-access/package.json +1 -1
  373. package/node_modules/pi-web-access/page-query.ts +4 -2
  374. package/node_modules/pi-web-access/parallel-mcp.ts +249 -0
  375. package/node_modules/pi-web-access/parallel.ts +2 -2
  376. package/node_modules/pi-web-access/pdf-extract.ts +18 -5
  377. package/node_modules/pi-web-access/query-rewrite.ts +51 -0
  378. package/node_modules/pi-web-access/serper.ts +211 -0
  379. package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
  380. package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
  381. package/node_modules/pi-web-access/summary-review.ts +44 -22
  382. package/node_modules/pi-web-access/tavily.ts +16 -4
  383. package/node_modules/pi-web-access/utils.ts +87 -0
  384. package/node_modules/pi-web-access/valyu.ts +199 -0
  385. package/package.json +14 -14
@@ -14,6 +14,7 @@ import * as path from "node:path";
14
14
  import { Type } from "../clients/deps/typebox.js";
15
15
  import { anchorsForDiagnostic, applyDispositions, applyWeakDispositions, getDisposition, } from "../clients/diagnostic-dispositions.js";
16
16
  import { applyInlineSuppressions } from "../clients/dispatch/inline-suppressions.js";
17
+ import { gateFindingsByPathFreshness } from "../clients/advisory-provenance.js";
17
18
  import { normalizeRuleId } from "../clients/dispatch/rule-id-normalize.js";
18
19
  import { applyRulePolicy, rulePolicyMapFromConfig } from "../clients/dispatch/rule-policy.js";
19
20
  import { loadPiLensProjectConfig } from "../clients/project-lens-config.js";
@@ -23,18 +24,23 @@ import { getProjectIgnoreMatcher } from "../clients/file-utils.js";
23
24
  import { isAtOrAboveHomeDir, normalizeFilePath, } from "../clients/path-utils.js";
24
25
  import { getLSPService } from "../clients/lsp/index.js";
25
26
  import { primaryServerId } from "../clients/lsp/config.js";
27
+ import { getFullScanWallClockMs } from "../clients/lsp/workspace-sweep-hold.js";
28
+ import { demoteInferredProjectSweepResults } from "../clients/lsp/inferred-project.js";
26
29
  import { hashDiagnosticContent, } from "../clients/lsp/diagnostic-binding.js";
27
30
  import { loadProjectDiagnosticsDeltaReport, loadProjectDiagnosticsSnapshot, PROJECT_DIAGNOSTICS_CACHE_VERSION, reconcileProjectDiagnosticsSnapshot, } from "../clients/project-diagnostics/cache.js";
28
- import { warmTriggerFor } from "../clients/project-diagnostics/extractors.js";
29
- import { fetchFreshProjectDiagnostics } from "../clients/project-diagnostics/fresh-fetch.js";
31
+ import { formatCacheAgeOld, formatNotRunEntry, } from "../clients/project-diagnostics/extractors.js";
32
+ import { ANALYZER_IDS, fetchFreshProjectDiagnostics, } from "../clients/project-diagnostics/fresh-fetch.js";
30
33
  import { loadBootstrapClients } from "../clients/bootstrap.js";
31
34
  import { generatedSkipNotice, scanTruncationNotice, } from "../clients/lens-engine.js";
32
35
  import { scanProjectDiagnostics } from "../clients/project-diagnostics/scanner.js";
33
- import { getFileDiagnosticSummaries, reconcileScanDiagnostics, reconcileStaleWidgetFiles, } from "../clients/widget-state.js";
36
+ import { getFileDiagnosticSummaries, reconcileCorrelatedScanDiagnostics, reconcileScanDiagnostics, reconcileStaleWidgetDependencyBlockers, reconcileStaleWidgetFiles, widgetDiagnosticUri, } from "../clients/widget-state.js";
37
+ import { logLatency } from "../clients/latency-logger.js";
34
38
  import { convertLspDiagnostics } from "../clients/dispatch/utils/lsp-diagnostics.js";
35
39
  import { retagAuxiliaryDiagnostics } from "../clients/dispatch/auxiliary-lsp.js";
36
40
  import { detectFileRole } from "../clients/file-role.js";
41
+ import { STALE_LINE_MARKER } from "../clients/stale-marker.js";
37
42
  import { makeProgressReporter, scanningSummaryLine } from "./scan-progress.js";
43
+ import { demotePastEofDiagnostics, PAST_EOF_STALE_MARKER, resyncDocumentOnPastEof, } from "../clients/diagnostic-line-freshness.js";
38
44
  // The widget state exposes the full per-file diagnostic set; this is the tool's
39
45
  // own generous display budget per file (independent of the TUI's 12 cap), to
40
46
  // keep output bounded on a pathologically broken file.
@@ -45,15 +51,23 @@ const MAX_DIAGNOSTICS_PER_FILE = 50;
45
51
  // caller can never believe it checked files it didn't (issue's stated
46
52
  // invariant).
47
53
  const MAX_PATHS_ENTRIES = 200;
54
+ // #1623: the reason rendered for every heavyweight-analyzer lane (gitleaks,
55
+ // trivy, govulncheck, dead-code, knip, jscpd, madge, opengrep, test-runner)
56
+ // when mode=full is called without refreshRunners=cheap/all/cached — the
57
+ // "quick mode" gate that skips the expensive fresh-fetch entirely (see
58
+ // `formatFullMode`'s `analyzersPromise`). One shared string so it renders
59
+ // identically everywhere it's used.
60
+ const NOT_REQUESTED_REASON = "refreshRunners not requested this call (quick mode) — pass refreshRunners=cheap/all/cached to lens_diagnostics mode=full to run it";
48
61
  // Wall-clock ceiling for the whole mode=full scan. Even with per-file budgets
49
62
  // and abort-signal honoring, a pathological state (a language server hanging on
50
63
  // spawn/initialize across many files) could otherwise stall the tool
51
64
  // indefinitely — an unattended session was observed hung for ~8h. This hard cap
52
65
  // aborts the scan so it always returns (partial) rather than never. Env-tunable.
53
- const FULL_SCAN_WALL_CLOCK_MS = (() => {
54
- const raw = Number(process.env.PI_LENS_LENS_DIAGNOSTICS_FULL_TIMEOUT_MS);
55
- return Number.isFinite(raw) && raw > 0 ? raw : 300_000; // 5 min default
56
- })();
66
+ // #1618: single source with `clients/runtime-turn.ts`'s idle-reset base delay
67
+ // (see `getBaseLspIdleResetMs`), which derives itself from this value so the
68
+ // two constants can no longer drift back into the idle-reset-fires-mid-sweep
69
+ // relationship that caused #1618 in the first place.
70
+ const FULL_SCAN_WALL_CLOCK_MS = getFullScanWallClockMs();
57
71
  // Binding verification is a fallback for result producers that supplied a
58
72
  // content hash without an already-computed disk verdict. Keep it one-file-at-a-
59
73
  // time and yield periodically: a full scan can contain thousands of results,
@@ -174,6 +188,17 @@ captureLspStatusRepaint, getRuntime) {
174
188
  return `lens_diagnostics delta — clean${coldSuffix}${failedSuffix}`;
175
189
  return `lens_diagnostics delta — ${aw} actionable · ${cq} quality · ${pd} project${coldSuffix}${failedSuffix}`;
176
190
  }
191
+ // #1799: `semantic === "blocking"` iff `severity === "error"` holds for
192
+ // RAW classification, but not for the rendered counts — a
193
+ // dependency-drift demotion (#1631, widget-state.ts `isBlocking` vs
194
+ // `countDiagnostics`) revokes an error's blocking authority (stale)
195
+ // while the finding itself stays real evidence, so it lands in
196
+ // `totalErrors` with `totalBlocking` unchanged. That is the one case
197
+ // where the two totals genuinely disagree, and it must still render —
198
+ // otherwise a file with drift-demoted errors reads as "clean". What
199
+ // must NOT happen is printing both terms for the SAME findings (the
200
+ // actual #1799 bug), so `errors` renders only when `blocking === 0`,
201
+ // mirroring the per-file row's own guard below.
177
202
  const b = details?.totalBlocking ?? 0;
178
203
  const e = details?.totalErrors ?? 0;
179
204
  const w = details?.totalWarnings ?? 0;
@@ -181,7 +206,11 @@ captureLspStatusRepaint, getRuntime) {
181
206
  if (b + e + w === 0) {
182
207
  return `lens_diagnostics ${mode} — clean (${files} files)${coldSuffix}${failedSuffix}`;
183
208
  }
184
- return `lens_diagnostics ${mode} — ${b} blocking · ${e} errors · ${w} warnings (${files} files)${coldSuffix}${failedSuffix}`;
209
+ const parts = [`${b} blocking`];
210
+ if (b === 0 && e > 0)
211
+ parts.push(`${e} errors`);
212
+ parts.push(`${w} warnings`);
213
+ return `lens_diagnostics ${mode} — ${parts.join(" · ")} (${files} files)${coldSuffix}${failedSuffix}`;
185
214
  }),
186
215
  parameters: Type.Object({
187
216
  mode: Type.Optional(Type.String({
@@ -271,7 +300,25 @@ captureLspStatusRepaint, getRuntime) {
271
300
  await flushPending();
272
301
  const staleDropped = await reconcileStaleWidgetFiles();
273
302
  if (mode === "all") {
274
- return formatAllMode(cwd, severity, undefined, undefined, staleDropped, pathsScope);
303
+ // #1631 dependency-axis gate for the mode=all store. `reconcileStaleWidgetFiles`
304
+ // (above) only sees the diagnosed file's OWN mtime; this demotes blocking
305
+ // findings whose forward imports drifted out-of-band. Paired reconciles, one
306
+ // read site — the same gate that covers the turn-end inline-blocker store.
307
+ const dependencyGateStart = Date.now();
308
+ const { demoted: dependencyDemoted, truncatedImports } = await reconcileStaleWidgetDependencyBlockers(cwd, getRuntime?.()?.turnIndex);
309
+ logLatency({
310
+ type: "phase",
311
+ toolName: "lens_diagnostics",
312
+ filePath: cwd,
313
+ phase: "blocker_freshness_widget_gate",
314
+ durationMs: Date.now() - dependencyGateStart,
315
+ metadata: {
316
+ demoted: dependencyDemoted,
317
+ truncatedImports,
318
+ surface: "mode=all",
319
+ },
320
+ });
321
+ return formatAllMode(cwd, severity, undefined, undefined, staleDropped, pathsScope, dependencyDemoted);
275
322
  }
276
323
  if (mode === "full") {
277
324
  // Fold a hard wall-clock ceiling into the abort signal so the scan
@@ -301,16 +348,48 @@ captureLspStatusRepaint, getRuntime) {
301
348
  };
302
349
  }
303
350
  // ── delta mode ────────────────────────────────────────────────────────────────
304
- function formatProjectDeltaDiagnostic(diagnostic) {
305
- const marker = diagnostic.semantic === "blocking" || diagnostic.severity === "error"
306
- ? "🔴"
307
- : "ℹ";
351
+ function formatProjectDeltaDiagnostic(diagnostic, stale) {
352
+ // #1944: a demoted row loses the authority marker along with its
353
+ // coordinate. It kept the 🔴 while its line was replaced by the stale
354
+ // marker, which is the same "changed the channel, not the body" defect the
355
+ // turn-end advisory carried — `formatFullMode` (this file, the `d.stale`
356
+ // arm of its marker) already drops the marker for exactly this reason.
357
+ const marker = stale
358
+ ? "○"
359
+ : diagnostic.semantic === "blocking" || diagnostic.severity === "error"
360
+ ? "🔴"
361
+ : "ℹ";
308
362
  const rule = diagnostic.rule ?? diagnostic.code ?? diagnostic.runner;
309
- return ` ${marker} L${diagnostic.line ?? "?"} ${rule} ${diagnostic.message}`;
363
+ const where = stale ? STALE_LINE_MARKER : `L${diagnostic.line ?? "?"}`;
364
+ return ` ${marker} ${where} ${rule} ${diagnostic.message}`;
310
365
  }
366
+ /**
367
+ * #1634 review round R3: `appendProjectDiagnosticsDeltaLines` re-serves the
368
+ * persisted project-diagnostics DELTA report verbatim — the third of
369
+ * `mode=delta`'s three arms (the other two, actionable/quality warnings, were
370
+ * gated in the F3 pass above), and it carried the identical unfixed shape:
371
+ * cited `file:line` findings from a report with its own `generatedAt`, no
372
+ * freshness check. Same gate, same policy as `applyDeltaFreshnessGate`:
373
+ * missing file -> DROP, edited-since -> DEMOTE (loses its line, keeps
374
+ * rule/message), live -> unchanged. See `clients/finding-delivery-gate.ts`
375
+ * surface `lens-diagnostics:mode-delta`.
376
+ */
311
377
  function appendProjectDiagnosticsDeltaLines(lines, cwd, report, severity, includeFile) {
312
- const diagnostics = (report?.diagnostics ?? []).filter((diagnostic) => includeFile(diagnostic.filePath) &&
313
- matchesSeverity(projectDiagnosticToWidget(diagnostic), severity));
378
+ const scoped = (report?.diagnostics ?? []).filter((diagnostic) => includeFile(diagnostic.filePath) &&
379
+ matchesSeverity(projectDiagnosticToWidget(diagnostic, diagnostic.filePath), severity));
380
+ const gated = gateFindingsByPathFreshness({
381
+ store: "lens-diagnostics-delta-project",
382
+ findings: scoped,
383
+ cwd,
384
+ scannedAt: report?.generatedAt,
385
+ citedPath: (d) => d.filePath,
386
+ onMissing: "drop",
387
+ });
388
+ const staleSet = new Set(gated.stale);
389
+ // Concatenating live-then-stale reorders a file's demoted rows to the end
390
+ // of its bucket below (rather than each diagnostic's original report
391
+ // order) — cosmetic only, findings are neither dropped nor duplicated.
392
+ const diagnostics = [...gated.live, ...gated.stale];
314
393
  const byFile = new Map();
315
394
  for (const diagnostic of diagnostics) {
316
395
  const filePath = path.resolve(diagnostic.filePath);
@@ -323,7 +402,7 @@ function appendProjectDiagnosticsDeltaLines(lines, cwd, report, severity, includ
323
402
  if (!lines.includes(rel))
324
403
  lines.push(rel);
325
404
  for (const diagnostic of fileDiagnostics) {
326
- lines.push(formatProjectDeltaDiagnostic(diagnostic));
405
+ lines.push(formatProjectDeltaDiagnostic(diagnostic, staleSet.has(diagnostic)));
327
406
  }
328
407
  }
329
408
  return diagnostics.length;
@@ -342,7 +421,7 @@ function appendProjectDiagnosticsDeltaLines(lines, cwd, report, severity, includ
342
421
  function filterDeltaReportDispositions(report, cwd, policyMap) {
343
422
  if (!report?.diagnostics.length)
344
423
  return report;
345
- const kept = report.diagnostics.filter((d) => applyWeakDispositions([projectDiagnosticToWidget(d)], cwd, d.filePath)
424
+ const kept = report.diagnostics.filter((d) => applyWeakDispositions([projectDiagnosticToWidget(d, d.filePath)], cwd, d.filePath)
346
425
  .length === 1);
347
426
  const policyKept = applyRulePolicy(kept, policyMap);
348
427
  if (policyKept.length === report.diagnostics.length)
@@ -360,6 +439,58 @@ function filterDeltaReportDispositions(report, cwd, policyMap) {
360
439
  function loadProjectRulePolicyMap(cwd) {
361
440
  return rulePolicyMapFromConfig(loadPiLensProjectConfig(cwd).rules);
362
441
  }
442
+ /**
443
+ * #1634 review round: `formatDeltaMode` re-serves the `actionable-warnings`/
444
+ * `code-quality-warnings` caches verbatim — each cited `file:line`, no
445
+ * freshness check — the SAME shape #1622 fixed for gitleaks/trivy-secrets,
446
+ * unfixed here because `mode=delta` is the tool's DEFAULT and easy to miss
447
+ * in a per-store sweep. Both reports carry a report-level `generatedAt`; a
448
+ * warning is gated against it exactly like a scanner finding against
449
+ * `scannedAt`: missing file → DROP (nothing to fix in a file that's gone),
450
+ * edited-since → DEMOTE (loses its line, keeps its rule/message, marked
451
+ * `STALE_LINE_MARKER`), live → unchanged. See
452
+ * `clients/finding-delivery-gate.ts` surface `lens-diagnostics:mode-delta`.
453
+ */
454
+ function applyDeltaFreshnessGate(files, cwd, generatedAt) {
455
+ if (!generatedAt)
456
+ return files;
457
+ const flat = [];
458
+ for (const file of files) {
459
+ for (const warning of file.warnings)
460
+ flat.push({ filePath: file.filePath, warning });
461
+ }
462
+ if (flat.length === 0)
463
+ return files;
464
+ const gated = gateFindingsByPathFreshness({
465
+ store: "lens-diagnostics-delta",
466
+ findings: flat,
467
+ cwd,
468
+ scannedAt: generatedAt,
469
+ citedPath: (f) => f.filePath,
470
+ onMissing: "drop",
471
+ });
472
+ // Two passes (live, then stale) reorder a file's demoted rows to the end
473
+ // of its warnings array rather than the original report order — cosmetic
474
+ // only, nothing is dropped or duplicated.
475
+ const byFile = new Map();
476
+ for (const f of gated.live) {
477
+ const arr = byFile.get(f.filePath) ?? [];
478
+ arr.push(f.warning);
479
+ byFile.set(f.filePath, arr);
480
+ }
481
+ for (const f of gated.stale) {
482
+ const arr = byFile.get(f.filePath) ?? [];
483
+ arr.push({ ...f.warning, stale: true, line: undefined });
484
+ byFile.set(f.filePath, arr);
485
+ }
486
+ return files
487
+ .map((file) => ({
488
+ filePath: file.filePath,
489
+ warnings: byFile.get(file.filePath) ?? [],
490
+ }))
491
+ .filter((file) => file.warnings.length > 0);
492
+ }
493
+ // @delivery-surface: lens-diagnostics:mode-delta
363
494
  function formatDeltaMode(cacheManager, cwd, severity, pathsScope) {
364
495
  const actionableEntry = cacheManager.readCache("actionable-warnings", cwd);
365
496
  const qualityEntry = cacheManager.readCache("code-quality-warnings", cwd);
@@ -387,8 +518,8 @@ function formatDeltaMode(cacheManager, cwd, severity, pathsScope) {
387
518
  warnings: applyRulePolicy(applyWeakDispositions(file.warnings ?? [], cwd, file.filePath), policyMap),
388
519
  }))
389
520
  .filter((file) => file.warnings.length > 0);
390
- const actionableFiles = visibleWarningFiles(actionable?.files);
391
- const qualityFiles = visibleWarningFiles(quality?.files);
521
+ const actionableFiles = applyDeltaFreshnessGate(visibleWarningFiles(actionable?.files), cwd, actionable?.generatedAt);
522
+ const qualityFiles = applyDeltaFreshnessGate(visibleWarningFiles(quality?.files), cwd, quality?.generatedAt);
392
523
  const lines = [];
393
524
  // Fixable warnings from actionable-warnings
394
525
  if (severity !== "error") {
@@ -396,7 +527,8 @@ function formatDeltaMode(cacheManager, cwd, severity, pathsScope) {
396
527
  const rel = path.relative(cwd, file.filePath);
397
528
  lines.push(`${rel}`);
398
529
  for (const w of file.warnings) {
399
- lines.push(` ⚠ L${w.line ?? "?"} ${w.rule ?? w.code ?? w.tool} ${w.message}`);
530
+ const where = w.stale ? STALE_LINE_MARKER : `L${w.line ?? "?"}`;
531
+ lines.push(` ⚠ ${where} ${w.rule ?? w.code ?? w.tool} ${w.message}`);
400
532
  }
401
533
  }
402
534
  }
@@ -407,7 +539,8 @@ function formatDeltaMode(cacheManager, cwd, severity, pathsScope) {
407
539
  if (!lines.includes(rel))
408
540
  lines.push(rel);
409
541
  for (const w of file.warnings) {
410
- lines.push(` ℹ L${w.line ?? "?"} ${w.rule ?? w.code ?? w.tool} ${w.message}`);
542
+ const where = w.stale ? STALE_LINE_MARKER : `L${w.line ?? "?"}`;
543
+ lines.push(` ℹ ${where} ${w.rule ?? w.code ?? w.tool} ${w.message}`);
411
544
  }
412
545
  }
413
546
  }
@@ -568,8 +701,12 @@ function applyProjectRulePolicy(value, policyMap) {
568
701
  return undefined;
569
702
  return { ...value, diagnostics: applyRulePolicy(value.diagnostics, policyMap) };
570
703
  }
571
- /** A diagnostic counts as error-like when it blocks or has error severity. */
704
+ /** A diagnostic counts as error-like when it blocks or has error severity.
705
+ * A `stale` (#1641 past-EOF) entry is excluded — its cited line no longer
706
+ * exists in the current file, so it can no longer count toward a hard stop. */
572
707
  function isErrorLike(d) {
708
+ if (d.stale)
709
+ return false;
573
710
  return d.semantic === "blocking" || d.severity === "error";
574
711
  }
575
712
  function matchesSeverity(d, severity) {
@@ -620,7 +757,7 @@ function lspDiagnosticToWidget(diagnostic) {
620
757
  tool: "lsp",
621
758
  };
622
759
  }
623
- function projectDiagnosticToWidget(diagnostic) {
760
+ function projectDiagnosticToWidget(diagnostic, filePath) {
624
761
  return {
625
762
  severity: diagnostic.severity,
626
763
  semantic: diagnostic.semantic,
@@ -629,6 +766,7 @@ function projectDiagnosticToWidget(diagnostic) {
629
766
  col: diagnostic.column,
630
767
  rule: diagnostic.rule ?? diagnostic.code,
631
768
  tool: diagnostic.runner || diagnostic.tool,
769
+ uri: widgetDiagnosticUri(filePath, diagnostic.line, diagnostic.column),
632
770
  };
633
771
  }
634
772
  // The ast-grep LSP keys its diagnostics `rule = "ast-grep:<id>"` (source:code,
@@ -650,11 +788,23 @@ function summarizeDiagnostics(filePath, diagnostics, hasFinalSnapshot) {
650
788
  let errors = 0;
651
789
  let warnings = 0;
652
790
  for (const diagnostic of diagnostics) {
791
+ // #1641: a demoted past-EOF entry keeps its severity for display but
792
+ // drops out of the tallies — same reasoning as widget-state's `isBlocking`.
793
+ if (diagnostic.stale)
794
+ continue;
653
795
  if (diagnostic.semantic === "blocking")
654
796
  blocking++;
655
797
  if (diagnostic.severity === "error")
656
798
  errors++;
657
- else if (diagnostic.severity === "warning")
799
+ // #1777: `hint` and `info` tally alongside `warning`, matching
800
+ // `countDiagnostics` in `clients/widget-state.ts` — mode=all reads that
801
+ // tally and mode=full reads this one, so the two must agree. An exact
802
+ // `=== "warning"` test scored a hint-only file 0/0/0, the `withIssues`
803
+ // filter below then dropped it, and mode=full rendered "No issues" for a
804
+ // file whose own `details` still carried the diagnostic.
805
+ else if (diagnostic.severity === "warning" ||
806
+ diagnostic.severity === "hint" ||
807
+ diagnostic.severity === "info")
658
808
  warnings++;
659
809
  }
660
810
  return {
@@ -666,6 +816,85 @@ function summarizeDiagnostics(filePath, diagnostics, hasFinalSnapshot) {
666
816
  diagnostics,
667
817
  };
668
818
  }
819
+ // #1618: sentence form used when exactly one reason accounts for every
820
+ // unconfirmed file — mirrors the pre-#1618 two-case ternary's phrasing so
821
+ // existing single-reason callers see unchanged text.
822
+ const UNCONFIRMED_REASON_SENTENCE = {
823
+ budget: "check didn't complete within budget",
824
+ inconclusive: "check was inconclusive (notify-write or diagnostics wait timed out)",
825
+ coverage_gap: "an auxiliary scanner coverage gap",
826
+ service_destroyed: "the LSP service was reset mid-sweep",
827
+ error: "check errored",
828
+ // #1618 review round 2: the LSP layer considered this touch CONFIRMED —
829
+ // the file's content changed under it (stale binding), which is a
830
+ // completely different failure from a timeout. Must never collapse into
831
+ // "within budget", the exact string this PR exists to stop misrendering.
832
+ binding_mismatch: "the file changed on disk since this result was computed",
833
+ };
834
+ // Short per-reason label used only when MORE than one reason is present, so
835
+ // the note can still say "N timed out, M errored" instead of collapsing to
836
+ // one sentence that can't carry two counts.
837
+ const UNCONFIRMED_REASON_COUNT_LABEL = {
838
+ budget: "timed out",
839
+ inconclusive: "inconclusive",
840
+ coverage_gap: "coverage gap",
841
+ service_destroyed: "service reset mid-sweep",
842
+ error: "errored",
843
+ binding_mismatch: "stale binding",
844
+ };
845
+ /**
846
+ * #1618: a result predating the `unconfirmedReason` field (a legacy/test
847
+ * double, or `LSPWorkspaceDiagnosticResult`'s own pre-#1618 shape) is
848
+ * classified from its OTHER fields exactly the way the dead
849
+ * `unconfirmedErrored = length - unconfirmedTimedOut` subtraction meant to,
850
+ * before that subtraction was structurally always 0 (the sweep's catch
851
+ * block sets both `error` and `timedOut`, so `unconfirmedErrored` never had
852
+ * a way to become nonzero). An explicit `result.error` now wins even when
853
+ * `unconfirmedReason` is absent, so an errored file is never rendered as
854
+ * "within budget".
855
+ *
856
+ * #1618 review round 2: `mismatched` MUST be checked first. A binding
857
+ * mismatch is discovered HERE, after the sweep already returned the result
858
+ * as confirmed (`!timedOut`, no `.error`, no `.unconfirmedReason`) — the
859
+ * `?? "budget"` fallback below would otherwise silently claim a
860
+ * stale-binding file as "within budget", the exact string this PR exists to
861
+ * stop misrendering.
862
+ */
863
+ function classifyUnconfirmedReason(result, mismatched) {
864
+ if (mismatched.has(result))
865
+ return "binding_mismatch";
866
+ return result.unconfirmedReason ?? (result.error ? "error" : "budget");
867
+ }
868
+ /**
869
+ * #1618: replaces the `unconfirmedTimedOut`/`unconfirmedErrored` dead
870
+ * subtraction with a real per-reason tally read off each result's own
871
+ * `unconfirmedReason` (falling back to `classifyUnconfirmedReason` for a
872
+ * legacy result that predates the field). `mismatched` is threaded through
873
+ * so a stale-binding file is never counted under a fallback reason it
874
+ * doesn't have (review round 2).
875
+ */
876
+ function tallyUnconfirmedReasons(results, mismatched) {
877
+ const tally = new Map();
878
+ for (const result of results) {
879
+ const reason = classifyUnconfirmedReason(result, mismatched);
880
+ tally.set(reason, (tally.get(reason) ?? 0) + 1);
881
+ }
882
+ return tally;
883
+ }
884
+ /** Render `tallyUnconfirmedReasons`' output as the clause inside the
885
+ * "unconfirmed (...)" note — a single sentence when one reason accounts for
886
+ * everything, else a comma-joined "N label" breakdown. */
887
+ function formatUnconfirmedReasonClause(tally) {
888
+ const entries = [...tally.entries()].filter(([, count]) => count > 0);
889
+ if (entries.length === 0)
890
+ return "";
891
+ if (entries.length === 1) {
892
+ return UNCONFIRMED_REASON_SENTENCE[entries[0][0]];
893
+ }
894
+ return entries
895
+ .map(([reason, count]) => `${count} ${UNCONFIRMED_REASON_COUNT_LABEL[reason]}`)
896
+ .join(", ");
897
+ }
669
898
  /**
670
899
  * #646: break the confirmed/unconfirmed LSP-sweep tally down PER primary
671
900
  * server id (typescript, pyright, marksman, ...) instead of one flat
@@ -770,10 +999,10 @@ function mergeDiagnosticsWithWidgetSummaries(widgetSummaries, lspResults, projec
770
999
  }
771
1000
  }
772
1001
  for (const diagnostic of projectSnapshot?.diagnostics ?? []) {
773
- addDiagnostic(path.resolve(diagnostic.filePath), projectDiagnosticToWidget(diagnostic));
1002
+ addDiagnostic(path.resolve(diagnostic.filePath), projectDiagnosticToWidget(diagnostic, diagnostic.filePath));
774
1003
  }
775
1004
  for (const diagnostic of projectDelta?.diagnostics ?? []) {
776
- addDiagnostic(path.resolve(diagnostic.filePath), projectDiagnosticToWidget(diagnostic));
1005
+ addDiagnostic(path.resolve(diagnostic.filePath), projectDiagnosticToWidget(diagnostic, diagnostic.filePath));
777
1006
  }
778
1007
  return [...byFile.values()];
779
1008
  }
@@ -896,6 +1125,7 @@ async function getProjectDiagnosticsSnapshotForFullMode(cwd, options) {
896
1125
  }
897
1126
  return undefined;
898
1127
  }
1128
+ // @delivery-surface: lens-diagnostics:mode-full
899
1129
  async function formatFullMode(cwd, severity, lspService, cacheManager, options = {}) {
900
1130
  const runWorkspaceDiagnostics = lspService.runWorkspaceDiagnostics;
901
1131
  if (typeof runWorkspaceDiagnostics !== "function") {
@@ -935,12 +1165,22 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
935
1165
  // could complete. Building its promise here, alongside the sweep and cheap
936
1166
  // scan, makes it genuinely concurrent — all three phases now race the SAME
937
1167
  // signal from the same starting point instead of stacking.
938
- const analyzersPromise = shouldIncludeProjectRunners(options.refreshRunners)
1168
+ const projectRunnersRequested = shouldIncludeProjectRunners(options.refreshRunners);
1169
+ const analyzersPromise = projectRunnersRequested
939
1170
  ? loadBootstrapClients().then((clients) => fetchFreshProjectDiagnostics(cacheManager, cwd, clients, signal, { runtime: options.runtime }))
940
1171
  : Promise.resolve({
941
1172
  diagnostics: [],
942
1173
  runners: [],
943
- cold: [],
1174
+ // #1623: every heavyweight analyzer is ELIGIBLE for this project but
1175
+ // this call never asked for it (refreshRunners wasn't cheap/all/
1176
+ // cached) — the expensive fetch below deliberately never runs in
1177
+ // that case (see the comment above), but rendering total silence
1178
+ // here reads exactly as "ran clean", the false-empty #1623 exists to
1179
+ // close. `ANALYZER_IDS` (fresh-fetch.ts) is the single source of
1180
+ // truth for which lanes this covers — reuse it rather than
1181
+ // hand-listing ids here.
1182
+ cold: [...ANALYZER_IDS],
1183
+ coldReasons: Object.fromEntries(ANALYZER_IDS.map((id) => [id, NOT_REQUESTED_REASON])),
944
1184
  failed: [],
945
1185
  timings: {},
946
1186
  });
@@ -960,7 +1200,14 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
960
1200
  analyzersPromise,
961
1201
  ]);
962
1202
  const aborted = signal?.aborted ?? false;
963
- const lspResults = rawLspResults.filter((result) => includeFile(result.filePath));
1203
+ // #1640: before ANY consumer sees them — the footer reconcile, the widget
1204
+ // merge, the rendered counts — demote TypeScript errors on files tsserver
1205
+ // checked in its INFERRED project. Applied once, here, so a single seam
1206
+ // governs the whole sweep instead of each renderer learning the rule.
1207
+ const lspResults = await demoteInferredProjectSweepResults(rawLspResults.filter((result) => includeFile(result.filePath)), cwd, lspService,
1208
+ // #1645 review F1: the sweep that produced these results is signal-bounded,
1209
+ // so the probe loop that post-processes them must be too.
1210
+ signal);
964
1211
  // A result bound to a different document must not replace the current
965
1212
  // widget state, even when it contains real diagnostics. Pull results may carry
966
1213
  // a content hash without the push binding verdict, so verify that hash against
@@ -981,13 +1228,15 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
981
1228
  const confirmedLspResults = lspResults.filter((result) => !result.timedOut &&
982
1229
  !result.error &&
983
1230
  !mismatchedLspResults.has(result));
1231
+ const fullyCoveredLspResults = confirmedLspResults.filter((result) => (result.unconfirmedServerIds?.length ?? 0) === 0);
1232
+ const partiallyCoveredLspResults = confirmedLspResults.filter((result) => (result.unconfirmedServerIds?.length ?? 0) > 0);
984
1233
  const unconfirmedLspResults = lspResults.filter((result) => result.timedOut ||
985
1234
  result.error ||
986
1235
  mismatchedLspResults.has(result));
987
1236
  // #571: reconcile this scan's fresh, CONFIRMED per-file results into the
988
1237
  // footer cache. A footer write is never allowed to fail the tool call, so
989
1238
  // any unexpected throw is swallowed.
990
- for (const result of confirmedLspResults) {
1239
+ for (const result of fullyCoveredLspResults) {
991
1240
  try {
992
1241
  // #692: provenance label ONLY — must never affect `rule`/identity (see
993
1242
  // `ConvertLspDiagnosticsOptions.scanOrigin`'s doc comment).
@@ -1053,6 +1302,22 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1053
1302
  // legitimately show diagnostics from widgetSummaries/project-runner state
1054
1303
  // below if those independently have entries for it.
1055
1304
  const summaries = await applyInlineSuppressionsToSummaries(mergeDiagnosticsWithWidgetSummaries(getFileDiagnosticSummaries().filter((summary) => includeFile(summary.filePath)), confirmedLspResults, projectSnapshot, projectDelta), cwd, policyMap);
1305
+ // #1888: the full-mode summary above is the first seam where every
1306
+ // producing lane is correlated. The earlier footer loop intentionally writes
1307
+ // only CONFIRMED LSP results, so an ast-grep backpressure failure left
1308
+ // project-scan (`ast-grep-napi`) findings visible to lens_diagnostics but
1309
+ // absent from the widget. Commit this same post-policy, post-suppression set
1310
+ // to the widget so its human-facing count cannot silently become single-lane.
1311
+ const parsedProjectScanObservedAt = projectSnapshot?.scannedAt
1312
+ ? Date.parse(projectSnapshot.scannedAt)
1313
+ : undefined;
1314
+ const projectScanObservedAt = parsedProjectScanObservedAt !== undefined &&
1315
+ Number.isFinite(parsedProjectScanObservedAt)
1316
+ ? parsedProjectScanObservedAt
1317
+ : undefined;
1318
+ for (const summary of summaries) {
1319
+ reconcileCorrelatedScanDiagnostics(summary.filePath, summary.diagnostics, nextWriteIndex?.(), projectScanObservedAt);
1320
+ }
1056
1321
  const result = formatAllMode(cwd, severity, summaries, {
1057
1322
  mode: "full",
1058
1323
  lspFilesChecked: rawLspResults.length,
@@ -1083,8 +1348,7 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1083
1348
  // as "0 diagnostics" — say explicitly which files the LSP sweep could not
1084
1349
  // confirm and why, distinguishing a hard error from a soft timeout the
1085
1350
  // same way #570 does upstream.
1086
- const unconfirmedTimedOut = unconfirmedLspResults.filter((result) => result.timedOut).length;
1087
- const unconfirmedErrored = unconfirmedLspResults.length - unconfirmedTimedOut;
1351
+ const unconfirmedByReason = tallyUnconfirmedReasons(unconfirmedLspResults, mismatchedLspResults);
1088
1352
  // #646: per-primary-server breakdown of the same confirmed/unconfirmed
1089
1353
  // tally — lets an agent see AT A GLANCE which server is responsible for
1090
1354
  // any unconfirmed files (e.g. "marksman: 0/34") instead of having to
@@ -1099,15 +1363,21 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1099
1363
  : "";
1100
1364
  const unconfirmedLspNote = unconfirmedLspResults.length > 0
1101
1365
  ? `\n\n⚠ LSP sweep: ${confirmedLspResults.length} file(s) confirmed via LSP, ` +
1102
- `${unconfirmedLspResults.length} unconfirmed (${unconfirmedTimedOut > 0 && unconfirmedErrored > 0
1103
- ? `${unconfirmedTimedOut} timed out, ${unconfirmedErrored} errored`
1104
- : unconfirmedTimedOut > 0
1105
- ? "check didn't complete within budget"
1106
- : "check errored"})${serverBreakdownClause} — NOT the same as 0 diagnostics for: ${unconfirmedLspResults
1366
+ `${unconfirmedLspResults.length} unconfirmed (${formatUnconfirmedReasonClause(unconfirmedByReason)})${serverBreakdownClause} — NOT the same as 0 diagnostics for: ${unconfirmedLspResults
1107
1367
  .slice(0, 20)
1108
1368
  .map((result) => result.filePath)
1109
1369
  .join(", ")}${unconfirmedLspResults.length > 20 ? ", …" : ""}. These files' LSP contribution is excluded from this result; re-run mode=full to retry them (they may still show findings above from cached/project-runner state).`
1110
1370
  : "";
1371
+ // Code-unit comparator (#1883): this list ships as the
1372
+ // `unconfirmedLspServerIds` structured field and as the lane names in the
1373
+ // agent-visible coverage note, so its order must be deterministic across
1374
+ // locales — localeCompare is deliberately avoided.
1375
+ const uncoveredScannerIds = [
1376
+ ...new Set(partiallyCoveredLspResults.flatMap((result) => result.unconfirmedServerIds ?? [])),
1377
+ ].sort((a, b) => Number(a > b) - Number(a < b));
1378
+ const auxiliaryCoverageNote = uncoveredScannerIds.length > 0
1379
+ ? `\n\n⚠ Auxiliary coverage incomplete: ${uncoveredScannerIds.join(", ")} did not answer for ${partiallyCoveredLspResults.length} file(s). Findings from answering servers are included; this is not a clean verdict for the named scanner lane(s).`
1380
+ : "";
1111
1381
  // #646: primary-vs-auxiliary split of the raw LSP-sweep findings (before
1112
1382
  // they're merged into the widget-state summaries below), mirroring
1113
1383
  // `lsp_diagnostics`' "Primary findings"/"Auxiliary findings" separation —
@@ -1139,13 +1409,31 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1139
1409
  // analyzers before anything spawns — rendering that as the per-analyzer
1140
1410
  // "not applicable / unavailable" list would send the caller chasing seven
1141
1411
  // wrong reasons. One note with the real one instead.
1412
+ // #1623: prefer the SPECIFIC reason captured at each gate
1413
+ // (`extracted.coldReasons`) over a generic guess — `formatNotRunEntry`
1414
+ // falls back to `warmTriggerFor` only for an id this call's `coldReasons`
1415
+ // doesn't cover (e.g. a caller-supplied `cold` list from an older cache
1416
+ // shape). Single formatter (extractors.ts) so this note's wording can't
1417
+ // drift from any other caller that renders the same `cold` list.
1418
+ // #1623 fix-round F5: the "not requested" (quick-mode) batch shares ONE
1419
+ // reason (`NOT_REQUESTED_REASON`) across every id — `formatNotRunEntry`
1420
+ // repeating that same ~130-char sentence per id makes the note nearly
1421
+ // unreadable, and the "not applicable / unavailable this run" header
1422
+ // below is a dishonest label for it: these lanes ARE applicable and
1423
+ // available, this call just never asked for them. Render that batch as
1424
+ // its own compact note — bare ids, the shared reason stated once — and
1425
+ // keep the detailed per-id format for genuinely cold lanes, where each
1426
+ // id's reason really does differ (not a git repo vs binary unavailable
1427
+ // vs retry cooldown, ...).
1142
1428
  const coldNote = extracted.unsafeRoot
1143
1429
  ? `\n\nheavyweight analyzers skipped: the working directory resolves at or above the home directory, so a fresh knip/jscpd/madge/gitleaks/govulncheck/trivy/dead-code scan would walk every unrelated tree under it. Re-run from inside a project directory. Absence of their findings is NOT a clean verdict.`
1144
- : genuinelyColdIds.length > 0
1145
- ? `\n\ncold (not applicable / unavailable this run): ${genuinelyColdIds
1146
- .map((id) => `${id} — ${warmTriggerFor(id)}`)
1147
- .join(", ")}. These analyzers have not contributed to this result — absence of their findings is NOT a clean verdict.`
1148
- : "";
1430
+ : !projectRunnersRequested && genuinelyColdIds.length > 0
1431
+ ? `\n\nnot run this call (quick mode): ${genuinelyColdIds.join(", ")}. ${NOT_REQUESTED_REASON}. Absence of their findings is NOT a clean verdict.`
1432
+ : genuinelyColdIds.length > 0
1433
+ ? `\n\ncold (not applicable / unavailable this run): ${genuinelyColdIds
1434
+ .map((id) => formatNotRunEntry(id, extracted.coldReasons))
1435
+ .join(", ")}. These analyzers have not contributed to this result — absence of their findings is NOT a clean verdict.`
1436
+ : "";
1149
1437
  // #1004: unlike every other analyzer above (knip/jscpd/madge/gitleaks/
1150
1438
  // govulncheck/opengrep/trivy/dead-code all run a FRESH whole-project scan
1151
1439
  // per the fresh-fetch.ts header, so "clean" there really does mean "the
@@ -1170,6 +1458,21 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1170
1458
  .map(({ id, summary }) => `${id} — ${summary}`)
1171
1459
  .join(", ")}. These analyzers were not cached and will be retried; absence of their findings is NOT a clean verdict.`
1172
1460
  : "";
1461
+ // #1617: the #1616 suppressed-bucket rule applied to mode=full — a finding
1462
+ // an agent/user marked false-positive/won't-fix now drops out of
1463
+ // `diagnostics` (fresh-fetch.ts's `record()`), but that drop must stay
1464
+ // visible as a count rather than reading as "nothing was wrong here".
1465
+ // Review-round F4 (#1625): per-lane attribution — "gitleaks 2, knip 1" says
1466
+ // WHICH analyzer's marks are doing the suppressing, not just a bare total.
1467
+ // A lane that's 100% suppressed still won't appear in `runners`/`cold` as
1468
+ // distinct from "ran clean" — that gap is #1623's lane-status territory,
1469
+ // not fixed here.
1470
+ const dispositionSuppressedByLaneText = Object.entries(extracted.dispositionSuppressedByLane ?? {})
1471
+ .map(([id, count]) => `${id} ${count}`)
1472
+ .join(", ");
1473
+ const dispositionSuppressedNote = (extracted.dispositionSuppressed ?? 0) > 0
1474
+ ? `\n\nsuppressed by disposition: ${extracted.dispositionSuppressed} finding(s) dropped from this result because they're marked false-positive or won't-fix (${dispositionSuppressedByLaneText}). Not a clean verdict for those locations — they were found, then intentionally hidden.`
1475
+ : "";
1173
1476
  // #747/#250: the cheap project-diagnostics scan (scanProjectDiagnostics) and
1174
1477
  // the LSP workspace sweep (collectWorkspaceDiagnosticFiles) both refuse to
1175
1478
  // WALK from a cwd at/above $HOME — from there, walking would enumerate every
@@ -1199,6 +1502,28 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1199
1502
  const generatedSkipNote = generatedSkipNoticeText
1200
1503
  ? `\n\n${generatedSkipNoticeText}`
1201
1504
  : "";
1505
+ // #1623: the cheap in-process project scan (tree-sitter/fact-rules/
1506
+ // ast-grep) is a THIRD lane `coldNote` never covers (it isn't one of
1507
+ // `ANALYZER_IDS` — that list is fetch-fetch.ts's heavyweight analyzers
1508
+ // only). It has three distinct states, all of which must render — the
1509
+ // fix-round F2 finding: with `refreshRunners=cached` and no snapshot ever
1510
+ // written yet, BOTH of the two notes below used to stay empty (one gated
1511
+ // on `scannedAt` being present, the other on the "not requested" branch
1512
+ // this call isn't in), so the original #1623 silence survived in exactly
1513
+ // this mode. `cheapScanScannedAtMs` centralizes the one bit every branch
1514
+ // needs: whether the stored snapshot has a usable timestamp (#1623
1515
+ // fix-round F4 — a corrupt/missing `scannedAt` must not compute a NaN
1516
+ // age, mirroring `./cache.ts`'s `Number.isFinite` guard).
1517
+ const cheapScanScannedAtMs = rawProjectSnapshot?.scannedAt
1518
+ ? Date.parse(rawProjectSnapshot.scannedAt)
1519
+ : undefined;
1520
+ const cheapScanStatusNote = !projectRunnersRequested
1521
+ ? `\n\ncheap project scan (tree-sitter/fact-rules/ast-grep): not run this call (${NOT_REQUESTED_REASON}).`
1522
+ : options.refreshRunners === "cached"
1523
+ ? cheapScanScannedAtMs !== undefined && Number.isFinite(cheapScanScannedAtMs)
1524
+ ? `\n\ncheap project scan (tree-sitter/fact-rules/ast-grep): served from cache, ${formatCacheAgeOld(Date.now() - cheapScanScannedAtMs)} — not re-run this call.`
1525
+ : `\n\ncheap project scan (tree-sitter/fact-rules/ast-grep): not run (no cached scan; refresh with refreshRunners=cheap/all to populate).`
1526
+ : "";
1202
1527
  const abortedNote = abortedIds.size > 0
1203
1528
  ? `\n\nstopped mid-scan (still running in the background, not reflected in this result): ${[
1204
1529
  ...abortedIds,
@@ -1208,12 +1533,25 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1208
1533
  // possibly-stale session_start cache — say so honestly, with per-analyzer
1209
1534
  // elapsed time, since this can legitimately take a while (trivy's own
1210
1535
  // ~180s ceiling).
1211
- const timingEntries = Object.entries(extracted.timings ?? {});
1536
+ // #1623: `extracted.cachedAgeMs` marks ids that are a cache-read BY DESIGN
1537
+ // (today, only test-runner — see fresh-fetch.ts) rather than a fresh
1538
+ // execution this call. Exclude those from "fetched fresh" below — folding
1539
+ // a cache replay into the same list as a genuine run is exactly the
1540
+ // misread the second #1623 dogfood pass reported (a cached result read as
1541
+ // "just ran"). They get their own note with an actual age instead.
1542
+ const cachedAgeMs = extracted.cachedAgeMs ?? {};
1543
+ const timingEntries = Object.entries(extracted.timings ?? {}).filter(([id]) => !(id in cachedAgeMs));
1212
1544
  const freshNote = timingEntries.length > 0
1213
1545
  ? `\n\nfetched fresh this call: ${timingEntries
1214
1546
  .map(([id, ms]) => `${id} (${Math.round(ms)}ms)`)
1215
1547
  .join(", ")}.`
1216
1548
  : "";
1549
+ const cachedAgeEntries = Object.entries(cachedAgeMs);
1550
+ const cachedAgeNote = cachedAgeEntries.length > 0
1551
+ ? `\n\nserved from cache this call (not re-run): ${cachedAgeEntries
1552
+ .map(([id, ms]) => `${id} (${formatCacheAgeOld(ms)})`)
1553
+ .join(", ")}.`
1554
+ : "";
1217
1555
  // coldRunners always lands in details (even when empty) so a caller can
1218
1556
  // reliably check "were any extractors cold" without a presence check.
1219
1557
  // analyzerTimingsMs surfaces the #585 fresh-fetch elapsed time per
@@ -1224,8 +1562,19 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1224
1562
  details: {
1225
1563
  ...result.details,
1226
1564
  coldRunners: extracted.cold,
1565
+ // #1623: the specific reason each cold id was skipped (single source
1566
+ // of truth: extractors.ts's `formatNotRunEntry` renders the same map
1567
+ // into the text note above) — lets a caller check WHY without parsing
1568
+ // text.
1569
+ coldReasons: extracted.coldReasons ?? {},
1227
1570
  failedAnalyzers,
1228
1571
  analyzerTimingsMs: extracted.timings,
1572
+ dispositionSuppressed: extracted.dispositionSuppressed ?? 0,
1573
+ dispositionSuppressedByLane: extracted.dispositionSuppressedByLane ?? {},
1574
+ // #1623: ms-old each cache-read-by-design lane's data was (today, only
1575
+ // test-runner) — lets a caller distinguish "just ran" from "served
1576
+ // from cache" without parsing the text note.
1577
+ analyzersCachedAgeMs: cachedAgeMs,
1229
1578
  analyzersAborted: extracted.aborted ?? false,
1230
1579
  analyzersAbortedIds: extracted.abortedIds ?? [],
1231
1580
  // #747: true when the fresh fetch refused an at-or-above-$HOME root —
@@ -1241,6 +1590,8 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1241
1590
  // any files unconfirmed" without re-deriving it from the text.
1242
1591
  lspFilesConfirmed: confirmedLspResults.length,
1243
1592
  lspFilesUnconfirmed: unconfirmedLspResults.length,
1593
+ lspFilesPartiallyCovered: partiallyCoveredLspResults.length,
1594
+ unconfirmedLspServerIds: uncoveredScannerIds,
1244
1595
  unconfirmedLspFiles: unconfirmedLspResults.map((result) => result.filePath),
1245
1596
  // #646: per-primary-server breakdown of the same tally, plus the
1246
1597
  // primary/auxiliary findings split — mirrors `lsp_diagnostics`'
@@ -1287,15 +1638,19 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1287
1638
  text: result.content[0].text +
1288
1639
  note +
1289
1640
  unconfirmedLspNote +
1641
+ auxiliaryCoverageNote +
1290
1642
  lspPrimaryVsAuxiliaryNote +
1291
1643
  coldNote +
1292
1644
  testRunnerEditScopedNote +
1293
1645
  failedNote +
1646
+ dispositionSuppressedNote +
1294
1647
  walkUnsafeRootNote +
1295
1648
  scanTruncatedNote +
1296
1649
  generatedSkipNote +
1297
1650
  abortedNote +
1298
1651
  freshNote +
1652
+ cachedAgeNote +
1653
+ cheapScanStatusNote +
1299
1654
  missingNote,
1300
1655
  },
1301
1656
  ],
@@ -1306,12 +1661,16 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1306
1661
  coldNote ||
1307
1662
  testRunnerEditScopedNote ||
1308
1663
  failedNote ||
1664
+ dispositionSuppressedNote ||
1309
1665
  walkUnsafeRootNote ||
1310
1666
  scanTruncatedNote ||
1311
1667
  generatedSkipNote ||
1312
1668
  abortedNote ||
1313
1669
  freshNote ||
1670
+ cachedAgeNote ||
1671
+ cheapScanStatusNote ||
1314
1672
  unconfirmedLspNote ||
1673
+ auxiliaryCoverageNote ||
1315
1674
  lspPrimaryVsAuxiliaryNote) {
1316
1675
  return {
1317
1676
  content: [
@@ -1319,15 +1678,19 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1319
1678
  type: "text",
1320
1679
  text: result.content[0].text +
1321
1680
  unconfirmedLspNote +
1681
+ auxiliaryCoverageNote +
1322
1682
  lspPrimaryVsAuxiliaryNote +
1323
1683
  coldNote +
1324
1684
  testRunnerEditScopedNote +
1325
1685
  failedNote +
1686
+ dispositionSuppressedNote +
1326
1687
  walkUnsafeRootNote +
1327
1688
  scanTruncatedNote +
1328
1689
  generatedSkipNote +
1329
1690
  abortedNote +
1330
1691
  freshNote +
1692
+ cachedAgeNote +
1693
+ cheapScanStatusNote +
1331
1694
  missingNote,
1332
1695
  },
1333
1696
  ],
@@ -1336,13 +1699,17 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1336
1699
  }
1337
1700
  return resultWithCold;
1338
1701
  }
1339
- function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(), detailOverrides = { mode: "all" }, staleDropped = 0, pathsScope) {
1702
+ // @delivery-surface: lens-diagnostics:mode-all
1703
+ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(), detailOverrides = { mode: "all" }, staleDropped = 0, pathsScope, dependencyDemoted = 0) {
1340
1704
  // Files changed/deleted since their diagnostics were recorded have already
1341
1705
  // been dropped by reconcileStaleWidgetFiles; note them so the agent knows
1342
1706
  // those aren't "clean", just un-rescanned (use mode=full to refresh).
1343
- const staleNote = staleDropped > 0
1707
+ const staleNote = (staleDropped > 0
1344
1708
  ? ` (${staleDropped} changed file${staleDropped === 1 ? "" : "s"} omitted as stale — use mode=full to rescan)`
1345
- : "";
1709
+ : "") +
1710
+ (dependencyDemoted > 0
1711
+ ? ` (${dependencyDemoted} blocking finding${dependencyDemoted === 1 ? "" : "s"} demoted to [stale] — an imported file changed since; re-run to confirm)`
1712
+ : "");
1346
1713
  // mode=full already actively scanned exactly the requested paths, so a zero
1347
1714
  // result there IS a legitimate clean read — the cache-only note only applies
1348
1715
  // to delta/all, which is why this is gated on detailOverrides.mode !== "full".
@@ -1370,14 +1737,43 @@ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(),
1370
1737
  return s;
1371
1738
  return summarizeDiagnostics(s.filePath, policyKept, s.hasFinalSnapshot);
1372
1739
  });
1373
- const visibleSummaries = dispositioned.filter((s) => includeFile(s.filePath));
1374
- // Filter to files with actual issues
1740
+ // #1641: the single chokepoint both mode=all (cache-only `summaries`) and
1741
+ // mode=full (widget cache merged with a FRESH LSP sweep, above) converge
1742
+ // through — a fresh sweep's server can still be citing its own stale
1743
+ // in-memory document, so gating only the cached half would miss exactly
1744
+ // the live incident this issue reports. Runs AFTER dispositions/rule
1745
+ // policy (not before): a finding a mark or a `.pi-lens.json` rule policy
1746
+ // already dropped is not being served, so it must never trigger a resync
1747
+ // or a `diagnostic_past_eof` telemetry record on this call.
1748
+ const eofGated = dispositioned.map((s) => {
1749
+ const { diagnostics, demotedCount } = demotePastEofDiagnostics({
1750
+ store: "lens_diagnostics",
1751
+ cwd,
1752
+ filePath: s.filePath,
1753
+ diagnostics: s.diagnostics ?? [],
1754
+ resync: resyncDocumentOnPastEof,
1755
+ });
1756
+ if (demotedCount === 0)
1757
+ return s;
1758
+ return summarizeDiagnostics(s.filePath, diagnostics, s.hasFinalSnapshot);
1759
+ });
1760
+ const visibleSummaries = eofGated.filter((s) => includeFile(s.filePath));
1761
+ // Filter to files with actual issues. A file whose only findings were
1762
+ // demoted by the #1641 past-EOF gate has blocking/errors/warnings at 0 —
1763
+ // counting it as "issue-free" would tell the agent the file is CLEAN when
1764
+ // it actually has an unconfirmed finding waiting on a rescan. Surface it
1765
+ // under `severity: "all"` (the gate's own advisory tier) so it stays
1766
+ // visible; a narrower `error`/`warning` filter still excludes it, matching
1767
+ // how those filters already treat any other non-matching severity.
1375
1768
  const withIssues = visibleSummaries.filter((s) => {
1376
1769
  if (severity === "error")
1377
1770
  return s.blocking > 0 || s.errors > 0;
1378
1771
  if (severity === "warning")
1379
1772
  return s.warnings > 0;
1380
- return s.blocking > 0 || s.errors > 0 || s.warnings > 0;
1773
+ return (s.blocking > 0 ||
1774
+ s.errors > 0 ||
1775
+ s.warnings > 0 ||
1776
+ (s.diagnostics ?? []).some((d) => d.stale));
1381
1777
  });
1382
1778
  const pathsNote = isFullMode ? "" : pathsScopeCacheOnlyNote(pathsScope);
1383
1779
  if (withIssues.length === 0) {
@@ -1412,6 +1808,10 @@ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(),
1412
1808
  parts.push(`${s.warnings}W`);
1413
1809
  if (!s.hasFinalSnapshot)
1414
1810
  parts.push(`(pending)`);
1811
+ const staleCount = (s.diagnostics ?? []).filter((d) => d.stale).length;
1812
+ if (staleCount > 0) {
1813
+ parts.push(`${staleCount} stale — re-run to confirm`);
1814
+ }
1415
1815
  lines.push(`${rel} ${parts.join(" ")}`);
1416
1816
  // List the actual diagnostics (not just counts) so the agent can act on
1417
1817
  // them without re-running anything — same "L<line>: <message>" shape as the
@@ -1423,16 +1823,27 @@ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(),
1423
1823
  .sort(bySeverityThenLine);
1424
1824
  const shown = matching.slice(0, MAX_DIAGNOSTICS_PER_FILE);
1425
1825
  for (const d of shown) {
1426
- const marker = isErrorLike(d)
1427
- ? d.semantic === "blocking"
1428
- ? "🔴 "
1429
- : ""
1430
- : "";
1826
+ const marker = d.stale
1827
+ ? ""
1828
+ : isErrorLike(d)
1829
+ ? d.semantic === "blocking"
1830
+ ? "🔴 "
1831
+ : ""
1832
+ : "";
1431
1833
  const label = d.rule ?? d.tool;
1432
1834
  const tag = label ? ` [${label}]` : "";
1433
1835
  const flaggedTag = d.flagged ? " 📌 flagged-to-fix" : "";
1434
1836
  const msg = d.message.replace(/\s+/g, " ").trim();
1435
- lines.push(` ${marker}L${d.line ?? "?"}: ${msg}${tag}${flaggedTag}`);
1837
+ // #1641: a demoted past-EOF entry no longer has a trustworthy line —
1838
+ // show the marker instead of a coordinate that doesn't exist in the
1839
+ // current file, matching the #1622/#1627 stale-line render convention.
1840
+ // Past-EOF demotions replace the untrustworthy coordinate; other
1841
+ // demotions (#1631 drift) keep their in-bounds line and append the
1842
+ // shared stale marker instead.
1843
+ const pastEof = d.stale && (d.staleReason ?? "past-eof") === "past-eof";
1844
+ const loc = pastEof ? PAST_EOF_STALE_MARKER : `L${d.line ?? "?"}`;
1845
+ const staleTag = d.stale && !pastEof ? ` ${STALE_LINE_MARKER}` : "";
1846
+ lines.push(` ${marker}${loc}: ${msg}${tag}${flaggedTag}${staleTag}`);
1436
1847
  }
1437
1848
  if (matching.length > shown.length) {
1438
1849
  lines.push(` … ${matching.length - shown.length} more in this file (showing ${shown.length} of ${matching.length})`);
@@ -1441,12 +1852,20 @@ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(),
1441
1852
  totalErrors += s.errors;
1442
1853
  totalWarnings += s.warnings;
1443
1854
  }
1855
+ // #1799: `totalBlocking` and `totalErrors` count the same findings UNLESS a
1856
+ // dependency-drift demotion (#1631) has revoked blocking authority from an
1857
+ // error while leaving it in the error tally (widget-state.ts `isBlocking`
1858
+ // vs `countDiagnostics`) — that's a real, live disagreement between the two
1859
+ // totals, not double-counting. So `errors` renders only when
1860
+ // `blocking === 0`, same guard as the per-file row above: it shows the
1861
+ // drift-demoted findings the blocking line can't, without ever printing the
1862
+ // same findings twice under two labels when blocking > 0.
1444
1863
  const summary = [
1445
1864
  `\nSummary (${visibleSummaries.length} files diagnosed this session):`,
1446
1865
  totalBlocking > 0
1447
1866
  ? ` 🔴 ${totalBlocking} blocking error${totalBlocking === 1 ? "" : "s"}`
1448
1867
  : null,
1449
- totalErrors > 0
1868
+ totalBlocking === 0 && totalErrors > 0
1450
1869
  ? ` ${totalErrors} error${totalErrors === 1 ? "" : "s"}`
1451
1870
  : null,
1452
1871
  totalWarnings > 0