@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
@@ -4,6 +4,7 @@
4
4
  * Provides utilities for integrating the declarative dispatch system
5
5
  * with the existing index.ts tool_result handler.
6
6
  */
7
+ import { CASCADE_DIAGNOSTICS_TTL_MS } from "../cascade-types.js";
7
8
  import { getDiagnosticLogger } from "../diagnostic-logger.js";
8
9
  import { detectFileKind } from "../file-kinds.js";
9
10
  import { getLspCapableKinds, getPrimaryDispatchGroup, } from "../language-policy.js";
@@ -16,17 +17,18 @@ export { clearLatencyReports, formatLatencyReport, getLatencyReports };
16
17
  import * as nodeFs from "node:fs";
17
18
  import * as nodePath from "node:path";
18
19
  import { fileURLToPath } from "node:url";
20
+ import { CASCADE_NEIGHBOUR_BUDGET, deriveCascadeNeighbourBudget, } from "../cascade-budget.js";
19
21
  import { formatCascadeNeighborDiagnostics } from "../cascade-format.js";
20
22
  import { logCascade } from "../cascade-logger.js";
21
23
  import { getDiagnosticTracker } from "../diagnostic-tracker.js";
22
- import { isTierAwareCascadeEnabled, recordOutstandingCascadeTouch, } from "../lsp/cascade-tier.js";
23
- import { classifyCascadeWaitTier } from "../lsp/wait-policy/index.js";
24
- import { bindingStateLabel, } from "../lsp/diagnostic-binding.js";
24
+ import { classifyCascadeWaitTier, isTierAwareCascadeEnabled, recordOutstandingCascadeTouch, } from "../lsp/cascade-tier.js";
25
+ import { bindingStateLabel, touchCoverageGap, } from "../lsp/diagnostic-binding.js";
25
26
  import { getServersForFileWithConfig } from "../lsp/config.js";
26
27
  import { getLSPService } from "../lsp/index.js";
27
28
  import { isExternalOrVendorFile, normalizeMapKey } from "../path-utils.js";
28
29
  import { getProjectIgnoreMatcher } from "../file-utils.js";
29
- import { resetAstGrepUnsupportedLanguageLog } from "./runners/ast-grep-napi.js";
30
+ import { resetAstGrepNapiLoadState, resetAstGrepUnsupportedLanguageLog, } from "./runners/ast-grep-napi.js";
31
+ import { resetTreeSitterClientLoadState } from "../tree-sitter-shared.js";
30
32
  import { isTestRoleCollateral } from "../collateral-test-role.js";
31
33
  import { clearReviewGraphWorkspaceCache, getGraphBuildInfoForGraph, getGraphImportChanges, graphBuildInfoIsTrustworthy, } from "../review-graph/builder.js";
32
34
  import { buildReverseDependencyIndexFromGraph, getAffectedFilesFromIndex, patchReverseDependencyIndex, writeReverseDependencyIndexToSnapshot, } from "../reverse-deps.js";
