pi-lens 4.3.0 → 4.4.0

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 (650) hide show
  1. package/CHANGELOG.md +1064 -0
  2. package/README.md +115 -25
  3. package/config/dependency-cruiser-eager-allowlist.json +6 -2
  4. package/dist/chunk-22AVIGXS.js +560 -0
  5. package/dist/chunk-27BBHSZC.js +106 -0
  6. package/dist/chunk-2BEMIWEW.js +51 -0
  7. package/dist/chunk-2EECBNC5.js +89 -0
  8. package/dist/chunk-34U7PLGE.js +23 -0
  9. package/dist/chunk-3I5QFT3N.js +181 -0
  10. package/dist/chunk-3MED75HT.js +37 -0
  11. package/dist/chunk-47D2BO4S.js +87 -0
  12. package/dist/chunk-4RERDEXR.js +348 -0
  13. package/dist/chunk-4W2PLC4J.js +27 -0
  14. package/dist/chunk-52H5B5TC.js +202 -0
  15. package/dist/chunk-54B42YRX.js +27 -0
  16. package/dist/chunk-5RXBNSW2.js +266 -0
  17. package/dist/chunk-5THXD6X5.js +201 -0
  18. package/dist/chunk-5W3R6LFT.js +24 -0
  19. package/dist/chunk-5ZEJHV35.js +341 -0
  20. package/dist/chunk-6P62QVEA.js +118 -0
  21. package/dist/chunk-6QIUX4EP.js +459 -0
  22. package/dist/chunk-6VCD4VKZ.js +78 -0
  23. package/dist/chunk-7F72PXY2.js +326 -0
  24. package/dist/chunk-7FBF7NPR.js +998 -0
  25. package/dist/chunk-7U2IDNXN.js +37 -0
  26. package/dist/chunk-ABBOA7UD.js +157 -0
  27. package/dist/chunk-AHDW3GPE.js +50 -0
  28. package/dist/chunk-AJ3QXTRQ.js +756 -0
  29. package/dist/chunk-AJVLWNFN.js +1610 -0
  30. package/dist/chunk-AJXODZY6.js +112 -0
  31. package/dist/chunk-ARFCXG7S.js +84 -0
  32. package/dist/chunk-AS44CFQ4.js +708 -0
  33. package/dist/chunk-AWE5TMPO.js +38 -0
  34. package/dist/chunk-B3I2UUT4.js +4692 -0
  35. package/dist/chunk-BRJKOBZP.js +331 -0
  36. package/dist/chunk-BW5KFIQC.js +59 -0
  37. package/dist/chunk-C2AUYXQH.js +149 -0
  38. package/dist/chunk-CGGAIP7J.js +29 -0
  39. package/dist/chunk-D6QS3U6M.js +30 -0
  40. package/dist/chunk-DFVMIKMQ.js +349 -0
  41. package/dist/chunk-DIULIWKU.js +169 -0
  42. package/dist/chunk-DQ2NZ7C4.js +2317 -0
  43. package/dist/chunk-E2WFBRHM.js +213 -0
  44. package/dist/chunk-E4MAEDPM.js +50 -0
  45. package/dist/chunk-EOKRQ3SA.js +30 -0
  46. package/dist/chunk-F3AU2TMM.js +2626 -0
  47. package/dist/chunk-F3P7VQ7H.js +170 -0
  48. package/dist/chunk-FC4RP4I2.js +12 -0
  49. package/dist/chunk-FJNKANOS.js +32 -0
  50. package/dist/chunk-GUOGVTCQ.js +146 -0
  51. package/dist/chunk-GVXA6CCE.js +18 -0
  52. package/dist/chunk-HHYAELLJ.js +2334 -0
  53. package/dist/chunk-HMFXILPV.js +83 -0
  54. package/dist/chunk-HVUZXSWK.js +421 -0
  55. package/dist/chunk-HWBZIGMJ.js +62 -0
  56. package/dist/chunk-I3R7GKDR.js +17 -0
  57. package/dist/chunk-I7SPGOWI.js +55 -0
  58. package/dist/chunk-IIXOAGHR.js +38 -0
  59. package/dist/chunk-IVP5KAB7.js +2250 -0
  60. package/dist/chunk-IXEBJSUE.js +22 -0
  61. package/dist/chunk-JOLITGCC.js +27 -0
  62. package/dist/chunk-K6VYLB2U.js +145 -0
  63. package/dist/chunk-LJDODBBZ.js +78 -0
  64. package/dist/chunk-LNAKEO63.js +78 -0
  65. package/dist/chunk-LSMJ6KBE.js +4139 -0
  66. package/dist/chunk-LW6XKDZQ.js +5565 -0
  67. package/dist/chunk-N3YQJI6O.js +899 -0
  68. package/dist/chunk-NABLC7W3.js +148 -0
  69. package/dist/chunk-NKOUFPQF.js +659 -0
  70. package/dist/chunk-NL5GLMFI.js +211 -0
  71. package/dist/chunk-NMABWBZN.js +327 -0
  72. package/dist/chunk-NTHH2J74.js +27 -0
  73. package/dist/chunk-NTUJEGDZ.js +59 -0
  74. package/dist/chunk-NXL4FFQQ.js +47 -0
  75. package/dist/chunk-NXPS2L6U.js +466 -0
  76. package/dist/chunk-O5WILGMI.js +42 -0
  77. package/dist/chunk-O6TQT6RI.js +48 -0
  78. package/dist/chunk-OAXNDZJ7.js +323 -0
  79. package/dist/chunk-ODEEOXPH.js +44 -0
  80. package/dist/chunk-OQGFCZJ4.js +4533 -0
  81. package/dist/chunk-ORTMY4Q4.js +282 -0
  82. package/dist/chunk-OS32RNR2.js +442 -0
  83. package/dist/chunk-PAK6IDME.js +189 -0
  84. package/dist/chunk-PWFHBTSF.js +406 -0
  85. package/dist/chunk-Q5U6FMDI.js +2352 -0
  86. package/dist/chunk-QOZXJ4NA.js +3364 -0
  87. package/dist/chunk-QYF5S6P6.js +45 -0
  88. package/dist/chunk-RIISMHKN.js +36 -0
  89. package/dist/chunk-RPD4ZSB7.js +25 -0
  90. package/dist/chunk-RPD5K5NC.js +79 -0
  91. package/dist/chunk-RUOG5D2C.js +262 -0
  92. package/dist/chunk-T4ERT73Y.js +599 -0
  93. package/dist/chunk-T6D4Q4AQ.js +54 -0
  94. package/dist/chunk-TGPXLY2J.js +391 -0
  95. package/dist/chunk-TLGFBM3F.js +12116 -0
  96. package/dist/chunk-TO6WL3GM.js +88 -0
  97. package/dist/chunk-TT22N74J.js +517 -0
  98. package/dist/chunk-TVN5N6GY.js +21216 -0
  99. package/dist/chunk-U3VGR4VU.js +308 -0
  100. package/dist/chunk-UK3CMEAL.js +1250 -0
  101. package/dist/chunk-UOPVJQ4N.js +103 -0
  102. package/dist/chunk-UXVNL3D7.js +23 -0
  103. package/dist/chunk-UY752QSN.js +45 -0
  104. package/dist/chunk-V5PXFSZK.js +188 -0
  105. package/dist/chunk-V7R425XG.js +398 -0
  106. package/dist/chunk-VCSRSYLF.js +81 -0
  107. package/dist/chunk-VD4JQCQC.js +162 -0
  108. package/dist/chunk-VDBPVIKD.js +62 -0
  109. package/dist/chunk-VHQLPIRJ.js +114 -0
  110. package/dist/chunk-VN2AXLEX.js +5414 -0
  111. package/dist/chunk-VNMT7C64.js +478 -0
  112. package/dist/chunk-WONERJTP.js +309 -0
  113. package/dist/chunk-XECLWQZN.js +85 -0
  114. package/dist/chunk-XIMYZ3JA.js +27 -0
  115. package/dist/chunk-XK4CIILI.js +144 -0
  116. package/dist/chunk-Y4T27COV.js +562 -0
  117. package/dist/chunk-YGVYKNSN.js +1860 -0
  118. package/dist/chunk-YLF3PPT5.js +308 -0
  119. package/dist/chunk-YSNFMJKN.js +14 -0
  120. package/dist/chunk-Z3CCVDUB.js +76 -0
  121. package/dist/chunk-Z54ZSOBZ.js +61 -0
  122. package/dist/chunk-ZAWW3AF6.js +97 -0
  123. package/dist/chunk-ZMOL6RQC.js +167 -0
  124. package/dist/index.js +16456 -8335
  125. package/dist/mcp/analyze-cli.js +186 -232
  126. package/dist/mcp/cli.js +137 -98
  127. package/dist/mcp/server.js +22589 -1573
  128. package/dist/mcp/worker.js +97 -38
  129. package/dist/probes/installer.js +135 -0
  130. package/dist/workers/project-snapshot-persist-worker.js +22 -0
  131. package/dist/workers/review-graph-persist-worker.js +15 -0
  132. package/docs/agent-guide.md +13 -4
  133. package/docs/agent-tools.md +42 -0
  134. package/docs/ast-grep_rules_catalog.md +3 -2
  135. package/docs/configuration.md +20 -1
  136. package/docs/environment-variables.md +52 -2
  137. package/docs/features.md +8 -4
  138. package/docs/globalconfig.md +1 -0
  139. package/docs/io-bridge-v2-migration.md +94 -0
  140. package/docs/lsp-capability-matrix.md +36 -4
  141. package/docs/lsp-idle-eviction.md +124 -0
  142. package/docs/mcp.md +21 -5
  143. package/docs/merge-queue-rollout.md +77 -0
  144. package/docs/pi-lens-fixer.md +156 -88
  145. package/docs/pi-lens-investigator.md +74 -26
  146. package/docs/pi-lens-merge-policy.md +694 -0
  147. package/docs/pi-lens-monitor.md +73 -7
  148. package/docs/pi-lens-retro.md +6 -4
  149. package/docs/pi-lens-reviewer.md +188 -48
  150. package/docs/pi-lens-subagent.md +246 -45
  151. package/docs/pi-lens-warden.md +18 -7
  152. package/docs/public-api-stability.md +55 -0
  153. package/docs/release-qa-baseline.md +84 -17
  154. package/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  155. package/docs/servercapabilities.md +53 -49
  156. package/docs/settings.md +1 -0
  157. package/docs/usage.md +5 -1
  158. package/grammars/tree-sitter-bash.wasm +0 -0
  159. package/grammars/tree-sitter-bash.wasm.json +3 -3
  160. package/package.json +19 -7
  161. package/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  162. package/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  163. package/scripts/analyze-pi-lens-logs.mjs +1275 -62
  164. package/scripts/download-grammars.js +5 -0
  165. package/scripts/grammars.lock.json +6 -1
  166. package/scripts/install-selftest.mjs +33 -34
  167. package/scripts/lib/host-provided-deps.mjs +13 -4
  168. package/scripts/lib/web-tree-sitter-dir.mjs +18 -0
  169. package/dist/clients/actionable-warnings-logger.js +0 -35
  170. package/dist/clients/actionable-warnings.js +0 -1610
  171. package/dist/clients/advisory-provenance.js +0 -593
  172. package/dist/clients/agent-behavior-client.js +0 -147
  173. package/dist/clients/agent-nudge.js +0 -325
  174. package/dist/clients/analysed-root.js +0 -1
  175. package/dist/clients/ast-grep-client.js +0 -731
  176. package/dist/clients/ast-grep-rule-manager.js +0 -146
  177. package/dist/clients/ast-grep-tool-logger.js +0 -118
  178. package/dist/clients/ast-grep-types.js +0 -10
  179. package/dist/clients/ast-grep-yaml-synth.js +0 -154
  180. package/dist/clients/atomic-write-staging.js +0 -75
  181. package/dist/clients/atomic-write.js +0 -189
  182. package/dist/clients/availability-probe-flight.js +0 -12
  183. package/dist/clients/bash-file-access.js +0 -1056
  184. package/dist/clients/biome-client.js +0 -357
  185. package/dist/clients/blocker-freshness.js +0 -837
  186. package/dist/clients/blocker-past-eof.js +0 -129
  187. package/dist/clients/bootstrap.js +0 -569
  188. package/dist/clients/bounded-cache.js +0 -190
  189. package/dist/clients/bounded-pid-file-lock.js +0 -202
  190. package/dist/clients/bounded-telemetry.js +0 -295
  191. package/dist/clients/build-identity.js +0 -95
  192. package/dist/clients/bundled-resource-health.js +0 -113
  193. package/dist/clients/bus-events-logger.js +0 -105
  194. package/dist/clients/bus-publish.js +0 -171
  195. package/dist/clients/cache/rule-cache.js +0 -190
  196. package/dist/clients/cache-manager.js +0 -524
  197. package/dist/clients/cache-observability.js +0 -1313
  198. package/dist/clients/call-graph.js +0 -852
  199. package/dist/clients/cargo-manifest.js +0 -422
  200. package/dist/clients/cascade-budget.js +0 -196
  201. package/dist/clients/cascade-format.js +0 -315
  202. package/dist/clients/cascade-logger.js +0 -31
  203. package/dist/clients/cascade-types.js +0 -9
  204. package/dist/clients/child-unref.js +0 -124
  205. package/dist/clients/code-quality-warnings.js +0 -286
  206. package/dist/clients/codebase-model.js +0 -173
  207. package/dist/clients/collateral-test-role.js +0 -40
  208. package/dist/clients/complexity-client.js +0 -462
  209. package/dist/clients/config-core/deny.js +0 -221
  210. package/dist/clients/config-core/index.js +0 -50
  211. package/dist/clients/config-core/merge.js +0 -357
  212. package/dist/clients/config-core/normalize.js +0 -340
  213. package/dist/clients/config-core/process-spec.js +0 -248
  214. package/dist/clients/config-core/provenance.js +0 -164
  215. package/dist/clients/config-core/records.js +0 -218
  216. package/dist/clients/config-core/resolve.js +0 -125
  217. package/dist/clients/config-core/safe-object.js +0 -78
  218. package/dist/clients/config-core/schema.js +0 -165
  219. package/dist/clients/config-diagnostic-codes.js +0 -285
  220. package/dist/clients/config-locations.js +0 -388
  221. package/dist/clients/config-resolve.js +0 -798
  222. package/dist/clients/config-schema.js +0 -251
  223. package/dist/clients/config-warn.js +0 -407
  224. package/dist/clients/console-guard-install.js +0 -26
  225. package/dist/clients/cooperative-budget.js +0 -42
  226. package/dist/clients/dead-code-client.js +0 -510
  227. package/dist/clients/dead-code-logger.js +0 -30
  228. package/dist/clients/deadline-utils.js +0 -280
  229. package/dist/clients/debug-handles.js +0 -211
  230. package/dist/clients/debug-heap.js +0 -174
  231. package/dist/clients/deferred-lsp-work.js +0 -106
  232. package/dist/clients/degradation-ledger.js +0 -426
  233. package/dist/clients/demoted-finding-render.js +0 -169
  234. package/dist/clients/dependency-checker.js +0 -885
  235. package/dist/clients/deps/ast-grep-napi.js +0 -22
  236. package/dist/clients/deps/js-yaml.js +0 -9
  237. package/dist/clients/deps/minimatch.js +0 -4
  238. package/dist/clients/deps/pi-tui.js +0 -14
  239. package/dist/clients/deps/typebox.js +0 -14
  240. package/dist/clients/deps/vscode-jsonrpc.js +0 -1
  241. package/dist/clients/deps/web-tree-sitter.js +0 -24
  242. package/dist/clients/diagnostic-dispositions.js +0 -630
  243. package/dist/clients/diagnostic-line-freshness.js +0 -305
  244. package/dist/clients/diagnostic-logger.js +0 -73
  245. package/dist/clients/diagnostic-tracker.js +0 -105
  246. package/dist/clients/diagnostics-publish.js +0 -230
  247. package/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  248. package/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  249. package/dist/clients/dispatch/collect-later-tier.js +0 -35
  250. package/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  251. package/dist/clients/dispatch/dispatcher.js +0 -1269
  252. package/dist/clients/dispatch/fact-provider-types.js +0 -1
  253. package/dist/clients/dispatch/fact-rule-runner.js +0 -17
  254. package/dist/clients/dispatch/fact-runner.js +0 -30
  255. package/dist/clients/dispatch/fact-scheduler.js +0 -68
  256. package/dist/clients/dispatch/fact-store.js +0 -377
  257. package/dist/clients/dispatch/facts/comment-facts.js +0 -22
  258. package/dist/clients/dispatch/facts/file-content.js +0 -19
  259. package/dist/clients/dispatch/facts/function-facts.js +0 -348
  260. package/dist/clients/dispatch/facts/import-facts.js +0 -204
  261. package/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  262. package/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  263. package/dist/clients/dispatch/finding-policy.js +0 -339
  264. package/dist/clients/dispatch/indent-detect.js +0 -417
  265. package/dist/clients/dispatch/inline-suppressions.js +0 -89
  266. package/dist/clients/dispatch/integration.js +0 -2377
  267. package/dist/clients/dispatch/lazy.js +0 -11
  268. package/dist/clients/dispatch/pending-runner-findings.js +0 -87
  269. package/dist/clients/dispatch/plan.js +0 -278
  270. package/dist/clients/dispatch/priorities.js +0 -20
  271. package/dist/clients/dispatch/rule-id-normalize.js +0 -50
  272. package/dist/clients/dispatch/rule-ignores.js +0 -69
  273. package/dist/clients/dispatch/rule-policy.js +0 -135
  274. package/dist/clients/dispatch/rules/async-noise.js +0 -40
  275. package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  276. package/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  277. package/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  278. package/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  279. package/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  280. package/dist/clients/dispatch/rules/high-complexity.js +0 -72
  281. package/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  282. package/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  283. package/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  284. package/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  285. package/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  286. package/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  287. package/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  288. package/dist/clients/dispatch/runner-context.js +0 -52
  289. package/dist/clients/dispatch/runners/actionlint.js +0 -111
  290. package/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  291. package/dist/clients/dispatch/runners/biome-check.js +0 -296
  292. package/dist/clients/dispatch/runners/cpp-check.js +0 -219
  293. package/dist/clients/dispatch/runners/credo.js +0 -82
  294. package/dist/clients/dispatch/runners/cue-vet.js +0 -416
  295. package/dist/clients/dispatch/runners/dart-analyze.js +0 -183
  296. package/dist/clients/dispatch/runners/detekt.js +0 -176
  297. package/dist/clients/dispatch/runners/dotnet-build.js +0 -164
  298. package/dist/clients/dispatch/runners/elixir-check.js +0 -195
  299. package/dist/clients/dispatch/runners/eslint.js +0 -137
  300. package/dist/clients/dispatch/runners/fact-rules.js +0 -29
  301. package/dist/clients/dispatch/runners/fish-indent.js +0 -69
  302. package/dist/clients/dispatch/runners/gleam-check.js +0 -167
  303. package/dist/clients/dispatch/runners/go-vet.js +0 -104
  304. package/dist/clients/dispatch/runners/golangci-lint.js +0 -135
  305. package/dist/clients/dispatch/runners/hadolint.js +0 -91
  306. package/dist/clients/dispatch/runners/helm-lint.js +0 -171
  307. package/dist/clients/dispatch/runners/helm-render.js +0 -940
  308. package/dist/clients/dispatch/runners/htmlhint.js +0 -117
  309. package/dist/clients/dispatch/runners/index.js +0 -108
  310. package/dist/clients/dispatch/runners/javac.js +0 -99
  311. package/dist/clients/dispatch/runners/ktlint.js +0 -110
  312. package/dist/clients/dispatch/runners/lsp.js +0 -379
  313. package/dist/clients/dispatch/runners/markdownlint.js +0 -167
  314. package/dist/clients/dispatch/runners/mypy.js +0 -102
  315. package/dist/clients/dispatch/runners/oxlint.js +0 -414
  316. package/dist/clients/dispatch/runners/php-lint.js +0 -85
  317. package/dist/clients/dispatch/runners/phpstan.js +0 -120
  318. package/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  319. package/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  320. package/dist/clients/dispatch/runners/pyright.js +0 -169
  321. package/dist/clients/dispatch/runners/rubocop.js +0 -99
  322. package/dist/clients/dispatch/runners/ruff.js +0 -88
  323. package/dist/clients/dispatch/runners/rust-clippy.js +0 -256
  324. package/dist/clients/dispatch/runners/shellcheck.js +0 -173
  325. package/dist/clients/dispatch/runners/shfmt.js +0 -102
  326. package/dist/clients/dispatch/runners/spellcheck.js +0 -128
  327. package/dist/clients/dispatch/runners/spotbugs.js +0 -259
  328. package/dist/clients/dispatch/runners/sqlfluff.js +0 -183
  329. package/dist/clients/dispatch/runners/stylelint.js +0 -166
  330. package/dist/clients/dispatch/runners/swiftlint.js +0 -163
  331. package/dist/clients/dispatch/runners/taplo.js +0 -174
  332. package/dist/clients/dispatch/runners/terragrunt.js +0 -168
  333. package/dist/clients/dispatch/runners/tflint.js +0 -125
  334. package/dist/clients/dispatch/runners/tree-sitter.js +0 -643
  335. package/dist/clients/dispatch/runners/trivy-config.js +0 -253
  336. package/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  337. package/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  338. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
  339. package/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  340. package/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  341. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  342. package/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  343. package/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  344. package/dist/clients/dispatch/runners/utils.js +0 -15
  345. package/dist/clients/dispatch/runners/vale.js +0 -128
  346. package/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  347. package/dist/clients/dispatch/runners/yamllint.js +0 -95
  348. package/dist/clients/dispatch/runners/zig-check.js +0 -79
  349. package/dist/clients/dispatch/suppress-writer.js +0 -79
  350. package/dist/clients/dispatch/tool-profile.js +0 -30
  351. package/dist/clients/dispatch/types.js +0 -21
  352. package/dist/clients/dispatch/utils/format-utils.js +0 -45
  353. package/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  354. package/dist/clients/disposition-logger.js +0 -32
  355. package/dist/clients/disposition-publish.js +0 -124
  356. package/dist/clients/durable-store.js +0 -97
  357. package/dist/clients/effective-config.js +0 -415
  358. package/dist/clients/env-utils.js +0 -83
  359. package/dist/clients/error-class.js +0 -23
  360. package/dist/clients/eval-timestamp.js +0 -17
  361. package/dist/clients/event-loop-hold.js +0 -274
  362. package/dist/clients/event-loop-monitor.js +0 -250
  363. package/dist/clients/extension-log.js +0 -504
  364. package/dist/clients/extension-mode.js +0 -73
  365. package/dist/clients/feature-hints.js +0 -103
  366. package/dist/clients/file-kinds.js +0 -412
  367. package/dist/clients/file-role.js +0 -134
  368. package/dist/clients/file-time.js +0 -160
  369. package/dist/clients/file-utils.js +0 -1032
  370. package/dist/clients/finding-delivery-gate.js +0 -494
  371. package/dist/clients/finding-identity.js +0 -88
  372. package/dist/clients/fix-worklog.js +0 -103
  373. package/dist/clients/format-events-publish.js +0 -343
  374. package/dist/clients/format-service.js +0 -222
  375. package/dist/clients/formatters-lazy.js +0 -9
  376. package/dist/clients/formatters.js +0 -2032
  377. package/dist/clients/freshness-cadence.js +0 -17
  378. package/dist/clients/freshness.js +0 -38
  379. package/dist/clients/generated-artifacts.js +0 -394
  380. package/dist/clients/generation-guard.js +0 -215
  381. package/dist/clients/git-guard.js +0 -1136
  382. package/dist/clients/git-tracked-ignore.js +0 -243
  383. package/dist/clients/gitleaks-client.js +0 -509
  384. package/dist/clients/go-client.js +0 -104
  385. package/dist/clients/govulncheck-client.js +0 -605
  386. package/dist/clients/gradle-ktfmt-style.js +0 -348
  387. package/dist/clients/grammar-source.js +0 -465
  388. package/dist/clients/gzip-stage-write.js +0 -135
  389. package/dist/clients/hashline-anchor.js +0 -424
  390. package/dist/clients/hook-budgets.js +0 -76
  391. package/dist/clients/host-edit-normalize.js +0 -140
  392. package/dist/clients/host-ports.js +0 -39
  393. package/dist/clients/indent-retarget.js +0 -154
  394. package/dist/clients/inline-blocker-dispositions.js +0 -186
  395. package/dist/clients/install-diagnostics.js +0 -156
  396. package/dist/clients/installer/index.js +0 -5416
  397. package/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  398. package/dist/clients/installer/managed-tool-refresh.js +0 -783
  399. package/dist/clients/instance-reaper-state.js +0 -7
  400. package/dist/clients/instance-reaper.js +0 -1137
  401. package/dist/clients/instance-registry-lock.js +0 -145
  402. package/dist/clients/instance-registry.js +0 -773
  403. package/dist/clients/jscpd-client.js +0 -378
  404. package/dist/clients/json-cache-read.js +0 -65
  405. package/dist/clients/knip-client.js +0 -788
  406. package/dist/clients/language-policy.js +0 -252
  407. package/dist/clients/language-profile.js +0 -361
  408. package/dist/clients/language-registry.js +0 -603
  409. package/dist/clients/latency-logger.js +0 -727
  410. package/dist/clients/lazy-import.js +0 -21
  411. package/dist/clients/ledger-bounds.js +0 -54
  412. package/dist/clients/lens-config.js +0 -404
  413. package/dist/clients/lens-engine.js +0 -358
  414. package/dist/clients/lens-events.js +0 -125
  415. package/dist/clients/lens-flag-registry.js +0 -467
  416. package/dist/clients/lens-map.js +0 -1156
  417. package/dist/clients/live-bus-emitter.js +0 -74
  418. package/dist/clients/log-cleanup.js +0 -222
  419. package/dist/clients/lsp/aggregation.js +0 -178
  420. package/dist/clients/lsp/cascade-tier.js +0 -421
  421. package/dist/clients/lsp/client.js +0 -4593
  422. package/dist/clients/lsp/config.js +0 -691
  423. package/dist/clients/lsp/content-limits.js +0 -41
  424. package/dist/clients/lsp/diagnostic-binding.js +0 -225
  425. package/dist/clients/lsp/document-drift.js +0 -467
  426. package/dist/clients/lsp/edits.js +0 -1219
  427. package/dist/clients/lsp/index.js +0 -8305
  428. package/dist/clients/lsp/inferred-project.js +0 -216
  429. package/dist/clients/lsp/jvm-runtime.js +0 -298
  430. package/dist/clients/lsp/language.js +0 -63
  431. package/dist/clients/lsp/launch.js +0 -746
  432. package/dist/clients/lsp/lombok.js +0 -158
  433. package/dist/clients/lsp/path-utils.js +0 -5
  434. package/dist/clients/lsp/pending-aux-coverage.js +0 -251
  435. package/dist/clients/lsp/position-encoding.js +0 -61
  436. package/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  437. package/dist/clients/lsp/server.js +0 -3290
  438. package/dist/clients/lsp/session-roots.js +0 -192
  439. package/dist/clients/lsp/spawn-history.js +0 -17
  440. package/dist/clients/lsp/sync-kind.js +0 -56
  441. package/dist/clients/lsp/tsserver-sync.js +0 -332
  442. package/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  443. package/dist/clients/lsp/wait-policy/classification.js +0 -85
  444. package/dist/clients/lsp/wait-policy/index.js +0 -10
  445. package/dist/clients/lsp/wait-policy/strategies.js +0 -392
  446. package/dist/clients/lsp/watch-queue.js +0 -61
  447. package/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  448. package/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  449. package/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  450. package/dist/clients/lsp-budget.js +0 -186
  451. package/dist/clients/lsp-document-symbols.js +0 -110
  452. package/dist/clients/lsp-lazy.js +0 -9
  453. package/dist/clients/lsp-mutation.js +0 -396
  454. package/dist/clients/lsp-status.js +0 -59
  455. package/dist/clients/map-with-concurrency.js +0 -40
  456. package/dist/clients/mcp/analyze.js +0 -420
  457. package/dist/clients/mcp/host-shim.js +0 -31
  458. package/dist/clients/mcp/ipc.js +0 -523
  459. package/dist/clients/mcp/review.js +0 -197
  460. package/dist/clients/mcp/session.js +0 -412
  461. package/dist/clients/memory-sampler.js +0 -208
  462. package/dist/clients/message-end-attribution.js +0 -30
  463. package/dist/clients/metrics-client.js +0 -107
  464. package/dist/clients/metrics-history.js +0 -327
  465. package/dist/clients/middle-man-analysis.js +0 -263
  466. package/dist/clients/model-provider.js +0 -53
  467. package/dist/clients/module-report-lsp.js +0 -189
  468. package/dist/clients/module-report.js +0 -2150
  469. package/dist/clients/mutating-tool.js +0 -651
  470. package/dist/clients/mutation-attribution.js +0 -368
  471. package/dist/clients/mutation-bridge.js +0 -240
  472. package/dist/clients/ndjson-logger.js +0 -734
  473. package/dist/clients/observed-mutation-sources.js +0 -101
  474. package/dist/clients/observed-mutation.js +0 -1253
  475. package/dist/clients/oldtext-autopatch.js +0 -128
  476. package/dist/clients/opaque-mutation-scan.js +0 -496
  477. package/dist/clients/opengrep-client.js +0 -333
  478. package/dist/clients/opengrep-config.js +0 -76
  479. package/dist/clients/package-manager.js +0 -607
  480. package/dist/clients/package-root.js +0 -38
  481. package/dist/clients/partial-edit-apply.js +0 -439
  482. package/dist/clients/path-attribution-telemetry.js +0 -43
  483. package/dist/clients/path-keyed-map.js +0 -108
  484. package/dist/clients/path-utils.js +0 -1244
  485. package/dist/clients/performance-report.js +0 -331
  486. package/dist/clients/persist-debounce.js +0 -70
  487. package/dist/clients/persistent-reverify.js +0 -271
  488. package/dist/clients/php-cs-fixer-config.js +0 -119
  489. package/dist/clients/pipeline.js +0 -1480
  490. package/dist/clients/post-autofix-notice.js +0 -37
  491. package/dist/clients/print-mode.js +0 -21
  492. package/dist/clients/probe-home-state.js +0 -228
  493. package/dist/clients/process-bridge.js +0 -66
  494. package/dist/clients/process-singletons.js +0 -210
  495. package/dist/clients/process-snapshot.js +0 -68
  496. package/dist/clients/project-changes.js +0 -181
  497. package/dist/clients/project-conventions.js +0 -171
  498. package/dist/clients/project-diagnostics/cache.js +0 -137
  499. package/dist/clients/project-diagnostics/extractors.js +0 -109
  500. package/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
  501. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  502. package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  503. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  504. package/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  505. package/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  506. package/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  507. package/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  508. package/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  509. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  510. package/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  511. package/dist/clients/project-diagnostics/scanner.js +0 -520
  512. package/dist/clients/project-diagnostics/types.js +0 -1
  513. package/dist/clients/project-lens-config.js +0 -807
  514. package/dist/clients/project-report.js +0 -701
  515. package/dist/clients/project-scale.js +0 -237
  516. package/dist/clients/project-scan-policy.js +0 -11
  517. package/dist/clients/project-snapshot-fingerprint.js +0 -48
  518. package/dist/clients/project-snapshot-persist-worker.js +0 -20
  519. package/dist/clients/project-snapshot.js +0 -1653
  520. package/dist/clients/project-trust.js +0 -181
  521. package/dist/clients/python-environment.js +0 -217
  522. package/dist/clients/python-provenance.js +0 -639
  523. package/dist/clients/quiet-window-config.js +0 -47
  524. package/dist/clients/quiet-window.js +0 -236
  525. package/dist/clients/read-bridge.js +0 -139
  526. package/dist/clients/read-expansion.js +0 -266
  527. package/dist/clients/read-guard-logger.js +0 -251
  528. package/dist/clients/read-guard-tool-lines.js +0 -1306
  529. package/dist/clients/read-guard.js +0 -1561
  530. package/dist/clients/recent-touches.js +0 -240
  531. package/dist/clients/redact/secrets.js +0 -449
  532. package/dist/clients/resource-sampler.js +0 -628
  533. package/dist/clients/reverse-deps.js +0 -270
  534. package/dist/clients/review-graph/builder.js +0 -5126
  535. package/dist/clients/review-graph/format.js +0 -33
  536. package/dist/clients/review-graph/git-identity.js +0 -163
  537. package/dist/clients/review-graph/import-resolvers.js +0 -481
  538. package/dist/clients/review-graph/persist-worker.js +0 -10
  539. package/dist/clients/review-graph/query.js +0 -244
  540. package/dist/clients/review-graph/revision-drift.js +0 -21
  541. package/dist/clients/review-graph/service.js +0 -96
  542. package/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  543. package/dist/clients/review-graph/symbol-id.js +0 -103
  544. package/dist/clients/review-graph/tsconfig-paths.js +0 -299
  545. package/dist/clients/review-graph/types.js +0 -1
  546. package/dist/clients/review-graph/workspace-modules.js +0 -449
  547. package/dist/clients/review-graph-logger.js +0 -81
  548. package/dist/clients/ruff-client.js +0 -179
  549. package/dist/clients/rules-scanner.js +0 -131
  550. package/dist/clients/run-duration.js +0 -55
  551. package/dist/clients/runtime-agent-end.js +0 -746
  552. package/dist/clients/runtime-config.js +0 -77
  553. package/dist/clients/runtime-context.js +0 -211
  554. package/dist/clients/runtime-coordinator.js +0 -1324
  555. package/dist/clients/runtime-session.js +0 -2485
  556. package/dist/clients/runtime-tool-call.js +0 -1295
  557. package/dist/clients/runtime-tool-result.js +0 -2049
  558. package/dist/clients/runtime-turn.js +0 -3767
  559. package/dist/clients/rust-client.js +0 -97
  560. package/dist/clients/safe-spawn.js +0 -2357
  561. package/dist/clients/sanitize.js +0 -286
  562. package/dist/clients/scan-utils.js +0 -25
  563. package/dist/clients/scratch-tree-policy.js +0 -165
  564. package/dist/clients/search-read-registration.js +0 -94
  565. package/dist/clients/secret-findings.js +0 -114
  566. package/dist/clients/security-scan-client.js +0 -352
  567. package/dist/clients/session-event-guard.js +0 -297
  568. package/dist/clients/session-lifecycle.js +0 -484
  569. package/dist/clients/session-start-observability.js +0 -94
  570. package/dist/clients/session-state-store.js +0 -101
  571. package/dist/clients/session-summary.js +0 -15
  572. package/dist/clients/sessionstart-logger.js +0 -27
  573. package/dist/clients/sg-runner.js +0 -876
  574. package/dist/clients/sgconfig.js +0 -347
  575. package/dist/clients/shared-checkout-guard.js +0 -338
  576. package/dist/clients/single-flight.js +0 -158
  577. package/dist/clients/situational-tool-telemetry.js +0 -108
  578. package/dist/clients/skills-resolver.js +0 -141
  579. package/dist/clients/slow-fs.js +0 -138
  580. package/dist/clients/smells-rollup.js +0 -231
  581. package/dist/clients/source-filter.js +0 -596
  582. package/dist/clients/source-walker.js +0 -211
  583. package/dist/clients/spawn-output-cap.js +0 -120
  584. package/dist/clients/spawn-timeout-cooldown.js +0 -110
  585. package/dist/clients/stale-marker.js +0 -17
  586. package/dist/clients/startup-scan.js +0 -400
  587. package/dist/clients/startup-timing.js +0 -130
  588. package/dist/clients/string-utils.js +0 -33
  589. package/dist/clients/subagent-mode.js +0 -111
  590. package/dist/clients/symbol-containment.js +0 -24
  591. package/dist/clients/symbol-types.js +0 -5
  592. package/dist/clients/test-runner-client.js +0 -2507
  593. package/dist/clients/test-runner-delivery.js +0 -287
  594. package/dist/clients/todo-scanner.js +0 -229
  595. package/dist/clients/tool-agreement.js +0 -423
  596. package/dist/clients/tool-config.js +0 -249
  597. package/dist/clients/tool-cwd.js +0 -270
  598. package/dist/clients/tool-definition.js +0 -47
  599. package/dist/clients/tool-event.js +0 -105
  600. package/dist/clients/tool-policy.js +0 -2755
  601. package/dist/clients/tool-probe.js +0 -53
  602. package/dist/clients/tool-render.js +0 -156
  603. package/dist/clients/tool-set-policy.js +0 -118
  604. package/dist/clients/tree-sitter-cache.js +0 -364
  605. package/dist/clients/tree-sitter-client.js +0 -4195
  606. package/dist/clients/tree-sitter-logger.js +0 -100
  607. package/dist/clients/tree-sitter-navigator.js +0 -269
  608. package/dist/clients/tree-sitter-query-loader.js +0 -569
  609. package/dist/clients/tree-sitter-shared.js +0 -158
  610. package/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  611. package/dist/clients/trivy-client.js +0 -467
  612. package/dist/clients/tui-fit.js +0 -50
  613. package/dist/clients/turn-context.js +0 -52
  614. package/dist/clients/turn-end/lane.js +0 -51
  615. package/dist/clients/turn-end/lanes/govulncheck.js +0 -112
  616. package/dist/clients/turn-end/lanes/secrets.js +0 -204
  617. package/dist/clients/turn-summary-render.js +0 -72
  618. package/dist/clients/turn-summary.js +0 -132
  619. package/dist/clients/typos-config.js +0 -29
  620. package/dist/clients/user-notify.js +0 -51
  621. package/dist/clients/vanished-instance-marker.js +0 -57
  622. package/dist/clients/warm-attach.js +0 -305
  623. package/dist/clients/widget-state.js +0 -1808
  624. package/dist/clients/word-index-logger.js +0 -49
  625. package/dist/clients/word-index-store.js +0 -459
  626. package/dist/clients/word-index.js +0 -2080
  627. package/dist/clients/workspace-topology.js +0 -443
  628. package/dist/clients/write-ordering-guard.js +0 -65
  629. package/dist/clients/zizmor-config.js +0 -340
  630. package/dist/i18n.js +0 -69
  631. package/dist/mcp/build-staleness.js +0 -124
  632. package/dist/scripts/lib/process-scan.mjs +0 -583
  633. package/dist/scripts/lib/skills-predicate.mjs +0 -129
  634. package/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
  635. package/dist/tools/activate-tools.js +0 -87
  636. package/dist/tools/ast-grep-outline.js +0 -137
  637. package/dist/tools/ast-grep-replace.js +0 -244
  638. package/dist/tools/ast-grep-search.js +0 -762
  639. package/dist/tools/effective-config.js +0 -90
  640. package/dist/tools/lens-diagnostic-mark.js +0 -375
  641. package/dist/tools/lens-diagnostics.js +0 -2571
  642. package/dist/tools/lsp-diagnostics.js +0 -1745
  643. package/dist/tools/lsp-navigation.js +0 -1367
  644. package/dist/tools/lsp-structured-output.js +0 -294
  645. package/dist/tools/module-report.js +0 -382
  646. package/dist/tools/project-report.js +0 -86
  647. package/dist/tools/render-compact.js +0 -374
  648. package/dist/tools/scan-progress.js +0 -53
  649. package/dist/tools/shared.js +0 -29
  650. package/dist/tools/symbol-search.js +0 -155
