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
@@ -1,3 +1,5 @@
1
+ import { recordDegradationOnce } from "../degradation-ledger.js";
2
+ import { isAbsoluteFilePath, normalizeMapKey } from "../path-utils.js";
1
3
  import { computeImpactCascade as computeImpactCascadeImpl, computeTransitiveImpact as computeTransitiveImpactImpl, } from "./query.js";
2
4
  import { buildOrUpdateGraph as buildOrUpdateGraphImpl, } from "./builder.js";
3
5
  import { formatImpactCascade as formatImpactCascadeImpl } from "./format.js";
@@ -19,7 +21,57 @@ export function computeTransitiveImpact(graph, seedFile, options) {
19
21
  return computeTransitiveImpactImpl(graph, seedFile, options);
20
22
  }
21
23
  export function recordEntitySnapshotDiff(facts, filePath, nextSnapshot) {
22
- const prev = facts.getSessionFact(`${ENTITY_SNAPSHOT_PREFIX}${filePath}`) ?? new Map();
24
+ // #2477 round 2 (reviewer F1/F2): folding `cwd` into this key was REVERTED.
25
+ // The writer (`clients/dispatch/runners/tree-sitter.ts`) passes `ctx.cwd`,
26
+ // which `createDispatchContext` (`clients/dispatch/dispatcher.ts:324-326`)
27
+ // sets to the file's NEAREST LANGUAGE-ROOT (`resolveLanguageRootForFile`),
28
+ // while the reader (`upsertChangedSymbols`, `clients/review-graph/builder.ts`,
29
+ // reached via `computeCascadeForFile` → `buildOrUpdateGraph`) keys off the
30
+ // WORKSPACE cwd. In a monorepo/cargo-workspace/go-module layout those two
31
+ // cwd values diverge, so a folded `(cwd, path)` key the builder reader can
32
+ // never hit zeroed `graphChangedSymbolCount` on every build — reproduced:
33
+ // master returns `["alpha","beta"]`, the folded-key version returns `[]`
34
+ // — and made the cascade over-fan to every symbol (`query.ts:230-238`
35
+ // treats an empty `changedSymbols` as "no narrowing").
36
+ //
37
+ // The #2477 cross-project collision this was meant to close cannot reach
38
+ // this function in production: the sole writer always passes `ctx.filePath`,
39
+ // which `createDispatchContext` sets via `resolveRunnerPath` to an
40
+ // ALWAYS-ABSOLUTE path (the #2016 invariant, `dispatcher.ts:320,327,346`).
41
+ // A relative `filePath` here can only happen through a caller regression.
42
+ // Enforce that instead of re-keying: reject a non-absolute `filePath` with
43
+ // a visible, bounded ledger record (never silently, per AGENTS.md) and
44
+ // skip the diff rather than compute one under a key the real reader could
45
+ // never reach anyway. `recordDegradationOnce` (not a thrown error) is used
46
+ // because the sole call site's surrounding try/catch
47
+ // (`tree-sitter.ts` — "entity snapshot / blast-radius enrichment is
48
+ // best-effort") is a BARE, UNLOGGED catch: a thrown error there vanishes
49
+ // with no trace, which is strictly worse than a ledger record.
50
+ if (!isAbsoluteFilePath(filePath)) {
51
+ recordDegradationOnce({
52
+ kind: "review-graph-non-absolute-entity-path",
53
+ subject: filePath,
54
+ reason: "recordEntitySnapshotDiff requires an absolute filePath (refs #2477)",
55
+ });
56
+ return { added: [], removed: [], modified: [] };
57
+ }
58
+ // Normalize once at this boundary, then reuse the folded path for both
59
+ // per-file facts. An unnormalized write is a key the builder reader can never
60
+ // hit, and an unnormalized snapshot forks a second empty diff (#2355).
61
+ const normalizedFilePath = normalizeMapKey(filePath);
62
+ const snapshotKey = `${ENTITY_SNAPSHOT_PREFIX}${normalizedFilePath}`;
63
+ const changedSymbolsKey = `${CHANGED_SYMBOLS_PREFIX}${normalizedFilePath}`;
64
+ const stored = facts.getBoundedSessionFact(snapshotKey);
65
+ // An evicted snapshot is unknown, not empty. Diffing against an empty Map
66
+ // puts every entity in `added`, which reads downstream as "the whole file
67
+ // changed" and schedules a blast-radius run for a file that did not change.
68
+ // Re-seed the snapshot and report no diff instead (#2282 review F1).
69
+ if (stored === undefined && facts.wasBoundedSessionFactEvicted(snapshotKey)) {
70
+ facts.setBoundedSessionFact(snapshotKey, new Map(nextSnapshot));
71
+ facts.setBoundedSessionFact(changedSymbolsKey, []);
72
+ return { added: [], removed: [], modified: [] };
73
+ }
74
+ const prev = stored ?? new Map();
23
75
  const added = [];
24
76
  const removed = [];
25
77
  const modified = [];
@@ -38,7 +90,7 @@ export function recordEntitySnapshotDiff(facts, filePath, nextSnapshot) {
38
90
  .map((key) => key.split(":")[1])
39
91
  .filter(Boolean)),
40
92
  ];