@@ -299,13 +301,18 @@ export function resetDispatchBaselines(cwd) {
299
301
  if (cwd)
300
302
  applyProjectLensConfig(cwd);
301
303
  resetAstGrepUnsupportedLanguageLog();
304
+ // #1567: a genuine-failure verdict or a live transient cooldown on the
305
+ // napi load latch is session-scoped state, not process-lifetime state.
306
+ resetAstGrepNapiLoadState();
307
+ // #1592: same shape for the web-tree-sitter load latch — an evaluation-
308
+ // shaped rejection is session-scoped state, not process-lifetime state.
309
+ resetTreeSitterClientLoadState();
302
310
  sessionFacts.clearAll();
303
311
  resetSessionSlopScore();
304
312
  clearCoverageNoticeState();
305
313
  clearReviewGraphWorkspaceCache();
306
314
  clearReverseDepsIndexCache();
307
315
  clearModuleGraphCache();
308
- neighborTouchCache.clear();
309
316
  recentlyCleanNeighborCache.clear();
310
317
  primaryFilesThisTurn.clear();
311
318
  cascadeDiagnosticBaselines.clear();
@@ -326,13 +333,11 @@ let cascadeSessionStats = {
326
333
  export function getCascadeSessionStats() {
327
334
  return { ...cascadeSessionStats };
328
335
  }
329
- const neighborTouchCache = new Map();
330
336
  const recentlyCleanNeighborCache = new Map();
331
- /** O(1) entry counts of this module's turn-bounded caches (#1123 item 2
332
- * memory attribution) — both are `Map.size` reads, never iterated. */
337
+ /** O(1) entry count of this module's turn-bounded cache (#1123 item 2
338
+ * memory attribution) — a `Map.size` read, never iterated. */
333
339
  export function getDispatchCascadeCacheStats() {
334
340
  return {
335
- neighborTouchCacheSize: neighborTouchCache.size,
336
341
  recentlyCleanNeighborCacheSize: recentlyCleanNeighborCache.size,
337
342
  };
338
343
  }
@@ -347,14 +352,12 @@ function ensureCascadeTurnScope(turnSeq) {
347
352
  return;
348
353
  cascadeTurnScope = turnSeq;
349
354
  primaryFilesThisTurn.clear();
350
- neighborTouchCache.clear();
351
355
  for (const [key, entry] of recentlyCleanNeighborCache) {
352
356
  if (turnSeq - entry.turnSeq > RECENTLY_CLEAN_TTL_TURNS) {
353
357
  recentlyCleanNeighborCache.delete(key);
354
358
  }
355
359
  }
356
360
  }
357
- const CASCADE_TTL_MS = 240_000;
358
361
  const MAX_PER_FILE = RUNTIME_CONFIG.pipeline.cascadeMaxDiagnosticsPerFile;
359
362
  const MAX_FILES = RUNTIME_CONFIG.pipeline.cascadeMaxFiles;
360
363
  /**
@@ -412,6 +415,17 @@ function readBoundToCurrentDisk(rawDiags) {
412
415
  * a future flag cannot be silently missed at just one of the several gate sites:
413
416
  * - `inconclusive` (#1093/#571): the notify/diagnostics wait lapsed its deadline,
414
417
  * so a resolved `[]` is NOT a confirmed clean (the #533 false-clean trap).
418
+ * #1549: this now means a PRIMARY's deadline lapsed. A slow auxiliary lands in
419
+ * the coverage-gap bullet below instead of collapsing the whole touch, so this
420
+ * predicate still fails closed for it while the primary's findings survive —
421
+ * which is the point: the two disqualifiers were doing one job for two causes.
422
+ * - a COVERAGE GAP (#1470/#1493): an auxiliary never reported — its push wait was
423
+ * cut off by the aux grace timer (#1470), or it stayed silent with no stored
424
+ * publication for this content (#1493) — so the merged result is missing whatever
425
+ * that scanner would have said. Such a touch is deliberately NOT `inconclusive`
426
+ * (the primary answered), which is exactly why it needs naming here: reading
427
+ * `!inconclusive` alone would let a hung or silent opengrep wipe a live footer
428
+ * finding and seed the recently-clean cache.
415
429
  * - `binding.boundToCurrentDisk === false` (#1095): the diagnostics were computed
416
430
  * against a DIFFERENT disk state than what is on disk now (the server's view
417
431
  * diverged / a pre-fix buffer) — not an observation of current disk. `true` and
@@ -425,15 +439,38 @@ function readBoundToCurrentDisk(rawDiags) {
425
439
  function readInconclusive(rawDiags) {
426
440
  return rawDiags?.inconclusive === true;
427
441
  }
442
+ /**
443
+ * #1549: an inconclusive `neighbor_touch` row used to carry NO attribution, so a
444
+ * forensic sweep could only infer the cause from duration histograms. `touchFile`
445
+ * now names the PRIMARY server(s) whose deadline produced the verdict and which
446
+ * deadline it was (`notify-write` vs `diagnostics-wait`, or `mixed`) — carry both
447
+ * into cascade.log so the record stands on its own. Read off the wrapper, like
448
+ * every other flag here (#1179).
449
+ */
450
+ function readInconclusiveAttribution(rawDiags) {
451
+ const wrapper = rawDiags;
452
+ return {
453
+ ...(wrapper?.inconclusiveServerIds?.length && {
454
+ inconclusiveServerIds: [...wrapper.inconclusiveServerIds],
455
+ }),
456
+ ...(wrapper?.inconclusiveReason && {
457
+ inconclusiveReason: wrapper.inconclusiveReason,
458
+ }),
459
+ };
460
+ }
428
461
  function isConfirmedTouch(rawDiags) {
429
- return !readInconclusive(rawDiags) && readBoundToCurrentDisk(rawDiags) !== false;
462
+ return (!readInconclusive(rawDiags) &&
463
+ touchCoverageGap(rawDiags).length === 0 &&
464
+ readBoundToCurrentDisk(rawDiags) !== false);
430
465
  }
431
466
  const reverseDepsIndexCache = new Map();
432
467
  const REVERSE_DEPS_MAX_WARM_ROOTS = 8;
433
468
  const REVERSE_DEPS_IDLE_EVICT_MS_DEFAULT = 20 * 60_000;
434
469
  function reverseDepsIdleEvictMs() {
435
470
  const value = Number.parseInt(process.env.PI_LENS_REVERSE_DEPS_IDLE_EVICT_MS ?? "", 10);
436
- return Number.isSafeInteger(value) && value > 0 ? value : REVERSE_DEPS_IDLE_EVICT_MS_DEFAULT;
471
+ return Number.isSafeInteger(value) && value > 0
472
+ ? value
473
+ : REVERSE_DEPS_IDLE_EVICT_MS_DEFAULT;
437
474
  }
438
475
  function deleteReverseDepsEntry(key) {
439
476
  const entry = reverseDepsIndexCache.get(key);
@@ -494,8 +531,9 @@ export function _seedReverseDepsIndexCacheForTests(key, index, generation) {
494
531
  // one-hop set is always the floor (it sorts first, before depth-2). Both
495
532
  // env-tunable; set depth to 1 to restore the old one-hop-only behaviour.
496
533
  const CASCADE_TRANSITIVE_DEPTH = Math.max(1, Number.parseInt(process.env.PI_LENS_CASCADE_TRANSITIVE_DEPTH ?? "2", 10) || 2);
497
- const CASCADE_NEIGHBOUR_BUDGET = Math.max(MAX_FILES, Number.parseInt(process.env.PI_LENS_CASCADE_NEIGHBOUR_BUDGET ?? "40", 10) ||
498
- 40);
534
+ // #1462: the cap itself, and the derivation that narrows it when this run has
535
+ // already spent the settle window, live in clients/cascade-budget.ts alongside
536
+ // `cascadeSettleWaitMs` — the two knobs only make sense read together.
499
537
  // Exported (not just module-local) so the MCP warm-analyze seam
500
538
  // (clients/mcp/analyze.ts, #536) can gate its own buildOrUpdateGraph call on
501
539
  // the SAME file-kind eligibility this cascade path uses — one source of truth
@@ -599,8 +637,12 @@ function updateWordIndexForCascade(args) {
599
637
  export async function computeCascadeForFile(filePath, cwd, options = {}) {
600
638
  const reverseDepsTimersToRelease = new Set();
601
639
  const reverseDepsEntriesAtStart = new Set(reverseDepsIndexCache.values());
640
+ // #1462: legacy callers use this run's age when sizing the neighbour walk.
641
+ // The pipeline supplies a live turn_end clock so deferred work does not charge
642
+ // time that elapsed before turn_end began waiting.
643
+ const cascadeStart = Date.now();
602
644
  try {
603
- const { hasBlockers = false, dbg, turnSeq = 0, writeSeq, seqState, fileContent, wordIndex, onWordIndexUpdated, } = options;
645
+ const { hasBlockers = false, dbg, turnSeq = 0, writeSeq, seqState, turnEndCascadeSettleStart, fileContent, wordIndex, onWordIndexUpdated, } = options;
604
646
  ensureCascadeTurnScope(turnSeq);
605
647
  if (hasBlockers) {
606
648
  logCascade({
@@ -666,6 +708,24 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
666
708
  let importerSet = new Set();
667
709
  let callerSet = new Set();
668
710
  let referenceCount = 0;
711
+ let cascadeCandidateCount = 0;
712
+ let cascadeEligibleCount = 0;
713
+ let cascadeFilterDroppedCount = 0;
714
+ // #1446 item 4: how many eligible neighbors the flat CASCADE_NEIGHBOUR_BUDGET
715
+ // cut off, distinct from candidates dropped by the filters above it
716
+ // (missing on disk, vendor, ignored, already-primary-this-turn) — those are
717
+ // never actionable regardless of budget, so counting them as "truncated"
718
+ // would overstate what a larger budget could actually recover.
719
+ let cascadeBudgetTruncated = 0;
720
+ let transitiveTruncated = false;
721
+ // #1462: the budget actually in force for THIS run and the settle time left
722
+ // when it was sized. Both are overwritten inside the graph branch below,
723
+ // which is the only path that reaches `cascade_result` — the other branch
724
+ // returns a `non_code` skip before any of this is logged.
725
+ let cascadeNeighbourBudget = CASCADE_NEIGHBOUR_BUDGET;
726
+ let cascadeBudgetRemainingMs = 0;
727
+ let cascadeBudgetZone = "fits";
728
+ let cascadeDeliveryWindowMs = 0;
669
729
  if (CASCADE_GRAPH_KINDS.has(fileKind)) {
670
730
  const graphStart = Date.now();
671
731
  const graph = await buildOrUpdateGraph(cwd, [normalizedFile], sessionFacts, seqState);
@@ -947,6 +1007,29 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
947
1007
  });
948
1008
  }
949
1009
  }
1010
+ // #1462: size the rest of this run against the on-time window it has NOT
1011
+ // already spent. Derived HERE — after the graph build, the reverse-deps
1012
+ // refresh and the LSP-reference expansion, which is where the measured
1013
+ // overruns were bought — and used for both the transitive BFS cap and the
1014
+ // final slice, so a rescued run expands less as well as walking less.
1015
+ // Narrowing happens ONLY inside the rescue band (see cascade-budget.ts):
1016
+ // a run that is merely late keeps the whole set and is delivered complete
1017
+ // by #1443's carry-over, because a dropped neighbour is lost for good
1018
+ // while a late one is not. A wired callback that cannot observe an active
1019
+ // turn_end wait uses zero elapsed time rather than charging pre-turn age.
1020
+ {
1021
+ const settleStart = turnEndCascadeSettleStart?.();
1022
+ const elapsedMs = turnEndCascadeSettleStart
1023
+ ? settleStart !== undefined && Number.isFinite(settleStart)
1024
+ ? Math.max(0, Date.now() - settleStart)
1025
+ : 0
1026
+ : Date.now() - cascadeStart;
1027
+ const decision = deriveCascadeNeighbourBudget({ elapsedMs });
1028
+ cascadeNeighbourBudget = decision.budget;
1029
+ cascadeBudgetRemainingMs = decision.remainingMs;
1030
+ cascadeBudgetZone = decision.zone;
1031
+ cascadeDeliveryWindowMs = decision.deliveryWindowMs;
1032
+ }
950
1033
  // Bounded transitive expansion: add depth>1 dependents (indirect
951
1034
  // importers/callers/referencers) so the blast radius isn't limited to one
952
1035
  // hop. The one-hop sets above remain the floor (they sort first); these
@@ -954,8 +1037,9 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
954
1037
  if (CASCADE_TRANSITIVE_DEPTH > 1) {
955
1038
  const transitive = computeTransitiveImpact(graph, normalizedFile, {
956
1039
  maxDepth: CASCADE_TRANSITIVE_DEPTH,
957
- maxHits: CASCADE_NEIGHBOUR_BUDGET,
1040
+ maxHits: cascadeNeighbourBudget,
958
1041
  });
1042
+ transitiveTruncated = transitive.truncated;
959
1043
  const added = [
960
1044
  ...new Set(transitive.hits
961
1045
  .map((hit) => hit.file)
@@ -991,6 +1075,7 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
991
1075
  // touched solely for cascade diagnostics. Composes the shared `detectFileRole`
992
1076
  // seam; a classifier failure RETAINS the candidate (honest — never a false
993
1077
  // clean). The project ignore filter below is separate and unchanged (#297).
1078
+ cascadeCandidateCount = impact.neighborFiles.length;
994
1079
  impact.directImporters = impact.directImporters.filter((f) => !isTestRoleCollateral(f));
995
1080
  impact.directCallers = impact.directCallers.filter((f) => !isTestRoleCollateral(f));
996
1081
  impact.neighborFiles = impact.neighborFiles.filter((f) => !isTestRoleCollateral(f));
@@ -1004,7 +1089,7 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1004
1089
  ...impact.directCallers,
1005
1090
  ]);
1006
1091
  referenceCount = impact.neighborFiles.filter((n) => !importerOrCallerSet.has(n)).length;
1007
- sortedNeighbors = [...impact.neighborFiles]
1092
+ const eligibleNeighbors = [...impact.neighborFiles]
1008
1093
  .filter((n) => nodeFs.existsSync(n))
1009
1094
  .filter((n) => !isExternalOrVendorFile(n, cwd))
1010
1095
  // Honour the project's ignore config: a user-ignored neighbour (e.g.
@@ -1016,8 +1101,43 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1016
1101
  .sort((a, b) => {
1017
1102
  const rank = (p) => importerSet.has(p) ? 0 : callerSet.has(p) ? 1 : 2;
1018
1103
  return rank(a) - rank(b);
1019
- })
1020
- .slice(0, CASCADE_NEIGHBOUR_BUDGET);
1104
+ });
1105
+ cascadeEligibleCount = eligibleNeighbors.length;
1106
+ cascadeFilterDroppedCount = Math.max(0, cascadeCandidateCount - cascadeEligibleCount);
1107
+ cascadeBudgetTruncated = Math.max(0, eligibleNeighbors.length - cascadeNeighbourBudget);
1108
+ sortedNeighbors = eligibleNeighbors.slice(0, cascadeNeighbourBudget);
1109
+ if (cascadeBudgetTruncated > 0 || transitiveTruncated) {
1110
+ const budget = {
1111
+ candidateCount: cascadeCandidateCount,
1112
+ eligibleCount: cascadeEligibleCount,
1113
+ selectedCount: sortedNeighbors.length,
1114
+ truncatedCount: cascadeBudgetTruncated,
1115
+ ...(transitiveTruncated && { transitiveTruncated: true }),
1116
+ };
1117
+ const detailParts = [
1118
+ ...(cascadeBudgetTruncated > 0
1119
+ ? [
1120
+ `cascade neighbor budget checked ${budget.selectedCount} of ${budget.eligibleCount} eligible dependents ` +
1121
+ `(${budget.truncatedCount} omitted)`,
1122
+ ]
1123
+ : []),
1124
+ ...(transitiveTruncated
1125
+ ? [
1126
+ "transitive expansion was capped before all eligible dependents were enumerated",
1127
+ ]
1128
+ : []),
1129
+ ];
1130
+ const detail = detailParts.join("; ");
1131
+ impact.indeterminate = impact.indeterminate
1132
+ ? {
1133
+ ...impact.indeterminate,
1134
+ detail: impact.indeterminate.detail
1135
+ ? `${impact.indeterminate.detail}; ${detail}`
1136
+ : detail,
1137
+ budget,
1138
+ }
1139
+ : { reason: "budget_truncated", detail, budget };
1140
+ }
1021
1141
  }
1022
1142
  else {
1023
1143
  logCascade({
@@ -1043,7 +1163,13 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1043
1163
  callerCount: impact.directCallers.length,
1044
1164
  referenceCount: Math.max(0, referenceCount),
1045
1165
  riskFlags: impact.riskFlags,
1046
- metadata: { neighbors: sortedNeighbors.slice(0, 10) },
1166
+ metadata: {
1167
+ neighbors: sortedNeighbors.slice(0, 10),
1168
+ candidateNeighborCount: cascadeCandidateCount,
1169
+ eligibleNeighborCount: cascadeEligibleCount,
1170
+ filterDroppedCount: cascadeFilterDroppedCount,
1171
+ budgetTrimmedCount: cascadeBudgetTruncated,
1172
+ },
1047
1173
  });
1048
1174
  const lspService = getLSPService();
1049
1175
  // Hoist passive snapshot once — used for auto-propagating LSPs and fallback path.
@@ -1066,6 +1192,33 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1066
1192
  // the latter must not collapse into a clean-looking result (#1104 honesty
1067
1193
  // rule, same doctrine as #1023's graph-degraded indeterminate marker).
1068
1194
  let fallbackBindingRejected = false;
1195
+ const noLspCandidatePaths = new Set();
1196
+ // #1444: neighbours whose in-lane wait was skipped for the quiet-window
1197
+ // reconcile to answer later. Logged on `cascade_result` so a cascade that
1198
+ // deferred EVERY neighbour is distinguishable from a genuine leaf (both are
1199
+ // `neighborCount: 0` with no output otherwise).
1200
+ let collectLaterSkipped = 0;
1201
+ // #1446 item 5: `recentlyCleanNeighborCache` hits are the whole point of the
1202
+ // cache, but they were never counted — the only visible signal was 267s/day
1203
+ // of touch wall time with no way to tell whether the cache was absorbing
1204
+ // repeat work or every touch was cold. (#1899 removed the sibling
1205
+ // `cacheHits` counter along with the dead `neighborTouchCache`.)
1206
+ let recentlyCleanHits = 0;
1207
+ // F1 (#1446 follow-up): `coldTouches` must be counted at the point each
1208
+ // neighbour's OUTCOME is actually known, not derived from `coldSnapshotPaths`
1209
+ // (finalized earlier, before the cache-hit checks below run against it). Using
1210
+ // the pre-outcome list let a neighbour double-count (cold-snapshot AND cache/
1211
+ // recently-clean hit) or vanish from every bucket (an `activePaths` neighbour —
1212
+ // e.g. Python/Go — that misses both caches). These counters partition the
1213
+ // selected-neighbour outcomes; passive snapshots and no-server selections are
1214
+ // tracked separately instead of silently disappearing from the denominator.
1215
+ // `coldTouches` includes every genuine active touch attempt, while
1216
+ // `touchFailures` is its separate bounded failure subtype.
1217
+ let deferredTouches = 0;
1218
+ let coldTouches = 0;
1219
+ let passiveSnapshotHits = 0;
1220
+ let noLspConfigured = 0;
1221
+ let touchFailures = 0;
1069
1222
  if (sortedNeighbors.length > 0) {
1070
1223
  const snapshotPaths = sortedNeighbors.filter(shouldReadCascadeFromSnapshot);
1071
1224
  const activePaths = sortedNeighbors.filter((n) => !shouldReadCascadeFromSnapshot(n));
@@ -1080,7 +1233,7 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1080
1233
  const snapshotAgeSec = entry
1081
1234
  ? Math.round((Date.now() - entry.ts) / 1000)
1082
1235
  : undefined;
1083
- const ttlFresh = entry != null && Date.now() - entry.ts < CASCADE_TTL_MS;
1236
+ const ttlFresh = entry != null && Date.now() - entry.ts < CASCADE_DIAGNOSTICS_TTL_MS;
1084
1237
  // #1095: content binding is the INNER gate; TTL stays the outer bound.
1085
1238
  // false → the server's diagnostics were computed against a DIFFERENT
1086
1239
  // disk state (e.g. the PRE-fix content) — don't trust or
@@ -1128,6 +1281,7 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1128
1281
  // had no remaining purpose and is simply dropped rather than migrated.
1129
1282
  const diags = convertLspDiagnostics(entry.diags.filter((d) => d.severity === 1).slice(0, MAX_PER_FILE), neighborPath);
1130
1283
  producedLspData = true;
1284
+ passiveSnapshotHits++;
1131
1285
  const durationMs = Date.now() - neighborStart;
1132
1286
  logCascade({
1133
1287
  phase: "neighbor_snapshot",
@@ -1149,7 +1303,7 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1149
1303
  // LSP warning on the neighbor is preserved. Keyed by the primary edit's
1150
1304
  // `writeSeq` so a genuinely newer per-edit write still wins the
1151
1305
  // WriteOrderingGuard. `observedAt = entry.ts` (the snapshot's own publish
1152
- // time, up to CASCADE_TTL_MS old) — NOT now() — so replaying an aging
1306
+ // time, up to CASCADE_DIAGNOSTICS_TTL_MS old) — NOT now() — so replaying an aging
1153
1307
  // snapshot never re-arms the mtime-staleness gate (the same #1092
1154
1308
  // re-arming defect this PR fixes for cache hits).
1155
1309
  //
@@ -1179,13 +1333,14 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1179
1333
  const cacheKey = normalizeMapKey(neighborPath);
1180
1334
  const passiveEntry = allDiags.get(cacheKey);
1181
1335
  const hasFreshPassiveErrors = passiveEntry != null &&
1182
- Date.now() - passiveEntry.ts < CASCADE_TTL_MS &&
1336
+ Date.now() - passiveEntry.ts < CASCADE_DIAGNOSTICS_TTL_MS &&
1183
1337
  passiveEntry.diags.some((d) => d.severity === 1);
1184
1338
  const recentlyClean = recentlyCleanNeighborCache.get(cacheKey);
1185
1339
  if (recentlyClean &&
1186
1340
  turnSeq - recentlyClean.turnSeq <= RECENTLY_CLEAN_TTL_TURNS &&
1187
1341
  !hasFreshPassiveErrors) {
1188
1342
  producedLspData = true;
1343
+ recentlyCleanHits++;
1189
1344
  const durationMs = Date.now() - neighborStart;
1190
1345
  logCascade({
1191
1346
  phase: "neighbor_snapshot",
@@ -1208,32 +1363,13 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1208
1363
  durationMs,
1209
1364
  };
1210
1365
  }
1211
- // A5: skip re-touch if this neighbor was already diagnosed at the current
1212
- // write sequence. A new write (higher writeSeq) invalidates the cache entry.
1213
- const cached = writeSeq != null ? neighborTouchCache.get(cacheKey) : undefined;
1214
- if (cached?.turnSeq === turnSeq && cached?.writeSeq === writeSeq) {
1215
- producedLspData = true;
1216
- const durationMs = Date.now() - neighborStart;
1217
- logCascade({
1218
- phase: "neighbor_snapshot",
1219
- filePath,
1220
- neighborFile: neighborPath,
1221
- diagnosticCount: cached.diagnostics.length,
1222
- durationMs,
1223
- autoPropagate: false,
1224
- snapshotMissing: false,
1225
- metadata: { cachedWriteSeq: writeSeq },
1226
- });
1227
- return {
1228
- filePath: neighborPath,
1229
- reason: neighborReason(importerSet, callerSet, neighborPath),
1230
- diagnostics: cached.diagnostics,
1231
- lspTouched: false,
1232
- durationMs,
1233
- };
1234
- }
1366
+ // #1899: the A5 same-write cache read stood here. See the removal note
1367
+ // at the module's cache declarations — the gate could only pass inside
1368
+ // a single write, which never runs two cascades.
1235
1369
  const configuredServerCount = getServersForFileWithConfig(neighborPath).length;
1236
1370
  if (configuredServerCount === 0) {
1371
+ noLspConfigured++;
1372
+ noLspCandidatePaths.add(cacheKey);
1237
1373
  logCascade({
1238
1374
  phase: "neighbor_fallback",
1239
1375
  filePath,
@@ -1245,10 +1381,10 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1245
1381
  }
1246
1382
  // A6: async read to avoid blocking event loop on network-mounted drives
1247
1383
  const content = await nodeFs.promises.readFile(neighborPath, "utf8");
1248
- // #458: tier-aware cascade-lane wait. A Tier-3 (push-only,
1249
- // silent-on-clean — typescript is the lone core-set instance today)
1250
- // primary can never give this in-lane wait an affirmative clean
1251
- // signal, so the budget is pure cost. Fire the touch (didOpen/
1384
+ // #458/#1444: tier-aware cascade-lane wait. A Tier-3 silent server
1385
+ // cannot give this wait an affirmative clean signal. Native TS7 does
1386
+ // publish, but not inside the cold-snapshot budget. In both cases the
1387
+ // in-lane budget is pure cost. Fire the touch (didOpen/
1252
1388
  // didChange still happens — the server starts real work) and record
1253
1389
  // it as outstanding for the agent_settled quiet window to reconcile
1254
1390
  // instead of waiting here. Ambiguous/missing capability data always
@@ -1261,7 +1397,7 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1261
1397
  try {
1262
1398
  const snapshots = (await lspService.getCapabilitySnapshots?.(neighborPath)) ?? [];
1263
1399
  const tier = classifyCascadeWaitTier(lspService, neighborPath, snapshots);
1264
- if (tier === "tier3-silent") {
1400
+ if (tier === "tier3-silent" || tier === "collect-later") {
1265
1401
  const spawnedForTouch = await lspService.getClientForFile(neighborPath);
1266
1402
  if (spawnedForTouch) {
1267
1403
  // Sampled BEFORE the touchFile notify: a publish landing
@@ -1283,6 +1419,14 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1283
1419
  touchedAt,
1284
1420
  });
1285
1421
  const durationMs = Date.now() - neighborStart;
1422
+ if (tier === "collect-later")
1423
+ collectLaterSkipped++;
1424
+ // F1: both tier3-silent and collect-later skip the in-lane
1425
+ // wait and record an outstanding touch for the quiet-window
1426
+ // reconcile — neither a cache hit nor a genuine completed
1427
+ // cold touch, so both share this explicit "deferred" bucket
1428
+ // instead of falling out of the partition uncounted.
1429
+ deferredTouches++;
1286
1430
  logCascade({
1287
1431
  phase: "cascade_tier3_skip",
1288
1432
  filePath,
@@ -1290,11 +1434,14 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1290
1434
  durationMs,
1291
1435
  lspServerCount: configuredServerCount,
1292
1436
  coldSnapshot: isColdSnapshot,
1293
- metadata: { serverId: spawnedForTouch.client.serverId },
1437
+ metadata: {
1438
+ serverId: spawnedForTouch.client.serverId,
1439
+ waitTier: tier,
1440
+ },
1294
1441
  });
1295
1442
  // Deliberately NOT cached as clean/diagnosed — the wait was
1296
- // skipped, not resolved, so neither neighborTouchCache nor
1297
- // recentlyCleanNeighborCache may treat this as a real answer
1443
+ // skipped, not resolved, so recentlyCleanNeighborCache must
1444
+ // not treat this as a real answer
1298
1445
  // (#240 doctrine). Return undefined: the degraded-fallback
1299
1446
  // path below still has a chance to surface a passive/stale
1300
1447
  // snapshot, same as any other "no fresh data this touch" case.
@@ -1311,13 +1458,35 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1311
1458
  // Cold-snapshot neighbors (autoPropagate LSP, server warm) use a tighter
1312
1459
  // 1000ms budget — they should respond quickly; we'd rather return zero
1313
1460
  // than block cascade for 2s on a slow open.
1461
+ // F1: this is the ONE remaining outcome after cache hit, recently-clean
1462
+ // hit, and tier-aware deferral have all been ruled out — a genuine
1463
+ // active LSP touch is being issued right now. Count it here (an
1464
+ // attempt, whether it resolves, times out, or the promise rejects
1465
+ // below in the allSettled catch) rather than from `coldSnapshotPaths`,
1466
+ // which is finalized before any of the above checks run and includes
1467
+ // neighbours that resolve via cache/recently-clean instead.
1468
+ coldTouches++;
1469
+ // #1720: language-server scope only, matching the tier-aware touch
1470
+ // above (`clientScope: "primary"`, integration.ts:1826). A neighbor's
1471
+ // content did not change — only its import target did — so an
1472
+ // auxiliary scanner's (ast-grep/opengrep/typos) file-local verdict for
1473
+ // it cannot have changed; only cross-file type semantics (the
1474
+ // language server) can. `reconcileCascadeNeighborLspErrors` already
1475
+ // merges errors-only and preserves the neighbor's existing aux
1476
+ // findings by construction (see its doc comment and
1477
+ // `cascadeReconcilableLspErrors` above), so an `"all"`-scope touch
1478
+ // paid full aux notify traffic and aux confirmation-wait latency for
1479
+ // a re-derivation nothing downstream ever reads. `"primary"` resolves
1480
+ // per `neighborPath` (`getClientForFile` → `getServersForFileWithConfig`),
1481
+ // so a multi-language neighbor still gets its OWN language server,
1482
+ // not the primary edit's.
1314
1483
  const rawDiags = await lspService.touchFile(neighborPath, content, {
1315
1484
  diagnostics: "document",
1316
1485
  collectDiagnostics: true,
1317
1486
  maxClientWaitMs: isColdSnapshot ? 1000 : 2000,
1318
1487
  silent: true,
1319
1488
  source: "cascade",
1320
- clientScope: "all",
1489
+ clientScope: "primary",
1321
1490
  });
1322
1491
  if (!rawDiags)
1323
1492
  return undefined;
@@ -1336,6 +1505,11 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1336
1505
  const confirmed = isConfirmedTouch(rawDiags);
1337
1506
  const bindingRejected = readBoundToCurrentDisk(rawDiags) === false;
1338
1507
  const inconclusive = readInconclusive(rawDiags);
1508
+ // #1470/#1493: the third, independent reason a touch is unconfirmed — an
1509
+ // auxiliary that never reported, cut off by our grace timer or silent
1510
+ // with nothing published for this content. Logged alongside the other two
1511
+ // so cascade.log alone still tells the three apart.
1512
+ const unconfirmedServerIds = touchCoverageGap(rawDiags);
1339
1513
  // #692: `source: "cascade"` no longer overrides `rule` (see the
1340
1514
  // doc comment on the sibling call above) — dropped rather than
1341
1515
  // migrated to `scanOrigin` since cascade output never touches
@@ -1345,14 +1519,7 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1345
1519
  // wrapper and are unaffected by this copy (the shape-5 fix).
1346
1520
  const diags = convertLspDiagnostics(rawDiags.diags.filter((d) => d.severity === 1).slice(0, MAX_PER_FILE), neighborPath);
1347
1521
  const durationMs = Date.now() - neighborStart;
1348
- // Update cache for this neighbor at the current write sequence
1349
- if (writeSeq != null) {
1350
- neighborTouchCache.set(cacheKey, {
1351
- turnSeq,
1352
- writeSeq,
1353
- diagnostics: diags,
1354
- });
1355
- }
1522
+ // #1899: the A5 same-write cache write stood here.
1356
1523
  if (diags.length === 0) {
1357
1524
  // Only a CONFIRMED clean touch may seed the recently-clean cache
1358
1525
  // (#1095: a bound-false touch is unconfirmed, exactly like inconclusive).
@@ -1385,7 +1552,15 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1385
1552
  // stays conditional since "bound" carries no extra signal.
1386
1553
  metadata: {
1387
1554
  inconclusive,
1388
- ...(bindingRejected && { bindingState: bindingStateLabel(false) }),
1555
+ // #1549: WHICH primary and WHICH deadline, present only when the
1556
+ // touch actually reported itself inconclusive.
1557
+ ...(inconclusive && readInconclusiveAttribution(rawDiags)),
1558
+ ...(bindingRejected && {
1559
+ bindingState: bindingStateLabel(false),
1560
+ }),
1561
+ ...(unconfirmedServerIds.length > 0 && {
1562
+ unconfirmedServerIds: [...unconfirmedServerIds],
1563
+ }),
1389
1564
  },
1390
1565
  });
1391
1566
  // #1093/#1095: a completed, CONFIRMED active touch is a confirmed
@@ -1411,6 +1586,16 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1411
1586
  reason: neighborReason(importerSet, callerSet, neighborPath),
1412
1587
  diagnostics: diags,
1413
1588
  lspTouched: true,
1589
+ ...(inconclusive && { inconclusive: true }),
1590
+ // #1459: carry the coverage gap to the agent-facing surface. A
1591
+ // scanner whose breaker was open, whose resync the fan-out gate
1592
+ // deferred, or whom the aux grace timer cut off never looked at this
1593
+ // file — without this the neighbour reaches the agent as
1594
+ // `{ diagnostics: [] }` with no marker, which is the false-clean the
1595
+ // touch already refuses to claim for itself.
1596
+ ...(unconfirmedServerIds.length > 0 && {
1597
+ unconfirmedServerIds: [...unconfirmedServerIds],
1598
+ }),
1414
1599
  durationMs,
1415
1600
  };
1416
1601
  }));
@@ -1423,10 +1608,11 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1423
1608
  neighbors.push(result.value);
1424
1609
  }
1425
1610
  else {
1611
+ touchFailures++;
1426
1612
  // A3: one failed LSP doesn't kill the rest — fall back to passive snapshot
1427
1613
  dbg?.(`cascade neighbor touch error for ${neighborPath}: ${result.reason}`);
1428
1614
  const entry = allDiags.get(normalizeMapKey(neighborPath));
1429
- const ttlFresh = entry != null && Date.now() - entry.ts < CASCADE_TTL_MS;
1615
+ const ttlFresh = entry != null && Date.now() - entry.ts < CASCADE_DIAGNOSTICS_TTL_MS;
1430
1616
  // #1104: consult binding before trusting a TTL-fresh fallback snapshot —
1431
1617
  // MATCH #1100/#1095 semantics (false → skip, "unknown" → keep the
1432
1618
  // pre-#1104 TTL-only behavior, true → use). Without this, a failed
@@ -1473,8 +1659,15 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1473
1659
  }
1474
1660
  }
1475
1661
  // CR-3/A2: degraded fallback when no neighbor produced trustworthy LSP data —
1476
- // not merely when the graph returned zero neighbors.
1477
- if (!producedLspData) {
1662
+ // not merely when the graph returned zero neighbors. In a mixed run, only
1663
+ // candidates that had no configured LSP may use passive fallback; broadening
1664
+ // to unrelated cache entries would make the partial active result misleading.
1665
+ if (producedLspData && noLspCandidatePaths.size > 0) {
1666
+ const bindingRejected = appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd, filePath, noLspCandidatePaths);
1667
+ if (bindingRejected)
1668
+ fallbackBindingRejected = true;
1669
+ }
1670
+ else if (!producedLspData) {
1478
1671
  const bindingRejected = appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd, filePath);
1479
1672
  if (bindingRejected)
1480
1673
  fallbackBindingRejected = true;
@@ -1505,6 +1698,12 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1505
1698
  };
1506
1699
  }
1507
1700
  const filesWithErrors = visibleNeighbors.filter((n) => n.diagnostics.length > 0).length;
1701
+ const selectedOutcomeCount = passiveSnapshotHits +
1702
+ recentlyCleanHits +
1703
+ deferredTouches +
1704
+ coldTouches +
1705
+ noLspConfigured;
1706
+ const selectedOutcomeGap = Math.max(0, sortedNeighbors.length - selectedOutcomeCount);
1508
1707
  logCascade({
1509
1708
  phase: "cascade_result",
1510
1709
  filePath,
@@ -1513,9 +1712,60 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1513
1712
  metadata: {
1514
1713
  filesWithErrors,
1515
1714
  hasOutput: formatted.length > 0,
1715
+ indeterminateReason: impact.indeterminate?.reason,
1716
+ candidateNeighborCount: cascadeCandidateCount,
1717
+ eligibleNeighborCount: cascadeEligibleCount,
1718
+ filterDroppedCount: cascadeFilterDroppedCount,
1719
+ selectedNeighborCount: sortedNeighbors.length,
1720
+ selectedOutcomeCount,
1721
+ selectedOutcomeGap,
1722
+ passiveSnapshotHits,
1723
+ noLspConfigured,
1724
+ touchFailures,
1725
+ // #1444: >0 means "answers are still outstanding", not "nothing found".
1726
+ collectLaterSkipped,
1516
1727
  // Log when cascade ran but found nothing — distinguishes "clean" from "no signal"
1517
1728
  noNeighbors: visibleNeighbors.length === 0,
1518
1729
  noErrors: visibleNeighbors.length > 0 && filesWithErrors === 0,
1730
+ // #1446 item 5: cache effectiveness as a number instead of an inference
1731
+ // from `coldSnapshot`/`snapshotMissing` flags scattered across
1732
+ // per-neighbor `neighbor_touch`/`neighbor_snapshot` rows.
1733
+ // F1: recentlyCleanHits + deferredTouches + coldTouches
1734
+ // retain their outcome semantics. Passive snapshots and no-server selections
1735
+ // complete the selected-neighbor denominator; touchFailures is a bounded
1736
+ // subtype of coldTouches, not an additional outcome.
1737
+ recentlyCleanHits,
1738
+ deferredTouches,
1739
+ coldTouches,
1740
+ // #1446 item 4: the budget in force and how many eligible candidates
1741
+ // it cut off this run — the correctness half (a truncated run being
1742
+ // silently discarded) is #1443.
1743
+ // #1462: `neighborBudget` is now the DERIVED budget (still "the one in
1744
+ // force"); `neighborBudgetCeiling` is the flat cap, `budgetRemainingMs`
1745
+ // the on-time window left when it was sized, and `budgetZone` the rule
1746
+ // that decided it. Only `budgetZone: "narrowed"` shortens a walk — the
1747
+ // other three keep the flat cap, so a row where budget < ceiling is a
1748
+ // deliberate rescue, never a cold-start stub. `budgetDeliveryWindowMs`
1749
+ // records how long the pipeline (turn_end settle + quiet-window drain)
1750
+ // keeps a slow run alive, which is why the other zones can afford to
1751
+ // stay wide.
1752
+ //
1753
+ // NOTE for whoever reads these rows: `budgetTruncated` INVERTS under a
1754
+ // narrowed budget — it is `eligible - budget`, so a smaller budget
1755
+ // mechanically raises it. Compare truncation WITHIN a zone, never
1756
+ // across one.
1757
+ neighborBudget: cascadeNeighbourBudget,
1758
+ neighborBudgetCeiling: CASCADE_NEIGHBOUR_BUDGET,
1759
+ budgetRemainingMs: cascadeBudgetRemainingMs,
1760
+ budgetZone: cascadeBudgetZone,
1761
+ budgetDeliveryWindowMs: cascadeDeliveryWindowMs,
1762
+ budgetTruncated: cascadeBudgetTruncated,
1763
+ transitiveTruncated,
1764
+ budgetTrimmedCount: cascadeBudgetTruncated,
1765
+ budgetDerivationDisabled: cascadeBudgetZone === "no-rescue-window",
1766
+ budgetDerivationDisabledReason: cascadeBudgetZone === "no-rescue-window"
1767
+ ? "no-rescue-window"
1768
+ : undefined,
1519
1769
  neighbors: visibleNeighbors.slice(0, 10).map((n) => ({
1520
1770
  file: n.filePath.replace(/\\/g, "/").split("/").slice(-2).join("/"),
1521
1771
  diagnostics: n.diagnostics.length,
@@ -1539,6 +1789,7 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1539
1789
  diagnosticCount: diagCount,
1540
1790
  skipReason: "indeterminate",
1541
1791
  indeterminate: impact.indeterminate,
1792
+ selectedNeighborPaths: sortedNeighbors.slice(),
1542
1793
  };
1543
1794
  }
1544
1795
  const skipReason = visibleNeighbors.length === 0 ? "no_neighbors" : "clean";
@@ -1616,7 +1867,7 @@ function applyCascadeDeltaBaselines(neighbors) {
1616
1867
  * because its content binding was rejected (#1104) — the caller folds this
1617
1868
  * into the run-level `fallbackBindingRejected` flag for the honesty check.
1618
1869
  */
1619
- function appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd, filePath) {
1870
+ function appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd, filePath, allowedPaths) {
1620
1871
  const now = Date.now();
1621
1872
  const seen = new Set(neighbors.map((n) => normalizeMapKey(n.filePath)));
1622
1873
  let bindingRejected = false;
@@ -1625,6 +1876,8 @@ function appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd, fi
1625
1876
  const diagKey = normalizeMapKey(diagPath);
1626
1877
  if (diagKey === normalizedFileKey || seen.has(diagKey))
1627
1878
  continue;
1879
+ if (allowedPaths && !allowedPaths.has(diagKey))
1880
+ continue;
1628
1881
  if (primaryFilesThisTurn.has(diagKey))
1629
1882
  continue;
1630
1883
  if (isExternalOrVendorFile(diagPath, cwd))
@@ -1638,7 +1891,7 @@ function appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd, fi
1638
1891
  continue;
1639
1892
  if (!nodeFs.existsSync(diagPath))
1640
1893
  continue;
1641
- if (now - ts > CASCADE_TTL_MS)
1894
+ if (now - ts > CASCADE_DIAGNOSTICS_TTL_MS)
1642
1895
  continue;
1643
1896
  // #1104: a TTL-fresh entry is not automatically trustworthy — consult
1644
1897
  // binding the same way the reconcile path (#1100) and the touch-error
@@ -1742,7 +1995,7 @@ export async function dispatchLintWithResult(filePath, cwd, pi, modifiedRanges,
1742
1995
  // Default true preserves the per-edit fast path (errors only). Callers that
1743
1996
  // want the full picture (warnings + structural smells), e.g. the MCP review
1744
1997
  // facade, pass blockingOnly=false to run every runner.
1745
- const ctx = createDispatchContext(filePath, cwd, pi, sessionFacts, options?.blockingOnly ?? true, modifiedRanges, options?.projectRoot, options?.writeIndex);
1998
+ const ctx = createDispatchContext(filePath, cwd, pi, sessionFacts, options?.blockingOnly ?? true, modifiedRanges, options?.projectRoot, options?.writeIndex, options?.telemetryModel, options?.telemetryProvider);
1746
1999
  sessionFacts.clearFileFactsFor(ctx.filePath);
1747
2000
  const kind = ctx.kind;
1748
2001
  if (!kind) {