pi-lens 4.1.2 → 4.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (359) hide show
  1. package/CHANGELOG.md +1308 -0
  2. package/README.md +3 -0
  3. package/config/biome/core.jsonc +11 -2
  4. package/config/dependency-cruiser-eager-allowlist.json +91 -0
  5. package/dist/clients/actionable-warnings-logger.js +18 -3
  6. package/dist/clients/actionable-warnings.js +927 -80
  7. package/dist/clients/agent-behavior-client.js +13 -4
  8. package/dist/clients/ast-grep-client.js +136 -9
  9. package/dist/clients/ast-grep-rule-manager.js +60 -5
  10. package/dist/clients/ast-grep-tool-logger.js +2 -2
  11. package/dist/clients/availability-probe-flight.js +12 -0
  12. package/dist/clients/bash-file-access.js +1 -3
  13. package/dist/clients/biome-client.js +16 -2
  14. package/dist/clients/blocker-freshness.js +34 -0
  15. package/dist/clients/bootstrap.js +509 -73
  16. package/dist/clients/bounded-cache.js +152 -12
  17. package/dist/clients/bounded-telemetry.js +115 -23
  18. package/dist/clients/build-identity.js +95 -0
  19. package/dist/clients/bundled-resource-health.js +113 -0
  20. package/dist/clients/bus-events-logger.js +2 -2
  21. package/dist/clients/bus-publish.js +3 -0
  22. package/dist/clients/cache/rule-cache.js +33 -5
  23. package/dist/clients/cache-manager.js +105 -6
  24. package/dist/clients/cache-observability.js +46 -19
  25. package/dist/clients/cargo-manifest.js +422 -0
  26. package/dist/clients/cascade-logger.js +17 -3
  27. package/dist/clients/code-quality-warnings.js +13 -3
  28. package/dist/clients/complexity-client.js +16 -5
  29. package/dist/clients/config-core/deny.js +221 -0
  30. package/dist/clients/config-core/index.js +47 -0
  31. package/dist/clients/config-core/merge.js +357 -0
  32. package/dist/clients/config-core/normalize.js +340 -0
  33. package/dist/clients/config-core/process-spec.js +248 -0
  34. package/dist/clients/config-core/provenance.js +171 -0
  35. package/dist/clients/config-core/records.js +218 -0
  36. package/dist/clients/config-core/resolve.js +125 -0
  37. package/dist/clients/config-core/safe-object.js +78 -0
  38. package/dist/clients/config-core/schema.js +167 -0
  39. package/dist/clients/config-diagnostic-codes.js +281 -0
  40. package/dist/clients/config-locations.js +160 -0
  41. package/dist/clients/config-resolve.js +789 -0
  42. package/dist/clients/config-schema.js +197 -0
  43. package/dist/clients/config-warn.js +407 -0
  44. package/dist/clients/dead-code-client.js +33 -25
  45. package/dist/clients/dead-code-logger.js +2 -2
  46. package/dist/clients/deadline-utils.js +178 -0
  47. package/dist/clients/debug-handles.js +2 -2
  48. package/dist/clients/debug-heap.js +3 -3
  49. package/dist/clients/deferred-lsp-work.js +106 -0
  50. package/dist/clients/degradation-ledger.js +166 -8
  51. package/dist/clients/demoted-finding-render.js +14 -0
  52. package/dist/clients/dependency-checker.js +28 -22
  53. package/dist/clients/deps/minimatch.js +1 -1
  54. package/dist/clients/diagnostic-line-freshness.js +12 -9
  55. package/dist/clients/diagnostic-logger.js +14 -4
  56. package/dist/clients/dispatch/collect-later-tier.js +39 -0
  57. package/dist/clients/dispatch/dispatcher.js +304 -19
  58. package/dist/clients/dispatch/fact-store.js +328 -9
  59. package/dist/clients/dispatch/integration.js +214 -107
  60. package/dist/clients/dispatch/pending-runner-findings.js +87 -0
  61. package/dist/clients/dispatch/plan.js +21 -5
  62. package/dist/clients/dispatch/runners/actionlint.js +0 -1
  63. package/dist/clients/dispatch/runners/ast-grep-napi.js +550 -77
  64. package/dist/clients/dispatch/runners/biome-check.js +0 -1
  65. package/dist/clients/dispatch/runners/cpp-check.js +0 -1
  66. package/dist/clients/dispatch/runners/credo.js +0 -1
  67. package/dist/clients/dispatch/runners/cue-vet.js +0 -1
  68. package/dist/clients/dispatch/runners/dart-analyze.js +0 -1
  69. package/dist/clients/dispatch/runners/detekt.js +0 -1
  70. package/dist/clients/dispatch/runners/dotnet-build.js +0 -1
  71. package/dist/clients/dispatch/runners/elixir-check.js +0 -1
  72. package/dist/clients/dispatch/runners/eslint.js +5 -2
  73. package/dist/clients/dispatch/runners/fact-rules.js +0 -1
  74. package/dist/clients/dispatch/runners/fish-indent.js +0 -1
  75. package/dist/clients/dispatch/runners/gleam-check.js +0 -1
  76. package/dist/clients/dispatch/runners/go-vet.js +1 -3
  77. package/dist/clients/dispatch/runners/golangci-lint.js +0 -1
  78. package/dist/clients/dispatch/runners/hadolint.js +0 -1
  79. package/dist/clients/dispatch/runners/helm-lint.js +16 -4
  80. package/dist/clients/dispatch/runners/helm-render.js +54 -10
  81. package/dist/clients/dispatch/runners/htmlhint.js +0 -1
  82. package/dist/clients/dispatch/runners/index.js +1 -3
  83. package/dist/clients/dispatch/runners/javac.js +0 -1
  84. package/dist/clients/dispatch/runners/ktlint.js +0 -1
  85. package/dist/clients/dispatch/runners/lsp.js +37 -2
  86. package/dist/clients/dispatch/runners/markdownlint.js +4 -2
  87. package/dist/clients/dispatch/runners/mypy.js +0 -1
  88. package/dist/clients/dispatch/runners/oxlint.js +24 -22
  89. package/dist/clients/dispatch/runners/php-lint.js +0 -1
  90. package/dist/clients/dispatch/runners/phpstan.js +0 -1
  91. package/dist/clients/dispatch/runners/prisma-validate.js +0 -1
  92. package/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -1
  93. package/dist/clients/dispatch/runners/pyright.js +7 -4
  94. package/dist/clients/dispatch/runners/rubocop.js +0 -1
  95. package/dist/clients/dispatch/runners/ruff.js +0 -1
  96. package/dist/clients/dispatch/runners/rust-clippy.js +9 -6
  97. package/dist/clients/dispatch/runners/shellcheck.js +0 -1
  98. package/dist/clients/dispatch/runners/shfmt.js +0 -1
  99. package/dist/clients/dispatch/runners/spellcheck.js +0 -1
  100. package/dist/clients/dispatch/runners/spotbugs.js +0 -1
  101. package/dist/clients/dispatch/runners/sqlfluff.js +0 -1
  102. package/dist/clients/dispatch/runners/stylelint.js +0 -1
  103. package/dist/clients/dispatch/runners/swiftlint.js +0 -1
  104. package/dist/clients/dispatch/runners/taplo.js +0 -1
  105. package/dist/clients/dispatch/runners/terragrunt.js +0 -1
  106. package/dist/clients/dispatch/runners/tflint.js +0 -1
  107. package/dist/clients/dispatch/runners/tree-sitter.js +0 -1
  108. package/dist/clients/dispatch/runners/trivy-config.js +0 -1
  109. package/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
  110. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
  111. package/dist/clients/dispatch/runners/utils/runner-helpers.js +270 -105
  112. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
  113. package/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
  114. package/dist/clients/dispatch/runners/utils/toolchain-availability.js +89 -26
  115. package/dist/clients/dispatch/runners/vale.js +0 -1
  116. package/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
  117. package/dist/clients/dispatch/runners/yamllint.js +0 -1
  118. package/dist/clients/dispatch/runners/zig-check.js +0 -1
  119. package/dist/clients/disposition-logger.js +3 -3
  120. package/dist/clients/effective-config.js +403 -0
  121. package/dist/clients/error-class.js +23 -0
  122. package/dist/clients/event-loop-hold.js +274 -0
  123. package/dist/clients/event-loop-monitor.js +66 -1
  124. package/dist/clients/extension-log.js +2 -2
  125. package/dist/clients/feature-hints.js +2 -1
  126. package/dist/clients/file-time.js +6 -1
  127. package/dist/clients/file-utils.js +266 -13
  128. package/dist/clients/finding-delivery-gate.js +55 -13
  129. package/dist/clients/format-service.js +6 -1
  130. package/dist/clients/formatters.js +303 -104
  131. package/dist/clients/freshness-cadence.js +17 -0
  132. package/dist/clients/generated-artifacts.js +114 -19
  133. package/dist/clients/generation-guard.js +4 -9
  134. package/dist/clients/git-guard.js +198 -48
  135. package/dist/clients/git-tracked-ignore.js +32 -1
  136. package/dist/clients/go-client.js +39 -0
  137. package/dist/clients/govulncheck-client.js +5 -1
  138. package/dist/clients/gradle-ktfmt-style.js +252 -0
  139. package/dist/clients/gzip-stage-write.js +7 -0
  140. package/dist/clients/hashline-anchor.js +424 -0
  141. package/dist/clients/hook-budgets.js +76 -0
  142. package/dist/clients/host-edit-normalize.js +5 -2
  143. package/dist/clients/host-ports.js +1 -1
  144. package/dist/clients/installer/index.js +200 -84
  145. package/dist/clients/installer/managed-tool-refresh.js +3 -1
  146. package/dist/clients/instance-reaper.js +108 -243
  147. package/dist/clients/instance-registry-lock.js +145 -0
  148. package/dist/clients/instance-registry.js +435 -122
  149. package/dist/clients/jscpd-client.js +6 -1
  150. package/dist/clients/language-profile.js +4 -2
  151. package/dist/clients/language-registry.js +595 -0
  152. package/dist/clients/latency-logger.js +179 -3
  153. package/dist/clients/ledger-bounds.js +13 -0
  154. package/dist/clients/lens-config.js +141 -30
  155. package/dist/clients/lens-engine.js +30 -37
  156. package/dist/clients/lens-flag-registry.js +67 -6
  157. package/dist/clients/lens-map.js +3 -1
  158. package/dist/clients/log-cleanup.js +2 -2
  159. package/dist/clients/lsp/aggregation.js +3 -1
  160. package/dist/clients/lsp/cascade-tier.js +4 -0
  161. package/dist/clients/lsp/client.js +672 -49
  162. package/dist/clients/lsp/config.js +504 -156
  163. package/dist/clients/lsp/diagnostic-binding.js +10 -5
  164. package/dist/clients/lsp/edits.js +15 -2
  165. package/dist/clients/lsp/index.js +1179 -247
  166. package/dist/clients/lsp/jvm-runtime.js +5 -0
  167. package/dist/clients/lsp/language.js +18 -169
  168. package/dist/clients/lsp/launch.js +38 -9
  169. package/dist/clients/lsp/pending-aux-coverage.js +113 -12
  170. package/dist/clients/lsp/server.js +426 -160
  171. package/dist/clients/lsp/session-roots.js +192 -0
  172. package/dist/clients/lsp/spawn-history.js +2 -7
  173. package/dist/clients/lsp/tsserver-sync.js +9 -2
  174. package/dist/clients/lsp/workspace-diagnostics-cache.js +17 -7
  175. package/dist/clients/lsp/workspace-sweep-hold.js +32 -23
  176. package/dist/clients/lsp-mutation.js +179 -23
  177. package/dist/clients/map-with-concurrency.js +36 -0
  178. package/dist/clients/mcp/analyze.js +45 -10
  179. package/dist/clients/mcp/session.js +19 -17
  180. package/dist/clients/memory-sampler.js +28 -1
  181. package/dist/clients/message-end-attribution.js +30 -0
  182. package/dist/clients/metrics-history.js +21 -4
  183. package/dist/clients/middle-man-analysis.js +2 -4
  184. package/dist/clients/module-report.js +20 -45
  185. package/dist/clients/mutating-tool.js +651 -0
  186. package/dist/clients/mutation-attribution.js +368 -0
  187. package/dist/clients/mutation-bridge.js +240 -0
  188. package/dist/clients/ndjson-logger.js +247 -20
  189. package/dist/clients/observed-mutation-sources.js +101 -0
  190. package/dist/clients/observed-mutation.js +1215 -0
  191. package/dist/clients/opaque-mutation-scan.js +300 -40
  192. package/dist/clients/opengrep-config.js +4 -0
  193. package/dist/clients/package-manager.js +201 -44
  194. package/dist/clients/partial-edit-apply.js +359 -80
  195. package/dist/clients/path-attribution-telemetry.js +23 -7
  196. package/dist/clients/path-keyed-map.js +35 -3
  197. package/dist/clients/path-utils.js +460 -13
  198. package/dist/clients/persist-debounce.js +8 -1
  199. package/dist/clients/php-cs-fixer-config.js +114 -0
  200. package/dist/clients/pipeline.js +77 -24
  201. package/dist/clients/probe-home-state.js +228 -0
  202. package/dist/clients/process-bridge.js +66 -0
  203. package/dist/clients/process-singletons.js +210 -0
  204. package/dist/clients/process-snapshot.js +68 -0
  205. package/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
  206. package/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
  207. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
  208. package/dist/clients/project-diagnostics/scanner.js +61 -24
  209. package/dist/clients/project-lens-config.js +393 -122
  210. package/dist/clients/project-snapshot.js +85 -35
  211. package/dist/clients/python-environment.js +217 -0
  212. package/dist/clients/python-provenance.js +639 -0
  213. package/dist/clients/quiet-window.js +6 -1
  214. package/dist/clients/read-bridge.js +13 -21
  215. package/dist/clients/read-expansion.js +9 -46
  216. package/dist/clients/read-guard-logger.js +39 -6
  217. package/dist/clients/read-guard-tool-lines.js +364 -212
  218. package/dist/clients/read-guard.js +95 -7
  219. package/dist/clients/recent-touches.js +17 -5
  220. package/dist/clients/resource-sampler.js +195 -96
  221. package/dist/clients/review-graph/builder.js +752 -132
  222. package/dist/clients/review-graph/service.js +55 -3
  223. package/dist/clients/review-graph/shared-extraction-ir.js +7 -15
  224. package/dist/clients/review-graph/tsconfig-paths.js +5 -1
  225. package/dist/clients/review-graph/workspace-modules.js +10 -74
  226. package/dist/clients/review-graph-logger.js +30 -4
  227. package/dist/clients/runtime-agent-end.js +18 -2
  228. package/dist/clients/runtime-context.js +49 -16
  229. package/dist/clients/runtime-coordinator.js +93 -6
  230. package/dist/clients/runtime-session.js +169 -34
  231. package/dist/clients/runtime-tool-call.js +183 -35
  232. package/dist/clients/runtime-tool-result.js +894 -241
  233. package/dist/clients/runtime-turn.js +1264 -74
  234. package/dist/clients/rust-client.js +31 -0
  235. package/dist/clients/safe-spawn.js +191 -28
  236. package/dist/clients/sanitize.js +7 -1
  237. package/dist/clients/scratch-tree-policy.js +1 -3
  238. package/dist/clients/security-scan-client.js +51 -17
  239. package/dist/clients/session-lifecycle.js +238 -42
  240. package/dist/clients/session-start-observability.js +79 -0
  241. package/dist/clients/sessionstart-logger.js +12 -3
  242. package/dist/clients/sg-runner.js +27 -15
  243. package/dist/clients/sgconfig.js +6 -1
  244. package/dist/clients/shared-checkout-guard.js +338 -0
  245. package/dist/clients/skills-resolver.js +105 -0
  246. package/dist/clients/smells-rollup.js +2 -2
  247. package/dist/clients/spawn-output-cap.js +37 -0
  248. package/dist/clients/spawn-timeout-cooldown.js +15 -1
  249. package/dist/clients/startup-scan.js +6 -7
  250. package/dist/clients/startup-timing.js +69 -4
  251. package/dist/clients/string-utils.js +33 -0
  252. package/dist/clients/subagent-mode.js +17 -4
  253. package/dist/clients/test-runner-client.js +586 -118
  254. package/dist/clients/test-runner-delivery.js +239 -0
  255. package/dist/clients/tool-definition.js +41 -1
  256. package/dist/clients/tool-policy.js +92 -22
  257. package/dist/clients/tree-sitter-cache.js +40 -40
  258. package/dist/clients/tree-sitter-client.js +97 -53
  259. package/dist/clients/tree-sitter-logger.js +19 -3
  260. package/dist/clients/tree-sitter-query-loader.js +81 -1
  261. package/dist/clients/tree-sitter-shared.js +51 -46
  262. package/dist/clients/tree-sitter-symbol-extractor.js +28 -0
  263. package/dist/clients/typos-config.js +5 -0
  264. package/dist/clients/user-notify.js +6 -2
  265. package/dist/clients/warm-attach.js +8 -10
  266. package/dist/clients/widget-state.js +191 -26
  267. package/dist/clients/word-index-logger.js +15 -3
  268. package/dist/clients/word-index-store.js +463 -0
  269. package/dist/clients/word-index.js +815 -125
  270. package/dist/clients/workspace-topology.js +25 -1
  271. package/dist/clients/zizmor-config.js +29 -6
  272. package/dist/index.js +68577 -53950
  273. package/dist/mcp/analyze-cli.js +3 -1
  274. package/dist/mcp/build-staleness.js +8 -7
  275. package/dist/mcp/cli.js +3 -1
  276. package/dist/mcp/server.js +133 -6
  277. package/dist/scripts/lib/process-scan.mjs +583 -0
  278. package/dist/scripts/lib/skills-predicate.mjs +129 -0
  279. package/dist/tools/effective-config.js +89 -0
  280. package/dist/tools/lens-diagnostics.js +99 -24
  281. package/dist/tools/lsp-diagnostics.js +81 -85
  282. package/dist/tools/lsp-navigation.js +40 -17
  283. package/dist/tools/shared.js +0 -1
  284. package/docs/agent-guide.md +68 -3
  285. package/docs/ast-grep_rules_catalog.md +41 -10
  286. package/docs/astplayground.md +26 -17
  287. package/docs/configuration.md +222 -0
  288. package/docs/dependencies.md +3 -3
  289. package/docs/features.md +44 -5
  290. package/docs/language-coverage.md +2 -2
  291. package/docs/pi-lens-fixer.md +25 -0
  292. package/docs/pi-lens-investigator.md +25 -0
  293. package/docs/pi-lens-reviewer.md +27 -0
  294. package/docs/pi-lens-subagent.md +38 -0
  295. package/docs/pi-lens-warden.md +55 -0
  296. package/docs/public-api-stability.md +359 -0
  297. package/docs/release-qa-baseline.md +182 -0
  298. package/docs/subagent-compat.md +110 -30
  299. package/docs/tree-sitter_rules_catalog.md +2 -2
  300. package/package.json +17 -21
  301. package/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
  302. package/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
  303. package/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
  304. package/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
  305. package/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
  306. package/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
  307. package/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
  308. package/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
  309. package/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
  310. package/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
  311. package/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
  312. package/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
  313. package/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
  314. package/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
  315. package/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
  316. package/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
  317. package/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
  318. package/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
  319. package/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
  320. package/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
  321. package/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
  322. package/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
  323. package/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
  324. package/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
  325. package/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
  326. package/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
  327. package/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
  328. package/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
  329. package/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
  330. package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
  331. package/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
  332. package/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
  333. package/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
  334. package/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
  335. package/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
  336. package/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
  337. package/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
  338. package/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
  339. package/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
  340. package/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
  341. package/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
  342. package/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
  343. package/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
  344. package/rules/ast-grep-rules/rules/no-important.yml +21 -0
  345. package/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
  346. package/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
  347. package/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
  348. package/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
  349. package/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
  350. package/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
  351. package/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
  352. package/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
  353. package/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
  354. package/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
  355. package/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
  356. package/scripts/analyze-pi-lens-logs.mjs +192 -1
  357. package/scripts/install-selftest.mjs +58 -2
  358. package/scripts/lib/skills-predicate.mjs +129 -0
  359. package/scripts/rpc-load-check.mjs +3 -1