41
- facts.setSessionFact(`${ENTITY_SNAPSHOT_PREFIX}${filePath}`, new Map(nextSnapshot));
42
- facts.setSessionFact(`${CHANGED_SYMBOLS_PREFIX}${filePath}`, changedSymbols);
93
+ facts.setBoundedSessionFact(snapshotKey, new Map(nextSnapshot));
94
+ facts.setBoundedSessionFact(changedSymbolsKey, changedSymbols);
43
95
  return { added, removed, modified };
44
96
  }
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { BoundedFifoMap } from "../bounded-cache.js";
2
3
  import { normalizeMapKey } from "../path-utils.js";
3
4
  /**
4
5
  * THE producer/consumer hash contract, in one place (#955 review): scanner
@@ -17,7 +18,10 @@ export function reviewGraphIrContentHash(content) {
17
18
  // registry's lifetime is the scan-to-build window, not the process lifetime.
18
19
  const MAX_ROOTS = 8;
19
20
  const MAX_FILES_PER_ROOT = 20_000;
20
- const roots = new Map();
21
+ // Both levels are FIFO, matching the hand-rolled blocks these replaced: no
22
+ // read on either map refreshed recency before #2442, and the consume-once
23
+ // contract above means a read is normally a DELETE anyway.
24
+ const roots = new BoundedFifoMap(MAX_ROOTS);
21
25
  let accepted = 0;
22
26
  let rejected = 0;
23
27
  function rootKey(cwd) {
@@ -33,23 +37,11 @@ export function publishReviewGraphFileIr(cwd, entry) {
33
37
  const key = rootKey(cwd);
34
38
  let files = roots.get(key);
35
39
  if (!files) {
36
- files = new Map();
40
+ files = new BoundedFifoMap(MAX_FILES_PER_ROOT);
37
41
  roots.set(key, files);
38
- while (roots.size > MAX_ROOTS) {
39
- const oldest = roots.keys().next().value;
40
- if (oldest === undefined)
41
- break;
42
- roots.delete(oldest);
43
- }
44
42
  }
45
43
  const fileKey = normalizeMapKey(entry.filePath);
46
- files.delete(fileKey);
47
- while (files.size >= MAX_FILES_PER_ROOT) {
48
- const oldest = files.keys().next().value;
49
- if (oldest === undefined)
50
- break;
51
- files.delete(oldest);
52
- }
44
+ files.delete(fileKey); // republish is a refresh: it becomes the newest
53
45
  files.set(fileKey, { ...entry, filePath: fileKey });
54
46
  }
55
47
  /**
@@ -2,9 +2,13 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import { BoundedLruCache } from "../bounded-cache.js";
4
4
  import * as path from "node:path";
5
- import { findGoverningTsconfigDir, getDirectoryMarkers, } from "../workspace-topology.js";
5
+ import { findGoverningTsconfigDir, getDirectoryMarkers, registerWorkspaceTopologyReset, } from "../workspace-topology.js";
6
6
  const cache = new BoundedLruCache(64);
7
7
  const referencesCache = new BoundedLruCache(64);
8
+ registerWorkspaceTopologyReset(() => {
9
+ cache.clear();
10
+ referencesCache.clear();
11
+ });
8
12
  /** Strip JSONC comments and trailing commas without touching string contents. */
