@herbertgao/pi-extensions 2026.9.1 → 2026.9.3

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 (341) hide show
  1. package/README.md +3 -2
  2. package/THIRD_PARTY_NOTICES.md +24 -0
  3. package/node_modules/@herbertgao/sol-pi/LICENSE +19 -0
  4. package/node_modules/@herbertgao/sol-pi/README.md +159 -0
  5. package/node_modules/@herbertgao/sol-pi/SECURITY.md +26 -0
  6. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +19 -0
  7. package/node_modules/@herbertgao/sol-pi/agents-install.md +150 -0
  8. package/node_modules/@herbertgao/sol-pi/assets/sol-pi-hero.png +0 -0
  9. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +69 -0
  10. package/node_modules/@herbertgao/sol-pi/docs/configuration.md +75 -0
  11. package/node_modules/@herbertgao/sol-pi/package.json +76 -0
  12. package/node_modules/@herbertgao/sol-pi/scripts/check-pi-compat.mjs +32 -0
  13. package/node_modules/@herbertgao/sol-pi/scripts/check-sol-pi-config.mjs +120 -0
  14. package/node_modules/@herbertgao/sol-pi/sol-pi.example.json +10 -0
  15. package/node_modules/@herbertgao/sol-pi/src/sol-pi/config.ts +135 -0
  16. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/file-queue.ts +74 -0
  17. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +185 -0
  18. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/then-run.ts +128 -0
  19. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/archive.ts +53 -0
  20. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/candidate.ts +101 -0
  21. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/config.ts +71 -0
  22. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/index.ts +220 -0
  23. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/journal.ts +25 -0
  24. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/provider.ts +164 -0
  25. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/receipt.ts +177 -0
  26. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/index.ts +227 -0
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/ledger.ts +20 -0
  28. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/observation.ts +252 -0
  29. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +237 -0
  30. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +455 -0
  31. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/index.ts +49 -0
  32. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/plan.ts +79 -0
  33. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +208 -0
  34. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/tools.ts +100 -0
  35. package/node_modules/@herbertgao/sol-pi/src/sol-pi/index.ts +42 -0
  36. package/node_modules/@herbertgao/sol-pi/src/sol-pi/runtime-paths.ts +17 -0
  37. package/node_modules/@herbertgao/sol-pi/src/sol-pi/tui.ts +71 -0
  38. package/node_modules/@narumitw/pi-btw/README.md +57 -141
  39. package/node_modules/@narumitw/pi-btw/dist/index.ts +478 -43
  40. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  41. package/node_modules/@narumitw/pi-btw/docs/workflows.md +74 -0
  42. package/node_modules/@narumitw/pi-btw/package.json +2 -1
  43. package/node_modules/@narumitw/pi-btw/src/btw.ts +28 -3
  44. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +32 -2
  45. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +330 -0
  46. package/node_modules/@narumitw/pi-btw/src/menu.ts +136 -19
  47. package/node_modules/@narumitw/pi-btw/src/settings.ts +32 -0
  48. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +57 -5
  49. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +52 -30
  50. package/node_modules/pi-lens/CHANGELOG.md +260 -0
  51. package/node_modules/pi-lens/README.md +13 -8
  52. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -1
  53. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -7
  54. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +1 -1
  55. package/node_modules/pi-lens/dist/clients/analysed-root.js +1 -0
  56. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +1 -1
  57. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +1 -8
  58. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +2 -0
  59. package/node_modules/pi-lens/dist/clients/bash-file-access.js +133 -1
  60. package/node_modules/pi-lens/dist/clients/biome-client.js +13 -2
  61. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -6
  62. package/node_modules/pi-lens/dist/clients/bootstrap.js +4 -4
  63. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +1 -1
  64. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +2 -5
  65. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +1 -9
  66. package/node_modules/pi-lens/dist/clients/cache-observability.js +122 -6
  67. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +2 -2
  68. package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -7
  69. package/node_modules/pi-lens/dist/clients/child-unref.js +1 -19
  70. package/node_modules/pi-lens/dist/clients/complexity-client.js +5 -0
  71. package/node_modules/pi-lens/dist/clients/config-core/index.js +11 -8
  72. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +1 -1
  73. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +2 -9
  74. package/node_modules/pi-lens/dist/clients/config-core/records.js +1 -1
  75. package/node_modules/pi-lens/dist/clients/config-core/schema.js +3 -5
  76. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +3 -1
  77. package/node_modules/pi-lens/dist/clients/config-resolve.js +10 -7
  78. package/node_modules/pi-lens/dist/clients/config-schema.js +56 -2
  79. package/node_modules/pi-lens/dist/clients/config-warn.js +2 -2
  80. package/node_modules/pi-lens/dist/clients/dead-code-client.js +8 -1
  81. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -4
  82. package/node_modules/pi-lens/dist/clients/dependency-checker.js +5 -1
  83. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +1 -1
  84. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +1 -1
  85. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -4
  86. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +38 -41
  87. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +1 -1
  88. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +5 -1
  89. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +18 -1
  90. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -6
  91. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +1 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -3
  93. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +2 -2
  94. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +1 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +2 -1
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +3 -1
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +2 -3
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +7 -2
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +2 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +2 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +2 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +2 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +2 -1
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +2 -1
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +2 -1
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +2 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +2 -1
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +2 -1
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +2 -1
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +3 -1
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +3 -2
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +2 -1
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +2 -1
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +2 -1
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +19 -1
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +3 -2
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +3 -1
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +2 -1
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +2 -1
  122. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +2 -1
  123. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +15 -11
  124. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +2 -1
  125. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +2 -1
  126. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +3 -3
  127. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +12 -11
  128. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +3 -2
  129. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -1
  130. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +5 -3
  131. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  132. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +3 -2
  133. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +3 -2
  134. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +2 -1
  135. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  136. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +2 -1
  137. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +2 -1
  138. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +2 -1
  139. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
  140. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
  141. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +1 -64
  142. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
  143. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -21
  144. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +2 -1
  145. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +2 -20
  146. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +5 -3
  147. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +2 -1
  148. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +2 -2
  149. package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -7
  150. package/node_modules/pi-lens/dist/clients/effective-config.js +12 -2
  151. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +2 -2
  152. package/node_modules/pi-lens/dist/clients/extension-log.js +2 -0
  153. package/node_modules/pi-lens/dist/clients/file-kinds.js +1 -38
  154. package/node_modules/pi-lens/dist/clients/file-role.js +18 -1
  155. package/node_modules/pi-lens/dist/clients/file-time.js +1 -1
  156. package/node_modules/pi-lens/dist/clients/file-utils.js +5 -1
  157. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +4 -0
  158. package/node_modules/pi-lens/dist/clients/format-service.js +0 -1
  159. package/node_modules/pi-lens/dist/clients/formatters.js +66 -93
  160. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +3 -4
  161. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -0
  162. package/node_modules/pi-lens/dist/clients/git-guard.js +5 -0
  163. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +42 -6
  164. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +48 -7
  165. package/node_modules/pi-lens/dist/clients/grammar-source.js +3 -3
  166. package/node_modules/pi-lens/dist/clients/installer/index.js +416 -61
  167. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +84 -18
  168. package/node_modules/pi-lens/dist/clients/instance-reaper.js +6 -6
  169. package/node_modules/pi-lens/dist/clients/jscpd-client.js +9 -1
  170. package/node_modules/pi-lens/dist/clients/knip-client.js +9 -2
  171. package/node_modules/pi-lens/dist/clients/language-profile.js +12 -11
  172. package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
  173. package/node_modules/pi-lens/dist/clients/latency-logger.js +7 -7
  174. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -1
  175. package/node_modules/pi-lens/dist/clients/lens-config.js +23 -0
  176. package/node_modules/pi-lens/dist/clients/lens-engine.js +3 -4
  177. package/node_modules/pi-lens/dist/clients/lens-events.js +1 -1
  178. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +18 -0
  179. package/node_modules/pi-lens/dist/clients/lens-map.js +1 -1
  180. package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -36
  181. package/node_modules/pi-lens/dist/clients/lsp/client.js +4 -3
  182. package/node_modules/pi-lens/dist/clients/lsp/config.js +37 -7
  183. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +24 -1
  184. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +79 -1
  185. package/node_modules/pi-lens/dist/clients/lsp/edits.js +3 -3
  186. package/node_modules/pi-lens/dist/clients/lsp/index.js +491 -64
  187. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +1 -1
  188. package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -8
  189. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  190. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -2
  191. package/node_modules/pi-lens/dist/clients/lsp/server.js +62 -15
  192. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -1
  193. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +5 -5
  194. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +6 -0
  195. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +2 -1
  196. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +37 -4
  197. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +1 -1
  198. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +3 -0
  199. package/node_modules/pi-lens/dist/clients/mcp/session.js +2 -0
  200. package/node_modules/pi-lens/dist/clients/metrics-history.js +28 -107
  201. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +1 -1
  202. package/node_modules/pi-lens/dist/clients/module-report.js +1 -1
  203. package/node_modules/pi-lens/dist/clients/mutating-tool.js +3 -3
  204. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +3 -3
  205. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -4
  206. package/node_modules/pi-lens/dist/clients/observed-mutation.js +9 -9
  207. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +2 -6
  208. package/node_modules/pi-lens/dist/clients/opengrep-client.js +2 -0
  209. package/node_modules/pi-lens/dist/clients/opengrep-config.js +1 -1
  210. package/node_modules/pi-lens/dist/clients/package-manager.js +2 -1
  211. package/node_modules/pi-lens/dist/clients/path-utils.js +23 -2
  212. package/node_modules/pi-lens/dist/clients/performance-report.js +2 -2
  213. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +9 -4
  214. package/node_modules/pi-lens/dist/clients/pipeline.js +5 -4
  215. package/node_modules/pi-lens/dist/clients/project-changes.js +1 -1
  216. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +1 -1
  217. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +38 -10
  218. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
  219. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +1 -1
  220. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +1 -1
  221. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +1 -1
  222. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +1 -1
  223. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +1 -1
  224. package/node_modules/pi-lens/dist/clients/project-lens-config.js +49 -1
  225. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +3 -51
  226. package/node_modules/pi-lens/dist/clients/python-provenance.js +1 -1
  227. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +3 -1
  228. package/node_modules/pi-lens/dist/clients/read-guard.js +5 -2
  229. package/node_modules/pi-lens/dist/clients/recent-touches.js +2 -2
  230. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +3 -5
  231. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +1 -1
  232. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +2 -0
  233. package/node_modules/pi-lens/dist/clients/ruff-client.js +18 -9
  234. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +10 -1
  235. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +5 -0
  236. package/node_modules/pi-lens/dist/clients/runtime-session.js +58 -14
  237. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +11 -1
  238. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +4 -1
  239. package/node_modules/pi-lens/dist/clients/runtime-turn.js +48 -1
  240. package/node_modules/pi-lens/dist/clients/sanitize.js +0 -11
  241. package/node_modules/pi-lens/dist/clients/scan-utils.js +1 -56
  242. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -2
  243. package/node_modules/pi-lens/dist/clients/session-event-guard.js +69 -1
  244. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -24
  245. package/node_modules/pi-lens/dist/clients/sg-runner.js +2 -1
  246. package/node_modules/pi-lens/dist/clients/sgconfig.js +1 -5
  247. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +1 -1
  248. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +108 -0
  249. package/node_modules/pi-lens/dist/clients/smells-rollup.js +2 -2
  250. package/node_modules/pi-lens/dist/clients/startup-scan.js +2 -10
  251. package/node_modules/pi-lens/dist/clients/test-runner-client.js +291 -76
  252. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +77 -72
  253. package/node_modules/pi-lens/dist/clients/tool-config.js +199 -0
  254. package/node_modules/pi-lens/dist/clients/tool-cwd.js +299 -0
  255. package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
  256. package/node_modules/pi-lens/dist/clients/tool-probe.js +53 -0
  257. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +42 -0
  258. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +1 -52
  259. package/node_modules/pi-lens/dist/clients/trivy-client.js +9 -1
  260. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -4
  261. package/node_modules/pi-lens/dist/clients/turn-context.js +52 -0
  262. package/node_modules/pi-lens/dist/clients/user-notify.js +0 -4
  263. package/node_modules/pi-lens/dist/clients/widget-state.js +4 -3
  264. package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -7
  265. package/node_modules/pi-lens/dist/clients/word-index-store.js +3 -7
  266. package/node_modules/pi-lens/dist/clients/word-index.js +9 -16
  267. package/node_modules/pi-lens/dist/clients/zizmor-config.js +1 -1
  268. package/node_modules/pi-lens/dist/index.js +19969 -17481
  269. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -2
  270. package/node_modules/pi-lens/dist/mcp/build-staleness.js +1 -1
  271. package/node_modules/pi-lens/dist/mcp/server.js +135 -159
  272. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +1 -1
  273. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +1 -1
  274. package/node_modules/pi-lens/dist/tools/activate-tools.js +10 -7
  275. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +2 -13
  276. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +8 -13
  277. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +83 -39
  278. package/node_modules/pi-lens/dist/tools/effective-config.js +4 -3
  279. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +1 -10
  280. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +354 -86
  281. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +54 -112
  282. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +20 -44
  283. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +3 -3
  284. package/node_modules/pi-lens/dist/tools/module-report.js +5 -9
  285. package/node_modules/pi-lens/dist/tools/project-report.js +9 -17
  286. package/node_modules/pi-lens/dist/tools/render-compact.js +281 -2
  287. package/node_modules/pi-lens/dist/tools/symbol-search.js +3 -4
  288. package/node_modules/pi-lens/docs/agent-guide.md +3 -4
  289. package/node_modules/pi-lens/docs/agent-tools.md +46 -22
  290. package/node_modules/pi-lens/docs/configuration.md +9 -0
  291. package/node_modules/pi-lens/docs/environment-variables.md +0 -5
  292. package/node_modules/pi-lens/docs/features.md +5 -5
  293. package/node_modules/pi-lens/docs/globalconfig.md +31 -1
  294. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +9 -1
  295. package/node_modules/pi-lens/docs/mcp.md +13 -1
  296. package/node_modules/pi-lens/docs/pi-lens-fixer.md +71 -0
  297. package/node_modules/pi-lens/docs/pi-lens-investigator.md +15 -0
  298. package/node_modules/pi-lens/docs/pi-lens-monitor.md +88 -0
  299. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +41 -0
  300. package/node_modules/pi-lens/docs/pi-lens-subagent.md +21 -2
  301. package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
  302. package/node_modules/pi-lens/docs/real-harness.md +46 -0
  303. package/node_modules/pi-lens/docs/release-qa-baseline.md +17 -5
  304. package/node_modules/pi-lens/docs/servercapabilities.md +2 -3
  305. package/node_modules/pi-lens/docs/settings.md +37 -2
  306. package/node_modules/pi-lens/docs/tools_improvement2.md +4 -4
  307. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  308. package/node_modules/pi-lens/docs/usage.md +13 -3
  309. package/node_modules/pi-lens/docs/word-index.md +1 -1
  310. package/node_modules/pi-lens/package.json +15 -15
  311. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +7 -3
  312. package/node_modules/pi-lens/rules/typos/_typos.toml +4 -2
  313. package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +1 -1
  314. package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +1 -1
  315. package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +4 -2
  316. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +8 -6
  317. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +19 -9
  318. package/node_modules/pi-web-access/CHANGELOG.md +29 -0
  319. package/node_modules/pi-web-access/README.md +33 -22
  320. package/node_modules/pi-web-access/abortable.ts +17 -0
  321. package/node_modules/pi-web-access/crawl4ai.ts +204 -0
  322. package/node_modules/pi-web-access/credential-source.ts +1 -0
  323. package/node_modules/pi-web-access/curator-page.ts +20 -2
  324. package/node_modules/pi-web-access/curator-run.ts +44 -0
  325. package/node_modules/pi-web-access/curator-server.ts +3 -1
  326. package/node_modules/pi-web-access/duckduckgo.ts +1 -1
  327. package/node_modules/pi-web-access/extract.ts +57 -12
  328. package/node_modules/pi-web-access/fetch-params.ts +1 -1
  329. package/node_modules/pi-web-access/gemini-search.ts +9 -5
  330. package/node_modules/pi-web-access/index.ts +25 -18
  331. package/node_modules/pi-web-access/openai-search.ts +35 -13
  332. package/node_modules/pi-web-access/package.json +2 -2
  333. package/node_modules/pi-web-access/page-query.ts +5 -2
  334. package/node_modules/pi-web-access/query-rewrite.ts +5 -2
  335. package/node_modules/pi-web-access/serpapi.ts +220 -0
  336. package/node_modules/pi-web-access/ssrf-protection.ts +5 -1
  337. package/node_modules/pi-web-access/storage.ts +10 -0
  338. package/node_modules/pi-web-access/summary-review.ts +34 -21
  339. package/node_modules/pi-web-access/utils.ts +6 -4
  340. package/package.json +7 -4
  341. package/node_modules/pi-lens/dist/tools/ast-dump.js +0 -103