@@ -17,26 +17,33 @@ import { logExtension } from "../extension-log.js";
17
17
  import * as fs from "node:fs";
18
18
  import * as path from "node:path";
19
19
  import { recordRunner } from "../widget-state.js";
20
+ import { incrementDegradationCount, recordDegradationOnce, } from "../degradation-ledger.js";
20
21
  import { detectFileKind } from "../file-kinds.js";
21
22
  import { detectFileRole } from "../file-role.js";
23
+ import { classifyGeneratedOrArtifactDetailed, } from "../generated-artifacts.js";
22
24
  import { isTestFile } from "../file-utils.js";
23
25
  import { getPrimaryDispatchGroup } from "../language-policy.js";
24
26
  import { resolveLanguageRootForFile } from "../language-profile.js";
25
27
  import { logLatency, phaseFinished, phaseStarted } from "../latency-logger.js";
26
28
  import { isSpawnableCommand } from "../installer/index.js";
27
- import { normalizeMapKey } from "../path-utils.js";
29
+ import { isAbsoluteFilePath, normalizeEphemeralMapKey, normalizeMapKey, } from "../path-utils.js";
28
30
  import { loadPiLensProjectConfig } from "../project-lens-config.js";
29
31
  import { RUNTIME_CONFIG, getRunnerTimeoutFloorMs } from "../runtime-config.js";