9
13
  function parseJsonc(content) {
10
14
  let output = "";
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
+ import { hasCargoWorkspaceTable, readCargoDependencyNames, readCargoPackageName, readCargoWorkspaceMembers, } from "../cargo-manifest.js";
9
10
  import { getProjectIgnoreMatcher, isExcludedDirName, } from "../file-utils.js";
10
11
  import { normalizeMapKey } from "../path-utils.js";
11
12
  import { getWorkspaceManifestMarkers } from "../workspace-topology.js";
@@ -52,8 +53,12 @@ function detectWorkspaceType(cwd) {
52
53
  if (markers.hasCargoToml) {
53
54
  try {
54
55
  const content = fs.readFileSync(path.join(cwd, "Cargo.toml"), "utf-8");
55
- if (content.includes("[workspace]"))
56
+ // The shared presence check (#2498) — see its own doc comment in
57
+ // cargo-manifest.ts for why a bare `.includes`/regex reimplementation
58
+ // of this test is wrong (comment/sentinel handling).
59
+ if (hasCargoWorkspaceTable(content)) {
56
60
  return "cargo";
61
+ }
57
62
  }
58
63
  catch { }
59
64
  }
@@ -146,71 +151,6 @@ function extractYamlList(content, key) {
146
151
  }
147
152
  return values;
148
153
  }
149
- function extractTomlArray(content, key) {
150
- const prefix = `${key}`;
151
- let collecting = false;
152
- let buffer = "";
153
- for (const rawLine of content.split(/\r?\n/)) {
154
- const line = rawLine.split("#", 1)[0].trim();
155
- if (!line)
156
- continue;
157
- if (!collecting) {
158
- if (!line.startsWith(prefix))
159
- continue;
160
- const equalsIndex = line.indexOf("=");
161
- if (equalsIndex === -1 || line.slice(0, equalsIndex).trim() !== key)
162
- continue;
163
- const afterEquals = line.slice(equalsIndex + 1).trim();
164
- if (!afterEquals.startsWith("["))
165
- continue;
166
- collecting = true;
167
- buffer += afterEquals.slice(1);
168
- }
169
- else {
170
- buffer += `,${line}`;
171
- }
172
- const closeIndex = buffer.indexOf("]");
173
- if (closeIndex !== -1) {
174
- buffer = buffer.slice(0, closeIndex);
175
- break;
176
- }
177
- }
178
- return buffer
179
- .split(",")
180
- .map((s) => stripQuotes(s.trim()))
181
- .filter(Boolean);
182
- }
183
- function extractTomlSection(content, section) {
184
- const lines = [];
185
- let inSection = false;
186
- for (const rawLine of content.split(/\r?\n/)) {
187
- const trimmed = rawLine.trim();
188
- if (trimmed === `[${section}]`) {
189
- inSection = true;
190
- continue;
191
- }
192
- if (inSection && trimmed.startsWith("[") && trimmed.endsWith("]"))
193
- break;
194
- if (inSection)
195
- lines.push(rawLine);
196
- }
197
- return lines;
198
- }
199
- function extractTomlString(content, key) {
200
- for (const rawLine of content.split(/\r?\n/)) {
201
- const line = rawLine.split("#", 1)[0].trim();
202
- const equalsIndex = line.indexOf("=");
203
- if (equalsIndex === -1 || line.slice(0, equalsIndex).trim() !== key)
204
- continue;
205
- const value = line.slice(equalsIndex + 1).trim();
206
- const quote = value[0];
207
- if ((quote !== '"' && quote !== "'") || value.length < 2)
208
- return undefined;
209
- const endIndex = value.indexOf(quote, 1);
210
- return endIndex === -1 ? undefined : value.slice(1, endIndex);
211
- }
212
- return undefined;
213
- }
214
154
  function moduleFromPackageJson(cwd, pkgRoot) {
215
155
  const pkgJson = readJsonSafe(path.join(pkgRoot, "package.json"));
216
156
  if (!pkgJson?.name)
@@ -250,7 +190,7 @@ function scanCargoModules(cwd) {
250
190
  let members = [];
251
191
  try {
252
192
  const content = fs.readFileSync(path.join(cwd, "Cargo.toml"), "utf-8");
253
- members = extractTomlArray(content, "members");
193
+ members = readCargoWorkspaceMembers(content);
254
194
  }
255
195
  catch {
256
196
  return [];
@@ -260,15 +200,11 @@ function scanCargoModules(cwd) {
260
200
  const pkgRoot = path.resolve(cwd, member);
261
201
  const memberToml = path.join(pkgRoot, "Cargo.toml");
262
202
  let name = "";
263
- const deps = [];
203
+ let deps = [];
264
204
  try {
265
205
  const content = fs.readFileSync(memberToml, "utf-8");
266
- name = extractTomlString(content, "name") ?? "";
267
- for (const line of extractTomlSection(content, "dependencies")) {
268
- const depName = line.trim().match(/^([A-Za-z0-9_-]+)\s*=/);
269
- if (depName)
270
- deps.push(depName[1]);
271
- }
206
+ name = readCargoPackageName(content) ?? "";
207
+ deps = readCargoDependencyNames(content);
272
208
  }
273
209
  catch {
274
210
  continue;
@@ -1,10 +1,17 @@
1
1
  import * as path from "node:path";
2
2
  import { isTestMode } from "./env-utils.js";
3
- import { getGlobalPiLensDir } from "./file-utils.js";
3
+ import { getGlobalPiLensLogDir } from "./probe-home-state.js";
4
+ import { getMaxLogSizeMB } from "./log-cleanup.js";
4
5
  import { createNdjsonLogger } from "./ndjson-logger.js";
5
- const REVIEW_GRAPH_LOG_DIR = getGlobalPiLensDir();
6
+ import { normalizeFilePath } from "./path-utils.js";
7
+ const REVIEW_GRAPH_LOG_DIR = getGlobalPiLensLogDir();
6
8
  const REVIEW_GRAPH_LOG_FILE = path.join(REVIEW_GRAPH_LOG_DIR, "review-graph.log");
7
- const writer = createNdjsonLogger({ filePath: REVIEW_GRAPH_LOG_FILE });
9
+ // Bounded like every other global sink (#2505) — see sessionstart-logger.
10
+ const writer = createNdjsonLogger({
11
+ filePath: REVIEW_GRAPH_LOG_FILE,
12
+ maxBytes: getMaxLogSizeMB() * 1024 * 1024,
13
+ backupPath: `${REVIEW_GRAPH_LOG_FILE}.1`,
14
+ });
8
15
  /**
9
16
  * Build the one canonical graph metadata shape used by build and persist logs.
10
17
  * Counts come from the graph instance; source-file count may use the exact
@@ -27,6 +34,9 @@ export function makeReviewGraphBuildMetadata(graph, options = {}) {
27
34
  graph.persistCoverage?.sourceFilesTruncated
28
35
  ? { sourceFilesTruncated: true }
29
36
  : {}),
37
+ ...(options.pathNormalizeCalls === undefined
38
+ ? {}
39
+ : { pathNormalizeCalls: options.pathNormalizeCalls }),
30
40
  nodes: graph.nodes.size,
31
41
  edges: graph.edges.length,
32
42
  ...(graph.persistCoverage
@@ -34,11 +44,27 @@ export function makeReviewGraphBuildMetadata(graph, options = {}) {
34
44
  : {}),
35
45
  };
36
46
  }
47
+ /**
48
+ * #2141: call sites feed this a mix of raw `cwd` params and already-normalized
49
+ * cache keys (`normalizeMapKey(path.resolve(cwd))`), so the same project root
50
+ * showed up in the log as both `C:\...\pi-free` and `C:/.../pi-free`. Rather
51
+ * than auditing every call site, normalize once here — the single emit
52
+ * seam — matching the form `normalizeFilePath` already produces for every
53
+ * other structured log's `cwd` field (bus-publish, disposition-publish,
54
+ * diagnostics-publish, format-events-publish all normalize at their publish
55
+ * call sites; this pulls the same conversion in-house so no future call site
56
+ * can regress it).
57
+ */
37
58
  export function logReviewGraph(entry) {
38
59
  if (isTestMode()) {
39
60
  return;
40
61
  }
41
- writer.log({ ts: new Date().toISOString(), ...entry, pid: process.pid });
62
+ writer.log({
63
+ ts: new Date().toISOString(),
64
+ ...entry,
65
+ cwd: normalizeFilePath(entry.cwd),
66
+ pid: process.pid,
67
+ });
42
68
  }
43
69
  export function getReviewGraphLogPath() {
44
70
  return REVIEW_GRAPH_LOG_FILE;
@@ -190,6 +190,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
190
190
  changed: [],
191
191
  failed: [],
192
192
  skipped: [],
193
+ unavailable: [],
193
194
  };
194
195
  // #502 fix provenance: per-path {tool, kind} entries accumulated across the
195
196
  // deferred-format loop below, passed as `fixes` on the batch
@@ -348,7 +349,8 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
348
349
  return !disabled;
349
350
  });
350
351
  if (formatRecords.length > 0) {
351
- const work = new Array(formatRecords.length);
352
+ const work = [];
353
+ work.length = formatRecords.length;
352
354
  const started = new Set();
353
355
  let nextIndex = 0;
354
356
  const worker = async () => {
@@ -430,6 +432,18 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
430
432
  },
431
433
  ], "format-failed");
432
434
  }
435
+ // #2413: an unavailable formatter is durable, not transient — record it
436
+ // once and DO NOT requeue. Requeuing here is exactly what made
437
+ // `spawn oxfmt ENOENT` re-fire on every agent_end for the same files.
438
+ if (result.formatUnavailable.length > 0) {
439
+ for (const u of result.formatUnavailable) {
440
+ summary.unavailable.push({
441
+ filePath,
442
+ formatter: u.formatter,
443
+ reason: u.reason,
444
+ });
445
+ }
446
+ }
433
447
  if (result.formatChanged) {
434
448
  summary.changed.push(filePath);
435
449
  for (const tool of result.formattersUsed) {
@@ -663,6 +677,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
663
677
  changed: summary.changed.length,
664
678
  failed: summary.failed.length,
665
679
  skipped: summary.skipped.length,
680
+ unavailable: summary.unavailable.length,
666
681
  },
667
682
  });
668
683
  logLatency({
@@ -676,9 +691,10 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
676
691
  changed: summary.changed.length,
677
692
  failed: summary.failed.length,
678
693
  skipped: summary.skipped.length,
694
+ unavailable: summary.unavailable.length,
679
695
  },
680
696
  });
