@herbertgao/pi-extensions 2026.8.5 → 2026.8.7

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