30
32
  import { safeSpawnAsync } from "../safe-spawn.js";
31
33
  import { classifyDiagnostic } from "./diagnostic-taxonomy.js";
32
34
  import { classifyProbeFailure, logAvailabilityDecision, startHostStallSampler, transientRetryDelayMs, } from "./runners/utils/availability-policy.js";
35
+ import { recordAvailabilityProbeOverrun, getDispatchAvailabilityGeneration, } from "./runners/utils/runner-helpers.js";
36
+ import { createAvailabilityProbeFlight } from "../availability-probe-flight.js";
33
37
  import { applyDispositions } from "../diagnostic-dispositions.js";
34
38
  import { applyInlineSuppressions } from "./inline-suppressions.js";
35
39
  import { getToolPlan } from "./plan.js";
36
40
  import { resolveRunnerPath } from "./runner-context.js";
41
+ import { classifyObservedRunner, COLLECT_LATER_THRESHOLD_MS, observeRunnerLatency, } from "./collect-later-tier.js";
42
+ import { deferRunnerFindings } from "./pending-runner-findings.js";
37
43
  import { applyRulePolicy, rulePolicyMapFromConfig } from "./rule-policy.js";
38
44
  import { getToolProfile } from "./tool-profile.js";
39
45
  import { isRunnerSkipReason } from "./types.js";