681
- dbg(`agent_end deferred_format complete: formatted=${summary.formatted} changed=${summary.changed.length} failed=${summary.failed.length} skipped=${summary.skipped.length}`);
697
+ dbg(`agent_end deferred_format complete: formatted=${summary.formatted} changed=${summary.changed.length} failed=${summary.failed.length} skipped=${summary.skipped.length} unavailable=${summary.unavailable.length}`);
682
698
  if (summary.failed.length > 0) {
683
699
  notify(`pi-lens deferred format: ${summary.changed.length} changed, ${summary.failed.length} failed`, "warning");
684
700
  }
@@ -103,12 +103,58 @@ export function peekTestFindings(cacheManager, cwd, runtime, logDelivery = false
103
103
  {
104
104
  role: "user",
105
105
  content: current
106
- ? `${AUTOMATION_FRAMING}Test failures detected last turn — fix before continuing:\n\n${findings.data.content}`
106
+ ? `${AUTOMATION_FRAMING}${testFindingsCurrentPrefix(findings.data.runnerErrorOnly)}\n\n${findings.data.content}`
107
107
  : `${AUTOMATION_FRAMING}${historicalTestContent(findings.data.content, findings.data.provenance)}`,
108
108
  },
109
109
  ],
110
110
  };
111
111
  }