@@ -25,6 +25,7 @@ import { isLspSpawnAllowedByTrust, assertInstallAllowed, projectTrustDenialReaso
25
25
  import { shouldPreferPullOnlyDiagnostics } from "../lsp-budget.js";
26
26
  import { sampleProcessTreeCpuPercent } from "../resource-sampler.js";
27
27
  import { bounded, withDeadline, withTimeout } from "../deadline-utils.js";
28
+ import { HOOK_WALL_BUDGET_MS } from "../hook-budgets.js";
28
29
  import { getAmbientAbortSignal } from "../safe-spawn.js";
29
30
  import { abortDeferredLspWork } from "../deferred-lsp-work.js";
30
31
  import { acquireWorkspaceSweepHold, clearWorkspaceSweepHoldForSessionStart, } from "./workspace-sweep-hold.js";
@@ -33,7 +34,7 @@ import { markPendingAuxiliaryCoverage, napiFallbackCoveredSince, } from "./pendi
33
34
  import { isAtOrAboveHomeDir, isWindowsPath, normalizeMapKey, uriToPath, } from "../path-utils.js";
34
35
  import { recordLspMutation } from "../lsp-mutation.js";
35
36
  import { createLSPClient } from "./client.js";
36
- import { auxiliaryCoverageGap, bindingStateLabel, composeBoundToCurrentDisk, createDiskBindingCache, hashDiagnosticContent, resolveTouchVerdict, touchCoverageGap, } from "./diagnostic-binding.js";
37
+ import { auxiliaryCoverageGap, auxiliaryPublicationEvidence, bindingStateLabel, composeBoundToCurrentDisk, createDiskBindingCache, hashDiagnosticContent, resolveTouchVerdict, touchCoverageGap, } from "./diagnostic-binding.js";
37
38
  import { getServersForFileWithConfig, getServerInitOverride, } from "./config.js";
38
39
  // #2052: deliberately NOT taken from `config.js`. This module's import surface
39
40
  // from `config.js` is mirrored by explicit `vi.mock` factories in ~58 test
@@ -41,9 +42,14 @@ import { getServersForFileWithConfig, getServerInitOverride, } from "./config.js
41
42
  import { getSessionRootsForTelemetry, isOutsideAllSessionRoots, } from "./session-roots.js";
42
43
  import { getProcessSingleton } from "../process-singletons.js";
43
44
  import { getLanguageId } from "./language.js";
44
- import { LSP_SERVERS, enforceLspRootCeiling, hasProjectBoundaryMarker, isDirectLspCommandTemporarilyUnavailable, resetClassicTsRepairGuard, resetLspLaunchAvailabilityGeneration, } from "./server.js";
45
+ import { getReverseDepsFromIndex, loadReverseDependencyIndexFromSnapshot, } from "../reverse-deps.js";
46
+ import { LSP_SERVERS, enforceLspRootCeiling, hasProjectBoundaryMarker, isDirectLspCommandTemporarilyUnavailable, resetClassicTsRepairGuard, resetLspLaunchAvailabilityGeneration, resolveLspServerCwd, } from "./server.js";
45
47
  import { classifyCascadeWaitTier, classifyServerWaitTier, getStrategy, } from "./wait-policy/index.js";
46
48
  const WORKSPACE_ATTRIBUTION_CLIENT_CAP = 16;
49
+ const AUX_WAIT_DEMOTION_THRESHOLD = 5;
50
+ const AUX_WAIT_DEMOTION_RATIO = 0.9;
51
+ const AUX_WAIT_REPROMOTION_THRESHOLD = 5;
52
+ const AUX_WAIT_REPROMOTION_RATIO = 0.5;
47
53
  import { raceToCompletion } from "./aggregation.js";
48
54
  import { applyWorkspaceEdit, mergeWorkspaceTextEditsByPriority, summarizeWorkspaceEdit, validateWorkspaceEdit, } from "./edits.js";
49
55
  import { buildScopeKey, createWorkspaceDiagnosticsCacheContext, } from "./workspace-diagnostics-cache.js";
@@ -175,6 +181,7 @@ const NAV_CLIENT_WAIT_TIMEOUT_MS = Math.max(0, Number.parseInt(process.env.PI_LE
175
181
  1500);
176
182
  const TOUCH_DEBOUNCE_MS = Math.max(0, Number.parseInt(process.env.PI_LENS_LSP_TOUCH_DEBOUNCE_MS ?? "1500", 10) ||
177
183
  1500);
184
+ const SCOPED_DEPENDENCY_TOUCH_MAX = 32;
178
185
  // #1621: the rename-propagation notifies (`didClose` ahead of the rename,
179
186
  // `workspace/didRenameFiles` after) share the exit-notify defect #1620 fixed —
180
187
  // a notify write on a pipe that is not draining neither resolves nor rejects,
@@ -344,6 +351,9 @@ function mergeLspDiagnostics(diagnostics) {
344
351
  }
345
352
  return merged;
346
353
  }
354
+ function attributeDiagnostics(serverId, diagnostics) {
355
+ return diagnostics.map((diagnostic) => ({ ...diagnostic, serverId }));
356
+ }
347
357
  /**
348
358
  * Group files by their primary language server id (#387/#631 — extracted
349
359
  * from `runWorkspaceDiagnostics`'s inline grouping so other callers, e.g.
@@ -646,6 +656,17 @@ export class LSPService {
646
656
  /** Server/root pairs that already emitted unavailable for the current occurrence. */
647
657
  unavailableLogged = new Set();
648
658
  optionalDisabled = new Set();
659
+ /**
660
+ * One first-contact diagnostic wait per custom server in this service
661
+ * generation. Concurrent touches share the probe; a hook cutoff never arms
662
+ * the navigation-only latch, so the next touch can try again.
663
+ */
664
+ firstContactWaits = new Map();
665
+ /** Session-scoped adaptive demotion for budget-hitting auxiliaries. */
666
+ auxWaitPressureStreak = new Map();
667
+ auxWaitFastAnswerStreak = new Map();
668
+ demotedAuxiliaryServerIds = new Set();
669
+ demotedAuxiliaryBudgets = new Map();
649
670
  /**
650
671
  * #1934 review F1: what the last COMPLETED `spawnClient` call for a
651
672
  * (server, root) key decided, written by that call at every point it
@@ -833,6 +854,8 @@ export class LSPService {
833
854
  clientSpawnGate = Promise.resolve();
834
855
  /** True after shutdown() has been called; blocks new operations */
835
856
  isDestroyed = false;
857
+ /** Aborts bounded in-flight service work when this generation is retired. */
858
+ shutdownController = new AbortController();
836
859
  /**
837
860
  * #850: teardown completion for every singleton generation retired before
838
861
  * this service was published. Only replacement services receive one; direct
@@ -852,15 +875,65 @@ export class LSPService {
852
875
  clientSpawnedAt: new Map(),
853
876
  demonstratedReady: new Set(),
854
877
  demonstratedCold: new Set(),
878
+ diagnosticsPublished: new Set(),
879
+ diagnosticsUnsupported: new Set(),
855
880
  };
856
881
  }
882
+ isAuxiliaryWaitDemoted(serverId, root) {
883
+ return this.demotedAuxiliaryServerIds.has(`${serverId}:${normalizeMapKey(root ?? "")}`);
884
+ }
885
+ noteAuxiliaryWait(key, budgetMs, elapsedMs) {
886
+ if (this.demotedAuxiliaryServerIds.has(key))
887
+ return;
888
+ if (elapsedMs < AUX_WAIT_DEMOTION_RATIO * budgetMs) {
889
+ this.auxWaitPressureStreak.delete(key);
890
+ return;
891
+ }
892
+ const streak = (this.auxWaitPressureStreak.get(key) ?? 0) + 1;
893
+ if (streak >= AUX_WAIT_DEMOTION_THRESHOLD) {
894
+ this.demotedAuxiliaryServerIds.add(key);
895
+ this.demotedAuxiliaryBudgets.set(key, budgetMs);
896
+ this.auxWaitPressureStreak.delete(key);
897
+ this.auxWaitFastAnswerStreak.delete(key);
898
+ recordDegradationOnce({
899
+ kind: "aux_wait_demoted",
900
+ subject: key,
901
+ reason: "auxiliary reached at least 90% of its declared wait budget for five consecutive dispatches",
902
+ });
903
+ return;
904
+ }
905
+ this.auxWaitPressureStreak.set(key, streak);
906
+ }
907
+ noteAuxiliaryLateAnswer(key, elapsedMs) {
908
+ if (!this.demotedAuxiliaryServerIds.has(key))
909
+ return;
910
+ const budgetMs = this.demotedAuxiliaryBudgets.get(key);
911
+ if (budgetMs === undefined ||
912
+ elapsedMs >= AUX_WAIT_REPROMOTION_RATIO * budgetMs) {
913
+ this.auxWaitFastAnswerStreak.delete(key);
914
+ return;
915
+ }
916
+ const streak = (this.auxWaitFastAnswerStreak.get(key) ?? 0) + 1;
917
+ if (streak < AUX_WAIT_REPROMOTION_THRESHOLD) {
918
+ this.auxWaitFastAnswerStreak.set(key, streak);
919
+ return;
920
+ }
921
+ this.demotedAuxiliaryServerIds.delete(key);
922
+ this.demotedAuxiliaryBudgets.delete(key);
923
+ this.auxWaitFastAnswerStreak.delete(key);
924
+ recordDegradationOnce({
925
+ kind: "aux_wait_repromoted",
926
+ subject: key,
927
+ reason: "auxiliary produced five consecutive late answers below half its declared wait budget",
928
+ });
929
+ }
857
930
  /**
858
931
  * Resolve one server's client identity root. Root selection is hard-bounded
859
932
  * by the session cwd, then config-only nested roots reuse an already-hosted
860
933
  * same-server ancestor. Real nested projects retain independent clients.
861
934
  */
862
- async resolveServerRoot(server, filePath) {
863
- const candidate = await server.root(filePath);
935
+ async resolveServerRoot(server, filePath, onRootFailure) {
936
+ const candidate = await resolveLspServerCwd(server, filePath, this.sessionCwd ?? process.cwd(), onRootFailure);
864
937
  if (!candidate)
865
938
  return undefined;
866
939
  // #2052: a file outside EVERY initialized session cwd gets no client at
@@ -1984,11 +2057,14 @@ export class LSPService {
1984
2057
  */
1985
2058
  const partitionCandidates = async () => {
1986
2059
  const resolved = await Promise.all(servers.map(async (server) => {
2060
+ let rootFailureReason;
1987
2061
  try {
1988
2062
  return {
1989
2063
  server,
1990
- root: await this.resolveServerRootOnce(server, filePath, rootMemo),
1991
- reason: undefined,
2064
+ root: await this.resolveServerRootOnce(server, filePath, rootMemo, (reason) => {
2065
+ rootFailureReason = reason;
2066
+ }),
2067
+ reason: rootFailureReason,
1992
2068
  };
1993
2069
  }
1994
2070
  catch (error) {
@@ -2455,6 +2531,18 @@ export class LSPService {
2455
2531
  }
2456
2532
  return out;
2457
2533
  }
2534
+ /** Record a fast answer observed by the turn-end late-results path. */
2535
+ async observeLateAuxiliaryAnswer(filePath, serverId, elapsedMs) {
2536
+ for (const server of getServersForFileWithConfig(filePath)) {
2537
+ if (server.id !== serverId)
2538
+ continue;
2539
+ const root = await this.resolveServerRoot(server, filePath);
2540
+ if (root === undefined)
2541
+ return;
2542
+ this.noteAuxiliaryLateAnswer(`${serverId}:${normalizeMapKey(root)}`, elapsedMs);
2543
+ return;
2544
+ }
2545
+ }
2458
2546
  /**
2459
2547
  * #1668: deliver a `workspace/didChangeWatchedFiles` event for a disk
2460
2548
  * change the client did not author through open-document sync — a bash
@@ -2481,6 +2569,45 @@ export class LSPService {
2481
2569
  }
2482
2570
  }
2483
2571
  }
2572
+ /**
2573
+ * Re-sync the open views affected by one recovered Git tree change. The
2574
+ * recovery seam already owns the changed-path set, so this deliberately
2575
+ * reads only the cached reverse-import index and never runs another diff.
2576
+ */
2577
+ async resyncGitChangedFiles(changedPaths) {
2578
+ if (this.checkDestroyed() || changedPaths.length === 0)
2579
+ return;
2580
+ const targets = new Set();
2581
+ for (const changedPath of changedPaths) {
2582
+ const resolved = path.resolve(changedPath);
2583
+ for (const server of getServersForFileWithConfig(resolved)) {
2584
+ const root = await this.resolveServerRoot(server, resolved);
2585
+ if (!root)
2586
+ continue;
2587
+ const index = loadReverseDependencyIndexFromSnapshot({ cwd: root });
2588
+ for (const importer of index
2589
+ ? getReverseDepsFromIndex(index, resolved)
2590
+ : []) {
2591
+ targets.add(importer);
2592
+ }
2593
+ }
2594
+ targets.add(resolved);
2595
+ }
2596
+ const openTargets = [...targets].filter((filePath) => this.hasLiveClientHoldingDocument(filePath));
2597
+ this.documentDrift.enqueueResync(openTargets);
2598
+ const pass = await this.sweepDocumentDrift({ force: true });
2599
+ logLatency({
2600
+ type: "phase",
2601
+ phase: "lsp_external_change_resync",
2602
+ filePath: changedPaths[0] ?? "",
2603
+ durationMs: 0,
2604
+ metadata: {
2605
+ changedCount: changedPaths.length,
2606
+ enqueued: openTargets.length,
2607
+ deferred: pass?.deferred ?? this.documentDrift.pendingResyncCount,
2608
+ },
2609
+ });
2610
+ }
2484
2611
  /**
2485
2612
  * #1783: disk-drift backstop. Stat one batch of the documents a language
2486
2613
  * server currently holds, and re-push any whose bytes on disk no longer
@@ -2669,13 +2796,13 @@ export class LSPService {
2669
2796
  * instead of each taking their own. Without a `memo` this is a plain
2670
2797
  * passthrough, for callers that have no acquisition to share with.
2671
2798
  */
2672
- resolveServerRootOnce(server, filePath, memo) {
2799
+ resolveServerRootOnce(server, filePath, memo, onRootFailure) {
2673
2800
  if (!memo)
2674
- return this.resolveServerRoot(server, filePath);
2801
+ return this.resolveServerRoot(server, filePath, onRootFailure);
2675
2802
  const pending = memo.get(server.id);
2676
2803
  if (pending !== undefined)
2677
2804
  return pending;
2678
- const started = this.resolveServerRoot(server, filePath);
2805
+ const started = this.resolveServerRoot(server, filePath, onRootFailure);
2679
2806
  memo.set(server.id, started);
2680
2807
  return started;
2681
2808
  }
@@ -3117,6 +3244,12 @@ export class LSPService {
3117
3244
  }
3118
3245
  const override = getServerInitOverride(server.id, filePath);
3119
3246
  const mergedInit = mergeInitializationOptions(spawned.initialization, override?.initializationOptions);
3247
+ // A replacement process is a new first-contact identity even when the
3248
+ // configured server id is unchanged. Re-arm both capability latches before
3249
+ // the replacement can publish a snapshot.
3250
+ this.state.diagnosticsPublished.delete(server.id);
3251
+ this.state.diagnosticsUnsupported.delete(server.id);
3252
+ this.firstContactWaits.delete(server.id);
3120
3253
  const client = await createLSPClient({
3121
3254
  serverId: server.id,
3122
3255
  process: spawned.process,
@@ -3125,6 +3258,9 @@ export class LSPService {
3125
3258
  initialization: mergedInit,
3126
3259
  initializeTimeoutMs: server.initializeTimeoutMs,
3127
3260
  launchVariant: spawned.launchVariant,
3261
+ onDiagnosticsPublished: (serverId) => {
3262
+ this.state.diagnosticsPublished.add(serverId);
3263
+ },
3128
3264
  });
3129
3265
  // Guard 2: service was shut down while we were completing the initialize
3130
3266
  // handshake. Shut down the live client best-effort and do not register it.
@@ -3286,6 +3422,11 @@ export class LSPService {
3286
3422
  return;
3287
3423
  }
3288
3424
  const startedAt = Date.now();
3425
+ const hookDeadlineAt = options.hook !== undefined &&
3426
+ Object.hasOwn(HOOK_WALL_BUDGET_MS, options.hook)
3427
+ ? startedAt +
3428
+ HOOK_WALL_BUDGET_MS[options.hook]
3429
+ : undefined;
3289
3430
  const normalizedPath = normalizeMapKey(filePath);
3290
3431
  const outsideRoot = await this.findOutsideProjectRoot(filePath);
3291
3432
  if (outsideRoot) {
@@ -3394,6 +3535,19 @@ export class LSPService {
3394
3535
  if (!leaseKeys) {
3395
3536
  // An idle eviction won between selection and lease admission. Resolve the
3396
3537
  // now-current client set and replay; no notification targets the retiree.
3538
+ if (this.checkDestroyed()) {
3539
+ const primaryServerIds = spawned
3540
+ .filter((entry) => entry.info.role !== "auxiliary")
3541
+ .map((entry) => entry.info.id);
3542
+ return {
3543
+ diags: [],
3544
+ inconclusive: true,
3545
+ ...(primaryServerIds.length > 0 && {
3546
+ inconclusiveServerIds: primaryServerIds,
3547
+ }),
3548
+ inconclusiveReason: "diagnostics-wait",
3549
+ };
3550
+ }
3397
3551
  return this.touchFile(filePath, content, options);
3398
3552
  }
3399
3553
  try {
@@ -3475,7 +3629,9 @@ export class LSPService {
3475
3629
  if (!bindingMatchesTouchContent(binding))
3476
3630
  return [];
3477
3631
  const diags = entry.client.getDiagnostics(filePath);
3478
- return diags.length > 0 ? [{ diags, binding }] : [];
3632
+ return diags.length > 0
3633
+ ? [{ serverId: entry.info.id, diags, binding }]
3634
+ : [];
3479
3635
  })
3480
3636
  : [];
3481
3637
  // #1493: auxiliaries whose STORED publication already covers exactly the
@@ -3784,6 +3940,15 @@ export class LSPService {
3784
3940
  // confirmation; `auxCutOffServerIds` stays cut_off-only so the R8 latency
3785
3941
  // field keeps its original meaning.
3786
3942
  let auxUnconfirmedServerIds;
3943
+ // #2810: the auxiliaries this touch handed to the collect-later store —
3944
+ // exactly the servers whose findings a turn-end drain can still deliver.
3945
+ // Written by the ONE producer that marks them, from the same array it
3946
+ // marks, so the promise the agent is shown cannot drift from the store.
3947
+ // Round 3 derived the promise from the #1459 resync deferrals instead —
3948
+ // the one set `pending-aux-coverage.ts` never marks — so the notice
3949
+ // promised delivery that could not happen, while the demoted server,
3950
+ // which IS marked, was reported as an unexplained silence on every edit.
3951
+ let lateDeliveryServerIds;
3787
3952
  // #707: tsserver sync clean-confirm state. `tsserverSyncEligible` is the
3788
3953
  // full gate (evaluated once, before the wait); `tsserverSyncConfirmed`
3789
3954
  // holds the sync commands' answer when the racing confirm won the wait
@@ -3791,6 +3956,7 @@ export class LSPService {
3791
3956
  // fallback below may still fill it in on a timed-out empty result).
3792
3957
  let tsserverSyncEligible = false;
3793
3958
  let tsserverSyncConfirmed;
3959
+ let diagnosticsUnsupportedServerIds = [];
3794
3960
  if (diagnosticsMode !== "none") {
3795
3961
  // Resolution: env wins so users can tune the cap without rebuilding.
3796
3962
  // Otherwise, on the single-server hot path (primary scope), use that
@@ -3861,6 +4027,9 @@ export class LSPService {
3861
4027
  root: entry.client.root,
3862
4028
  operationSupport: entry.client.getOperationSupport(),
3863
4029
  workspaceDiagnosticsSupport: entry.client.getWorkspaceDiagnosticsSupport(),
4030
+ customServer: entry.info.custom,
4031
+ diagnosticsPublished: this.state.diagnosticsPublished.has(entry.info.id),
4032
+ diagnosticsUnsupported: this.state.diagnosticsUnsupported.has(entry.info.id),
3864
4033
  advertisedCommands: entry.client.getAdvertisedCommands(),
3865
4034
  rawCapabilityKeys: entry.client.getRawCapabilityKeys?.() ?? [],
3866
4035
  launchVariant: entry.client.getLaunchVariant?.(),
@@ -4004,6 +4173,29 @@ export class LSPService {
4004
4173
  const hasTouchAuxiliaries = clientScope === "with-auxiliary" &&
4005
4174
  options.collectDiagnostics === true &&
4006
4175
  spawned.some((e) => e.info.role === "auxiliary");
4176
+ if (spawned.some((entry) => entry.info.custom === true)) {
4177
+ try {
4178
+ const snapshotHook = options.hook ?? "tool_result_edit";
4179
+ const snapshotBudget = hookDeadlineAt === undefined
4180
+ ? HOOK_WALL_BUDGET_MS.tool_result_edit
4181
+ : Math.max(0, hookDeadlineAt - Date.now());
4182
+ const snapshots = await bounded(this.getCapabilitySnapshots(filePath), {
4183
+ ms: snapshotBudget,
4184
+ signal: getAmbientAbortSignal(),
4185
+ shutdownSignal: this.shutdownController.signal,
4186
+ hook: snapshotHook,
4187
+ label: `getCapabilitySnapshots:${filePath}`,
4188
+ });
4189
+ if (snapshots !== undefined) {
4190
+ diagnosticsUnsupportedServerIds = spawned
4191
+ .filter((entry) => classifyServerWaitTier(entry.info.id, snapshots.find((s) => s.serverId === entry.info.id)) === "diagnostics-unsupported")
4192
+ .map((entry) => entry.info.id);
4193
+ }
4194
+ }
4195
+ catch {
4196
+ // Capability uncertainty fails closed and retains the wait.
4197
+ }
4198
+ }
4007
4199
  // Per-server wait promises (each already bounded by its own
4008
4200
  // perServerTimeout — unchanged from before R8).
4009
4201
  let pressureSnapshots = [];
@@ -4028,12 +4220,40 @@ export class LSPService {
4028
4220
  ? auxWaitBudgetMs(entry.client.serverId, true, configuredAuxCeilingMs, declaredServerTimeout)
4029
4221
  : perServerDeclaredTimeouts[entryIndex];
4030
4222
  });
4223
+ const firstContactCutOffServerIds = new Set();
4224
+ const firstContactErroredServerIds = new Set();
4225
+ const answeredForThisTouch = (entry) => {
4226
+ const baseline = diagnosticBaselines.get(entry.client);
4227
+ const currentPathVersion = readPathVersion(entry.client);
4228
+ if (auxiliaryPublicationEvidence({
4229
+ bindingMatchesContent: false,
4230
+ baseline,
4231
+ currentPathVersion,
4232
+ })) {
4233
+ return true;
4234
+ }
4235
+ try {
4236
+ return (entry.client.getAllDiagnostics?.().has(normalizedPath) === true);
4237
+ }
4238
+ catch {
4239
+ return false;
4240
+ }
4241
+ };
4031
4242
  const perServerRaceBudgets = spawned.map((entry, entryIndex) => {
4032
4243
  return hasTouchAuxiliaries && entry.info.role === "auxiliary"
4033
4244
  ? auxWaitBudgetMs(entry.client.serverId, coldAuxiliaryServerIds.has(entry.client.serverId), configuredAuxCeilingMs, perServerDeclaredTimeouts[entryIndex])
4034
4245
  : perServerDeclaredTimeouts[entryIndex];
4035
4246
  });
4036
4247
  const perServerWaits = spawned.map((entry, entryIndex) => {
4248
+ if (hasTouchAuxiliaries &&
4249
+ entry.info.role === "auxiliary" &&
4250
+ this.isAuxiliaryWaitDemoted(entry.info.id, entry.client.root ?? filePath)) {
4251
+ return Promise.resolve(undefined);
4252
+ }
4253
+ if (this.state.diagnosticsUnsupported.has(entry.info.id) ||
4254
+ diagnosticsUnsupportedServerIds.includes(entry.info.id)) {
4255
+ return Promise.resolve(undefined);
4256
+ }
4037
4257
  // #1459: a DEFERRED server never received this content, so its version
4038
4258
  // can never advance past the baseline — waiting on it burns its whole
4039
4259
  // budget and would flip the touch to `inconclusive`, discarding a
@@ -4063,7 +4283,7 @@ export class LSPService {
4063
4283
  const isWarmupTouch = source === "lsp_sweep_warmup";
4064
4284
  // #743: per-server — a server we DID push to still gets the
4065
4285
  // version-baseline wait even when a sibling was debounced away.
4066
- const wait = !notifySkippedServerIds.has(entry.info.id) &&
4286
+ const waitForDiagnostics = () => !notifySkippedServerIds.has(entry.info.id) &&
4067
4287
  Number.isFinite(baseline)
4068
4288
  ? entry.client.waitForDiagnostics(filePath, serverTimeout, {
4069
4289
  minVersion: baseline,
@@ -4080,7 +4300,73 @@ export class LSPService {
4080
4300
  pullSettleSource: "pull-warmup",
4081
4301
  })
4082
4302
  : entry.client.waitForDiagnostics(filePath, serverTimeout);
4083
- return wait.catch(() => undefined);
4303
+ const firstContact = entry.info.custom === true &&
4304
+ !this.state.diagnosticsPublished.has(entry.info.id) &&
4305
+ !this.state.diagnosticsUnsupported.has(entry.info.id) &&
4306
+ entry.client.getWorkspaceDiagnosticsSupport().mode !== "pull";
4307
+ if (!firstContact)
4308
+ return waitForDiagnostics().catch(() => undefined);
4309
+ let shared = this.firstContactWaits.get(entry.info.id);
4310
+ if (!shared) {
4311
+ let removeShutdownListener;
4312
+ const shutdown = new Promise((resolve) => {
4313
+ const onShutdown = () => resolve("shutdown");
4314
+ if (this.shutdownController.signal.aborted) {
4315
+ onShutdown();
4316
+ return;
4317
+ }
4318
+ this.shutdownController.signal.addEventListener("abort", onShutdown, {
4319
+ once: true,
4320
+ });
4321
+ removeShutdownListener = () => this.shutdownController.signal.removeEventListener("abort", onShutdown);
4322
+ });
4323
+ const probe = Promise.race([
4324
+ waitForDiagnostics().then(() => "completed", () => "errored"),
4325
+ shutdown,
4326
+ ]);
4327
+ const completed = withDeadline(probe, {
4328
+ ms: serverTimeout,
4329
+ onTimeout: "undefined",
4330
+ });
4331
+ shared = completed
4332
+ .then((outcome) => {
4333
+ if (outcome === "errored" || outcome === "shutdown") {
4334
+ return "errored";
4335
+ }
4336
+ if (answeredForThisTouch(entry))
4337
+ return "answered";
4338
+ this.state.diagnosticsUnsupported.add(entry.info.id);
4339
+ recordDegradationOnce({
4340
+ kind: "lsp-diagnostics-unsupported",
4341
+ subject: entry.info.id,
4342
+ reason: "custom server has no pull provider and no publish evidence after first contact wait",
4343
+ });
4344
+ return "silent";
4345
+ })
4346
+ .catch(() => "errored")
4347
+ .finally(() => {
4348
+ removeShutdownListener?.();
4349
+ if (this.firstContactWaits.get(entry.info.id) === shared) {
4350
+ this.firstContactWaits.delete(entry.info.id);
4351
+ }
4352
+ });
4353
+ this.firstContactWaits.set(entry.info.id, shared);
4354
+ }
4355
+ const callerWait = hookDeadlineAt === undefined
4356
+ ? shared
4357
+ : withDeadline(shared, {
4358
+ ms: Math.max(0, hookDeadlineAt - Date.now()),
4359
+ onTimeout: "undefined",
4360
+ });
4361
+ return callerWait.then((outcome) => {
4362
+ if (outcome === undefined) {
4363
+ firstContactCutOffServerIds.add(entry.info.id);
4364
+ }
4365
+ else if (outcome === "errored") {
4366
+ firstContactErroredServerIds.add(entry.info.id);
4367
+ }
4368
+ return undefined;
4369
+ });
4084
4370
  });
4085
4371
  // The push wait — same per-server budget composition as before #707;
4086
4372
  // only the awaiting changed (assigned so it can be raced below).
@@ -4102,6 +4388,8 @@ export class LSPService {
4102
4388
  client: spawned[i].client,
4103
4389
  baseline: diagnosticBaselines.get(spawned[i].client),
4104
4390
  budgetMs: perServerRaceBudgets[i],
4391
+ demoted: this.isAuxiliaryWaitDemoted(spawned[i].info.id, spawned[i].client.root ?? filePath),
4392
+ root: spawned[i].client.root ?? filePath,
4105
4393
  }
4106
4394
  : null)
4107
4395
  .filter((x) => x !== null);
@@ -4121,6 +4409,22 @@ export class LSPService {
4121
4409
  const auxWaitStartedAt = Date.now();
4122
4410
  const outcomes = await Promise.all(auxWaits.map(async (aux) => {
4123
4411
  const { budgetMs } = aux;
4412
+ if (aux.demoted) {
4413
+ return {
4414
+ serverId: aux.serverId,
4415
+ outcome: deferredResyncServerIds.has(aux.serverId)
4416
+ ? "deferred"
4417
+ : "demoted",
4418
+ publishedThisContent: auxiliaryPublicationEvidence({
4419
+ bindingMatchesContent: auxCoversThisContent(aux.serverId),
4420
+ baseline: aux.baseline,
4421
+ currentPathVersion: readPathVersion(aux.client),
4422
+ }),
4423
+ budgetMs,
4424
+ elapsedMs: 0,
4425
+ elapsedSinceNotifyMs: 0,
4426
+ };
4427
+ }
4124
4428
  let timer;
4125
4429
  const timeout = new Promise((resolve) => {
4126
4430
  timer = setTimeout(() => resolve(false), budgetMs);
@@ -4159,10 +4463,12 @@ export class LSPService {
4159
4463
  // ignoring sibling paths — and it is the SAME axis `baseline`
4160
4464
  // was captured on above.
4161
4465
  const currentPathVersion = readPathVersion(aux.client);
4162
- const publishedEvidence = raced &&
4163
- Number.isFinite(aux.baseline) &&
4164
- currentPathVersion !== undefined &&
4165
- currentPathVersion > aux.baseline;
4466
+ const publishedEvidence = auxiliaryPublicationEvidence({
4467
+ bindingMatchesContent: false,
4468
+ baseline: aux.baseline,
4469
+ currentPathVersion,
4470
+ raced,
4471
+ });
4166
4472
  // #1459: a DEFERRED aux was never sent this content and is not
4167
4473
  // waited on at all, so its instantly-resolved placeholder
4168
4474
  // promise must not read as "silent". "Silent" is the reserved
@@ -4176,6 +4482,10 @@ export class LSPService {
4176
4482
  : publishedEvidence
4177
4483
  ? "answered"
4178
4484
  : "silent";
4485
+ const elapsedMs = Date.now() - auxWaitStartedAt;
4486
+ if (outcome !== "deferred") {
4487
+ this.noteAuxiliaryWait(`${aux.serverId}:${normalizeMapKey(aux.root)}`, budgetMs, elapsedMs);
4488
+ }
4179
4489
  return {
4180
4490
  serverId: aux.serverId,
4181
4491
  outcome,
@@ -4185,9 +4495,12 @@ export class LSPService {
4185
4495
  // did not narrow the touch.
4186
4496
  // #1586: through the one predicate, so this row and the merge
4187
4497
  // below cannot disagree about the same scanner.
4498
+ // #2914: binding-only by construction — the direct call
4499
+ // below is master's line; the stamp axis already decided
4500
+ // the outcome above and must not re-admit the row here.
4188
4501
  publishedThisContent: auxCoversThisContent(aux.serverId),
4189
4502
  budgetMs,
4190
- elapsedMs: Date.now() - auxWaitStartedAt,
4503
+ elapsedMs,
4191
4504
  // #1458 S3: elapsed measured from BEFORE the primary wait
4192
4505
  // (waitStartedAt), not just from auxWaitStartedAt — this is
4193
4506
  // what lets a latency row validate the ~1.3s warm-scanner
@@ -4220,7 +4533,9 @@ export class LSPService {
4220
4533
  // revision and probing it would replay stale findings.
4221
4534
  const collectLaterServerIds = outcomes
4222
4535
  .filter((o) => !o.publishedThisContent &&
4223
- (o.outcome === "cut_off" || o.outcome === "silent") &&
4536
+ (o.outcome === "cut_off" ||
4537
+ o.outcome === "silent" ||
4538
+ o.outcome === "demoted") &&
4224
4539
  // #2324 R2-A/R3-A: ast-grep's napi fallback is a
4225
4540
  // SECOND producer of coverage for this exact pair,
4226
4541
  // dispatched CONCURRENTLY with this whole touch
@@ -4240,7 +4555,8 @@ export class LSPService {
4240
4555
  napiFallbackCoveredSince(filePath, startedAt)))
4241
4556
  .map((o) => o.serverId);
4242
4557
  if (collectLaterServerIds.length > 0) {
4243
- markPendingAuxiliaryCoverage(filePath, collectLaterServerIds);
4558
+ lateDeliveryServerIds = collectLaterServerIds;
4559
+ markPendingAuxiliaryCoverage(filePath, collectLaterServerIds, Date.now());
4244
4560
  }
4245
4561
  logLatency({
4246
4562
  type: "phase",
@@ -4257,9 +4573,14 @@ export class LSPService {
4257
4573
  });
4258
4574
  })()
4259
4575
  : Promise.all(perServerWaits).then(() => { });
4260
- pushWait.then(() => {
4576
+ // Mark on BOTH paths: a rejected `pushWait` is settled too, and a
4577
+ // resolve-only `.then` left a derived promise that rejected unhandled
4578
+ // whenever a per-server wait failed (oxlint no-floating-promises,
4579
+ // 2026-09-07). The rejection itself still reaches the awaiters below.
4580
+ const markPushWaitSettled = () => {
4261
4581
  pushWaitSettled = true;
4262
- });
4582
+ };
4583
+ void pushWait.then(markPushWaitSettled, markPushWaitSettled);
4263
4584
  if (tsserverSyncEligible) {
4264
4585
  // #707 racing variant: rather than burning the full push-wait budget
4265
4586
  // on a silent-on-clean server (which by definition never answers on a
@@ -4331,6 +4652,35 @@ export class LSPService {
4331
4652
  await pushWait;
4332
4653
  }
4333
4654
  const waitedMs = Date.now() - waitStartedAt;
4655
+ const firstContactCustomServerIds = spawned
4656
+ .filter((entry) => entry.info.custom === true &&
4657
+ !this.state.diagnosticsPublished.has(entry.info.id) &&
4658
+ !this.state.diagnosticsUnsupported.has(entry.info.id) &&
4659
+ !firstContactCutOffServerIds.has(entry.info.id) &&
4660
+ !firstContactErroredServerIds.has(entry.info.id) &&
4661
+ entry.client.getWorkspaceDiagnosticsSupport().mode !== "pull")
4662
+ .map((entry) => entry.info.id);
4663
+ for (const serverId of firstContactCustomServerIds) {
4664
+ const entry = spawned.find((candidate) => candidate.info.id === serverId);
4665
+ if (!entry || answeredForThisTouch(entry))
4666
+ continue;
4667
+ this.state.diagnosticsUnsupported.add(serverId);
4668
+ diagnosticsUnsupportedServerIds.push(serverId);
4669
+ recordDegradationOnce({
4670
+ kind: "lsp-diagnostics-unsupported",
4671
+ subject: serverId,
4672
+ reason: "custom server has no pull provider and no publish evidence after first contact wait",
4673
+ });
4674
+ }
4675
+ for (const entry of spawned) {
4676
+ if (entry.info.custom === true &&
4677
+ this.state.diagnosticsUnsupported.has(entry.info.id)) {
4678
+ diagnosticsUnsupportedServerIds.push(entry.info.id);
4679
+ }
4680
+ }
4681
+ diagnosticsUnsupportedServerIds = [
4682
+ ...new Set(diagnosticsUnsupportedServerIds),
4683
+ ];
4334
4684
  // #1533: the same auxiliary coverage evidence for a collecting touch that
4335
4685
  // did NOT enter the aux-grace wait — in practice `clientScope: "all"`, the
4336
4686
  // batch/directory scan surface. Auxiliaries ARE spawned on that scope
@@ -4406,9 +4756,11 @@ export class LSPService {
4406
4756
  const outcomes = auxEntries.map((entry) => {
4407
4757
  const baseline = diagnosticBaselines.get(entry.client);
4408
4758
  const currentPathVersion = readPathVersion(entry.client);
4409
- const publishedEvidence = Number.isFinite(baseline) &&
4410
- currentPathVersion !== undefined &&
4411
- currentPathVersion > baseline;
4759
+ const publishedEvidence = auxiliaryPublicationEvidence({
4760
+ bindingMatchesContent: false,
4761
+ baseline,
4762
+ currentPathVersion,
4763
+ });
4412
4764
  return {
4413
4765
  serverId: entry.info.id,
4414
4766
  outcome: deferredResyncServerIds.has(entry.info.id)
@@ -4416,6 +4768,9 @@ export class LSPService {
4416
4768
  : publishedEvidence
4417
4769
  ? "answered"
4418
4770
  : "silent",
4771
+ // #2914: binding-only by construction — the direct call
4772
+ // below is master's line; the stamp axis already decided
4773
+ // the outcome above and must not re-admit the row here.
4419
4774
  publishedThisContent: auxCoversThisContent(entry.info.id),
4420
4775
  budgetMs: timeoutFor(entry.client.serverId),
4421
4776
  elapsedMs: waitedMs,
@@ -4454,7 +4809,8 @@ export class LSPService {
4454
4809
  },
4455
4810
  });
4456
4811
  }
4457
- else if (waitedMs + 20 >= timeoutMs) {
4812
+ else if (waitedMs + 20 >= timeoutMs ||
4813
+ firstContactErroredServerIds.size > 0) {
4458
4814
  // Within ~20 ms of the configured budget we treat it as a timeout;
4459
4815
  // the LSP didn't beat the cap. Diagnostics that arrive late still
4460
4816
  // land in the client's cache and surface on the next edit.
@@ -4479,25 +4835,10 @@ export class LSPService {
4479
4835
  // reads as unanswered, which for a primary is exactly the pre-#1549
4480
4836
  // verdict. This block can therefore only ever NARROW an inconclusive
4481
4837
  // touch, never create one.
4482
- const answeredForThisTouch = (entry) => {
4483
- const baseline = diagnosticBaselines.get(entry.client);
4484
- const currentPathVersion = readPathVersion(entry.client);
4485
- if (Number.isFinite(baseline) &&
4486
- currentPathVersion !== undefined &&
4487
- currentPathVersion > baseline) {
4488
- return true;
4489
- }
4490
- try {
4491
- return (entry.client.getAllDiagnostics?.().has(normalizedPath) === true);
4492
- }
4493
- catch {
4494
- // Fail closed: an unreadable cache is not evidence of an answer.
4495
- return false;
4496
- }
4497
- };
4498
4838
  // A deferred server was never sent this content and is not waited on, so
4499
4839
  // it cannot have "timed out" — it is already reported as a coverage gap.
4500
- const waited = spawned.filter((entry) => !deferredResyncServerIds.has(entry.info.id));
4840
+ const waited = spawned.filter((entry) => !deferredResyncServerIds.has(entry.info.id) &&
4841
+ !diagnosticsUnsupportedServerIds.includes(entry.info.id));
4501
4842
  const unanswered = waited.filter((entry) => !answeredForThisTouch(entry));
4502
4843
  diagnosticsUnansweredServerIds = unanswered.map((e) => e.info.id);
4503
4844
  diagnosticsUnansweredPrimaryServerIds = unanswered
@@ -4507,8 +4848,9 @@ export class LSPService {
4507
4848
  // preserve, so it keeps the pre-#1549 touch-wide verdict rather than
4508
4849
  // absolving itself on an auxiliary's evidence.
4509
4850
  const hasWaitedPrimary = waited.some((entry) => entry.info.role !== "auxiliary");
4851
+ const hasUnsupportedPrimary = diagnosticsUnsupportedServerIds.some((serverId) => spawned.some((entry) => entry.info.id === serverId && entry.info.role !== "auxiliary"));
4510
4852
  diagnosticsTimedOut =
4511
- !hasWaitedPrimary ||
4853
+ (!hasWaitedPrimary && !hasUnsupportedPrimary) ||
4512
4854
  diagnosticsUnansweredPrimaryServerIds.length > 0;
4513
4855
  for (const entry of unanswered) {
4514
4856
  incrementDegradationCount({
@@ -4693,7 +5035,7 @@ export class LSPService {
4693
5035
  // diagnostics from the client cache as always.
4694
5036
  let collected = options.collectDiagnostics
4695
5037
  ? tsserverSyncConfirmed !== undefined
4696
- ? mergeLspDiagnostics(tsserverSyncConfirmed)
5038
+ ? mergeLspDiagnostics(attributeDiagnostics(spawned[0]?.info.id ?? "unknown", tsserverSyncConfirmed))
4697
5039
  : mergeLspDiagnostics([
4698
5040
  // #1459: a DEFERRED server's cache still holds the PREVIOUS
4699
5041
  // content's findings — the resync that would have cleared it never
@@ -4705,8 +5047,8 @@ export class LSPService {
4705
5047
  // coverage naming and the merged binding read too.
4706
5048
  ...spawned.flatMap((entry) => droppedAuxiliaryServerIds.has(entry.info.id)
4707
5049
  ? []
4708
- : entry.client.getDiagnostics(filePath)),
4709
- ...carriedAuxiliary.flatMap((entry) => entry.diags),
5050
+ : attributeDiagnostics(entry.info.id, entry.client.getDiagnostics(filePath))),
5051
+ ...carriedAuxiliary.flatMap((entry) => attributeDiagnostics(entry.serverId, entry.diags)),
4710
5052
  ])
4711
5053
  : undefined;
4712
5054
  // #1095 (P3-b): whether `collected` came from a tsserver sync confirm
@@ -4746,7 +5088,9 @@ export class LSPService {
4746
5088
  retractPrimaryTimeoutAttribution(); // #1549
4747
5089
  syncConfirmed = true;
4748
5090
  collected =
4749
- syncResult.length > 0 ? mergeLspDiagnostics(syncResult) : [];
5091
+ syncResult.length > 0
5092
+ ? mergeLspDiagnostics(attributeDiagnostics(spawned[0]?.info.id ?? "unknown", syncResult))
5093
+ : [];
4750
5094
  logLatency({
4751
5095
  type: "phase",
4752
5096
  phase: "lsp_tsserver_sync_confirm",
@@ -5061,6 +5405,11 @@ export class LSPService {
5061
5405
  // collect, `binding` only for a collecting touch — a non-collecting touch
5062
5406
  // keeps resolving `{ diags: [] }`, no flags.
5063
5407
  const result = { diags: collected ?? [] };
5408
+ const primaryDiagnosticsUnsupported = diagnosticsUnsupportedServerIds.some((serverId) => spawned.some((entry) => entry.info.id === serverId && entry.info.role !== "auxiliary"));
5409
+ if (diagnosticsUnsupportedServerIds.length > 0) {
5410
+ result.diagnosticsUnsupportedServerIds =
5411
+ diagnosticsUnsupportedServerIds;
5412
+ }
5064
5413
  if (collected !== undefined && inconclusive) {
5065
5414
  result.inconclusive = true;
5066
5415
  // #1549: name the primary that produced the verdict and which deadline it
@@ -5073,6 +5422,13 @@ export class LSPService {
5073
5422
  }
5074
5423
  result.inconclusiveReason = verdict.inconclusiveReason;
5075
5424
  }
5425
+ else if (collected !== undefined && primaryDiagnosticsUnsupported) {
5426
+ // A navigation-only primary has no diagnostic confirmation to report,
5427
+ // but an uncovered auxiliary still needs to be named for delivery.
5428
+ if (unconfirmedServerIds.length > 0) {
5429
+ result.unconfirmedServerIds = [...unconfirmedServerIds];
5430
+ }
5431
+ }
5076
5432
  else if (collected !== undefined && coverageGap) {
5077
5433
  // #1470/#1493: narrowed, not collapsed. Reached for EITHER no-answer
5078
5434
  // shape — a cut-off auxiliary or a silent one with nothing published for
@@ -5083,7 +5439,7 @@ export class LSPService {
5083
5439
  result.confirmation = "partial";
5084
5440
  result.unconfirmedServerIds = [...unconfirmedServerIds];
5085
5441
  }
5086
- else if (collected !== undefined) {
5442
+ else if (collected !== undefined && !primaryDiagnosticsUnsupported) {
5087
5443
  // Preserve the lower-level affirmative result across consumers. In
5088
5444
  // particular, the silent-clean gates above clear diagnosticsTimedOut only
5089
5445
  // after a successful notify and capability-confirmed wait; reclassifying
@@ -5129,6 +5485,17 @@ export class LSPService {
5129
5485
  // fully delivered. Nothing to record here: a skipped server keeps its original
5130
5486
  // entry (and timestamp) so its window still expires naturally instead of being
5131
5487
  // extended by every reuse.
5488
+ // #2810: the half of the coverage gap that has a delivery path. Derived
5489
+ // from `unconfirmedServerIds` so the two can never disagree about the
5490
+ // same scanner, and intersected with the marked set so "findings will
5491
+ // arrive through the late path" is said only where a pending pair
5492
+ // exists. Everything else in the gap — a #1459 deferral, a breaker
5493
+ // skip, an ast-grep already covered by its napi fallback — keeps the
5494
+ // honest "silent, diagnostics are incomplete" half.
5495
+ const lateDeliveryPending = unconfirmedServerIds.filter((serverId) => (lateDeliveryServerIds ?? []).includes(serverId));
5496
+ if (lateDeliveryPending.length > 0) {
5497
+ result.deferredServerIds = lateDeliveryPending;
5498
+ }
5132
5499
  logLatency({
5133
5500
  type: "phase",
5134
5501
  phase: "lsp_touch_file",
@@ -5201,6 +5568,14 @@ export class LSPService {
5201
5568
  ...(uncoveredDeferredServerIds.length > 0 && {
5202
5569
  deferredResyncServerIds: uncoveredDeferredServerIds,
5203
5570
  }),
5571
+ // #2810: the scanners this touch promised late delivery for, so a
5572
+ // field query can join the promise to the pending pair that has to
5573
+ // honour it — and tell it apart from `deferredResyncServerIds`
5574
+ // above, which is the door that opened BEFORE any wait and has no
5575
+ // delivery path at all.
5576
+ ...(lateDeliveryPending.length > 0 && {
5577
+ lateDeliveryServerIds: lateDeliveryPending,
5578
+ }),
5204
5579
  // #1549: auxiliaries whose own deadline lapsed — the wait produced no
5205
5580
  // publication, or the notify write never landed. Distinct from the fields
5206
5581
  // above (a different door) and from `auxUnconfirmedServerIds` (derived
@@ -5437,7 +5812,7 @@ export class LSPService {
5437
5812
  if (seen.has(key))
5438
5813
  continue;
5439
5814
  seen.add(key);
5440
- merged.push(diagnostic);
5815
+ merged.push({ ...diagnostic, serverId: entry.serverId });
5441
5816
  }
5442
5817
  }
5443
5818
  const rawCount = perServerFull.reduce((sum, entry) => sum + entry.diagnosticCount, 0);
@@ -5854,6 +6229,9 @@ export class LSPService {
5854
6229
  root,
5855
6230
  operationSupport: client.getOperationSupport(),
5856
6231
  workspaceDiagnosticsSupport: client.getWorkspaceDiagnosticsSupport(),
6232
+ customServer: server.custom,
6233
+ diagnosticsPublished: this.state.diagnosticsPublished.has(server.id),
6234
+ diagnosticsUnsupported: this.state.diagnosticsUnsupported.has(server.id),
5857
6235
  advertisedCommands: client.getAdvertisedCommands(),
5858
6236
  rawCapabilityKeys: client.getRawCapabilityKeys?.() ?? [],
5859
6237
  launchVariant: client.getLaunchVariant?.(),
@@ -5871,6 +6249,9 @@ export class LSPService {
5871
6249
  root: client.root,
5872
6250
  operationSupport: client.getOperationSupport(),
5873
6251
  workspaceDiagnosticsSupport: client.getWorkspaceDiagnosticsSupport(),
6252
+ customServer: this.state.servers.get(serverId)?.custom,
6253
+ diagnosticsPublished: this.state.diagnosticsPublished.has(serverId),
6254
+ diagnosticsUnsupported: this.state.diagnosticsUnsupported.has(serverId),
5874
6255
  advertisedCommands: client.getAdvertisedCommands(),
5875
6256
  rawCapabilityKeys: client.getRawCapabilityKeys?.() ?? [],
5876
6257
  launchVariant: client.getLaunchVariant?.(),
@@ -6599,6 +6980,42 @@ export class LSPService {
6599
6980
  const workspaceSweepScopeKey = buildScopeKey("all", [
6600
6981
  ...WORKSPACE_SWEEP_EXCLUDED_SERVER_IDS,
6601
6982
  ]);
6983
+ const touchScopedOpenDependencies = async (filePath) => {
6984
+ if (options.files === undefined)
6985
+ return true;
6986
+ // Import coverage is language-neutral. Undefined means this file was
6987
+ // not covered by the facts seam, so its cache entry is not eligible.
6988
+ const imports = workspaceDiagnosticsCacheCtx.importsFor(filePath);
6989
+ if (!imports)
6990
+ return false;
6991
+ const openImports = imports.filter((dependency) => this.hasLiveClientHoldingDocument(dependency));
6992
+ const capped = openImports.length > SCOPED_DEPENDENCY_TOUCH_MAX;
6993
+ if (capped) {
6994
+ recordDegradationOnce({
6995
+ kind: "lsp_dependency_touch_capped",
6996
+ subject: filePath,
6997
+ reason: `scoped full scan limited open import touches to ${SCOPED_DEPENDENCY_TOUCH_MAX}`,
6998
+ });
6999
+ }
7000
+ await Promise.all(openImports
7001
+ .slice(0, SCOPED_DEPENDENCY_TOUCH_MAX)
7002
+ .map(async (dependency) => {
7003
+ let content;
7004
+ try {
7005
+ content = await fs.readFile(dependency, "utf8");
7006
+ }
7007
+ catch {
7008
+ return;
7009
+ }
7010
+ await this.touchFile(dependency, content, {
7011
+ diagnostics: "none",
7012
+ source: "lens_diagnostics_full_dependency",
7013
+ clientScope: "all",
7014
+ excludeServerIds: await this.serverIdsNotHoldingDocument(dependency),
7015
+ });
7016
+ }));
7017
+ return !capped;
7018
+ };
6602
7019
  const cachedResults = [];
6603
7020
  const filesToTouch = [];
6604
7021
  const writeIndexByPath = new Map();
@@ -6615,18 +7032,22 @@ export class LSPService {
6615
7032
  // exists to detect) must NOT be replayed and reconciled as confirmed via
6616
7033
  // mode=full. On a mismatch, fall through to a fresh touch.
6617
7034
  if (cached && cached.binding.boundToCurrentDisk !== false) {
6618
- cachedResults.push({
6619
- filePath,
6620
- diagnostics: cached.diagnostics,
6621
- count: cached.count,
6622
- // #1093: a cache hit replays an older observation — carry its
6623
- // scan time so mode=full's footer reconcile stamps `touchedAt`
6624
- // with when the truth was seen, not now().
6625
- observedAt: cached.scannedAt,
6626
- contentHash: cached.binding.contentHash,
6627
- boundToCurrentDisk: cached.binding.boundToCurrentDisk,
6628
- writeIndex: writeIndexByPath.get(normalizeMapKey(filePath)),
6629
- });
7035
+ const dependenciesConfirmed = await touchScopedOpenDependencies(filePath);
7036
+ if (dependenciesConfirmed)
7037
+ cachedResults.push({
7038
+ filePath,
7039
+ diagnostics: cached.diagnostics,
7040
+ count: cached.count,
7041
+ // #1093: a cache hit replays an older observation — carry its
7042
+ // scan time so mode=full's footer reconcile stamps `touchedAt`
7043
+ // with when the truth was seen, not now().
7044
+ observedAt: cached.scannedAt,
7045
+ contentHash: cached.binding.contentHash,
7046
+ boundToCurrentDisk: cached.binding.boundToCurrentDisk,
7047
+ writeIndex: writeIndexByPath.get(normalizeMapKey(filePath)),
7048
+ });
7049
+ else
7050
+ filesToTouch.push(filePath);
6630
7051
  }
6631
7052
  else {
6632
7053
  filesToTouch.push(filePath);
@@ -7042,7 +7463,7 @@ export class LSPService {
7042
7463
  // ONE bracket for the whole fan-out, not one per file (#2272 review F1).
7043
7464
  // Per-file bracketing was the first shape and it was inert for exactly
7044
7465
  // the case above: `phaseFinished` pushes onto a ring capped at
7045
- // `CLOSED_BRACKET_CAP` (5, clients/latency-logger.ts), while `turn_end`
7466
+ // `RECENT_PHASE_CAP` (5, clients/latency-logger.ts), while `turn_end`
7046
7467
  // reads `getPhaseForWindow` AFTER the sweep returns. A 225-file sweep
7047
7468
  // therefore evicted the blocking file's own bracket unless the block
7048
7469
  // landed in the last five files, and the survivors were then rejected by
@@ -7412,16 +7833,17 @@ export class LSPService {
7412
7833
  client.pruneDiagnostics((filePath, ts) => !existingPaths.has(filePath) || now - ts > CASCADE_DIAGNOSTICS_TTL_MS);
7413
7834
  const clientDiags = client.getAllDiagnostics();
7414
7835
  for (const [filePath, entry] of clientDiags) {
7836
+ const attributed = attributeDiagnostics(client.serverId, entry.diags);
7415
7837
  const existing = all.get(filePath);
7416
7838
  if (existing) {
7417
7839
  existing.diags = mergeLspDiagnostics([
7418
7840
  ...existing.diags,
7419
- ...entry.diags,
7841
+ ...attributed,
7420
7842
  ]);
7421
7843
  existing.ts = Math.max(existing.ts, entry.ts);
7422
7844
  }
7423
7845
  else {
7424
- all.set(filePath, { diags: [...entry.diags], ts: entry.ts });
7846
+ all.set(filePath, { diags: attributed, ts: entry.ts });
7425
7847
  }
7426
7848
  const list = bindingsByPath.get(filePath) ?? [];
7427
7849
  list.push(entry.binding);
@@ -7515,6 +7937,7 @@ export class LSPService {
7515
7937
  if (this.checkDestroyed())
7516
7938
  return;
7517
7939
  this.isDestroyed = true;
7940
+ this.shutdownController.abort();
7518
7941
  for (const [key, token] of this.outstandingAuxNotifyWrites) {
7519
7942
  this.releaseOutstandingAuxNotifyWrite(key, token);
7520
7943
  }
@@ -7563,6 +7986,7 @@ export class LSPService {
7563
7986
  });
7564
7987
  this.state.clients.clear();
7565
7988
  this.state.broken.clear();
7989
+ this.firstContactWaits.clear();
7566
7990
  // #1934 review F1: map hygiene alongside the breaker it sits next to.
7567
7991
  // Not load-bearing — every read follows its own attempt's write — but a
7568
7992
  // verdict for a client generation that no longer exists is dead weight.
@@ -7779,6 +8203,9 @@ export async function hasAuxiliaryLspPublishedForRoot(serverId, filePath) {
7779
8203
  export async function notifyExternalFileChange(filePath, type) {
7780
8204
  return getLSPService().notifyExternalFileChange(filePath, type);
7781
8205
  }
8206
+ export async function resyncGitChangedFiles(changedPaths) {
8207
+ return getLSPService().resyncGitChangedFiles(changedPaths);
8208
+ }
7782
8209
  export function resetLSPService(options = {}) {
7783
8210
  // #2504 review round 2 (F3): whatever the retiring service was still being
7784
8211
  // asked for off-hook must stop HERE, before any teardown and before the