46
+ const dispatcherProbeFlights = createAvailabilityProbeFlight({ generation: () => getDispatchAvailabilityGeneration() });
40
47
  import { formatDiagnostics } from "./utils/format-utils.js";
41
48
  // --- Runner Registry ---
42
49
  export class RunnerRegistry {
@@ -55,7 +62,7 @@ export class RunnerRegistry {
55
62
  for (const runner of this.runners.values()) {
56
63
  if (isTest && runner.skipTestFiles)
57
64
  continue;
58
- if (runner.appliesTo.includes(kind) || runner.appliesTo.length === 0) {
65
+ if (runnerAppliesToKind(runner, kind)) {
59
66
  matching.push(runner);
60
67
  }
61
68
  }
@@ -68,6 +75,10 @@ export class RunnerRegistry {
68
75
  this.runners.clear();
69
76
  }
70
77
  }
78
+ function runnerAppliesToKind(runner, kind) {
79
+ return (runner.appliesTo.length === 0 ||
80
+ (kind !== undefined && runner.appliesTo.includes(kind)));
81
+ }
71
82
  // --- Tool Availability Cache ---
72
83
  /**
73
84
  * Normalize a command name to a FactStore session key.
@@ -134,15 +145,19 @@ export async function checkToolAvailability(command, facts) {
134
145
  const startedAt = Date.now();
135
146
  let result;
136
147
  let hostStallMs;
148
+ let probeJoined = false;
137
149
  try {
138
- result = await safeSpawnAsync(command, ["--version"], {
150
+ const shared = dispatcherProbeFlights.run(`dispatcher:${key}`, () => safeSpawnAsync(command, ["--version"], {
139
151
  timeout: TOOL_PROBE_TIMEOUT_MS,
140
- });
152
+ }));
153
+ probeJoined = shared.joined;
154
+ result = await shared.promise;
141
155
  }
142
156
  finally {
143
157
  hostStallMs = sampler.stop();
144
158
  }
145
159
  const elapsedMs = Date.now() - startedAt;
160
+ recordAvailabilityProbeOverrun(command, key, elapsedMs, TOOL_PROBE_TIMEOUT_MS, result.failure);
146
161
  if (result.status === 0) {
147
162
  facts.setSessionFact(key, true);
148
163
  // The tool answered: retire the cooldown facts rather than leaving a
@@ -156,6 +171,7 @@ export async function checkToolAvailability(command, facts) {
156
171
  cause: "ok",
157
172
  elapsedMs,
158
173
  latched: true,
174
+ classifiedBy: probeJoined ? "joined" : "probe",
159
175
  hostStallMs,
160
176
  budgetMs: TOOL_PROBE_TIMEOUT_MS,
161
177
  });
@@ -188,7 +204,7 @@ export async function checkToolAvailability(command, facts) {
188
204
  hostStallMs,
189
205
  ...(retryAfterMs !== undefined && { retryAfterMs }),
190
206
  budgetMs: TOOL_PROBE_TIMEOUT_MS,
191
- classifiedBy: "probe",
207
+ classifiedBy: probeJoined ? "joined" : "probe",
192
208
  evidence,
193
209
  });
194
210
  return false;
@@ -234,7 +250,19 @@ telemetryModel, telemetryProvider) {
234
250
  const normalizedCwd = normalizeMapKey(resolveLanguageRootForFile(absoluteFilePath, cwd));
235
251
  const normalizedFilePath = normalizeMapKey(absoluteFilePath);
236
252
  const kind = detectFileKind(normalizedFilePath);
237
- const fileRole = detectFileRole(normalizedFilePath, readFilePrefix(normalizedFilePath));
253
+ const contentPrefix = readFilePrefix(normalizedFilePath);
254
+ const fileRole = detectFileRole(normalizedFilePath, contentPrefix);
255
+ // Captured once here so the generated short-circuit below can emit a
256
+ // `dispatch_skipped_generated` record carrying the deciding evidence tier
257
+ // and the measured line-shape statistic — without re-reading the file
258
+ // (refs #2346). The content passed is the same 4096-byte prefix already
259
+ // read for role detection, so this classification costs no extra I/O.
260
+ const generatedDetail = fileRole === "generated"
261
+ ? classifyGeneratedOrArtifactDetailed(normalizedFilePath, {
262
+ content: contentPrefix,
263
+ includeDeclarations: false,
264
+ })
265
+ : undefined;
238
266
  const projectConfig = loadPiLensProjectConfig(normalizedCwd);
239
267
  return {
240
268
  filePath: normalizedFilePath,
@@ -242,6 +270,8 @@ telemetryModel, telemetryProvider) {
242
270
  cwd: normalizedCwd,
243
271
  kind,
244
272
  fileRole,
273
+ generatedEvidence: generatedDetail?.evidence,
274
+ generatedLineShapeMean: generatedDetail?.lineShapeMean,
245
275
  pi,
246
276
  autofix: false,
247
277
  deltaMode: !pi.getFlag("no-delta"),
@@ -419,13 +449,19 @@ function buildCoverageNotice(ctx, runnerLatencies) {
419
449
  // The marker describes this exact silent-scanner set. A scanner can
420
450
  // recover while another goes dark on the same file, so the set belongs
421
451
  // in the session dedupe identity rather than only kind and path.
452
+ // #2016: these are SCANNER IDS, not filesystem paths. `normalizeMapKey`
453
+ // would realpath each one; on Windows that fails, falls through to
454
+ // `resolveNonExisting`, and resolves the id against the CURRENT process
455
+ // cwd, so the dedupe key differed by platform and by cwd (the #2219
456
+ // non-path-sentinel class). The cheap syntactic fold is what this
457
+ // session-scoped dedupe key actually needs.
422
458
  const silentScannerSet = [...new Set(unconfirmedServerIds)]
423
- .map(normalizeMapKey)
459
+ .map(normalizeEphemeralMapKey)
424
460
  // Code-unit comparator: the sorted set is a dedupe KEY, so ordering
425
461
  // must be deterministic across locales — localeCompare is not.
426
462
  .sort((a, b) => Number(a > b) - Number(a < b))
427
463
  .join(",");
428
- const onceKey = `${ctx.kind}:${normalizeMapKey(ctx.filePath)}:${silentScannerSet}`;
464
+ const onceKey = `${ctx.kind}:${ctx.filePath}:${silentScannerSet}`;
429
465
  if (coverageNoticeSeen.has(onceKey))
430
466
  return undefined;
431
467
  coverageNoticeSeen.add(onceKey);
@@ -445,7 +481,9 @@ function buildCoverageNotice(ctx, runnerLatencies) {
445
481
  const primaryHasCoverage = relevant.some((r) => r.status === "succeeded" || r.status === "failed");
446
482
  if (primaryHasCoverage)
447
483
  return undefined;
448
- const allPrimarySkipped = relevant.every((r) => r.status === "skipped" || r.status === "when_skipped");
484
+ const allPrimarySkipped = relevant.every((r) => r.status === "skipped" ||
485
+ r.status === "when_skipped" ||
486
+ r.status === "test_file_skipped");
449
487
  if (!allPrimarySkipped)
450
488
  return undefined;
451
489
  const plan = getToolPlan(ctx.kind);
@@ -467,7 +505,7 @@ function buildCoverageNotice(ctx, runnerLatencies) {
467
505
  (r.status === "succeeded" || r.status === "failed"));
468
506
  if (anyLinterHasCoverage)
469
507
  return undefined;
470
- const onceKey = `${ctx.kind}:${normalizeMapKey(ctx.filePath)}`;
508
+ const onceKey = `${ctx.kind}:${ctx.filePath}`;
471
509
  if (coverageNoticeSeen.has(onceKey))
472
510
  return undefined;
473
511
  coverageNoticeSeen.add(onceKey);
@@ -482,6 +520,11 @@ function buildCoverageNotice(ctx, runnerLatencies) {
482
520
  }
483
521
  const latencyReports = [];
484
522
  const coverageNoticeSeen = new Set();
523
+ // One `dispatch_skipped_generated` phase record per file per process (refs
524
+ // #2346): a generated file dispatched repeatedly must not spam latency.log,
525
+ // so only the first skip of each file emits the row; the degradation ledger
526
+ // below still tallies every repetition with a bounded per-subject count.
527
+ const generatedSkipRecorded = new Set();
485
528
  export function getLatencyReports() {
486
529
  return [...latencyReports];
487
530
  }
@@ -490,6 +533,7 @@ export function clearLatencyReports() {
490
533
  }
491
534
  export function clearCoverageNoticeState() {
492
535
  coverageNoticeSeen.clear();
536
+ generatedSkipRecorded.clear();
493
537
  }
494
538
  export function formatLatencyReport(report) {
495
539
  const lines = [];
@@ -571,6 +615,59 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
571
615
  });
572
616
  continue;
573
617
  }
618
+ // Same skipTestFiles gate RunnerRegistry.getForKind applies for the
619
+ // per-edit path (#2337): the plan/group path resolves runners by id via
620
+ // registry.get() instead, which bypasses getForKind entirely, so a
621
+ // runner declaring skipTestFiles never had it enforced here.
622
+ if (runner.skipTestFiles && isTestFile(ctx.filePath)) {
623
+ latencies.push({
624
+ runnerId,
625
+ startTime: runnerStart,
626
+ endTime: Date.now(),
627
+ durationMs: Date.now() - runnerStart,
628
+ status: "test_file_skipped",
629
+ diagnosticCount: 0,
630
+ semantic: "none",
631
+ });
632
+ logLatency({
633
+ type: "runner",
634
+ filePath: ctx.filePath,
635
+ runnerId,
636
+ durationMs: 0,
637
+ status: "test_file_skipped",
638
+ diagnosticCount: 0,
639
+ semantic: "none",
640
+ });
641
+ continue;
642
+ }
643
+ // Keep explicit groups aligned with RunnerRegistry.getForKind(): an empty
644
+ // appliesTo list means every kind, while a populated list must contain the
645
+ // current kind. A filtered runner remains visible as skipped telemetry so
646
+ // language mismatches cannot be mistaken for runner failures.
647
+ const appliesToCurrentKind = runnerAppliesToKind(runner, ctx.kind);
648
+ if (!appliesToCurrentKind) {
649
+ const runnerEnd = Date.now();
650
+ latencies.push({
651
+ runnerId,
652
+ startTime: runnerStart,
653
+ endTime: runnerEnd,
654
+ durationMs: 0,
655
+ status: "skipped",
656
+ diagnosticCount: 0,
657
+ semantic: "unknown",
658
+ });
659
+ logLatency({
660
+ type: "runner",
661
+ filePath: ctx.filePath,
662
+ runnerId,
663
+ durationMs: 0,
664
+ status: "skipped",
665
+ diagnosticCount: 0,
666
+ semantic: "unknown",
667
+ metadata: { reason: "applies_to", kind: ctx.kind },
668
+ });
669
+ continue;
670
+ }
574
671
  // Check preconditions
575
672
  let shouldRun = true;
576
673
  if (runner.when) {
@@ -603,10 +700,123 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
603
700
  });
604
701
  continue;
605
702
  }
703
+ const projectRoot = ctx.projectRoot ?? ctx.cwd;
704
+ // Only post-write dispatches participate. Project scans and direct API
705
+ // callers must retain their existing synchronous semantics.
706
+ const observedTier = ctx.writeIndex === undefined
707
+ ? "inline"
708
+ : classifyObservedRunner(projectRoot, runner.id);
709
+ if (observedTier === "collect-later") {
710
+ const markedAtMs = Date.now();
711
+ const deferred = runRunner(ctx, runner, semantic).then((result) => {
712
+ const durationMs = Date.now() - markedAtMs;
713
+ const tier = observeRunnerLatency({
714
+ projectRoot,
715
+ runnerId: runner.id,
716
+ durationMs,
717
+ timedOut: result.failureKind === "timeout",
718
+ });
719
+ if (tier !== observedTier) {
720
+ logLatency({
721
+ type: "phase",
722
+ filePath: ctx.filePath,
723
+ phase: "runner_collect_later_tier_flip",
724
+ durationMs: 0,
725
+ metadata: {
726
+ runnerId: runner.id,
727
+ from: observedTier,
728
+ to: tier,
729
+ projectRoot,
730
+ },
731
+ });
732
+ }
733
+ if (tier === "collect-later") {
734
+ incrementDegradationCount({
735
+ kind: "runner-collect-later",
736
+ subject: `${projectRoot}:${runner.id}`,
737
+ reason: `observed ${durationMs}ms, threshold ${COLLECT_LATER_THRESHOLD_MS}ms`,
738
+ });
739
+ }
740
+ logLatency({
741
+ type: "runner",
742
+ filePath: ctx.filePath,
743
+ runnerId: runner.id,
744
+ startedAt: new Date(markedAtMs).toISOString(),
745
+ durationMs,
746
+ status: result.status,
747
+ diagnosticCount: result.diagnostics.length,
748
+ semantic: result.semantic ?? semantic,
749
+ metadata: { tier: "collect-later", delivered: "turn_end" },
750
+ });
751
+ return result;
752
+ });
753
+ deferRunnerFindings({
754
+ filePath: ctx.filePath,
755
+ cwd: ctx.cwd,
756
+ projectRoot,
757
+ runnerId: runner.id,
758
+ markedAtMs,
759
+ writeIndex: ctx.writeIndex,
760
+ promise: deferred,
761
+ });
762
+ // A deferred runner is still an observed runner. Keep it visible in
763
+ // both the edit latency report and the widget until its turn-end result
764
+ // replaces this pending state (#2122 F1).
765
+ latencies.push({
766
+ runnerId: runner.id,
767
+ startTime: runnerStart,
768
+ endTime: runnerStart,
769
+ durationMs: 0,
770
+ status: "pending",
771
+ diagnosticCount: 0,
772
+ semantic: semantic,
773
+ });
774
+ recordRunner(ctx.filePath, runner.id, "pending", 0, 0, ctx.writeIndex);
775
+ logLatency({
776
+ type: "runner",
777
+ filePath: ctx.filePath,
778
+ runnerId: runner.id,
779
+ durationMs: 0,
780
+ status: "pending",
781
+ diagnosticCount: 0,
782
+ semantic,
783
+ metadata: { tier: "collect-later", delivered: "turn_end" },
784
+ });
785
+ continue;
786
+ }
606
787
  const result = await runRunner(ctx, runner, semantic);
607
788
  onRunnerResult?.(runnerId, result);
608
789
  const runnerEnd = Date.now();
609
790
  const duration = runnerEnd - runnerStart;
791
+ const tier = ctx.writeIndex === undefined
792
+ ? "inline"
793
+ : observeRunnerLatency({
794
+ projectRoot,
795
+ runnerId: runner.id,
796
+ durationMs: duration,
797
+ timedOut: result.failureKind === "timeout",
798
+ });
799
+ if (tier !== observedTier) {
800
+ logLatency({
801
+ type: "phase",
802
+ filePath: ctx.filePath,
803
+ phase: "runner_collect_later_tier_flip",
804
+ durationMs: 0,
805
+ metadata: {
806
+ runnerId: runner.id,
807
+ from: observedTier,
808
+ to: tier,
809
+ projectRoot,
810
+ },
811
+ });
812
+ }
813
+ if (tier === "collect-later") {
814
+ incrementDegradationCount({
815
+ kind: "runner-collect-later",
816
+ subject: `${projectRoot}:${runner.id}`,
817
+ reason: `observed ${duration}ms, threshold ${COLLECT_LATER_THRESHOLD_MS}ms`,
818
+ });
819
+ }
610
820
  // Runner definitions are a typed API, but embedders/plugins can still
611
821
  // return untyped objects at runtime. Admit only the closed taxonomy and
612
822
  // only on actual skips so free text cannot enter durable latency metadata.
@@ -672,6 +882,31 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
672
882
  export async function dispatchForFile(ctx, groups, registry, onRunnerResult) {
673
883
  const _overallStart = Date.now();
674
884
  if (ctx.fileRole === "generated") {
885
+ // The generated short-circuit (refs #2346): never ran before this fix
886
+ // for name-less machine-emitted files (a scraped/minified page has no
887
+ // `.min.js` pattern to catch it), so the classification now also has a
888
+ // content-shape tier. The skip is observable — one `dispatch_skipped_generated`
889
+ // phase record per file per process carrying the deciding evidence tier
890
+ // and the measured line-shape statistic. Deliberately NOT a degradation
891
+ // ledger entry: skipping a generated file is healthy behavior working
892
+ // as designed, and the ledger's bounded kind slots are reserved for
893
+ // genuine degradations (#2348 review F3 — the ledger precedent at the
894
+ // collect-later tier flip below records an actual capability loss).
895
+ const evidence = ctx.generatedEvidence;
896
+ const lineShapeMean = ctx.generatedLineShapeMean;
897
+ if (!generatedSkipRecorded.has(ctx.filePath)) {
898
+ generatedSkipRecorded.add(ctx.filePath);
899
+ logLatency({
900
+ type: "phase",
901
+ filePath: ctx.filePath,
902
+ phase: "dispatch_skipped_generated",
903
+ durationMs: 0,
904
+ metadata: {
905
+ evidence: evidence ?? "unknown",
906
+ ...(lineShapeMean !== undefined && { lineShapeMean }),
907
+ },
908
+ });
909
+ }
675
910
  return {
676
911
  diagnostics: [],
677
912
  blockers: [],
@@ -706,12 +941,46 @@ export async function dispatchForFile(ctx, groups, registry, onRunnerResult) {
706
941
  // group order so output is deterministic.
707
942
  const groupResults = await Promise.all(groups.map((group) => runGroup(ctx, group, registry, onRunnerResult)));
708
943
  // Count baseline warnings before filtering (for delta count display)
709
- const relativeKey = path.relative(ctx.cwd, ctx.filePath).replace(/\\/g, "/");
710
- const baselineAbsKey = `session.baseline.${normalizeMapKey(ctx.filePath)}`;
711
- const baselineRelKey = `session.baseline.${normalizeMapKey(relativeKey)}`;
712
- const previousBaseline = ctx.deltaMode
713
- ? (ctx.facts.getSessionFact(baselineAbsKey) ??
714
- ctx.facts.getSessionFact(baselineRelKey))
944
+ //
945
+ // #2489: `ctx.filePath` is `createDispatchContext`'s `normalizedFilePath`
946
+ // (`resolveRunnerPath` -> `path.resolve` -> `normalizeMapKey`), which is
947
+ // ALWAYS absolute — the #2016 invariant this seam already relies on. A
948
+ // prior relative-path fallback key (`session.baseline.<path relative to
949
+ // ctx.cwd>`) existed here for self-consistency across the abs/rel read
950
+ // and write, but on `ctx.facts` singletons that outlive one project root
951
+ // (`dispatch/integration.ts`'s module-scope `sessionFacts`, reached by
952
+ // the warm `pilens_analyze` MCP route across any number of cwds) two
953
+ // different projects dispatching files that share a relative path (e.g.
954
+ // both have `src/index.ts`) collided on that fallback key, so project
955
+ // B's first-ever delta baseline read could silently return project A's
956
+ // stored diagnostics. Since the absolute key can never collide across
957
+ // projects (an absolute path is unique on the filesystem) and both
958
+ // writes always land together, the relative fallback is both unneeded
959
+ // and the sole source of the collision — removed rather than cwd-scoped
960
+ // (`cwd` is not a stable identity across this seam's callers, #2494).
961
+ //
962
+ // #2489 round 2: the #2016 invariant above is enforced by
963
+ // `createDispatchContext`, the sole real constructor, but nothing enforced
964
+ // it AT THIS SEAM — a hand-built `DispatchContext` (test scaffolding, a
965
+ // future caller) carrying a relative `filePath` would silently key the
966
+ // baseline under a value the constructor could never produce, and every
967
+ // subsequent same-session dispatch of the real (absolute-keyed) file would
968
+ // read back `undefined`, never the collision itself. Mirrors the sibling
969
+ // guard `recordEntitySnapshotDiff` already enforces for the entity-snapshot
970
+ // seam (`clients/review-graph/service.ts`, #2477): reject visibly and skip
971
+ // the baseline read/write for this dispatch rather than compute one under
972
+ // an unreachable key.
973
+ const baselinePathSafe = isAbsoluteFilePath(ctx.filePath);
974
+ if (ctx.deltaMode && !baselinePathSafe) {
975
+ recordDegradationOnce({
976
+ kind: "dispatch-non-absolute-baseline-path",
977
+ subject: ctx.filePath,
978
+ reason: "dispatchForFile requires an absolute ctx.filePath to key the delta baseline (refs #2489)",
979
+ });
980
+ }
981
+ const baselineAbsKey = `session.baseline.${ctx.filePath}`;
982
+ const previousBaseline = ctx.deltaMode && baselinePathSafe
983
+ ? ctx.facts.getBoundedSessionFact(baselineAbsKey)
715
984
  : undefined;
716
985
  const baselineWarnings = previousBaseline?.filter((d) => d.semantic === "warning" || d.semantic === "none");
717
986
  const baselineWarningCount = baselineWarnings?.length ?? 0;
@@ -778,9 +1047,12 @@ export async function dispatchForFile(ctx, groups, registry, onRunnerResult) {
778
1047
  resolvedCount = filtered.fixed.length;
779
1048
  }
780
1049
  // Persist full current snapshot for next run (not delta-filtered subset).
781
- if (ctx.deltaMode) {
782
- ctx.facts.setSessionFact(baselineAbsKey, [...dedupedDiagnostics]);
783
- ctx.facts.setSessionFact(baselineRelKey, [...dedupedDiagnostics]);
1050
+ // Gated on `baselinePathSafe` too: a non-absolute `ctx.filePath` already
1051
+ // skipped the read above (the degradation record fired there), and writing
1052
+ // under that same unreachable key here would just seed a baseline no real
1053
+ // reader (always absolute) could ever read back.
1054
+ if (ctx.deltaMode && baselinePathSafe) {
1055
+ ctx.facts.setBoundedSessionFact(baselineAbsKey, [...dedupedDiagnostics]);
784
1056
  }
785
1057
  // Categorize results
786
1058
  const blockers = visibleDiagnostics.filter((d) => d.semantic === "blocking");
@@ -819,6 +1091,12 @@ export async function dispatchForFile(ctx, groups, registry, onRunnerResult) {
819
1091
  output += formatDiagnostics([coverageNotice], "warning", 1);
820
1092
  warnings.push(coverageNotice);
821
1093
  }
1094
+ const pendingRunners = runnerLatencies
1095
+ .filter((runner) => runner.status === "pending")
1096
+ .map((runner) => runner.runnerId);
1097
+ if (pendingRunners.length > 0) {
1098
+ output += `\n⏳ Pending runners (reported at turn end): ${pendingRunners.join(", ")}\n`;
1099
+ }
822
1100
  // Generate and store latency report
823
1101
  const overallEnd = Date.now();
824
1102
  const latencyReport = {
@@ -862,6 +1140,13 @@ export async function dispatchForFile(ctx, groups, registry, onRunnerResult) {
862
1140
  })),
863
1141
  totalDiagnostics: visibleDiagnostics.length,
864
1142
  blockers: blockers.length,
1143
+ // #2489 round 2: distinguishes a delta baseline HIT (a prior snapshot
1144
+ // was read for this file this session) from a MISS (first dispatch, or
1145
+ // the non-absolute-path guard above skipped the read) — `dispatch_start`
1146
+ // (above, `metadata: { groupCount, kind, runners }`) fires before the
1147
+ // baseline read ever runs and cannot carry either value.
1148
+ baselineHit: previousBaseline !== undefined,
1149
+ baselineWarningCount,
865
1150
  },
866
1151
  });
867
1152
  return {