112
+ /**
113
+ * #2522: a batch made entirely of RUNNER errors (timeout, missing
114
+ * provider/binary — see `TestRunnerFindingsCache.runnerErrorOnly`) is not a
115
+ * failure the agent introduced, so it must not read as a blocker the way a
116
+ * genuine failing test does.
117
+ */
118
+ function testFindingsCurrentPrefix(runnerErrorOnly) {
119
+ return runnerErrorOnly
120
+ ? "Test runner could not complete last turn (advisory — not a failure introduced this turn):"
121
+ : "Test failures detected last turn — fix before continuing:";
122
+ }
123
+ /**
124
+ * Retire a delivered test-findings record: blank the content, keep everything a
125
+ * delivery does not settle.
126
+ *
127
+ * ONE function, called by both `consumeTestFindings` (the in-process context
128
+ * hook) and `acknowledgeTestFindings` (the MCP Stop-hook commit), because
129
+ * "what survives a retire" is exactly the kind of rule that rots when it is
130
+ * stated twice. It already had: round 2 taught the consume copy to preserve
131
+ * `deferredTargets` and left the acknowledge copy dropping them, and the
132
+ * acknowledge copy is the LIVE Stop-hook path — including the branch where
133
+ * `handleTurnEnd` never runs at all — so on that path the deferral set was
134
+ * wiped before anything could ever dispatch it (#2522 review round 3, F2).
135
+ *
136
+ * What survives, and why:
137
+ * - `testRunGeneration` — nulling the slot would let a still-in-flight OLDER
138
+ * batch read `undefined`, pass the strictly-greater suppression check, and
139
+ * resurrect a consumed one-shot advisory with stale results.
140
+ * - `deferredTargets` — delivering the advisory says the agent has SEEN the
141
+ * deferral, not that those targets ran. Dropping the list silently un-defers
142
+ * them and the cut batch is never finished.
143
+ * - `retiredTargets` — the session-scoped deferral cap. Dropping it re-arms a
144
+ * suite already measured as too slow for the batch budget, which puts the
145
+ * livelock back.
146
+ *
147
+ * An empty-content record peeks as undelivered while keeping all three intact.
148
+ */
149
+ function retireTestFindings(cacheManager, cwd) {
150
+ const prior = cacheManager.readCache("test-runner-findings", cwd)?.data;
151
+ cacheManager.writeCache("test-runner-findings", {
152
+ content: "",
153
+ testRunGeneration: prior?.testRunGeneration,
154
+ deferredTargets: prior?.deferredTargets,
155
+ retiredTargets: prior?.retiredTargets,
156
+ }, cwd);
157
+ }
112
158
  export function consumeTestFindings(cacheManager, cwd, runtime) {
113
159
  const record = cacheManager.readCache("test-runner-findings", cwd);
114
160
  if (!record?.data?.content)
@@ -116,16 +162,7 @@ export function consumeTestFindings(cacheManager, cwd, runtime) {
116
162
  const findings = peekTestFindings(cacheManager, cwd, runtime, true);
117
163
  if (!findings)
118
164
  return;
119
- // Retire the content but PRESERVE the generation high-water mark: nulling
120
- // the whole slot would let a still-in-flight OLDER batch see `undefined`,
121
- // pass the strictly-greater suppression check, and resurrect a consumed
122
- // one-shot advisory with stale results. An empty-content record peeks as
123
- // undelivered while keeping late-generation ordering intact.
124
- const priorGeneration = cacheManager.readCache("test-runner-findings", cwd)?.data?.testRunGeneration;
125
- cacheManager.writeCache("test-runner-findings", {
126
- content: "",
127
- testRunGeneration: priorGeneration,
128
- }, cwd);
165
+ retireTestFindings(cacheManager, cwd);
129
166
  return findings;
130
167
  }
131
168
  /** Complete an acknowledged MCP delivery without re-validating or re-rendering it. */
@@ -145,11 +182,7 @@ export function acknowledgeTestFindings(cacheManager, cwd) {
145
182
  const findings = cacheManager.readCache("test-runner-findings", cwd);
146
183
  if (!findings?.data?.content)
147
184
  return;
148
- // Same high-water-mark preservation as consumeTestFindings.
149
- cacheManager.writeCache("test-runner-findings", {
150
- content: "",
151
- testRunGeneration: findings.data.testRunGeneration,
152
- }, cwd);
185
+ retireTestFindings(cacheManager, cwd);
153
186
  }
154
187
  export function consumeSessionStartGuidance(cacheManager, cwd) {
155
188
  const guidance = cacheManager.readCache("session-start-guidance", cwd);
@@ -6,6 +6,7 @@ import { appendProjectChange, } from "./project-changes.js";
6
6
  import { normalizeMapKey, pathsEqual } from "./path-utils.js";
7
7
  import { PathKeyedMap } from "./path-keyed-map.js";
8
8
  import { BoundedLruCache } from "./bounded-cache.js";
9
+ import { PartialApplyRecordStore } from "./partial-edit-apply.js";
9
10
  import { ReadGuard } from "./read-guard.js";
10
11
  import { RUNTIME_CONFIG } from "./runtime-config.js";
11
12
  import { TurnSummaryCollector } from "./turn-summary.js";
@@ -111,6 +112,11 @@ export class RuntimeCoordinator {
111
112
  // (cheap, empty Map) but callers gate recording behind the
112
113
  // `lens-turn-summary` flag so it's a true no-op when the feature is off.
113
114
  _turnSummary = new TurnSummaryCollector();
115
+ // #2402/#1053: session-scoped applied-edit records for exact-retry
116
+ // recognition. Both producers (partial-apply commit, full native-edit
117
+ // success) write here; the preflight consults it before declaring an
118
+ // oldText miss, so an identical retry never re-executes a committed write.
119
+ partialApplyRecords = new PartialApplyRecordStore();
114
120
  resetForSession(startedAt = Date.now()) {
115
121
  this._sessionGeneration += 1;
116
122
  this._sessionStartedAt = startedAt;
@@ -159,6 +165,9 @@ export class RuntimeCoordinator {
159
165
  this._actionableWarningsThisTurn.clear();
160
166
  this._codeQualityWarningsThisTurn.clear();
161
167
  this._turnSummary.clear();
168
+ // #2402: an applied-edit record is a fact about the session that applied
169
+ // it; a new session must re-resolve identical payloads from content.
170
+ this.partialApplyRecords.clear();
162
171
  }
163
172
  get sessionStartedAt() {
164
173
  return this._sessionStartedAt;
@@ -317,9 +326,15 @@ export class RuntimeCoordinator {
317
326
  get droppedMutationReceiptCount() {
318
327
  return this._droppedMutationReceipts;
319
328
  }
320
- /** Atomically records write/edit ordering before debounce can coalesce it. */
321
- recordMutationToolReceipt(filePath, toolName) {
322
- if (toolName === "write") {
329
+ /**
330
+ * Atomically records write/edit ordering before debounce can coalesce it.
331
+ *
332
+ * Takes the classified {@link MutationKind}, not a raw tool name (#2423):
333
+ * an edit-shaped tool pi-lens does not name still demotes this file to the
334
+ * deferred pass, which is the safe timing.
335
+ */
336
+ recordMutationToolReceipt(filePath, kind) {
337
+ if (kind === "write") {
323
338
  this._writtenThisTurn.set(filePath, true);
324
339
  }
325
340
  else if (this._writtenThisTurn.has(filePath)) {
@@ -329,7 +344,7 @@ export class RuntimeCoordinator {
329
344
  this._fixedThisTurn.delete(filePath);
330
345
  }
331
346
  return {
332
- autofixMode: toolName === "edit" || this._autofixDemotedThisTurn.has(filePath)
347
+ autofixMode: kind === "edit" || this._autofixDemotedThisTurn.has(filePath)
333
348
  ? "deferred"
334
349
  : "immediate",
335
350
  };
@@ -734,6 +749,74 @@ export class RuntimeCoordinator {
734
749
  this._pendingInlineBlockers.delete(key);
735
750
  return true;
736
751
  }
752
+ /**
753
+ * #1950 fix-round F1: read the current delivery count WITHOUT committing
754
+ * an increment. `runtime-turn.ts` needs the count a delivery would reach
755
+ * BEFORE it knows whether that delivery will actually reach the agent —
756
+ * the turn-end signature dedupe (`turn-end-findings-last`) can suppress a
757
+ * turn whose rendered content is identical to the last one, and a
758
+ * suppressed turn must not advance the counter (the agent never saw it).
759
+ * The caller peeks, renders the tentative body, and only calls
760
+ * {@link incrementInlineBlockerStaleDelivery} once it knows the turn's
761
+ * content is NOT being suppressed.
762
+ */
763
+ peekInlineBlockerStaleDeliveryCount(filePath) {
764
+ const key = path.resolve(filePath);
765
+ return this._pendingInlineBlockers.get(key)?.staleDeliveryCount ?? 0;
766
+ }
767
+ /**
768
+ * #1950: commit one more turn end that re-served this record while
769
+ * `stale`. The caller (`runtime-turn.ts`) only calls this for a
770
+ * `"dependency-drift"` demotion — a `"past-eof"` demotion retires after
771
+ * its own single delivery (#1944, `retireDemotedPastEofBlocker`) and
772
+ * never accumulates a count — and only AFTER confirming the turn's
773
+ * content will not be suppressed by the signature dedupe (see
774
+ * {@link peekInlineBlockerStaleDeliveryCount}), so this counts ACTUAL
775
+ * deliveries the agent saw, not turn-end loop iterations. Returns the new
776
+ * count, or 0 when there is no entry to count against (already retired,
777
+ * or never recorded).
778
+ */
779
+ incrementInlineBlockerStaleDelivery(filePath) {
780
+ const key = path.resolve(filePath);
781
+ const existing = this._pendingInlineBlockers.get(key);
782
+ if (!existing)
783
+ return 0;
784
+ const next = (existing.staleDeliveryCount ?? 0) + 1;
785
+ this._pendingInlineBlockers.set(key, {
786
+ ...existing,
787
+ staleDeliveryCount: next,
788
+ });
789
+ return next;
790
+ }
791
+ /**
792
+ * #1950: retire a dependency-drift demotion once it has been delivered
793
+ * `DEPENDENCY_DRIFT_MAX_DELIVERIES` times with no re-run confirming or
794
+ * clearing it. Unlike {@link retireDemotedPastEofBlocker}, the record is
795
+ * NOT provably unrecoverable — its coordinates are still in bounds, and a
796
+ * fresh dispatch of the file could still confirm or clear it. This is a
797
+ * bounded-noise cap, not a "this can never resolve" verdict, and the
798
+ * caller's ledger reason must say so (`demoted-finding-retired`'s "capped,
799
+ * re-run can still confirm" vs. past-EOF's "retired, unrecoverable").
800
+ *
801
+ * Only ever touches this gate's OWN demotion (`staleReason ===
802
+ * "dependency-drift"`) — a past-EOF demotion on the same file is that
803
+ * gate's own record to retire, not this one's.
804
+ *
805
+ * @returns true when an entry was retired — the caller records it, so the
806
+ * suppression is never silent (#1432 Gap 1).
807
+ */
808
+ retireDemotedDependencyDriftBlocker(filePath) {
809
+ const key = path.resolve(filePath);
810
+ const existing = this._pendingInlineBlockers.get(key);
811
+ if (!existing)
812
+ return false;
813
+ if (!existing.stale)
814
+ return false;
815
+ if (existing.staleReason !== "dependency-drift")
816
+ return false;
817
+ this._pendingInlineBlockers.delete(key);
818
+ return true;
819
+ }
737
820
  /**
738
821
  * Retire a file's inline blocker because a FRESH, content-bound diagnostic
739
822
  * verdict proved it gone (#1561).
@@ -931,7 +1014,9 @@ export class RuntimeCoordinator {
931
1014
  * for a same-kind re-touch. Callers publish each kind's first transition
932
1015
  * without spamming repeated edits before `agent_end`.
933
1016
  */
934
- deferMutation(filePath, cwd, toolName, turnStateCwd, kind, ownerSessionId, originCwd) {
1017
+ deferMutation(filePath, cwd,
1018
+ /** Host tool name; any mutating tool, not just `write`/`edit` (#2423). */
1019
+ toolName, turnStateCwd, kind, ownerSessionId, originCwd) {
935
1020
  const key = path.resolve(filePath);
936
1021
  const now = Date.now();
937
1022
  const resolvedOriginCwd = originCwd ?? turnStateCwd;
@@ -962,7 +1047,9 @@ export class RuntimeCoordinator {
962
1047
  });
963
1048
  return true;
964
1049
  }
965
- deferFormat(filePath, cwd, toolName, turnStateCwd, ownerSessionId, originCwd) {
1050
+ deferFormat(filePath, cwd,
1051
+ /** Host tool name; any mutating tool, not just `write`/`edit` (#2423). */
1052
+ toolName, turnStateCwd, ownerSessionId, originCwd) {
966
1053
  return this.deferMutation(filePath, cwd, toolName, turnStateCwd, "format", ownerSessionId, originCwd);
967
1054
  }
968
1055
  get pendingDeferredFormatCount() {