@@ -1,1610 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { hashText, normalizeMessage, stableFindingId, } from "./finding-identity.js";
5
- import { applyWorkspaceEdit } from "./lsp/edits.js";
6
- import { getLSPService } from "./lsp/index.js";
7
- import { isUnderDir, normalizeMapKey } from "./path-utils.js";
8
- import { bounded, combineAbortSignals, withDeadline, } from "./deadline-utils.js";
9
- import { armDeferredLspWork, awaitDeferredLspWork, registerDeferredLspWork, } from "./deferred-lsp-work.js";
10
- import { incrementDegradationCount, recordDegradationOnce, } from "./degradation-ledger.js";
11
- import { recordLspMutationBatch, } from "./lsp-mutation.js";
12
- import { toRunnerDisplayPath } from "./dispatch/runner-context.js";
13
- import { logActionableWarningsEvent } from "./actionable-warnings-logger.js";
14
- import { displayProjectDataPath, getProjectDataDir } from "./file-utils.js";
15
- import { commitDurableStore } from "./durable-store.js";
16
- import { establishToolAgreement } from "./tool-agreement.js";
17
- import { resolveLensToolName } from "./tool-config.js";
18
- let beforeWarningStateLockForTests = null;
19
- /** Test seam for a sibling process commit immediately before lock acquisition. */
20
- export function _setBeforeWarningStateLockForTests(hook) {
21
- beforeWarningStateLockForTests = hook;
22
- }
23
- /** #1816: was a local `relativeFile|tool|source|code|rule|normalizedMessage|
24
- * line` hash, hand-rolled independently of `diagnostic-dispositions.ts`'s
25
- * canonicalizing version — this is now the shared `finding-identity.js`
26
- * builder (which DOES canonicalize both `cwd` and `filePath` through
27
- * `normalizeMapKey` before relativizing, and hashes to 12 chars, matching
28
- * dispositions). See `legacyActionableWarningId` below for the pre-#1816
29
- * formula, kept only for on-disk suppression-store migration. */
30
- export function createActionableWarningId(args) {
31
- return stableFindingId("aw:", {
32
- cwd: args.cwd,
33
- filePath: args.filePath,
34
- parts: [
35
- args.tool,
36
- args.source,
37
- args.code,
38
- args.rule,
39
- normalizeMessage(args.message),
40
- args.line,
41
- ],
42
- });
43
- }
44
- /** PRE-#1816 id formula (raw, non-canonicalized `relativeFile`; 10-char
45
- * hash). `actionable-warning-state.json` is a keyed, persisted store — a
46
- * warning suppressed under the old formula must not silently reappear as
47
- * unsuppressed just because this module unified onto the canonical,
48
- * 12-char id. `suppressionFor`/`updateWarningState` use this ONLY to look up
49
- * and migrate a still-pending old entry forward; nothing ever WRITES under
50
- * this id. Do not canonicalize this function — that would make it identical
51
- * to `createActionableWarningId` and silently defeat the migration lookup
52
- * for every path that actually needed canonicalizing (the #533 class this
53
- * whole item exists to fix). Review-round F3 (#1816): this guard is only
54
- * provable under a MIS-CASED path fixture — a fixture whose raw and
55
- * canonical forms coincide (a bare mkdtempSync path) makes canonicalizing
56
- * this function a no-op, so the regression test must seed under a mis-cased
57
- * segment (see `actionable-warnings.test.ts`'s migration describe block) or
58
- * the guard passes vacuously either way. */
59
- function legacyActionableWarningId(args) {
60
- const rel = path.relative(args.cwd, args.filePath).replace(/\\/g, "/");
61
- const legacyRelativeFile = rel && !rel.startsWith("..") ? rel : normalizeMapKey(args.filePath);
62
- const parts = [
63
- legacyRelativeFile,
64
- args.tool ?? "",
65
- args.source ?? "",
66
- String(args.code ?? ""),
67
- args.rule ?? "",
68
- normalizeMessage(args.message),
69
- String(args.line ?? ""),
70
- ];
71
- return `aw:${hashText(parts.join("|"), 10)}`;
72
- }
73
- function actionSafety(action) {
74
- const kind = action.kind ?? "";
75
- if (!kind.startsWith("quickfix"))
76
- return { eligible: false, reason: "not_quickfix" };
77
- if (!action.isPreferred)
78
- return { eligible: false, reason: "not_preferred" };
79
- if (!action.edit)
80
- return { eligible: false, reason: "no_edit" };
81
- if (action.command)
82
- return { eligible: false, reason: "has_command" };
83
- return { eligible: true };
84
- }
85
- function serializeAction(action) {
86
- const safety = actionSafety(action);
87
- return {
88
- title: action.title,
89
- kind: action.kind,
90
- isPreferred: action.isPreferred,
91
- hasEdit: Boolean(action.edit),
92
- hasCommand: Boolean(action.command),
93
- autoFixEligible: safety.eligible,
94
- skipReason: safety.reason,
95
- };
96
- }
97
- function deserializeSuppressionState(contents) {
98
- try {
99
- const parsed = JSON.parse(contents ?? "");
100
- return parsed && typeof parsed === "object"
101
- ? parsed
102
- : {};
103
- }
104
- catch {
105
- return {};
106
- }
107
- }
108
- function readSuppressionState(cwd) {
109
- const statePath = path.join(getProjectDataDir(cwd), "cache", "actionable-warning-state.json");
110
- try {
111
- return deserializeSuppressionState(fs.readFileSync(statePath, "utf8"));
112
- }
113
- catch {
114
- return {};
115
- }
116
- }
117
- function updateWarningState(cwd, warnings) {
118
- const statePath = path.join(getProjectDataDir(cwd), "cache", "actionable-warning-state.json");
119
- fs.mkdirSync(path.dirname(statePath), { recursive: true });
120
- const hook = beforeWarningStateLockForTests;
121
- beforeWarningStateLockForTests = null;
122
- hook?.();
123
- commitDurableStore({
124
- path: statePath,
125
- deserialize: deserializeSuppressionState,
126
- merge: (state) => {
127
- const now = new Date().toISOString();
128
- state.warnings ??= {};
129
- for (const warning of warnings) {
130
- // #1816 migration: this warning may still be recorded under the
131
- // pre-#1816 id (raw relativeFile, 10-char hash) from before this
132
- // store unified onto the canonical id. Fold that entry forward
133
- // onto the current id and drop the stale key, so a warning
134
- // suppressed before the migration stays suppressed, and repeated
135
- // re-encounters converge the store onto one id per warning
136
- // instead of accumulating both forever.
137
- //
138
- // Review-round F1: this MUST read `warning.legacyId` — the value
139
- // `suppressionFor` already computed from the exact identity args
140
- // used at lookup time — and must NEVER re-derive a legacy id from
141
- // `warning.rule`/`tool`/`source`/`code`. Those fields hold
142
- // DISPLAY values that diverge from the id-construction args for
143
- // LSP-origin records (see `ActionableWarningRecord.legacyId`'s
144
- // doc comment), so a re-derivation here would compute a
145
- // different legacy id than the one `suppressionFor` checked,
146
- // permanently bifurcating the store.
147
- const legacyId = warning.legacyId;
148
- const legacyEntry = legacyId && legacyId !== warning.id
149
- ? state.warnings[legacyId]
150
- : undefined;
151
- const existing = state.warnings[warning.id] ?? legacyEntry ?? {};
152
- state.warnings[warning.id] = {
153
- ...existing,
154
- status: existing.status ?? "active",
155
- firstSeenAt: existing.firstSeenAt ?? now,
156
- lastSeenAt: now,
157
- seenCount: (existing.seenCount ?? 0) + 1,
158
- };
159
- if (legacyEntry && legacyId)
160
- delete state.warnings[legacyId];
161
- }
162
- return state;
163
- },
164
- serialize: (state) => JSON.stringify(state, null, 2),
165
- waitMs: 2_000,
166
- retryMs: 10,
167
- timeoutMessage: "timed out acquiring actionable warning store lock",
168
- onContention: "skip-log",
169
- logContention: () => logActionableWarningsEvent({
170
- event: "warning_state_write_dropped",
171
- metadata: { reason: "lock_contention" },
172
- }),
173
- });
174
- }
175
- /** Looks up suppression under the current id, falling back to the pre-#1816
176
- * id (see `legacyActionableWarningId`) so a warning suppressed before this
177
- * migration doesn't silently reappear as unsuppressed. `args` is the exact
178
- * identity shape both id builders take.
179
- *
180
- * Also RETURNS the `legacyId` it computed (review-round F1, #1816): the
181
- * caller carries it onto the record's `legacyId` field so
182
- * `updateWarningState` can migrate the SAME legacy id this lookup used,
183
- * instead of re-deriving one from the record's own `rule`/`tool`/`source`/
184
- * `code` fields later. Re-deriving is unsound for LSP-origin records —
185
- * `recordFromLspDiagnostic` passes no `rule` into this function (LSP
186
- * diagnostics don't have one), then sets `record.rule` afterward to
187
- * `${source}:${code}` purely for display. Recomputing from that display
188
- * value would silently compute a DIFFERENT legacy id than the one actually
189
- * checked here, permanently bifurcating the store: a suppression written
190
- * this turn under the current id would never be found again next turn. */
191
- function suppressionFor(cwd, id, args) {
192
- const state = readSuppressionState(cwd);
193
- const legacyId = legacyActionableWarningId({ cwd, ...args });
194
- const entry = state.warnings?.[id] ??
195
- (legacyId !== id ? state.warnings?.[legacyId] : undefined);
196
- return {
197
- suppressed: entry?.status === "suppressed",
198
- reason: entry?.reason,
199
- legacyId,
200
- };
201
- }
202
- export function recordFromDispatchDiagnostic(diagnostic, cwd) {
203
- if (diagnostic.semantic !== "warning")
204
- return undefined;
205
- // #1777: the old gate demanded `severity === "warning"` exactly, which was
206
- // invisible while the ast-grep runner collapsed every non-error tier to
207
- // "warning". Now that hint and info survive the dispatch path, admit them:
208
- // a fix is a fix, and the tier governs how loudly a finding renders, not
209
- // whether its fix is worth offering. `error` still routes to the blocking
210
- // path, so it stays out even when a runner leaves `semantic` at "warning".
211
- if (diagnostic.severity === "error")
212
- return undefined;
213
- if (!diagnostic.fixable && !diagnostic.fixSuggestion)
214
- return undefined;
215
- const filePath = path.resolve(cwd, diagnostic.filePath);
216
- const identityArgs = {
217
- filePath,
218
- tool: diagnostic.tool,
219
- code: diagnostic.code,
220
- rule: diagnostic.rule,
221
- message: diagnostic.message,
222
- line: diagnostic.line,
223
- };
224
- const id = createActionableWarningId({ cwd, ...identityArgs });
225
- const suppression = suppressionFor(cwd, id, identityArgs);
226
- return {
227
- id,
228
- filePath,
229
- displayPath: toRunnerDisplayPath(cwd, filePath),
230
- line: diagnostic.line,
231
- column: diagnostic.column,
232
- severity: diagnostic.severity,
233
- tool: diagnostic.tool,
234
- code: diagnostic.code,
235
- rule: diagnostic.rule,
236
- message: diagnostic.message,
237
- fixSuggestion: diagnostic.fixSuggestion,
238
- fixKind: diagnostic.fixKind,
239
- autoFixAvailable: diagnostic.autoFixAvailable,
240
- actions: [],
241
- suppressed: suppression.suppressed,
242
- suppressionReason: suppression.reason,
243
- legacyId: suppression.legacyId,
244
- origin: "dispatch",
245
- };
246
- }
247
- function lineInModifiedRanges(line, ranges) {
248
- if (line === undefined)
249
- return true;
250
- if (ranges.length === 0)
251
- return true;
252
- return ranges.some((range) => line >= range.start - 2 && line <= range.end + 2);
253
- }
254
- export function recordFromLspDiagnostic(diag, filePath, cwd) {
255
- const line = diag.range.start.line + 1;
256
- const column = diag.range.start.character + 1;
257
- // Keep agreement keyed to the producer that supplied the diagnostic. The
258
- // generic `lsp` label is only a last-resort identity: treating it as a
259
- // registered writer would establish every LSP quickfix without evidence.
260
- const producer = diag.source && diag.source !== "lsp" ? diag.source : diag.serverId;
261
- const source = diag.source ?? "lsp";
262
- const code = diag.code === undefined ? undefined : String(diag.code);
263
- const identityArgs = {
264
- filePath,
265
- tool: producer ?? "lsp",
266
- source,
267
- code,
268
- message: diag.message,
269
- line,
270
- };
271
- const id = createActionableWarningId({ cwd, ...identityArgs });
272
- const suppression = suppressionFor(cwd, id, identityArgs);
273
- return {
274
- id,
275
- filePath,
276
- displayPath: toRunnerDisplayPath(cwd, filePath),
277
- line,
278
- column,
279
- severity: "warning",
280
- tool: producer ?? "lsp",
281
- source,
282
- code,
283
- rule: code ? `${source}:${code}` : source,
284
- message: diag.message,
285
- actions: [],
286
- suppressed: suppression.suppressed,
287
- suppressionReason: suppression.reason,
288
- legacyId: suppression.legacyId,
289
- origin: "lsp",
290
- };
291
- }
292
- function mergeWarnings(records) {
293
- const byId = new Map();
294
- for (const record of records) {
295
- const existing = byId.get(record.id);
296
- if (!existing) {
297
- byId.set(record.id, { ...record, actions: [...record.actions] });
298
- continue;
299
- }
300
- existing.origin =
301
- existing.origin === record.origin ? existing.origin : "merged";
302
- existing.fixSuggestion ??= record.fixSuggestion;
303
- existing.fixKind ??= record.fixKind;
304
- existing.autoFixAvailable ||= record.autoFixAvailable;
305
- const seenActions = new Set(existing.actions.map((a) => `${a.kind ?? ""}|${a.title}`));
306
- for (const action of record.actions) {
307
- const key = `${action.kind ?? ""}|${action.title}`;
308
- if (!seenActions.has(key)) {
309
- existing.actions.push(action);
310
- seenActions.add(key);
311
- }
312
- }
313
- }
314
- return [...byId.values()].sort((a, b) => a.displayPath.localeCompare(b.displayPath) ||
315
- (a.line ?? 0) - (b.line ?? 0));
316
- }
317
- /**
318
- * #2504 — bounds on the LSP enrichment loop.
319
- *
320
- * This function runs on the AWAITED turn_end hook. With `includeLspCodeActions`
321
- * on and a cold LSP cache it opened every file it was handed and pulled fresh
322
- * per-file diagnostics serially at ~880 ms each: 147 files, 187 891 ms, for
323
- * `warnings: 0`. Three bounds, plus a project-root filter (it had opened
324
- * `~/.claude/plans/*.md` in an LSP client), plus a deferral: when the turn
325
- * primed NO cache, every file would be a fresh pull, so the whole loop moves
326
- * off the hook and delivers through the cached channel instead.
327
- */
328
- export const ACTIONABLE_WARNINGS_LSP_FILE_CAP = 25;
329
- export const ACTIONABLE_WARNINGS_LSP_BUDGET_MS = 2_500;
330
- export const ACTIONABLE_WARNINGS_DEFERRED_BUDGET_MS = 60_000;
331
- /**
332
- * #2504 review round 2 (F3): the per-round-trip bound. The batch deadlines
333
- * above are checked only BETWEEN files, so a single wedged `getDiagnostics`
334
- * (or `openFile`, or `codeAction`) was unbounded no matter how small the
335
- * batch budget was. Generous relative to the ~880 ms a real cold pull costs —
336
- * this is a wedge detector, not a latency target.
337
- */
338
- export const ACTIONABLE_WARNINGS_LSP_PULL_TIMEOUT_MS = 10_000;
339
- /**
340
- * #2504 review round 3 (S-2): the third bound, per FILE.
341
- *
342
- * The wall budgets above are re-checked BETWEEN files only — deliberately, so
343
- * that a file already opened is finished rather than half-enriched — and the
344
- * per-round-trip timeout bounds one call. Neither bounds the COUNT of calls
345
- * one file can demand: a generated or vendored file with hundreds of warnings
346
- * on modified lines costs one `codeAction` round trip each, all inside a
347
- * single between-files interval. This caps that fan-out. The abort signal
348
- * still escapes promptly — `boundedLspCall` checks it on every trip.
349
- */
350
- export const ACTIONABLE_WARNINGS_MAX_CODE_ACTIONS_PER_FILE = 25;
351
- /**
352
- * The in-flight deferred fresh-pull, if any. Exposed for tests only: the
353
- * deferral is fire-and-forget by design, and a test asserting that the work
354
- * still happens off-hook needs a handle to await.
355
- *
356
- * #2504 review round 2 (F3): the handle no longer lives here as a bare
357
- * module-level `let` that a second deferral silently overwrote (leaving the
358
- * first loop running, untracked and unstoppable) and that nothing ever reset.
359
- * `clients/deferred-lsp-work.ts` owns the single slot and its abort signal.
360
- */
361
- export function _awaitDeferredLspPullForTest() {
362
- return awaitDeferredLspWork();
363
- }
364
- /**
365
- * Both bounds on ONE LSP round trip (#2504 review round 2, F3), as AGENTS.md
366
- * requires of any async step in a sweep loop: a per-call timeout AND the abort
367
- * signal. Resolves `undefined` when either bound wins; the caller reads that
368
- * as "this file was not checked", never as "this file is clean".
369
- *
370
- * #2523 slice 2 folded this onto `bounded()`. It used to be `withDeadline`
371
- * (deadline only) raced against a hand-built `abortRace` leg carried on the
372
- * deps — the fifth private spelling of "deadline AND signal".
373
- *
374
- * What the fold did NOT fix, stated because the first draft of this PR claimed
375
- * it did (#2557 review F5): the old shape did not silently degrade any wait.
376
- * Both construction sites derived the leg from the signal in the same
377
- * expression (`args.signal ? makeAbortRace(args.signal) : undefined`), so
378
- * `abortRace` was absent exactly when `signal` was, and the behaviour before
379
- * and after this fold is identical on that axis. What the fold actually buys
380
- * is that the two fields which had to AGREE are now one — a pairing invariant
381
- * that lived in prose is gone rather than enforced — plus the abandonment now
382
- * reaching the ledger, and the leg's listener now being released per call
383
- * instead of living as long as the loop's signal does.
384
- *
385
- * ## The loop's residual budget is a THIRD bound, and it must not reach the
386
- * ledger (#2557 review F-A)
387
- *
388
- * Round 1 clamped `bounded()`'s own timer to
389
- * `Math.min(pullTimeoutMs, remainingMs)`, so whenever the LOOP's own wall
390
- * budget was already low, `bounded()` armed at the shrunken value and — on
391
- * firing — recorded `hook-await-exceeded` naming THAT value as "the budget".
392
- * A healthy pull that simply started late in a big batch was reported as
393
- * exceeding a budget that exists in no configuration (measured: 400 ms
394
- * pulls, 12 files, "exceeded 65ms budget after 77ms"), and because
395
- * `recordDegradationOnce` is rising-edge, that benign row could silence a
396
- * genuinely wedged server later in the same session.
397
- *
398
- * `bounded()`'s OWN timer is now always armed at the full `pullTimeoutMs`, so
399
- * it only ever fires — and only ever records — when a call outlives its own
400
- * configured per-trip budget (`metadata.budgetMs === pullTimeoutMs`, always).
401
- * The loop's shrinking residual is enforced SEPARATELY, by racing `bounded()`
402
- * against `withDeadline(…, { onTimeout: "undefined" })` keyed to the loop's
403
- * own `deadlineAt` — a deadline-only wait with no ledger connection of its
404
- * own, the same primitive `bounded()` itself is built to replace when a
405
- * caller needs BOTH bounds. The still-running `bounded()` call is not
406
- * cancelled (nothing here can cancel already-started work); if it later
407
- * genuinely outlives `pullTimeoutMs`, it still records, correctly, on its own
408
- * clock.
409
- */
410
- async function boundedLspCall(call, deps) {
411
- if (deps.signal?.aborted)
412
- return undefined;
413
- // #2504 review round 4 (F2): the loop's wall budget bounds THIS trip too. A
414
- // round trip that starts with 5 ms of budget left may not run for 10 s just
415
- // because the per-call timeout says so -- that is how one file held the
416
- // awaited hook for minutes past a spent batch budget.
417
- const remainingMs = deps.deadlineAt !== undefined ? deps.deadlineAt - Date.now() : undefined;
418
- // Returned BEFORE `bounded()` rather than handed to it as a zero budget: a
419
- // trip that never started did not exceed anything, and recording it as an
420
- // exceedance would attribute the loop's spent budget to whichever call
421
- // happened to be next.
422
- if (remainingMs !== undefined && remainingMs <= 0)
423
- return undefined;
424
- const inner = bounded(call().then((value) => ({ value })), {
425
- // Always the FULL per-trip budget -- never clamped to the loop's own
426
- // residual. The residual is enforced below, off the ledger.
427
- ms: deps.pullTimeoutMs,
428
- // Optional for the in-band caller; `bounded()` reads a missing signal
429
- // as one that never aborts, so the deadline half stays live.
430
- signal: deps.signal,
431
- // From the deps, never a literal: the in-band and deferred loops must
432
- // key separately (#2557 review F3).
433
- hook: deps.site.hook,
434
- label: deps.site.label,
435
- });
436
- const boxed = deps.deadlineAt !== undefined
437
- ? await withDeadline(inner, {
438
- deadlineAt: deps.deadlineAt,
439
- onTimeout: "undefined",
440
- })
441
- : await inner;
442
- // Boxed because an LSP pull legitimately resolves `undefined` (no
443
- // diagnostics for this file), which must stay distinguishable from a bound
444
- // firing even though both currently mean "not checked" to the caller.
445
- return boxed?.value;
446
- }
447
- /** Positive finite bound, else the default. Guards NaN from env/config. */
448
- function boundedNumber(value, fallback) {
449
- return value !== undefined && Number.isFinite(value) && value > 0
450
- ? value
451
- : fallback;
452
- }
453
- export async function buildActionableWarningsReport(args) {
454
- const cwd = path.resolve(args.cwd);
455
- const records = [...args.dispatchWarnings];
456
- const lspService = getLSPService();
457
- // #2504 review round 5 (F2): when each file's LSP observation FINISHED,
458
- // keyed by normalized path. assembleReport used to stamp every entry with
459
- // one `new Date()` taken at ASSEMBLY -- which for the deferred loop is the
460
- // loop's END, so a file read 60 s and 24 files ago claimed to be
461
- // milliseconds old. applyDeltaFreshnessGate compares that stamp against the
462
- // file's mtime to catch an out-of-band edit made after the observation, and
463
- // a stamp that late hands it a window which has already closed. Shared with
464
- // the deferred closure below, so the in-band entries it carries over keep
465
- // the stamps their own pass gave them.
466
- const observedAtByPath = new Map();
467
- // The conservative stand-in for an entry no LSP pull ever touched (a
468
- // dispatch-origin warning): the moment this build began. Never LATER than
469
- // the observation it stands in for, which is the direction that matters --
470
- // an over-old stamp costs a line number, an over-new one leaks a stale one.
471
- const buildStartedAt = new Date().toISOString();
472
- logActionableWarningsEvent({
473
- event: "report_started",
474
- sessionId: args.sessionId,
475
- metadata: {
476
- turnIndex: args.turnIndex,
477
- filesCount: args.files.length,
478
- dispatchWarningsCount: args.dispatchWarnings.length,
479
- deltaOnly: args.deltaOnly !== false,
480
- includeLspCodeActions: args.includeLspCodeActions,
481
- },
482
- });
483
- if (args.includeLspCodeActions) {
484
- const fileCap = Math.floor(boundedNumber(args.lspFileCap, ACTIONABLE_WARNINGS_LSP_FILE_CAP));
485
- const budgetMs = boundedNumber(args.lspBudgetMs, ACTIONABLE_WARNINGS_LSP_BUDGET_MS);
486
- // #2504 (1) project-root filter. The worklist this loop is handed had
487
- // accumulated paths from two other agents' scratchpads, `~/.claude/plans`
488
- // and `~/.plegma/work` — none of which belong to an LSP client rooted at
489
- // this project. Rejected before any file read.
490
- const eligible = [];
491
- let outsideRoot = 0;
492
- for (const file of args.files) {
493
- const filePath = path.resolve(cwd, file);
494
- if (normalizeMapKey(filePath) === normalizeMapKey(cwd) ||
495
- !isUnderDir(filePath, cwd)) {
496
- outsideRoot++;
497
- logActionableWarningsEvent({
498
- event: "lsp_file_skipped",
499
- sessionId: args.sessionId,
500
- filePath,
501
- metadata: { reason: "outside_project_root" },
502
- });
503
- continue;
504
- }
505
- if (!lspService.supportsLSP(filePath)) {
506
- logActionableWarningsEvent({
507
- event: "lsp_file_skipped",
508
- sessionId: args.sessionId,
509
- filePath,
510
- metadata: { reason: "no_lsp_support" },
511
- });
512
- continue;
513
- }
514
- eligible.push(filePath);
515
- }
516
- if (outsideRoot > 0) {
517
- args.dbg?.(`actionable_warnings: skipped ${outsideRoot} file(s) outside the project root`);
518
- }
519
- // #2504 (2) file cap.
520
- const capped = eligible.slice(0, fileCap);
521
- if (capped.length < eligible.length) {
522
- recordDegradationOnce({
523
- kind: "actionable-warnings-cap",
524
- subject: `${cwd}:file-cap`,
525
- reason: `LSP enrichment capped at ${fileCap} file(s); ${eligible.length - capped.length} modified file(s) were not checked for code actions this turn`,
526
- });
527
- }
528
- // Resolve every file's cache state BEFORE doing any LSP work. This is a
529
- // read + a sha256 per file (sub-millisecond) and it is what tells us
530
- // whether the turn primed the cache at all — the difference between a
531
- // loop that costs nothing and one that costs ~880 ms per file.
532
- const primed = [];
533
- const cold = [];
534
- for (const filePath of capped) {
535
- // Reuse the cache primed by the dispatch pipeline's touchFile earlier
536
- // in this turn — but only when it is verified current. A second
537
- // open+wait here costs ~1 s/file with the LSP cold, so we pass the
538
- // hash of the current file bytes: getLastKnownDiagnostics returns the
539
- // entry only if it was primed for the SAME content, so a previous
540
- // turn's diagnostics are never served as current. On any miss (no
541
- // entry, content drift, or an entry written without content) the file
542
- // needs a fresh read.
543
- const content = fs.existsSync(filePath)
544
- ? fs.readFileSync(filePath, "utf-8")
545
- : undefined;
546
- const contentHash = content !== undefined
547
- ? createHash("sha256").update(content).digest("hex")
548
- : undefined;
549
- const cached = contentHash !== undefined
550
- ? lspService.getLastKnownDiagnostics(filePath, contentHash)
551
- : undefined;
552
- (cached !== undefined ? primed : cold).push({
553
- filePath,
554
- content,
555
- cached,
556
- });
557
- }
558
- // #2504 (3) wall budget, raced against the turn's abort signal. Both
559
- // bounds, per AGENTS.md: neither a cap nor a deadline alone stops a
560
- // cancelled turn from paying for work nobody is waiting for.
561
- const deadline = Date.now() + budgetMs;
562
- const exhausted = () => args.signal?.aborted === true || Date.now() >= deadline;
563
- const pullTimeoutMs = boundedNumber(args.lspPullTimeoutMs, ACTIONABLE_WARNINGS_LSP_PULL_TIMEOUT_MS);
564
- // #2504 review round 2 (F3): the in-band loop gets the same per-call
565
- // bound and the same once-resolved service as the deferred one — the
566
- // deferral must not be the only path that is bounded.
567
- const inBandDeps = {
568
- lspService,
569
- pullTimeoutMs,
570
- // #2504 review round 4 (F2). This is the AWAITED hook: the batch
571
- // budget has to bound what happens INSIDE a file, not only how many
572
- // files get started.
573
- deadlineAt: deadline,
574
- signal: args.signal,
575
- // This loop IS on the awaited hook, so its exceedances are turn_end's.
576
- site: { hook: "turn_end", label: "lspEnrichmentRoundTrip" },
577
- };
578
- let unchecked = 0;
579
- const runInBand = async (targets) => {
580
- for (const target of targets) {
581
- if (exhausted()) {
582
- unchecked += targets.length - targets.indexOf(target);
583
- break;
584
- }
585
- records.push(...(await enrichFileFromLsp(cwd, args, target, inBandDeps)));
586
- // #2504 review round 5 (F2): stamped when THIS file's pull
587
- // returned, not when the report is assembled.
588
- observedAtByPath.set(normalizeMapKey(target.filePath), new Date().toISOString());
589
- }
590
- };
591
- // Cached files first: they are free, and whether ANY of them exist is
592
- // what decides the cold set's fate below.
593
- await runInBand(primed);
594
- // #2504 (4) cold-cache deferral. When the turn primed nothing, every
595
- // remaining file is a full open + diagnostic wait; that whole loop is
596
- // what held the terminal for 187 s. It still runs — off the awaited
597
- // hook — and lands in the same `actionable-warnings` cache the in-band
598
- // report goes to, so the findings reach the agent by the same channel,
599
- // one turn later at worst.
600
- if (cold.length > 0 && primed.length === 0) {
601
- // #2504 review round 2 (F3). The pre-fix loop captured
602
- // `args.signal` — the COMPLETED turn's `ctx.signal`, which
603
- // `index.ts` clears from the ambient slot in its `finally`, so it
604
- // could never fire. `armDeferredLspWork` returns the module slot's
605
- // live signal; `resetLSPService` fires it, which is how
606
- // session_shutdown, session_start and the idle reset all reach this
607
- // loop. The turn's own signal is still folded in so a genuine
608
- // mid-turn abort counts.
609
- //
610
- // #2504 review round 3 (F-A(d)): it returns `undefined` when an
611
- // EARLIER deferral still holds the slot. The incumbent wins — see
612
- // `armDeferredLspWork` — and this turn states its loss instead of
613
- // cancelling a loop that is about to publish.
614
- const deferredSignal = armDeferredLspWork();
615
- if (deferredSignal === undefined) {
616
- incrementDegradationCount({
617
- kind: "actionable-warnings-cap",
618
- subject: `${cwd}:deferral-declined`,
619
- // #2504 review round 4 (F1): the old wording ended "rather than
620
- // cancel it", which asserted a preservation that did not
621
- // happen -- the incumbent's report was then discarded whole by
622
- // the persisted-newer guard. With the per-file merge below it
623
- // is true, so it now says exactly WHAT survives and what does
624
- // not.
625
- reason: `an earlier deferred LSP pull is still running; ${cold.length} file(s) went unchecked for code actions this turn. What IS preserved is the incumbent loop's work: when it lands, its per-file entries are merged into whatever report is persisted then, for every file whose fileSeq has not advanced. This turn's own cold files are the loss, and nothing re-derives them -- the next report is a delta over a different file set`,
626
- });
627
- logActionableWarningsEvent({
628
- event: "lsp_pull_deferral_declined",
629
- sessionId: args.sessionId,
630
- metadata: { turnIndex: args.turnIndex, files: cold.length },
631
- });
632
- args.dbg?.(`actionable_warnings: an earlier deferred LSP pull still holds the slot — skipping ${cold.length} fresh pull(s) this turn`);
633
- }
634
- else {
635
- const carried = [...records];
636
- const deferredArgs = args;
637
- const loopSignal = combineAbortSignals(args.signal, deferredSignal) ?? deferredSignal;
638
- const deferredDeps = {
639
- lspService,
640
- pullTimeoutMs,
641
- signal: loopSignal,
642
- // OFF the hook by construction: this loop is the answer to
643
- // turn_end's budget, not a spender of it, and it runs on its
644
- // own ACTIONABLE_WARNINGS_DEFERRED_BUDGET_MS deadline. Charging
645
- // its exceedances to `turn_end` would make the hook look over
646
- // budget for having correctly deferred the work (#2557 F3).
647
- site: {
648
- hook: "off_hook",
649
- label: "deferredLspEnrichmentRoundTrip",
650
- },
651
- };
652
- const deferredWork = (async () => {
653
- // Yield a full macrotask first. Without this the loop would run
654
- // its first open+pull inside the awaited call's own microtask
655
- // drain — "deferred" only on paper, and still on the hook.
656
- await new Promise((resolve) => setTimeout(resolve, 0));
657
- const deferredRecords = [...carried];
658
- const deferredDeadline = Date.now() + ACTIONABLE_WARNINGS_DEFERRED_BUDGET_MS;
659
- // #2504 review round 4 (F2): set here, not at construction --
660
- // the deferred loop's budget starts when the loop does, one
661
- // macrotask after these deps were built.
662
- deferredDeps.deadlineAt = deferredDeadline;
663
- let deferredUnchecked = 0;
664
- let abortedMidLoop = false;
665
- for (const target of cold) {
666
- if (loopSignal.aborted || Date.now() >= deferredDeadline) {
667
- abortedMidLoop = loopSignal.aborted;
668
- deferredUnchecked += cold.length - cold.indexOf(target);
669
- break;
670
- }
671
- deferredRecords.push(...(await enrichFileFromLsp(cwd, deferredArgs, target, deferredDeps)));
672
- // #2504 review round 5 (F2). This is the loop the defect
673
- // was about: it can run for a minute, and one stamp taken
674
- // at its end described all of it.
675
- observedAtByPath.set(normalizeMapKey(target.filePath), new Date().toISOString());
676
- }
677
- if (deferredUnchecked > 0) {
678
- recordDegradationOnce({
679
- kind: "actionable-warnings-cap",
680
- subject: `${cwd}:deferred-budget`,
681
- reason: `deferred LSP enrichment stopped early; ${deferredUnchecked} file(s) were not checked for code actions`,
682
- });
683
- }
684
- // An ABORTED loop delivers nothing (#2504 review round 2, F3).
685
- // The service it was reading is gone — session_shutdown,
686
- // session_start, or the idle reset retired it — so its partial
687
- // record set describes nothing current, and publishing it would
688
- // be exactly the stale-clobber F2 guards against on the other
689
- // side.
690
- if (abortedMidLoop || loopSignal.aborted) {
691
- logActionableWarningsEvent({
692
- event: "lsp_pull_aborted",
693
- sessionId: deferredArgs.sessionId,
694
- metadata: {
695
- turnIndex: deferredArgs.turnIndex,
696
- unchecked: deferredUnchecked,
697
- },
698
- });
699
- return;
700
- }
701
- deferredArgs.onDeferredReport?.(assembleReport(cwd, deferredArgs, deferredRecords, {
702
- observedAtByPath,
703
- fallbackObservedAt: buildStartedAt,
704
- }));
705
- })().catch((err) => {
706
- args.dbg?.(`actionable_warnings: deferred LSP pull failed: ${err}`);
707
- });
708
- registerDeferredLspWork(deferredSignal, deferredWork);
709
- logActionableWarningsEvent({
710
- event: "lsp_pull_deferred",
711
- sessionId: args.sessionId,
712
- metadata: { turnIndex: args.turnIndex, files: cold.length },
713
- });
714
- args.dbg?.(`actionable_warnings: no LSP cache primed this turn — deferring ${cold.length} fresh pull(s) off the turn_end hook`);
715
- }
716
- }
717
- else {
718
- await runInBand(cold);
719
- }
720
- if (unchecked > 0) {
721
- recordDegradationOnce({
722
- kind: "actionable-warnings-cap",
723
- subject: `${cwd}:wall-budget`,
724
- reason: `LSP enrichment hit its ${Math.round(budgetMs)}ms turn budget; ${unchecked} file(s) were not checked for code actions this turn`,
725
- });
726
- }
727
- }
728
- return assembleReport(cwd, args, records, {
729
- observedAtByPath,
730
- fallbackObservedAt: buildStartedAt,
731
- });
732
- }
733
- /**
734
- * Enrich one file's LSP warnings into records. Split out of
735
- * `buildActionableWarningsReport` (#2504) so the in-band loop and the deferred
736
- * off-hook loop run byte-identical logic — the deferral must not become a
737
- * second, drifting copy of the enrichment.
738
- */
739
- export async function enrichFileFromLsp(cwd, args, target, deps) {
740
- const { lspService } = deps;
741
- const { filePath } = target;
742
- const out = [];
743
- let diags;
744
- let lspSource = "cache";
745
- if (target.cached !== undefined) {
746
- diags = target.cached;
747
- }
748
- else {
749
- // #2504 review round 2 (F3): never OPEN a file once the loop has been
750
- // signalled. The checks inside `boundedLspCall` cover the round trips;
751
- // this one covers the decision to touch the file at all, which is what
752
- // makes a session_shutdown landing mid-loop a no-op rather than one
753
- // more document handed to a service that is being torn down.
754
- if (deps.signal?.aborted) {
755
- logActionableWarningsEvent({
756
- event: "lsp_file_skipped",
757
- sessionId: args.sessionId,
758
- filePath,
759
- metadata: { reason: "aborted" },
760
- });
761
- return out;
762
- }
763
- try {
764
- if (target.content) {
765
- // #2504 review round 3 (F-B), the #240 shape. `openFile` resolves
766
- // `void`, so the bounded call cannot distinguish success from
767
- // either bound winning unless the success path returns a value of
768
- // its own. Round 2 discarded the result entirely: a 10 s timeout
769
- // or an abort "succeeded" silently, and the pull below then asked
770
- // the server about a document it had never received. The `[]` that
771
- // answers means UNKNOWN, but it was recorded
772
- // `lsp_file_checked lspSource:"fresh"` — a failed pull read as
773
- // clean, which is precisely what the comment on the pull forbids.
774
- const opened = await boundedLspCall(async () => {
775
- await lspService.openFile(filePath, target.content);
776
- return true;
777
- }, deps);
778
- if (opened === undefined) {
779
- logActionableWarningsEvent({
780
- event: "lsp_file_skipped",
781
- sessionId: args.sessionId,
782
- filePath,
783
- metadata: {
784
- reason: deps.signal?.aborted ? "aborted" : "open_timeout",
785
- pullTimeoutMs: deps.pullTimeoutMs,
786
- },
787
- });
788
- return out;
789
- }
790
- }
791
- const pulled = await boundedLspCall(() => lspService.getDiagnostics(filePath), deps);
792
- if (pulled === undefined) {
793
- // Either bound won. A failed pull is NEVER read as clean (#240):
794
- // the file is reported unchecked and contributes no records.
795
- logActionableWarningsEvent({
796
- event: "lsp_file_skipped",
797
- sessionId: args.sessionId,
798
- filePath,
799
- metadata: {
800
- reason: deps.signal?.aborted ? "aborted" : "pull_timeout",
801
- pullTimeoutMs: deps.pullTimeoutMs,
802
- },
803
- });
804
- return out;
805
- }
806
- diags = pulled;
807
- lspSource = "fresh";
808
- }
809
- catch (err) {
810
- args.dbg?.(`actionable_warnings: LSP diagnostics failed for ${filePath}: ${err}`);
811
- logActionableWarningsEvent({
812
- event: "lsp_file_skipped",
813
- sessionId: args.sessionId,
814
- filePath,
815
- metadata: { reason: "lsp_error", error: String(err) },
816
- });
817
- return out;
818
- }
819
- }
820
- const ranges = args.modifiedRangesByFile.get(normalizeMapKey(filePath)) ?? [];
821
- const diagsWarning = diags.filter((d) => d.severity === 2);
822
- let deltaFiltered = 0;
823
- let enriched = 0;
824
- let actionPulls = 0;
825
- let actionCapped = 0;
826
- let budgetStopped = 0;
827
- for (const diag of diagsWarning) {
828
- // #2504 review round 4 (F2). The wall budget used to be re-checked
829
- // BETWEEN files only, so a single file could keep the AWAITED hook for
830
- // openFile + getDiagnostics + up to 25 codeAction round trips long after
831
- // the batch budget expired. The loop deadline is threaded through deps
832
- // and re-read HERE, between round trips: a file already opened still
833
- // finishes its cheap work, but it cannot buy more LSP time.
834
- if (deps.deadlineAt !== undefined && Date.now() >= deps.deadlineAt) {
835
- budgetStopped = diagsWarning.length - diagsWarning.indexOf(diag);
836
- break;
837
- }
838
- const line = diag.range.start.line + 1;
839
- if (args.deltaOnly !== false && !lineInModifiedRanges(line, ranges)) {
840
- deltaFiltered++;
841
- continue;
842
- }
843
- // #2504 review round 3 (S-2): bound the per-file codeAction fan-out.
844
- if (actionPulls >= ACTIONABLE_WARNINGS_MAX_CODE_ACTIONS_PER_FILE) {
845
- actionCapped++;
846
- continue;
847
- }
848
- actionPulls++;
849
- const record = recordFromLspDiagnostic(diag, filePath, cwd);
850
- try {
851
- const actions = await boundedLspCall(() => lspService.codeAction(filePath, diag.range.start.line, diag.range.start.character, diag.range.end.line, diag.range.end.character), deps);
852
- record.actions = (actions ?? []).map(serializeAction).slice(0, 5);
853
- }
854
- catch (err) {
855
- args.dbg?.(`actionable_warnings: LSP codeAction failed for ${filePath}: ${err}`);
856
- }
857
- if (record.actions.length > 0) {
858
- out.push(record);
859
- enriched++;
860
- }
861
- }
862
- if (actionCapped > 0) {
863
- incrementDegradationCount({
864
- kind: "actionable-warnings-cap",
865
- subject: `${cwd}:code-action-fanout`,
866
- // #2504 review round 4 (S-2): "reported without fix actions" was
867
- // wrong in both halves. A capped warning is skipped BEFORE its record
868
- // is built, and a record with no action is dropped below anyway, so
869
- // it is not reported at all -- it never reaches the agent on this
870
- // channel.
871
- reason: `a file exceeded the ${ACTIONABLE_WARNINGS_MAX_CODE_ACTIONS_PER_FILE}-warning code-action cap; ${actionCapped} warning(s) in ${toRunnerDisplayPath(cwd, filePath)} were NOT reported this turn`,
872
- });
873
- }
874
- if (budgetStopped > 0) {
875
- incrementDegradationCount({
876
- kind: "actionable-warnings-cap",
877
- subject: `${cwd}:in-file-budget`,
878
- reason: `the LSP enrichment wall budget expired part-way through a file; ${budgetStopped} warning(s) in ${toRunnerDisplayPath(cwd, filePath)} were NOT checked for fix actions`,
879
- });
880
- }
881
- logActionableWarningsEvent({
882
- event: "lsp_file_checked",
883
- sessionId: args.sessionId,
884
- filePath,
885
- metadata: {
886
- diagsTotal: diags.length,
887
- diagsWarning: diagsWarning.length,
888
- deltaFiltered,
889
- enriched,
890
- actionCapped,
891
- budgetStopped,
892
- modifiedRangesCount: ranges.length,
893
- lspSource,
894
- },
895
- });
896
- return out;
897
- }
898
- /**
899
- * Assemble the report from a record set. Called once for the in-band report
900
- * and again, off-hook, when the deferred fresh pull completes (#2504).
901
- */
902
- function assembleReport(cwd, args, records, stamps) {
903
- const merged = mergeWarnings(records);
904
- updateWarningState(cwd, merged);
905
- // legacyId is #1816 migration bookkeeping for updateWarningState above —
906
- // strip it before the report leaves this function, so it never lands in
907
- // the `<project-data-dir>/cache/actionable-warnings.json` cache file or any
908
- // agent-facing rendering of a warning record.
909
- const reportWarnings = merged.map(({ legacyId: _legacyId, ...rest }) => rest);
910
- const byFile = new Map();
911
- for (const warning of reportWarnings) {
912
- const arr = byFile.get(warning.filePath) ?? [];
913
- arr.push(warning);
914
- byFile.set(warning.filePath, arr);
915
- }
916
- // #2504 review round 4 (F1): every entry carries the moment it was
917
- // assembled, so a report that later absorbs a deferred pull's entries can
918
- // still age each half honestly. For a report built in one pass they are all
919
- // the report stamp.
920
- const generatedAt = new Date().toISOString();
921
- const files = [...byFile.entries()].map(([filePath, warnings]) => ({
922
- filePath,
923
- displayPath: toRunnerDisplayPath(cwd, filePath),
924
- fileSeq: args.fileSeqByPath?.get(normalizeMapKey(filePath)),
925
- // #2504 review round 5 (F2): the moment THIS file was observed. The
926
- // report-level stamp remains the assembly moment and is only the
927
- // last-resort fallback, for a caller that supplied no observations
928
- // at all.
929
- generatedAt: stamps?.observedAtByPath.get(normalizeMapKey(filePath)) ??
930
- stamps?.fallbackObservedAt ??
931
- generatedAt,
932
- warnings,
933
- }));
934
- const summary = summarizeReportFiles(files);
935
- logActionableWarningsEvent({
936
- event: "report_complete",
937
- sessionId: args.sessionId,
938
- metadata: { turnIndex: args.turnIndex, summary },
939
- });
940
- return {
941
- generatedAt,
942
- scope: "turn_delta",
943
- sessionId: args.sessionId,
944
- turnIndex: args.turnIndex,
945
- projectSeqStart: args.projectSeqStart,
946
- projectSeqEnd: args.projectSeqEnd,
947
- deltaOnly: args.deltaOnly !== false,
948
- includeLspCodeActions: args.includeLspCodeActions,
949
- files,
950
- summary,
951
- };
952
- }
953
- /**
954
- * The report summary, derived from the per-file entries.
955
- *
956
- * ONE derivation (#2504 review round 4, F1): a merged report's summary has to
957
- * describe the merged file set, and a second hand-rolled tally beside
958
- * assembleReport's would be the mirrored-registry defect AGENTS.md names.
959
- * Every warning belongs to exactly one file entry, so summing over entries and
960
- * summing over the flat record list give the same numbers.
961
- */
962
- function summarizeReportFiles(files) {
963
- const warnings = files.flatMap((file) => file.warnings);
964
- const unsuppressed = warnings.filter((warning) => !warning.suppressed);
965
- const allActions = warnings.flatMap((warning) => warning.actions);
966
- const countTier = (tier) => unsuppressed.filter((warning) => warning.severity === tier).length;
967
- return {
968
- warnings: warnings.length,
969
- unsuppressed: unsuppressed.length,
970
- byTier: {
971
- warning: countTier("warning"),
972
- info: countTier("info"),
973
- hint: countTier("hint"),
974
- },
975
- suppressed: warnings.filter((warning) => warning.suppressed).length,
976
- files: files.length,
977
- actions: allActions.length,
978
- autoFixEligible: allActions.filter((action) => action.autoFixEligible)
979
- .length,
980
- };
981
- }
982
- /**
983
- * The ONE place an actionable-warnings report reaches disk.
984
- *
985
- * Deliberately NOT exported (#2504 review round 5, F1). Two writers used to
986
- * publish to this key: the in-band turn_end report, which OVERWROTE blindly,
987
- * and the deferred off-hook report, which read-modify-wrote. A deferred merge
988
- * that landed anywhere inside turn N+1's handleTurnEnd -- the cascade settle,
989
- * knip, madge, the test batch, the in-band LSP enrichment; a window seconds
990
- * wide, measured at 607 ms in the reviewer's trace -- was erased by that
991
- * turn's blind write, and the findings the whole deferral exists to deliver
992
- * were gone with it. A blind writer and a merging writer on one key can only
993
- * ever be a race. There is now one publisher and it always merges; the private
994
- * scope is what keeps it that way.
995
- */
996
- function writeActionableWarningsReport(cacheManager, cwd, report) {
997
- cacheManager.writeCache("actionable-warnings", report, cwd);
998
- }
999
- /**
1000
- * Merge a report about to be published into whatever is already persisted,
1001
- * PER FILE.
1002
- *
1003
- * #2504 review round 4 (F1) established the shape: a report is a MAP of
1004
- * per-file entries, each stamped with that file's fileSeq and its own
1005
- * observation time, so ordering belongs PER FILE and not to the report as a
1006
- * whole. Rounds 2 and 3 ordered whole reports -- publish, or discard on a
1007
- * newer turnIndex/projectSeqEnd -- and that composed with incumbent-wins into
1008
- * "publish nothing".
1009
- *
1010
- * #2504 review round 5 (F1) generalized it to BOTH publishers, because the
1011
- * in-band write was still blind and erased a deferred merge that landed
1012
- * mid-turn. The two directions are asymmetric and the asymmetry is the whole
1013
- * design:
1014
- *
1015
- * - A DEFERRED publish carries the OLDER observation. Everything persisted is
1016
- * newer and is kept unconditionally; the deferred entries upsert into it
1017
- * wherever their file has not moved.
1018
- * - An IN-BAND publish carries the NEWER observation. It is this turn's own
1019
- * report, and its identity (turnIndex, projectSeq window, deltaOnly) is
1020
- * published untouched. From the persisted report it carries forward ONLY
1021
- * entries a deferral produced -- the scope guard, see
1022
- * {@link ActionableWarningsReportFile.origin}. Carrying everything would
1023
- * accumulate every prior turn's findings into a report whose scope field
1024
- * says "turn_delta"; carrying nothing is the F1 defect.
1025
- *
1026
- * Where both halves hold a file the warnings are UNIONED through mergeWarnings
1027
- * (the same de-duplicating merge the dispatch/LSP union already uses), so a
1028
- * newer entry is never REPLACED by an older one -- it only gains what the
1029
- * older one found -- and the merged entry is aged by the OLDER of the two
1030
- * stamps, because it now holds both observations.
1031
- *
1032
- * Exported because the merge, not the write, is what has to be pinned: a test
1033
- * can hand it two reports and read the ordering decision directly.
1034
- */
1035
- export function mergeActionableWarningsReports(args) {
1036
- const { persisted, incoming, origin } = args;
1037
- const deferredPublish = origin === "deferred";
1038
- // The newer half wins identity and fileSeq; the older half is folded in.
1039
- // #2504 review round 6 (F1) removed a same-sessionId gate here, reasoning
1040
- // that the fileSeq equality check below subsumed it: pi's telemetry
1041
- // sessionId is STABLE across a quit->resume (setSessionLifecycle pins it,
1042
- // runtime-coordinator.ts:677-682), but resetForSession clears the live
1043
- // _fileSeq map for the new process, so a resumed process's getFileSeq
1044
- // answers 0 for a file a PRE-restart deferral had recorded at a nonzero
1045
- // seq -- the sessionId gate could not see that (same id, stale data), the
1046
- // equality check could.
1047
- // #2504 review round 7 (F4): that reasoning breaks at fileSeq 0. A file
1048
- // THIS process has never touched also answers getFileSeq 0 (a fresh
1049
- // RuntimeCoordinator's map starts empty), and an entry a DIFFERENT
1050
- // process stamped at fileSeq 0 can reach the persisted report entirely
1051
- // unrelated to a resume: `mcp/analyze.ts` and `mcp/session.ts` register
1052
- // modified ranges through their OWN CacheManager, which never bumps the
1053
- // extension runtime's `_fileSeq` map, so an MCP-touched file can persist
1054
- // at seq 0 under a foreign sessionId. `0 !== 0` then reads FALSE and the
1055
- // entry is carried as if it were this session's own unmoved file. The
1056
- // sessionId gate is restored ALONGSIDE the equality check, not instead of
1057
- // it: equality alone catches the resume-reset case (same session, seq
1058
- // forcibly reset to 0), the sessionId gate alone catches the
1059
- // foreign-session case (different session, seq coincidentally equal,
1060
- // often both 0) -- neither subsumes the other.
1061
- const sessionMismatch = !deferredPublish &&
1062
- persisted?.sessionId !== undefined &&
1063
- persisted.sessionId !== incoming.sessionId;
1064
- const newerHalf = deferredPublish ? (persisted?.files ?? []) : incoming.files;
1065
- const olderHalf = deferredPublish ? incoming.files : (persisted?.files ?? []);
1066
- const newerReport = deferredPublish ? persisted : incoming;
1067
- const olderReport = deferredPublish ? incoming : persisted;
1068
- const byPath = new Map();
1069
- for (const entry of newerHalf) {
1070
- byPath.set(normalizeMapKey(entry.filePath), entry);
1071
- }
1072
- const droppedFiles = [];
1073
- let mergedFiles = 0;
1074
- for (const entry of olderHalf) {
1075
- // The scope guard. An in-band publish exists to add THIS turn's
1076
- // findings; the only thing it owes the persisted report is the deferred
1077
- // work that would otherwise be erased between the read and the write.
1078
- // #2504 review round 6 (b): the marker spend (below) means an entry
1079
- // carried once and then dropped here on the NEXT in-band publish is
1080
- // expected, not a fault -- traced to dbg (informational) rather than
1081
- // the degradation ledger (which is for loss the agent didn't cause).
1082
- if (!deferredPublish && entry.origin !== "deferred") {
1083
- args.dbg?.(`actionable_warnings: in-band publish dropped ${entry.displayPath || entry.filePath} -- its carry-forward window (from an earlier deferred publish) already closed`);
1084
- continue;
1085
- }
1086
- const key = normalizeMapKey(entry.filePath);
1087
- const incumbent = byPath.get(key);
1088
- const liveBaseline = args.getFileSeq !== undefined;
1089
- const baselineSeq = args.getFileSeq?.(entry.filePath) ?? incumbent?.fileSeq;
1090
- // #2504 review round 6 (F1): the in-band carry-forward path, when a
1091
- // LIVE baseline is available, requires EXACT equality rather than
1092
- // "baseline advanced past it". An unmoved file's live fileSeq always
1093
- // equals its persisted entry's recorded fileSeq; any mismatch --
1094
- // higher (edited since) OR lower (a resumed process whose fileSeq map
1095
- // was cleared, #2504 r6 F1) -- means the entry no longer describes
1096
- // what is on disk now. The deferred-publish path keeps the coarser
1097
- // ">" check: a live process's own fileSeq only ever advances, so
1098
- // "advanced past it" and "mismatched" agree there, and the deferred
1099
- // loop's entries can legitimately equal a JUST-bumped incumbent seq
1100
- // it upserts into.
1101
- const stale = !deferredPublish && liveBaseline
1102
- ? sessionMismatch ||
1103
- (typeof entry.fileSeq === "number" && baselineSeq !== entry.fileSeq)
1104
- : typeof baselineSeq === "number" &&
1105
- typeof entry.fileSeq === "number" &&
1106
- baselineSeq > entry.fileSeq;
1107
- if (stale) {
1108
- droppedFiles.push(entry.displayPath || entry.filePath);
1109
- continue;
1110
- }
1111
- mergedFiles++;
1112
- const merged = incumbent
1113
- ? incumbent.reVerified || incumbent.reVerifyIncomplete
1114
- ? // #3170: the runtime just RE-OBSERVED this file (the bounded
1115
- // persistent-reverify pass) — the fresh observation supersedes
1116
- // the carried entry instead of unioning with it: a converged
1117
- // finding would otherwise survive its own supersession via the
1118
- // id-union in `mergeWarnings`.
1119
- {
1120
- ...incumbent,
1121
- fileSeq: incumbent.fileSeq ?? entry.fileSeq,
1122
- generatedAt: olderStamp(incumbent.generatedAt ?? newerReport?.generatedAt, entry.generatedAt ?? olderReport?.generatedAt),
1123
- warnings: incumbent.warnings,
1124
- }
1125
- : {
1126
- ...incumbent,
1127
- fileSeq: incumbent.fileSeq ?? entry.fileSeq,
1128
- generatedAt: olderStamp(incumbent.generatedAt ?? newerReport?.generatedAt, entry.generatedAt ?? olderReport?.generatedAt),
1129
- warnings: mergeWarnings([...incumbent.warnings, ...entry.warnings]),
1130
- }
1131
- : { ...entry };
1132
- if (deferredPublish) {
1133
- // Mark it, so the next in-band publish carries it forward across the
1134
- // window in which it would otherwise be overwritten.
1135
- merged.origin = "deferred";
1136
- }
1137
- else {
1138
- // Carried forward exactly once: the marker is spent here.
1139
- merged.origin = undefined;
1140
- }
1141
- byPath.set(key, merged);
1142
- }
1143
- const files = [...byPath.values()];
1144
- if (!deferredPublish) {
1145
- // This turn's report, with the rescued entries folded in. Its identity
1146
- // is published untouched: a carried entry's provenance lives ON the
1147
- // entry (its own fileSeq and generatedAt), which is exactly why round 4
1148
- // moved those fields there, and widening turnIndex/projectSeqStart to
1149
- // span the older half would make the report claim a window it never
1150
- // observed.
1151
- return {
1152
- report: { ...incoming, files, summary: summarizeReportFiles(files) },
1153
- mergedFiles,
1154
- droppedFiles,
1155
- // sessionMismatch is a single flag for the whole call, not per-entry:
1156
- // once true, every stale check above is forced true by the `||`, so
1157
- // EITHER every drop in this batch is a session-boundary drop, OR none
1158
- // are (droppedFiles is then purely fileSeq-mismatch drops).
1159
- droppedForSessionMismatch: sessionMismatch && droppedFiles.length > 0,
1160
- };
1161
- }
1162
- const base = persisted ?? incoming;
1163
- return {
1164
- report: {
1165
- ...base,
1166
- // The report-level stamp is only the fallback for entries with none
1167
- // of their own (a cache file from an older build), so it takes the
1168
- // NEWER of the two; per-entry stamps carry the real ages.
1169
- generatedAt: newerStamp(persisted?.generatedAt, incoming.generatedAt) ??
1170
- base.generatedAt,
1171
- turnIndex: Math.max(base.turnIndex, incoming.turnIndex),
1172
- projectSeqStart: minDefined(persisted?.projectSeqStart, incoming.projectSeqStart),
1173
- // max, so a merged report never claims to be fresher OR staler than
1174
- // the newest part it holds. checkActionableWarningsReportFresh still
1175
- // demands exact equality with the live projectSeq and re-checks every
1176
- // entry's fileSeq, so this widens nothing.
1177
- projectSeqEnd: maxDefined(persisted?.projectSeqEnd, incoming.projectSeqEnd),
1178
- includeLspCodeActions: base.includeLspCodeActions || incoming.includeLspCodeActions,
1179
- files,
1180
- summary: summarizeReportFiles(files),
1181
- },
1182
- mergedFiles,
1183
- droppedFiles,
1184
- // The deferred-publish path never sets sessionMismatch (gated by
1185
- // `!deferredPublish` above), so its drops are always fileSeq-mismatch.
1186
- droppedForSessionMismatch: false,
1187
- };
1188
- }
1189
- /**
1190
- * Publish an actionable-warnings report: read what is persisted, merge per
1191
- * file, write. THE choke point (#2504 review round 5, F1) -- every publisher
1192
- * goes through it, so no writer can clobber another's entries.
1193
- *
1194
- * The read and the merge and the write are one synchronous block, which is
1195
- * what makes this safe against the deferred callback: node runs no other
1196
- * continuation between them, so the only interleaving left is publish-vs-
1197
- * publish, and that is exactly what the merge resolves.
1198
- */
1199
- export function publishActionableWarningsReport(cacheManager, cwd, report, opts = {}) {
1200
- const origin = opts.origin ?? "in-band";
1201
- // No TTL: "what is already on disk" is a question about ORDERING, not
1202
- // freshness, so an entry old enough to have expired for a CONSUMER still
1203
- // has to be merged into rather than clobbered.
1204
- const persisted = cacheManager.readCache("actionable-warnings", cwd, Number.MAX_SAFE_INTEGER)?.data;
1205
- const merged = mergeActionableWarningsReports({
1206
- persisted,
1207
- incoming: report,
1208
- origin,
1209
- getFileSeq: opts.getFileSeq,
1210
- dbg: opts.dbg,
1211
- });
1212
- writeActionableWarningsReport(cacheManager, cwd, merged.report);
1213
- // The history records what THIS publish OBSERVED, not what it carried: a
1214
- // merged-in entry is already on the NDJSON from the publish that produced
1215
- // it, and appending the merged report would duplicate every carried row on
1216
- // every subsequent turn.
1217
- appendActionableWarningsHistory(cwd, report);
1218
- if (merged.mergedFiles > 0) {
1219
- opts.dbg?.(`actionable_warnings: ${origin} publish merged ${merged.mergedFiles} persisted file entry/entries`);
1220
- }
1221
- // #2504 review round 7 (F5): the deferred origin's own loss is already
1222
- // recorded by {@link writeDeferredActionableWarningsReport}, which calls
1223
- // through here first. The IN-BAND origin has the same shape of loss --
1224
- // this turn's own publish carries a carried-forward deferred entry
1225
- // forward only while its file has not moved, and a file that moved
1226
- // between the deferral and this publish is dropped -- but nothing counted
1227
- // or traced it: only the benign, expected scope-guard spend (an entry
1228
- // whose carry-forward window already closed) got a dbg line. Same
1229
- // accounting, mirrored for the origin that was silent.
1230
- if (origin === "in-band" && merged.droppedFiles.length > 0) {
1231
- // #2504 review round 8 (S2): "changed" is only true on the fileSeq-
1232
- // mismatch path. A foreign sessionId (a resumed process, or a
1233
- // different session's write) drops the same carried-forward entries
1234
- // without any file having moved -- name the actual cause instead of
1235
- // asserting a change that may not have happened.
1236
- const cause = merged.droppedForSessionMismatch
1237
- ? "the publish crossed a session boundary (a different session's write, or a resumed process) before this turn's in-band publish could keep them"
1238
- : "changed before this turn's in-band publish could keep them";
1239
- incrementDegradationCount({
1240
- kind: "actionable-warnings-inband-superseded",
1241
- subject: `${path.resolve(cwd)}:inband-carry-superseded`,
1242
- reason: `${merged.droppedFiles.length} carried-forward deferred file entry/entries ${cause} (${merged.droppedFiles.slice(0, 3).join(", ")}${merged.droppedFiles.length > 3 ? ", ..." : ""}); their earlier findings are LOST on this channel rather than published against content that has since moved`,
1243
- });
1244
- opts.dbg?.(`actionable_warnings: in-band publish dropped ${merged.droppedFiles.length} superseded carried-forward file entry/entries (${merged.droppedFiles.slice(0, 3).join(", ")}${merged.droppedFiles.length > 3 ? ", ..." : ""})`);
1245
- }
1246
- return merged;
1247
- }
1248
- /** The earlier of two ISO stamps; either may be missing or unparseable. */
1249
- function olderStamp(a, b) {
1250
- if (a === undefined)
1251
- return b;
1252
- if (b === undefined)
1253
- return a;
1254
- const at = Date.parse(a);
1255
- const bt = Date.parse(b);
1256
- if (!Number.isFinite(at))
1257
- return b;
1258
- if (!Number.isFinite(bt))
1259
- return a;
1260
- return at <= bt ? a : b;
1261
- }
1262
- /** The later of two ISO stamps; either may be missing or unparseable. */
1263
- function newerStamp(a, b) {
1264
- const older = olderStamp(a, b);
1265
- if (a === undefined)
1266
- return b;
1267
- if (b === undefined)
1268
- return a;
1269
- return older === a ? b : a;
1270
- }
1271
- function minDefined(a, b) {
1272
- if (typeof a !== "number")
1273
- return b;
1274
- if (typeof b !== "number")
1275
- return a;
1276
- return Math.min(a, b);
1277
- }
1278
- function maxDefined(a, b) {
1279
- if (typeof a !== "number")
1280
- return b;
1281
- if (typeof b !== "number")
1282
- return a;
1283
- return Math.max(a, b);
1284
- }
1285
- /**
1286
- * Publish a DEFERRED off-hook report.
1287
- *
1288
- * The deferred fresh-pull loop is stamped with the ORIGINATING turn's
1289
- * turnIndex/projectSeq and may run for up to
1290
- * ACTIONABLE_WARNINGS_DEFERRED_BUDGET_MS -- many turns in a busy session. It
1291
- * must therefore neither overwrite a newer report nor be thrown away because
1292
- * one exists; see mergeActionableWarningsReports for why replace-or-discard
1293
- * could only ever discard. Per-file loss is the only loss left, and it is
1294
- * recorded rather than silent.
1295
- *
1296
- * A thin wrapper over the choke point since #2504 review round 5 (F1). It
1297
- * exists only to own the degradation record for the per-file loss -- the
1298
- * "declined the write" branches it used to carry are gone, because a publish
1299
- * that merges has nothing to decline.
1300
- */
1301
- export function writeDeferredActionableWarningsReport(args) {
1302
- const { mergedFiles, droppedFiles } = publishActionableWarningsReport(args.cacheManager, args.cwd, args.report, { origin: "deferred", getFileSeq: args.getFileSeq, dbg: args.dbg });
1303
- if (droppedFiles.length > 0) {
1304
- // Bounded and counted: one subject per project, a tally per occurrence.
1305
- incrementDegradationCount({
1306
- kind: "actionable-warnings-deferred-superseded",
1307
- subject: `${path.resolve(args.cwd)}:deferred-file-superseded`,
1308
- reason: `${droppedFiles.length} file(s) changed while the deferred LSP pull was reading them (${droppedFiles.slice(0, 3).join(", ")}${droppedFiles.length > 3 ? ", ..." : ""}); their warnings are LOST on this channel rather than published against content that has since moved. Every file that did NOT change was merged into the persisted report`,
1309
- });
1310
- args.dbg?.(`turn_end: deferred actionable-warnings dropped ${droppedFiles.length} superseded file entry/entries`);
1311
- }
1312
- args.dbg?.(`turn_end: deferred actionable-warnings merged ${mergedFiles} file entry/entries into the persisted report`);
1313
- return { mergedFiles, droppedFiles: droppedFiles.length };
1314
- }
1315
- export function getActionableWarningsHistoryPath(cwd) {
1316
- return path.join(getProjectDataDir(cwd), "actionable-warnings.jsonl");
1317
- }
1318
- /**
1319
- * Append every actionable warning from this turn to the project's rolling
1320
- * NDJSON history. Mirrors `appendCodeQualityWarningsHistory` so the two
1321
- * advisory families have the same shape of cross-turn persistence:
1322
- *
1323
- * - One line per warning (not per turn).
1324
- * - Carries the stable `aw:<hash>` id so callers can correlate the same
1325
- * warning across turns / sessions.
1326
- * - Captures suppression state at write time so historical analyses can
1327
- * reconstruct what the agent actually saw.
1328
- * - Captures action counts (and autoFixEligible counts) — the LSP code-
1329
- * action enrichment is the actionable-warnings-only signal; preserving
1330
- * it lets later analyses ask "which warnings ship with an autofix?".
1331
- *
1332
- * Skips the write entirely when no warnings exist — matching the code-
1333
- * quality history's no-op-on-empty behaviour and keeping the file from
1334
- * accumulating 0-warning noise.
1335
- */
1336
- export function appendActionableWarningsHistory(cwd, report) {
1337
- const entries = [];
1338
- for (const file of report.files) {
1339
- for (const warning of file.warnings) {
1340
- entries.push({
1341
- timestamp: report.generatedAt,
1342
- sessionId: report.sessionId,
1343
- turnIndex: report.turnIndex,
1344
- projectSeq: report.projectSeqEnd,
1345
- filePath: warning.filePath,
1346
- displayPath: warning.displayPath,
1347
- fileSeq: file.fileSeq,
1348
- line: warning.line,
1349
- column: warning.column,
1350
- severity: warning.severity,
1351
- tool: warning.tool,
1352
- source: warning.source,
1353
- rule: warning.rule,
1354
- code: warning.code,
1355
- message: warning.message,
1356
- fixKind: warning.fixKind,
1357
- autoFixAvailable: warning.autoFixAvailable,
1358
- actionCount: warning.actions.length,
1359
- autoFixEligibleActionCount: warning.actions.filter((action) => action.autoFixEligible).length,
1360
- suppressed: warning.suppressed,
1361
- suppressionReason: warning.suppressionReason,
1362
- origin: warning.origin,
1363
- warningId: warning.id,
1364
- });
1365
- }
1366
- }
1367
- if (entries.length === 0)
1368
- return;
1369
- const historyPath = getActionableWarningsHistoryPath(cwd);
1370
- try {
1371
- fs.mkdirSync(path.dirname(historyPath), { recursive: true });
1372
- fs.appendFileSync(historyPath, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf8");
1373
- }
1374
- catch {
1375
- // Non-fatal — history write failure must never surface to the agent.
1376
- }
1377
- }
1378
- export function checkActionableWarningsReportFresh(args) {
1379
- const reportProjectSeqEnd = args.report.projectSeqEnd;
1380
- if (typeof reportProjectSeqEnd !== "number") {
1381
- return {
1382
- fresh: false,
1383
- reason: "missing_project_seq",
1384
- currentProjectSeq: args.currentProjectSeq,
1385
- };
1386
- }
1387
- if (reportProjectSeqEnd !== args.currentProjectSeq) {
1388
- return {
1389
- fresh: false,
1390
- reason: "project_seq_mismatch",
1391
- reportProjectSeqEnd,
1392
- currentProjectSeq: args.currentProjectSeq,
1393
- };
1394
- }
1395
- if (args.getFileSeq) {
1396
- for (const file of args.report.files) {
1397
- if (typeof file.fileSeq !== "number")
1398
- continue;
1399
- const currentFileSeq = args.getFileSeq(file.filePath);
1400
- if (currentFileSeq !== file.fileSeq) {
1401
- return {
1402
- fresh: false,
1403
- reason: "file_seq_mismatch",
1404
- reportProjectSeqEnd,
1405
- currentProjectSeq: args.currentProjectSeq,
1406
- filePath: file.filePath,
1407
- reportFileSeq: file.fileSeq,
1408
- currentFileSeq,
1409
- };
1410
- }
1411
- }
1412
- }
1413
- return {
1414
- fresh: true,
1415
- reportProjectSeqEnd,
1416
- currentProjectSeq: args.currentProjectSeq,
1417
- };
1418
- }
1419
- export async function applyConservativeActionableWarningFixes(args) {
1420
- const summary = {
1421
- considered: 0,
1422
- applied: 0,
1423
- changedFiles: [],
1424
- skipped: [],
1425
- };
1426
- const changedFiles = new Set();
1427
- const appliedResults = [];
1428
- let failedCount = 0;
1429
- const lspService = getLSPService();
1430
- const maxFixes = Math.max(0, args.maxFixes ?? 5);
1431
- for (const file of args.report.files) {
1432
- if (summary.applied >= maxFixes)
1433
- break;
1434
- for (const warning of file.warnings) {
1435
- if (summary.applied >= maxFixes)
1436
- break;
1437
- if (warning.suppressed)
1438
- continue;
1439
- const eligibleActions = warning.actions.filter((action) => action.autoFixEligible);
1440
- if (eligibleActions.length !== 1) {
1441
- if (eligibleActions.length > 1)
1442
- summary.skipped.push({
1443
- id: warning.id,
1444
- reason: "multiple_eligible_actions",
1445
- });
1446
- continue;
1447
- }
1448
- summary.considered++;
1449
- const agreement = establishToolAgreement(warning.tool, args.cwd);
1450
- if (agreement.decision === "decline") {
1451
- recordDegradationOnce({
1452
- kind: "autofix-agreement-unavailable",
1453
- subject: agreement.subject,
1454
- reason: agreement.reason,
1455
- });
1456
- summary.skipped.push({
1457
- id: warning.id,
1458
- reason: "tool_agreement_unavailable",
1459
- });
1460
- continue;
1461
- }
1462
- if (!warning.line || !warning.column) {
1463
- summary.skipped.push({ id: warning.id, reason: "missing_position" });
1464
- continue;
1465
- }
1466
- if (!lspService.supportsLSP(warning.filePath)) {
1467
- summary.skipped.push({ id: warning.id, reason: "no_lsp" });
1468
- continue;
1469
- }
1470
- try {
1471
- const content = fs.existsSync(warning.filePath)
1472
- ? fs.readFileSync(warning.filePath, "utf-8")
1473
- : undefined;
1474
- if (content)
1475
- await lspService.openFile(warning.filePath, content);
1476
- const line = warning.line - 1;
1477
- const character = warning.column - 1;
1478
- const actions = await lspService.codeAction(warning.filePath, line, character, line, character);
1479
- const title = eligibleActions[0]?.title;
1480
- const selected = actions.find((action) => action.title === title);
1481
- if (!selected) {
1482
- summary.skipped.push({ id: warning.id, reason: "action_not_found" });
1483
- continue;
1484
- }
1485
- const safety = actionSafety(selected);
1486
- if (!safety.eligible) {
1487
- summary.skipped.push({
1488
- id: warning.id,
1489
- reason: safety.reason ?? "not_safe",
1490
- });
1491
- continue;
1492
- }
1493
- const edit = selected.edit;
1494
- const applied = await applyWorkspaceEdit(edit, args.cwd, args.mutationContext
1495
- ? {
1496
- mutationContext: {
1497
- ...args.mutationContext,
1498
- emitSummary: false,
1499
- },
1500
- }
1501
- : undefined);
1502
- appliedResults.push(applied);
1503
- for (const changedFile of applied.files)
1504
- changedFiles.add(changedFile);
1505
- summary.applied++;
1506
- }
1507
- catch (err) {
1508
- failedCount++;
1509
- const partial = err.appliedWorkspaceEdit;
1510
- if (partial) {
1511
- appliedResults.push(partial);
1512
- for (const changedFile of partial.files)
1513
- changedFiles.add(changedFile);
1514
- }
1515
- const message = err instanceof Error ? err.message : String(err);
1516
- args.dbg?.(`actionable_warnings_autofix failed for ${warning.id}: ${message}`);
1517
- summary.skipped.push({ id: warning.id, reason: "apply_failed" });
1518
- }
1519
- }
1520
- }
1521
- summary.changedFiles = [...changedFiles];
1522
- if (args.mutationContext &&
1523
- (summary.considered > 0 || appliedResults.length > 0)) {
1524
- recordLspMutationBatch(args.mutationContext, {
1525
- results: appliedResults,
1526
- considered: summary.considered,
1527
- completed: summary.applied,
1528
- failedCount,
1529
- status: failedCount > 0
1530
- ? "failed"
1531
- : appliedResults.length > 0
1532
- ? "success"
1533
- : "skipped",
1534
- bookkeep: false,
1535
- });
1536
- }
1537
- return summary;
1538
- }
1539
- /**
1540
- * The turn-end advisory for this report.
1541
- *
1542
- * `cwd` is REQUIRED (#2521): the advisory names where the report lives, and
1543
- * that location is `getProjectDataDir(cwd)`-resolved, not a constant. It was a
1544
- * hardcoded `.pi-lens/cache/actionable-warnings.json` — correct only for a
1545
- * project that already had a legacy `.pi-lens/` directory — so in every other
1546
- * project the agent's `cat` of the advised path failed while the report sat
1547
- * unread under `~/.pi-lens/projects/<slug>/cache/`. An OPTIONAL `cwd` would
1548
- * have kept that bug alive behind a default, so callers must pass it.
1549
- */
1550
- export function formatActionableWarningsAdvisory(report, cwd, host = "pi", filterBuiltReport) {
1551
- // The cache record is deliberately kept raw: lens_diagnostics mode=delta
1552
- // applies dispositions on read. The turn-end advisory is a separate
1553
- // delivery surface, so filter the report AFTER its builder and after the
1554
- // deferred merge (which is where LSP-enriched rows enter), then derive the
1555
- // summary from the same per-file rows before rendering.
1556
- const filtered = filterBuiltReport?.(report);
1557
- const advisoryReport = filtered
1558
- ? {
1559
- ...report,
1560
- files: filtered.files,
1561
- summary: summarizeReportFiles(filtered.files),
1562
- }
1563
- : report;
1564
- if (advisoryReport.summary.unsuppressed === 0)
1565
- return undefined;
1566
- const files = advisoryReport.files.filter((file) => file.warnings.some((warning) => !warning.suppressed));
1567
- const fileList = files
1568
- .slice(0, 5)
1569
- .map((file) => ` ${file.displayPath}: ${file.warnings.filter((warning) => !warning.suppressed).length}${file.reVerifyIncomplete ? " (re-verify incomplete)" : ""}`)
1570
- .join("\n");
1571
- const more = files.length > 5 ? `\n ... and ${files.length - 5} more file(s)` : "";
1572
- const safe = advisoryReport.summary.autoFixEligible > 0
1573
- ? ` ${advisoryReport.summary.autoFixEligible} appear to have conservative preferred quickfixes.`
1574
- : "";
1575
- // #1777: hint and info are style opinions, so say how much of the count is
1576
- // opinion. The line appears only when a quiet tier is actually present —
1577
- // an all-warning turn already says everything in the count above.
1578
- const byTier = advisoryReport.summary.byTier;
1579
- const quiet = byTier ? byTier.hint + byTier.info : 0;
1580
- const tierLine = quiet > 0
1581
- ? `${quiet} of those are hint/info tier — style opinions, worth fixing only while you are already in that code.`
1582
- : undefined;
1583
- // #2521: lead with the TOOL route. `lens_diagnostics mode=delta` reads the
1584
- // same cache this advisory describes, so an agent never needs to know the
1585
- // store's layout — which is exactly the knowledge the old hardcoded path
1586
- // got wrong. The resolved file stays as a named fallback for a human
1587
- // reading the transcript, rendered through the one helper that cannot
1588
- // disagree with the writer.
1589
- const reportPath = displayProjectDataPath(cwd, "cache", "actionable-warnings.json");
1590
- // #2535 F3: a known tool with no mapping on this host resolves to
1591
- // undefined — omit the instruction rather than naming a dead tool.
1592
- // `lens_diagnostics` is pinned available on both hosts, so this is
1593
- // defensive only.
1594
- const diagnosticsTool = resolveLensToolName("lens_diagnostics", host);
1595
- return [
1596
- `🟡 Fixable warnings introduced this turn: ${advisoryReport.summary.unsuppressed}.${safe}`,
1597
- filtered && filtered.suppressed > 0
1598
- ? `suppressed by disposition: ${filtered.suppressed} finding(s).`
1599
- : undefined,
1600
- tierLine,
1601
- diagnosticsTool
1602
- ? `Use ${diagnosticsTool} with mode=delta to inspect these warnings.`
1603
- : undefined,
1604
- fileList ? `Files:\n${fileList}${more}` : undefined,
1605
- "If continuing in these files, resolve warnings that are safe and relevant. Do not apply broad refactors unless requested.",
1606
- `Raw report (only if you need the JSON): ${reportPath}`,
1607
- ]
1608
- .filter(Boolean)
1609
- .join("\n");
1610
- }