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,1480 +0,0 @@
1
- /**
2
- * Post-write pipeline for pi-lens
3
- *
4
- * Extracted from index.ts tool_result handler.
5
- * Runs sequentially on every file write/edit:
6
- * 1. Secrets scan (blocking — early exit)
7
- * 2. Auto-format (Biome, Prettier, Ruff, gofmt, etc.)
8
- * 3. Auto-fix (Biome --write, Ruff --fix, ESLint --fix)
9
- * 4. LSP file sync (open/update in LSP servers)
10
- * 5. Dispatch lint (type errors, security rules)
11
- * 6. Test runner (run corresponding test file)
12
- * 7. Cascade diagnostics (other files with errors, LSP only)
13
- */
14
- import { createHash } from "node:crypto";
15
- import * as nodeFs from "node:fs";
16
- import * as nodeCrypto from "node:crypto";
17
- import * as path from "node:path";
18
- import { findNearestContaining, normalizeEphemeralMapKey, } from "./path-utils.js";
19
- import { recordFromDispatchDiagnostic, } from "./actionable-warnings.js";
20
- import { recordFromCodeQualityDiagnostic, } from "./code-quality-warnings.js";
21
- import { admitWidgetDiagnosticsWrite, recordDiagnostics, } from "./widget-state.js";
22
- import { getDiagnosticLogger } from "./diagnostic-logger.js";
23
- import { getDiagnosticTracker } from "./diagnostic-tracker.js";
24
- import { loadDispatchIntegration } from "./dispatch/lazy.js";
25
- import { toRunnerDisplayPath } from "./dispatch/runner-context.js";
26
- import { classifyProbeFailure, createAvailabilityChecker, createAvailabilityLatch, describeProbeEvidence, logAvailabilityDecision, startHostStallSampler, resolveAvailableOrInstall, resolveCommandArgsWithInstallFallback, resolveToolCommand, resolveToolCommandWithInstallFallback, } from "./dispatch/runners/utils/runner-helpers.js";
27
- import { findDetektConfig } from "./dispatch/runners/detekt.js";
28
- import { formatDiagnostics } from "./dispatch/utils/format-utils.js";
29
- import { detectFileKind, getFileKindLabel } from "./file-kinds.js";
30
- import { detectFileChangedAfterCommand, getProjectIgnoreMatcher, isExcludedDirName, } from "./file-utils.js";
31
- import { logLatency } from "./latency-logger.js";
32
- import { emitLensAnalysisComplete } from "./lens-events.js";
33
- import { publishFilesTouched } from "./bus-publish.js";
34
- import { publishDiagnostics, wasPreviouslyReportedDirty, } from "./diagnostics-publish.js";
35
- import { loadLspService } from "./lsp-lazy.js";
36
- import { clearGraphCache } from "./review-graph/builder.js";
37
- import { BoundedLruCache } from "./bounded-cache.js";
38
- import { RUNTIME_CONFIG } from "./runtime-config.js";
39
- import { getAmbientAbortSignal, safeSpawnAsync } from "./safe-spawn.js";
40
- import { probeToolAsync } from "./tool-probe.js";
41
- import { bounded } from "./deadline-utils.js";
42
- import { HOOK_WALL_BUDGET_MS } from "./hook-budgets.js";
43
- import { enabledAuxiliaryLspServerIds } from "./dispatch/auxiliary-lsp.js";
44
- import { incrementDegradationCount, recordDegradationOnce, } from "./degradation-ledger.js";
45
- import { establishToolAgreement } from "./tool-agreement.js";
46
- import { dropFindingsForMissingPaths } from "./advisory-provenance.js";
47
- import { getAutofixPolicyForFile, getPreferredAutofixTools, getRubocopCommand, hasBiomeConfig, hasDetektConfig, hasEslintConfig, hasGolangciConfig, hasKtfmtConfig, hasKtlintConfig, hasOxlintConfig, hasRubocopConfig, hasSqlfluffConfig, hasStylelintConfig, markdownlintConfigArgs, } from "./tool-policy.js";
48
- import { exceedsLspSyncLimits } from "./lsp/content-limits.js";
49
- const LSP_SPAWN_BUDGET_MS = RUNTIME_CONFIG.pipeline.lspSpawnBudgetMs;
50
- const AUTOFIX_CHANGED_FILE_SCAN_LIMIT = 5000;
51
- /**
52
- * Hard ceiling for the pre-dispatch LSP sync (`resyncLspFile`). The sync sends a
53
- * didChange/didOpen; that write can backpressure indefinitely when the language
54
- * server's stdin isn't being drained (a wedged/CPU-bound server), which would
55
- * hang the whole edit with no per-call bound — client acquisition is capped, but
56
- * the notify write is not. So the sync is abandoned after this budget (the edit
57
- * proceeds; the dispatch LSP runner, which has its own 30s cap, still tries).
58
- */
59
- function lspSyncBudgetMs() {
60
- const raw = Number(process.env.PI_LENS_LSP_SYNC_BUDGET_MS);
61
- return Number.isFinite(raw) && raw > 0 ? raw : 3000;
62
- }
63
- // Files stat'd between event-loop yields. The walk stays on the tool_result
64
- // hot path; yielding every N keeps its longest synchronous stretch well under
65
- // the <50ms hook-burst budget even at the AUTOFIX_CHANGED_FILE_SCAN_LIMIT cap.
66
- const SNAPSHOT_YIELD_EVERY = 500;
67
- // Scan one directory's entries into `snapshot`, pushing walkable subdirs onto
68
- // `stack`. Yields to the event loop every SNAPSHOT_YIELD_EVERY files (shared
69
- // `counter`) so a single huge directory can't hold the loop. Excluded/ignored
70
- // dirs are not descended; ignored/vanished files are skipped.
71
- async function snapshotDirInto(dir, ignoreMatcher, stack, snapshot, counter) {
72
- let entries;
73
- try {
74
- // Async (#1137): a synchronous per-directory read blocks the loop for the
75
- // whole stall on a slow cloud-backed (OneDrive) directory; fs.promises
76
- // keeps this tool_result-path walk off the event loop.
77
- entries = await nodeFs.promises.readdir(dir, { withFileTypes: true });
78
- }
79
- catch {
80
- return;
81
- }
82
- for (const entry of entries) {
83
- const fullPath = path.join(dir, entry.name);
84
- if (entry.isDirectory()) {
85
- if (!isExcludedDirName(entry.name) &&
86
- !ignoreMatcher.isIgnored(fullPath, true)) {
87
- stack.push(fullPath);
88
- }
89
- continue;
90
- }
91
- if (!entry.isFile())
92
- continue;
93
- if (ignoreMatcher.isIgnored(fullPath, false))
94
- continue;
95
- try {
96
- const stat = await nodeFs.promises.stat(fullPath);
97
- snapshot.set(path.resolve(fullPath), {
98
- mtimeMs: stat.mtimeMs,
99
- size: stat.size,
100
- });
101
- }
102
- catch {
103
- // ignore vanished files
104
- }
105
- if (++counter.n % SNAPSHOT_YIELD_EVERY === 0) {
106
- await new Promise((resolve) => setImmediate(resolve));
107
- }
108
- }
109
- }
110
- // Exported for the event-loop occupancy guard (#361/#368): an O(files) walk on
111
- // the tool_result autofix path, bounded by AUTOFIX_CHANGED_FILE_SCAN_LIMIT and
112
- // chunk-yielding every SNAPSHOT_YIELD_EVERY files so it never blocks the TUI.
113
- export async function snapshotProjectFiles(root) {
114
- const snapshot = new Map();
115
- const projectRoot = path.resolve(root);
116
- const ignoreMatcher = getProjectIgnoreMatcher(projectRoot);
117
- // #703: prime the tracked-files set once before the walk so a tracked file
118
- // matching a `.gitignore`/global pattern still shows up in the autofix
119
- // snapshot. Fail-open on no-git/spawn failure.
120
- await ignoreMatcher.ensureTrackedIndex();
121
- const stack = [projectRoot];
122
- const counter = { n: 0 };
123
- while (stack.length > 0 && snapshot.size < AUTOFIX_CHANGED_FILE_SCAN_LIMIT) {
124
- await snapshotDirInto(stack.pop(), ignoreMatcher, stack, snapshot, counter);
125
- }
126
- return snapshot;
127
- }
128
- async function diffProjectSnapshot(root, before) {
129
- const after = await snapshotProjectFiles(root);
130
- const changed = new Set();
131
- for (const [filePath, next] of after) {
132
- const prev = before.get(filePath);
133
- if (prev?.mtimeMs !== next.mtimeMs || prev?.size !== next.size) {
134
- changed.add(filePath);
135
- }
136
- }
137
- for (const filePath of before.keys()) {
138
- if (!after.has(filePath))
139
- changed.add(filePath);
140
- }
141
- return [...changed].sort((a, b) => a.localeCompare(b));
142
- }
143
- function createPhaseTracker(toolName, filePath) {
144
- const phases = [];
145
- return {
146
- start(name) {
147
- phases.push({ name, startTime: Date.now(), ended: false });
148
- },
149
- end(name, metadata) {
150
- const p = phases.find((x) => x.name === name && !x.ended);
151
- if (p) {
152
- p.ended = true;
153
- logLatency({
154
- type: "phase",
155
- toolName,
156
- filePath,
157
- phase: name,
158
- durationMs: Date.now() - p.startTime,
159
- metadata,
160
- });
161
- }
162
- },
163
- };
164
- }
165
- // --- ESLint autofix helpers ---
166
- export { hasEslintConfig, hasSqlfluffConfig, hasStylelintConfig };
167
- /**
168
- * eslint autofix availability, per cwd + PATH. The verdict is owned by the
169
- * shared availability latch (#1494): a `missing` / `non-installable` outcome
170
- * sticks for the session, a timeout expires on a cooldown and is re-probed, so
171
- * one stalled probe cannot silently stop autofixing a project's JS/TS.
172
- */
173
- const ESLINT_FIX_PROBE_BUDGET_MS = 5000;
174
- const _eslintCache = new BoundedLruCache(32);
175
- /**
176
- * Run eslint --fix on a file. Runs a single spawn and diffs the file before/after,
177
- * same idiom as the other autofix helpers below. Exit code 1 (unfixable problems
178
- * remain) is allowed because fixes may still have been applied; only exit code 2
179
- * (config/fatal error) is treated as failure.
180
- * Returns 1 if the file changed, 0 if ESLint is not configured / not available /
181
- * made no changes.
182
- */
183
- async function tryEslintFix(filePath, cwd) {
184
- const userHasConfig = hasEslintConfig(cwd);
185
- if (!userHasConfig)
186
- return 0;
187
- // PATH is part of command resolution; include it so an install or PATH
188
- // refresh cannot leave a negative eslint result live for the session.
189
- const cacheKey = `${path.resolve(cwd)}|${process.env.PATH ?? ""}`;
190
- let cached = _eslintCache.get(cacheKey);
191
- if (!cached) {
192
- cached = { latch: createAvailabilityLatch(), bin: null };
193
- _eslintCache.set(cacheKey, cached);
194
- }
195
- if (cached.latch.read() === null) {
196
- const candidate = resolveToolCommand(cwd, "eslint") ?? "eslint";
197
- // The budget is enforced by a HOST-side timer, so a stalled event loop
198
- // expires it while eslint is still healthy. Measure the overlapping stall
199
- // and hand it to the classifier, exactly like the runner path: without it a
200
- // host-stall timeout is re-caused as `probe-timeout` and gets the escalating
201
- // 30s→300s cooldown instead of the 5s host-stall one (#1467).
202
- const stallSampler = startHostStallSampler();
203
- const startedAt = Date.now();
204
- let check;
205
- let hostStallMs;
206
- try {
207
- check = await safeSpawnAsync(candidate, ["--version"], {
208
- timeout: ESLINT_FIX_PROBE_BUDGET_MS,
209
- cwd,
210
- });
211
- }
212
- finally {
213
- hostStallMs = stallSampler.stop();
214
- }
215
- const elapsedMs = Date.now() - startedAt;
216
- const decisionCwd = path.resolve(cwd);
217
- if (!check.error && check.status === 0) {
218
- cached.bin = candidate;
219
- cached.latch.noteAvailable();
220
- logAvailabilityDecision({
221
- tool: "eslint",
222
- verdict: "available",
223
- outcome: "success",
224
- cause: "ok",
225
- elapsedMs,
226
- latched: true,
227
- hostStallMs,
228
- budgetMs: ESLINT_FIX_PROBE_BUDGET_MS,
229
- classifiedBy: "probe",
230
- evidence: describeProbeEvidence(check, "eslint"),
231
- }, decisionCwd);
232
- }
233
- else {
234
- cached.bin = null;
235
- const { outcome, cause, evidence } = classifyProbeFailure(check, {
236
- hostStallMs,
237
- command: "eslint",
238
- });
239
- const retryAfterMs = cached.latch.noteUnavailable(outcome, cause);
240
- logAvailabilityDecision({
241
- tool: "eslint",
242
- verdict: "unavailable",
243
- outcome,
244
- cause,
245
- elapsedMs,
246
- latched: retryAfterMs === 0,
247
- hostStallMs,
248
- ...(retryAfterMs > 0 && { retryAfterMs }),
249
- budgetMs: ESLINT_FIX_PROBE_BUDGET_MS,
250
- classifiedBy: "probe",
251
- evidence,
252
- }, decisionCwd);
253
- }
254
- }
255
- if (cached.latch.read() !== true || !cached.bin)
256
- return 0;
257
- const cmd = cached.bin;
258
- return detectFileChangedAfterCommand(filePath, cmd, ["--fix", "--no-error-on-unmatched-pattern", filePath], cwd, [1]);
259
- }
260
- async function tryStylelintFix(filePath, cwd) {
261
- const cmd = await resolveToolCommandWithInstallFallback(cwd, "stylelint");
262
- if (!cmd)
263
- return 0;
264
- return detectFileChangedAfterCommand(filePath, cmd, ["--fix", "--allow-empty-input", filePath], cwd, [2]);
265
- }
266
- async function trySqlfluffFix(filePath, cwd) {
267
- const cmd = await resolveToolCommandWithInstallFallback(cwd, "sqlfluff");
268
- if (!cmd)
269
- return 0;
270
- const args = ["fix", "--force", filePath];
271
- if (!hasSqlfluffConfig(cwd)) {
272
- args.splice(2, 0, "--dialect", "ansi");
273
- }
274
- return detectFileChangedAfterCommand(filePath, cmd, args, cwd);
275
- }
276
- async function tryRubocopFix(filePath, cwd) {
277
- const resolved = await resolveCommandArgsWithInstallFallback(getRubocopCommand(cwd), "rubocop", cwd, ["--version"], 10000);
278
- if (!resolved)
279
- return 0;
280
- return detectFileChangedAfterCommand(filePath, resolved.cmd, [...resolved.args, "-a", "--no-color", filePath], cwd, [1]);
281
- }
282
- async function tryKtlintFix(filePath, cwd) {
283
- const cmd = await resolveToolCommandWithInstallFallback(cwd, "ktlint");
284
- if (!cmd)
285
- return 0;
286
- return detectFileChangedAfterCommand(filePath, cmd, ["-F", filePath], cwd, [1]);
287
- }
288
- // golangci-lint/detekt/ktfmt have no TOOL_COMMAND_SPEC; resolve via availability
289
- // checkers like their runners do.
290
- const golangciAutofixChecker = createAvailabilityChecker("golangci-lint", ".exe");
291
- const detektAutofixChecker = createAvailabilityChecker("detekt", ".bat");
292
- const ktfmtAutofixChecker = createAvailabilityChecker("ktfmt", ".bat");
293
- async function tryKtfmtFix(filePath, cwd) {
294
- // Config-first: the autofix policy only reaches here when the project opted
295
- // into ktfmt, so resolveAvailableOrInstall honors that gate. ktfmt writes the
296
- // formatted file in place and exits 0; treat any byte change as the fix.
297
- const cmd = await resolveAvailableOrInstall(ktfmtAutofixChecker, "ktfmt", cwd);
298
- if (!cmd)
299
- return 0;
300
- const absPath = path.resolve(cwd, filePath);
301
- return detectFileChangedAfterCommand(filePath, cmd, [absPath], cwd, [0]);
302
- }
303
- async function tryGolangciLintFix(filePath, cwd) {
304
- // Config-first: the autofix policy only reaches here when a .golangci.* config
305
- // exists. resolveAvailableOrInstall honors that gate (won't auto-install a
306
- // config-first tool). golangci-lint exits non-zero when issues remain after
307
- // --fix, so allow its issue-found codes.
308
- const cmd = await resolveAvailableOrInstall(golangciAutofixChecker, "golangci-lint", cwd);
309
- if (!cmd)
310
- return 0;
311
- return detectFileChangedAfterCommand(filePath, cmd, ["run", "--fix", filePath], cwd, [1, 7]);
312
- }
313
- async function tryDetektFix(filePath, cwd) {
314
- const configPath = findDetektConfig(cwd);
315
- if (!configPath)
316
- return 0;
317
- if (!(await detektAutofixChecker.isAvailableAsync(cwd)))
318
- return 0;
319
- const cmd = detektAutofixChecker.getCommand(cwd);
320
- if (!cmd)
321
- return 0;
322
- const absPath = path.resolve(cwd, filePath);
323
- return detectFileChangedAfterCommand(filePath, cmd, ["--auto-correct", "--input", absPath, "--config", configPath], cwd, [1, 2]);
324
- }
325
- async function tryMarkdownlintFix(filePath, cwd) {
326
- const cmd = await resolveToolCommandWithInstallFallback(cwd, "markdownlint");
327
- if (!cmd)
328
- return 0;
329
- // Shared config-args seam (#1247): the lint runner consumes the same
330
- // builder, so the bare --fix here can never fall back to markdownlint's
331
- // default all-rules-on config again (the whole-file CHANGELOG/AGENTS
332
- // reformat + `#946` → `# 946` corruption).
333
- // markdownlint-cli2 --fix exits non-zero when unfixable violations remain.
334
- return detectFileChangedAfterCommand(filePath, cmd, [...markdownlintConfigArgs(cwd), "--fix", filePath], cwd, [1]);
335
- }
336
- async function tryOxlintFix(filePath, cwd) {
337
- const cmd = await resolveToolCommandWithInstallFallback(cwd, "oxlint");
338
- if (!cmd)
339
- return 0;
340
- return detectFileChangedAfterCommand(filePath, cmd, ["--fix", filePath], cwd, [1]);
341
- }
342
- async function tryRustClippyFix(filePath) {
343
- const check = await probeToolAsync("cargo", ["--version"], { timeout: 5000 });
344
- if (check.error || check.status !== 0)
345
- return [];
346
- const cargoDir = findNearestContaining(path.dirname(path.resolve(filePath)), [
347
- "Cargo.toml",
348
- ]);
349
- if (!cargoDir)
350
- return [];
351
- const before = await snapshotProjectFiles(cargoDir);
352
- const result = await safeSpawnAsync("cargo", ["clippy", "--fix", "--allow-dirty", "--allow-staged", "-q"], { timeout: 30000, cwd: cargoDir });
353
- if (result.error || result.status !== 0)
354
- return [];
355
- return diffProjectSnapshot(cargoDir, before);
356
- }
357
- async function tryDartFix(filePath) {
358
- const check = await probeToolAsync("dart", ["--version"], { timeout: 5000 });
359
- if (check.error || check.status !== 0)
360
- return [];
361
- const pubspecDir = findNearestContaining(path.dirname(path.resolve(filePath)), ["pubspec.yaml"]);
362
- if (!pubspecDir)
363
- return [];
364
- const before = await snapshotProjectFiles(pubspecDir);
365
- const result = await safeSpawnAsync("dart", ["fix", "--apply"], {
366
- timeout: 30000,
367
- cwd: pubspecDir,
368
- });
369
- if (result.error || result.status !== 0)
370
- return [];
371
- return diffProjectSnapshot(pubspecDir, before);
372
- }
373
- // --- Pipeline phase helpers ---
374
- export async function runAutofix(filePath, cwd, getFlag, dbg, deps, getFlagSource) {
375
- const { biomeClient, ruffClient, fixedThisTurn } = deps;
376
- const noAutofix = getFlag("no-autofix", filePath);
377
- let fixedCount = 0;
378
- const autofixTools = [];
379
- const attemptedTools = [];
380
- const changedFiles = new Set();
381
- const markTargetChanged = () => changedFiles.add(path.resolve(filePath));
382
- let needsContentRefresh = false;
383
- if (fixedThisTurn.has(filePath)) {
384
- dbg(`autofix: skipped for ${filePath} (already fixed this turn)`);
385
- return {
386
- fixedCount,
387
- autofixTools,
388
- attemptedTools,
389
- changedFiles: [],
390
- needsContentRefresh,
391
- skipReason: "already_fixed_this_turn",
392
- };
393
- }
394
- if (noAutofix) {
395
- const source = getFlagSource?.("no-autofix", filePath);
396
- dbg(`autofix: skipped for ${filePath} (--no-autofix${source ? `, source=${source}` : ""})`);
397
- return {
398
- fixedCount,
399
- autofixTools,
400
- attemptedTools,
401
- changedFiles: [],
402
- needsContentRefresh,
403
- skipReason: "disabled_by_flag",
404
- };
405
- }
406
- const autofixContext = {
407
- hasEslintConfig: hasEslintConfig(cwd),
408
- hasStylelintConfig: hasStylelintConfig(cwd),
409
- hasSqlfluffConfig: hasSqlfluffConfig(cwd),
410
- hasRubocopConfig: hasRubocopConfig(cwd),
411
- hasBiomeConfig: hasBiomeConfig(cwd),
412
- hasGolangciConfig: hasGolangciConfig(cwd),
413
- hasDetektConfig: hasDetektConfig(cwd),
414
- hasKtfmtConfig: hasKtfmtConfig(cwd),
415
- hasKtlintConfig: hasKtlintConfig(cwd),
416
- hasOxlintConfig: hasOxlintConfig(cwd),
417
- };
418
- const autofixPolicy = getAutofixPolicyForFile(filePath, autofixContext);
419
- const preferredAutofixTools = autofixPolicy?.safe
420
- ? getPreferredAutofixTools(filePath, autofixContext)
421
- : [];
422
- dbg(`autofix: policy for ${filePath} -> ${autofixPolicy?.defaultTool ?? "none"} ` +
423
- `(preferred: ${preferredAutofixTools.join(",") || "none"}, gate: ${autofixPolicy?.gate ?? "none"}, safe: ${autofixPolicy?.safe ? "yes" : "no"})`);
424
- if (!autofixPolicy) {
425
- dbg(`autofix: no policy for ${filePath}`);
426
- return {
427
- fixedCount,
428
- autofixTools,
429
- attemptedTools,
430
- changedFiles: [],
431
- needsContentRefresh,
432
- skipReason: "no_policy",
433
- };
434
- }
435
- if (!autofixPolicy.safe || preferredAutofixTools.length === 0) {
436
- dbg(`autofix: no safe preferred tools for ${filePath}`);
437
- return {
438
- fixedCount,
439
- autofixTools,
440
- attemptedTools,
441
- changedFiles: [],
442
- needsContentRefresh,
443
- skipReason: "no_safe_tools",
444
- };
445
- }
446
- for (const toolName of preferredAutofixTools) {
447
- attemptedTools.push(toolName);
448
- const agreement = establishToolAgreement(toolName, cwd);
449
- if (agreement.decision === "decline") {
450
- const reason = agreement.reason;
451
- dbg(`autofix: ${toolName} declined (${reason})`);
452
- recordDegradationOnce({
453
- kind: "autofix-agreement-unavailable",
454
- subject: agreement.subject,
455
- reason,
456
- });
457
- continue;
458
- }
459
- if (toolName === "ruff") {
460
- const ruffReady = ruffClient.isPythonFile(filePath)
461
- ? await ruffClient.ensureAvailable()
462
- : false;
463
- if (!ruffReady) {
464
- dbg(`autofix: ruff unavailable for ${filePath}`);
465
- continue;
466
- }
467
- const result = await ruffClient.fixFileAsync(filePath, cwd);
468
- if (result.success && result.fixed > 0) {
469
- fixedCount += result.fixed;
470
- autofixTools.push(`ruff:${result.fixed}`);
471
- fixedThisTurn.add(filePath);
472
- markTargetChanged();
473
- dbg(`autofix: ruff fixed ${result.fixed} issue(s) in ${filePath}`);
474
- needsContentRefresh = true;
475
- }
476
- continue;
477
- }
478
- if (toolName === "biome") {
479
- const biomeReady = biomeClient.isSupportedFile(filePath)
480
- ? await biomeClient.ensureAvailable()
481
- : false;
482
- if (!biomeReady) {
483
- dbg(`autofix: biome unavailable or unsupported for ${filePath}`);
484
- continue;
485
- }
486
- const result = await biomeClient.fixFileAsync(filePath, cwd);
487
- if (result.success && result.fixed > 0) {
488
- fixedCount += result.fixed;
489
- autofixTools.push(`biome:${result.fixed}`);
490
- fixedThisTurn.add(filePath);
491
- markTargetChanged();
492
- dbg(`autofix: biome fixed ${result.fixed} issue(s) in ${filePath}`);
493
- needsContentRefresh = true;
494
- }
495
- continue;
496
- }
497
- if (toolName === "eslint") {
498
- const eslintFixed = await tryEslintFix(filePath, cwd);
499
- if (eslintFixed > 0) {
500
- fixedCount += eslintFixed;
501
- autofixTools.push(`eslint:${eslintFixed}`);
502
- fixedThisTurn.add(filePath);
503
- markTargetChanged();
504
- dbg(`autofix: eslint fixed ${eslintFixed} issue(s) in ${filePath}`);
505
- needsContentRefresh = true;
506
- }
507
- continue;
508
- }
509
- if (toolName === "stylelint") {
510
- const stylelintFixed = await tryStylelintFix(filePath, cwd);
511
- if (stylelintFixed > 0) {
512
- fixedCount += stylelintFixed;
513
- autofixTools.push(`stylelint:${stylelintFixed}`);
514
- fixedThisTurn.add(filePath);
515
- markTargetChanged();
516
- dbg(`autofix: stylelint fixed ${stylelintFixed} issue(s) in ${filePath}`);
517
- needsContentRefresh = true;
518
- }
519
- continue;
520
- }
521
- if (toolName === "sqlfluff") {
522
- const sqlfluffFixed = await trySqlfluffFix(filePath, cwd);
523
- if (sqlfluffFixed > 0) {
524
- fixedCount += sqlfluffFixed;
525
- autofixTools.push(`sqlfluff:${sqlfluffFixed}`);
526
- fixedThisTurn.add(filePath);
527
- markTargetChanged();
528
- dbg(`autofix: sqlfluff fixed ${sqlfluffFixed} issue(s) in ${filePath}`);
529
- needsContentRefresh = true;
530
- }
531
- continue;
532
- }
533
- if (toolName === "rubocop") {
534
- const rubocopFixed = await tryRubocopFix(filePath, cwd);
535
- if (rubocopFixed > 0) {
536
- fixedCount += rubocopFixed;
537
- autofixTools.push(`rubocop:${rubocopFixed}`);
538
- fixedThisTurn.add(filePath);
539
- markTargetChanged();
540
- dbg(`autofix: rubocop fixed ${rubocopFixed} issue(s) in ${filePath}`);
541
- needsContentRefresh = true;
542
- }
543
- continue;
544
- }
545
- if (toolName === "ktlint") {
546
- const ktlintFixed = await tryKtlintFix(filePath, cwd);
547
- if (ktlintFixed > 0) {
548
- fixedCount += ktlintFixed;
549
- autofixTools.push(`ktlint:${ktlintFixed}`);
550
- fixedThisTurn.add(filePath);
551
- markTargetChanged();
552
- dbg(`autofix: ktlint fixed ${ktlintFixed} issue(s) in ${filePath}`);
553
- needsContentRefresh = true;
554
- }
555
- continue;
556
- }
557
- if (toolName === "rust-clippy") {
558
- const clippyChangedFiles = await tryRustClippyFix(filePath);
559
- if (clippyChangedFiles.length > 0) {
560
- fixedCount += clippyChangedFiles.length;
561
- autofixTools.push(`rust-clippy:${clippyChangedFiles.length}`);
562
- fixedThisTurn.add(filePath);
563
- for (const changedFile of clippyChangedFiles)
564
- changedFiles.add(changedFile);
565
- dbg(`autofix: rust-clippy changed ${clippyChangedFiles.length} file(s) from ${filePath}`);
566
- needsContentRefresh = true;
567
- }
568
- continue;
569
- }
570
- if (toolName === "dart-analyze") {
571
- const dartChangedFiles = await tryDartFix(filePath);
572
- if (dartChangedFiles.length > 0) {
573
- fixedCount += dartChangedFiles.length;
574
- autofixTools.push(`dart-analyze:${dartChangedFiles.length}`);
575
- fixedThisTurn.add(filePath);
576
- for (const changedFile of dartChangedFiles)
577
- changedFiles.add(changedFile);
578
- dbg(`autofix: dart fix changed ${dartChangedFiles.length} file(s) from ${filePath}`);
579
- needsContentRefresh = true;
580
- }
581
- continue;
582
- }
583
- if (toolName === "golangci-lint") {
584
- const fixed = await tryGolangciLintFix(filePath, cwd);
585
- if (fixed > 0) {
586
- fixedCount += fixed;
587
- autofixTools.push(`golangci-lint:${fixed}`);
588
- fixedThisTurn.add(filePath);
589
- markTargetChanged();
590
- dbg(`autofix: golangci-lint fixed ${filePath}`);
591
- needsContentRefresh = true;
592
- }
593
- continue;
594
- }
595
- if (toolName === "detekt") {
596
- const fixed = await tryDetektFix(filePath, cwd);
597
- if (fixed > 0) {
598
- fixedCount += fixed;
599
- autofixTools.push(`detekt:${fixed}`);
600
- fixedThisTurn.add(filePath);
601
- markTargetChanged();
602
- dbg(`autofix: detekt --auto-correct fixed ${filePath}`);
603
- needsContentRefresh = true;
604
- }
605
- continue;
606
- }
607
- if (toolName === "ktfmt") {
608
- const fixed = await tryKtfmtFix(filePath, cwd);
609
- if (fixed > 0) {
610
- fixedCount += fixed;
611
- autofixTools.push(`ktfmt:${fixed}`);
612
- fixedThisTurn.add(filePath);
613
- markTargetChanged();
614
- dbg(`autofix: ktfmt formatted ${filePath}`);
615
- needsContentRefresh = true;
616
- }
617
- continue;
618
- }
619
- if (toolName === "markdownlint") {
620
- const fixed = await tryMarkdownlintFix(filePath, cwd);
621
- if (fixed > 0) {
622
- fixedCount += fixed;
623
- autofixTools.push(`markdownlint:${fixed}`);
624
- fixedThisTurn.add(filePath);
625
- markTargetChanged();
626
- dbg(`autofix: markdownlint --fix fixed ${filePath}`);
627
- needsContentRefresh = true;
628
- }
629
- continue;
630
- }
631
- if (toolName === "oxlint") {
632
- const fixed = await tryOxlintFix(filePath, cwd);
633
- if (fixed > 0) {
634
- fixedCount += fixed;
635
- autofixTools.push(`oxlint:${fixed}`);
636
- fixedThisTurn.add(filePath);
637
- markTargetChanged();
638
- dbg(`autofix: oxlint --fix fixed ${filePath}`);
639
- needsContentRefresh = true;
640
- }
641
- }
642
- }
643
- if (attemptedTools.length > 0 && autofixTools.length === 0) {
644
- dbg(`autofix: attempted ${attemptedTools.join(",")} for ${filePath}, but no fixes were applied`);
645
- }
646
- return {
647
- fixedCount,
648
- autofixTools,
649
- attemptedTools,
650
- changedFiles: [...changedFiles],
651
- needsContentRefresh,
652
- };
653
- }
654
- export async function resyncLspFile(filePath, fileContent, needsContentRefresh, lspSyncCompleted, getFlag, dbg) {
655
- if (getFlag("no-lsp"))
656
- return;
657
- if (!needsContentRefresh && lspSyncCompleted)
658
- return;
659
- // #3405 r2: the bound is `clients/lsp/content-limits.ts` now, shared with the
660
- // dispatch runner and the didSave payload. THIS call stays: a file past the
661
- // bound is not synced to the server AT ALL, which is a stricter policy than
662
- // the save seam's (drop the redundant text, still send the save) and the one
663
- // a post-write sync owes — removing it would start writing whole-file
664
- // didOpen frames for files this pipeline has always refused.
665
- const limitCheck = exceedsLspSyncLimits(fileContent);
666
- if (limitCheck.tooLarge)
667
- return;
668
- try {
669
- const lspService = (await loadLspService()).getLSPService();
670
- if (lspService.supportsLSP(filePath)) {
671
- // Push the final post-format/post-fix content through touchFile (not the
672
- // bare openFile) so it registers in the touch-debounce map via
673
- // markTouched. The dispatch-lsp-runner's touchFile fires ~80ms later with
674
- // identical content; registering this push makes its shouldSkipNotify
675
- // return true, so it reuses the diagnostics THIS push triggers instead of
676
- // re-clearing the cache and forcing the server to recompute from scratch —
677
- // the dominant per-edit LSP latency (#203). diagnostics:"none" keeps this
678
- // call non-blocking; the dispatch runner owns the diagnostics wait. We let
679
- // the cache clear (no preserveDiagnostics) so the wait resolves on fresh,
680
- // correctly-positioned diagnostics rather than stale pre-edit ones — the
681
- // didChange triggers a server recompute regardless of cache preservation.
682
- //
683
- // The touch is client-wait-capped, but its didChange/didOpen *write* can
684
- // backpressure forever on a wedged server (stdin not drained), which would
685
- // hang the whole edit with no bound and — until this — no log. Race it
686
- // against a hard budget + the turn's abort signal (Escape): whichever wins,
687
- // the edit proceeds. A wedged server no longer parks the pipeline.
688
- const budgetMs = lspSyncBudgetMs();
689
- const abort = getAmbientAbortSignal();
690
- if (abort?.aborted)
691
- return;
692
- const startedAt = Date.now();
693
- const touch = lspService
694
- .touchFile(filePath, fileContent, {
695
- diagnostics: "none",
696
- source: "lsp_sync",
697
- clientScope: "primary",
698
- maxClientWaitMs: LSP_SPAWN_BUDGET_MS,
699
- // #3405: pi-lens just wrote these bytes to disk, so this is the one
700
- // touch that is a save. A save-triggered server (Expert recompiles
701
- // the project on didSave and on nothing else) publishes for the edit
702
- // only because of this flag.
703
- saved: true,
704
- })
705
- .then(() => "done")
706
- .catch((err) => {
707
- dbg(`LSP resync after autofix error: ${err}`);
708
- return "done";
709
- });
710
- // #2540: Kick off auxiliary server acquisition concurrently and unawaited
711
- // with the ambient turn signal so auxiliary warmup overlaps with the primary
712
- // server during resync. Leaves the single-occupancy deferred slot (#2509)
713
- // free for actionable-warnings while Escape mid-turn abandons the wait.
714
- //
715
- // #2582 F3 — the #1766 F3 invariant re-opened for a different method.
716
- // #2540 placed this kick-off BEFORE the primary touch, inside
717
- // resyncLspFile's swallow-all catch, so anything that threw out of
718
- // auxiliary acquisition abandoned the PRIMARY sync before it started:
719
- // no touchFile, and no lsp_sync_abandoned record for the stall.
720
- // Auxiliary warmup is a best-effort OVERLAP, never a precondition:
721
- // the primary touch is already in flight above, and the async IIFE
722
- // turns a synchronous throw out of the call into a rejection so
723
- // `.catch` covers both failure directions. Order and containment are
724
- // the invariant; do not hoist this back above the touch.
725
- const auxServerIds = enabledAuxiliaryLspServerIds(getFlag);
726
- if (auxServerIds.length > 0) {
727
- void (async () => lspService.getAuxiliaryClientsForFile(filePath, new Set(auxServerIds), undefined, LSP_SPAWN_BUDGET_MS, abort, "tool_result_edit"))().catch(() => { });
728
- }
729
- // #2523 slice 2: this was `combineAbortSignals(abort,
730
- // AbortSignal.timeout(budgetMs))` fed into a hand-rolled
731
- // `Promise.race` — a private fifth copy of "deadline AND signal".
732
- // `bounded()` is the same thing with the abandonment recorded, and
733
- // without the composite signal that #2530 review F4 measured
734
- // accumulating on the SOURCE.
735
- //
736
- // `touch` never rejects (it catches above) and never resolves
737
- // `undefined`, so `undefined` here means exactly "a bound fired".
738
- const outcome = await bounded(touch, {
739
- ms: budgetMs,
740
- // The ambient turn signal: set for the whole tool_result path and
741
- // absent only in a bare unit harness, where the wall budget is
742
- // still live (`bounded()` reads a missing signal as one that
743
- // never aborts).
744
- signal: abort,
745
- hook: "tool_result_edit",
746
- label: "resyncLspFile",
747
- });
748
- if (outcome === undefined) {
749
- // Abandon the still-pending write; the edit continues. Log it so this
750
- // stall — previously an invisible hang — is queryable in latency.log.
751
- //
752
- // #1766: a resync deadline can expire while the target server's FIRST
753
- // spawn is still in flight (cold spawn > budget). That is not the same
754
- // state as a running server that stalled on a write — the server the
755
- // old wording blamed as "slow/wedged" did not exist yet. Distinguish
756
- // the two via a fresh, synchronous inFlight lookup so the record keeps
757
- // the discriminating identity (which server, which lifecycle state).
758
- // #2592: the `typeof lspService.isSpawnInFlight === "function"`
759
- // hedge that used to wrap this call is GONE. It existed for one
760
- // reason — 19 hand-rolled `getLSPService` doubles that lacked the
761
- // method, whose TypeError would have landed in the catch below and
762
- // suppressed this record entirely (#1766 F3) — and that population
763
- // is now zero: every double is seeded from
764
- // `makeLspServiceDouble`, `tests/support/lsp-double-baseline.json`
765
- // is `{}`, and `tests/config/lsp-service-double-sweep.test.ts` reds
766
- // on a fresh hand-rolled one. The real `LSPService` has always had
767
- // the method (clients/lsp/index.ts), so the fallback was a test
768
- // shape leaking into production — AGENTS.md shape 7.
769
- const spawnInFlight = !abort?.aborted && lspService.isSpawnInFlight(filePath);
770
- const reason = abort?.aborted
771
- ? "aborted"
772
- : spawnInFlight
773
- ? "spawn-in-flight"
774
- : "timeout";
775
- logLatency({
776
- type: "phase",
777
- phase: "lsp_sync_abandoned",
778
- filePath,
779
- durationMs: Date.now() - startedAt,
780
- metadata: {
781
- source: "lsp_sync",
782
- reason,
783
- budgetMs,
784
- },
785
- });
786
- const cause = abort?.aborted
787
- ? "aborted (Escape)"
788
- : spawnInFlight
789
- ? `timed out after ${budgetMs}ms; reason: spawn-in-flight (server still cold-spawning)`
790
- : `timed out after ${budgetMs}ms; server slow/wedged`;
791
- dbg(`LSP resync ${cause} for ${filePath}`);
792
- }
793
- }
794
- }
795
- catch (err) {
796
- dbg(`LSP resync after autofix error: ${err}`);
797
- }
798
- }
799
- /** Maps a dispatch `Diagnostic` (clients/dispatch/types.ts) to the #502 bus payload shape. */
800
- function toPilensDiagnosticEntry(d) {
801
- const entry = {
802
- severity: d.severity,
803
- message: d.message,
804
- tool: d.tool,
805
- };
806
- if (d.rule !== undefined)
807
- entry.ruleId = d.rule;
808
- if (d.line !== undefined)
809
- entry.line = d.line;
810
- if (d.column !== undefined)
811
- entry.col = d.column;
812
- if (d.fixable !== undefined)
813
- entry.fixable = d.fixable;
814
- return entry;
815
- }
816
- function buildAllClearOutput(_dispatchResult, elapsed, filePath) {
817
- const kind = detectFileKind(filePath);
818
- const langLabel = kind ? getFileKindLabel(kind) : path.extname(filePath);
819
- const parts = [];
820
- if (kind) {
821
- parts.push(`${langLabel} clean`);
822
- }
823
- parts.push(`${elapsed}ms`);
824
- return `checkmark ${parts.join(" · ")}`.replace("checkmark", "\u2713");
825
- }
826
- export async function runFormatPhase(filePath, getFormatService, dbg, signal, budgetMs = HOOK_WALL_BUDGET_MS.tool_result_edit, hook = "tool_result_edit") {
827
- let formatChanged = false;
828
- let formattersUsed = [];
829
- const formatFailures = [];
830
- const formatUnavailable = [];
831
- let fileContent;
832
- const formatService = getFormatService();
833
- try {
834
- formatService.recordRead(filePath);
835
- const result = await formatService.formatFile(filePath, {
836
- signal,
837
- budgetMs,
838
- hook,
839
- });
840
- // An unavailable tool is NOT a formatter that ran (#2413): keep it out of
841
- // `formattersUsed` (which drives change bookkeeping / turn summaries) and
842
- // out of `formatFailures` (which requeues). Record it once, distinctly.
843
- for (const f of result.formatters) {
844
- if (f.outcome !== "unavailable")
845
- continue;
846
- if (f.error?.includes("project tool agreement could not be established"))
847
- continue;
848
- const reason = f.error ?? "formatter executable not found";
849
- formatUnavailable.push({ formatter: f.name, reason });
850
- recordDegradationOnce({
851
- kind: "formatter-unavailable",
852
- subject: `${f.name}:${path.basename(filePath)}`,
853
- reason,
854
- });
855
- }
856
- formattersUsed = result.formatters
857
- .filter((f) => f.outcome !== "unavailable")
858
- .map((f) => f.name);
859
- if (result.anyChanged) {
860
- formatChanged = true;
861
- dbg("autoformat: " +
862
- result.formatters
863
- .map((f) => f.name + "(" + (f.changed ? "changed" : "unchanged") + ")")
864
- .join(", "));
865
- }
866
- if (!result.allSucceeded) {
867
- const failures = result.formatters.filter((f) => !f.success);
868
- for (const failure of failures) {
869
- recordDegradationOnce({
870
- kind: "formatter-failure",
871
- subject: `${failure.name}:${path.basename(filePath)}`,
872
- reason: failure.error ?? "unknown error",
873
- });
874
- }
875
- formatFailures.push(...failures.map((f) => `${f.name}: ${f.error ?? "unknown error"}`));
876
- dbg("autoformat: " +
877
- failures
878
- .map((f) => f.name + " failed: " + (f.error ?? "unknown error"))
879
- .join("; "));
880
- }
881
- }
882
- catch (err) {
883
- const message = err instanceof Error ? err.message : String(err);
884
- recordDegradationOnce({
885
- kind: "formatter-failure",
886
- subject: `format-service:${path.basename(filePath)}`,
887
- reason: message,
888
- });
889
- formatFailures.push(message);
890
- dbg(`autoformat error: ${err}`);
891
- }
892
- try {
893
- fileContent = nodeFs.readFileSync(filePath, "utf-8");
894
- }
895
- catch {
896
- fileContent = undefined;
897
- }
898
- return {
899
- formatChanged,
900
- formattersUsed,
901
- formatFailures,
902
- formatUnavailable,
903
- fileContent,
904
- };
905
- }
906
- /**
907
- * Build the 🔴 STOP blocker output with an inline code snippet for each
908
- * diagnostic so the agent can see the exact line it wrote without re-reading
909
- * the file.
910
- *
911
- * Example:
912
- * L4: 'randomInt' is declared but its value is never read.
913
- * → const randomInt = Math.floor(result);
914
- */
915
- function buildEnrichedBlockerOutput(blockers, fileContent = "") {
916
- // #3188: no blockers, no banner. The delivery gate above can retract every
917
- // blocker (all cited files deleted), and the header interpolates the count,
918
- // so without this the agent reads "🔴 STOP — 0 issue(s) must be fixed:" with
919
- // nothing under it. Same guard, same reason, as `formatDiagnostics` in
920
- // dispatch/utils/format-utils.ts — it belongs to the renderer, not to each
921
- // call site, because the #2028 branch below cannot gate on the count without
922
- // falling through to the UNGATED raw output.
923
- if (blockers.length === 0)
924
- return "";
925
- // Empty fileContent (readback failed, e.g. deleted-file race) still
926
- // renders the gated blocker list - just without per-line snippets.
927
- const fileLines = fileContent ? fileContent.split("\n") : [];
928
- const MAX_SNIPPET = 120; // chars — keep it tight in context
929
- let out = `\n\n🔴 STOP — ${blockers.length} issue(s) must be fixed:\n`;
930
- const shown = blockers.slice(0, 10);
931
- for (const d of shown) {
932
- const lineNo = d.line ?? 1;
933
- const nodeCtx = d.astNodeType ? ` (${d.astNodeType})` : "";
934
- out += ` L${lineNo}: ${d.message}${nodeCtx}\n`;
935
- // Prefer the exact matched node text (tree-sitter); fall back to the
936
- // full source line (LSP / other runners).
937
- const snippet = d.matchedText
938
- ? d.matchedText.trim().split("\n")[0]?.slice(0, MAX_SNIPPET)
939
- : fileLines[lineNo - 1]?.trim().slice(0, MAX_SNIPPET);
940
- if (snippet)
941
- out += ` → ${snippet}\n`;
942
- if (d.fixSuggestion)
943
- out += ` 💡 ${d.fixSuggestion}\n`;
944
- }
945
- if (blockers.length > 10) {
946
- out += ` ... and ${blockers.length - 10} more\n`;
947
- }
948
- return out;
949
- }
950
- // --- Main Pipeline ---
951
- export async function runPipeline(ctx, deps) {
952
- const { filePath, cwd, toolName, getFlag, getFlagSource, dbg } = ctx;
953
- const { getFormatService } = deps;
954
- const allowAutonomousWriters = ctx.allowAutonomousWriters !== false;
955
- if (!allowAutonomousWriters) {
956
- incrementDegradationCount({
957
- kind: "opaque-mutation-ownership-boundary",
958
- subject: "pipeline",
959
- reason: `observed mutation is not evidence of agent authorship (${filePath})`,
960
- });
961
- }
962
- admitWidgetDiagnosticsWrite(filePath, ctx.telemetry?.writeIndex);
963
- const phase = createPhaseTracker(toolName, filePath);
964
- const pipelineStart = Date.now();
965
- clearGraphCache();
966
- phase.start("total");
967
- // --- 1. Read file content ---
968
- phase.start("read_file");
969
- let fileContent;
970
- try {
971
- fileContent = nodeFs.readFileSync(filePath, "utf-8");
972
- }
973
- catch {
974
- // File may not exist (e.g., deleted)
975
- }
976
- phase.end("read_file");
977
- // --- 2. Auto-format ---
978
- phase.start("format");
979
- let formatChanged = false;
980
- let formattersUsed = [];
981
- let formatFailures = [];
982
- const piChangedFiles = new Set();
983
- const autoformatDisabled = !!getFlag("no-autoformat", filePath);
984
- const immediateFormat = !!getFlag("immediate-format");
985
- const formatDeferred = allowAutonomousWriters &&
986
- !autoformatDisabled &&
987
- !immediateFormat &&
988
- !!fileContent;
989
- if (allowAutonomousWriters &&
990
- !autoformatDisabled &&
991
- immediateFormat &&
992
- fileContent) {
993
- const formatResult = await runFormatPhase(filePath, getFormatService, dbg, ctx.signal);
994
- formatChanged = formatResult.formatChanged;
995
- formattersUsed = formatResult.formattersUsed;
996
- formatFailures = formatResult.formatFailures;
997
- fileContent = formatResult.fileContent;
998
- if (formatChanged) {
999
- const absPath = path.resolve(filePath);
1000
- piChangedFiles.add(absPath);
1001
- publishFilesTouched({
1002
- reason: "format",
1003
- paths: [absPath],
1004
- cwd,
1005
- dbg,
1006
- fixes: formattersUsed.map((tool) => ({
1007
- path: absPath,
1008
- tool,
1009
- kind: "format",
1010
- })),
1011
- });
1012
- }
1013
- }
1014
- else if (formatDeferred) {
1015
- dbg(`autoformat: deferred until agent_end for ${filePath}`);
1016
- }
1017
- else if (!allowAutonomousWriters) {
1018
- dbg(`autoformat: skipped for ${filePath} (mutation ownership is unproven)`);
1019
- }
1020
- else if (autoformatDisabled) {
1021
- const source = getFlagSource?.("no-autoformat", filePath);
1022
- dbg(`autoformat: skipped for ${filePath} (--no-autoformat${source ? `, source=${source}` : ""})`);
1023
- }
1024
- phase.end("format", {
1025
- formattersUsed,
1026
- formatChanged,
1027
- deferred: formatDeferred,
1028
- });
1029
- // --- 3. Auto-fix ---
1030
- phase.start("autofix");
1031
- let fixedCount = 0;
1032
- let autofixTools = [];
1033
- let attemptedTools = [];
1034
- let autofixChangedFiles = [];
1035
- let fixRefresh = false;
1036
- let autofixSkipReason;
1037
- if (!allowAutonomousWriters) {
1038
- autofixSkipReason = "mutation_ownership_unproven";
1039
- dbg(`autofix: skipped for ${filePath} (mutation ownership is unproven)`);
1040
- }
1041
- else if (ctx.autofixMode === "deferred") {
1042
- autofixSkipReason = "deferred_to_agent_end";
1043
- dbg(`autofix: deferred until agent_end for ${filePath}`);
1044
- }
1045
- else
1046
- ({
1047
- fixedCount,
1048
- autofixTools,
1049
- attemptedTools,
1050
- changedFiles: autofixChangedFiles,
1051
- needsContentRefresh: fixRefresh,
1052
- skipReason: autofixSkipReason,
1053
- } = await runAutofix(filePath, cwd, getFlag, dbg, deps, getFlagSource));
1054
- for (const changedFile of autofixChangedFiles) {
1055
- piChangedFiles.add(path.resolve(changedFile));
1056
- }
1057
- if (autofixChangedFiles.length > 0) {
1058
- // autofixTools entries are "tool:count" (e.g. "ruff:3"); runAutofix runs
1059
- // against ONE target filePath per call, so every tool that fired in this
1060
- // batch applies to every file the batch changed — best-effort attribution
1061
- // (not per-file precision) since the underlying tool runners don't report
1062
- // per-changed-file breakdown. Good enough for the "was this hunk
1063
- // mechanical" use case the fix-provenance field exists for.
1064
- const absChangedFiles = autofixChangedFiles.map((f) => path.resolve(f));
1065
- const toolNames = autofixTools.map((t) => t.split(":")[0]);
1066
- publishFilesTouched({
1067
- reason: "autofix",
1068
- paths: absChangedFiles,
1069
- cwd,
1070
- dbg,
1071
- fixes: absChangedFiles.flatMap((p) => toolNames.map((tool) => ({ path: p, tool, kind: "autofix" }))),
1072
- });
1073
- }
1074
- if (fixRefresh) {
1075
- try {
1076
- fileContent = nodeFs.readFileSync(filePath, "utf-8");
1077
- }
1078
- catch {
1079
- fileContent = undefined;
1080
- }
1081
- }
1082
- phase.end("autofix", {
1083
- fixedCount,
1084
- tools: autofixTools,
1085
- attemptedTools,
1086
- skipReason: autofixSkipReason,
1087
- });
1088
- // Capture the target's bytes immediately after pi-lens writes and before any
1089
- // awaitable LSP or dispatch work. `fileModified` also covers side-effect
1090
- // files, so target membership is the discriminator for pipeline ownership
1091
- // (#2499).
1092
- const fileModified = formatChanged || fixedCount > 0;
1093
- const targetFileModified = piChangedFiles.has(path.resolve(filePath));
1094
- const postWriteStateHash = fileModified && targetFileModified
1095
- ? (() => {
1096
- try {
1097
- return nodeCrypto
1098
- .createHash("sha256")
1099
- .update(nodeFs.readFileSync(filePath))
1100
- .digest("hex");
1101
- }
1102
- catch (error) {
1103
- recordDegradationOnce({
1104
- kind: "pipeline-post-write-hash-unavailable",
1105
- subject: filePath,
1106
- reason: error instanceof Error ? error.message : String(error),
1107
- });
1108
- return undefined;
1109
- }
1110
- })()
1111
- : undefined;
1112
- // --- 4. LSP file sync ---
1113
- // Sync once with final post-format/post-fix content so dispatch and cascade
1114
- // diagnostics do not observe stale pre-format text.
1115
- phase.start("lsp_sync");
1116
- let lspSyncCompleted = false;
1117
- if (fileContent) {
1118
- await resyncLspFile(filePath, fileContent, true, false, getFlag, dbg);
1119
- lspSyncCompleted = true;
1120
- }
1121
- phase.end("lsp_sync", { completed: lspSyncCompleted, finalContent: true });
1122
- // --- 5. Dispatch lint ---
1123
- phase.start("dispatch_lint");
1124
- dbg(`dispatch: running lint tools for ${filePath}`);
1125
- const piApi = {
1126
- getFlag: getFlag,
1127
- };
1128
- const { dispatchLintWithResult, computeCascadeForFile } = await loadDispatchIntegration();
1129
- // Capture the bytes the pipeline presents to analysis before the dispatch
1130
- // promise can yield to another writer. The blocker evidence below belongs to
1131
- // this analysis input, not to whatever happens to be on disk when the whole
1132
- // pipeline returns.
1133
- const inlineBlockerFileContent = fileContent
1134
- ? (() => {
1135
- const content = Buffer.from(fileContent, "utf8");
1136
- return content.byteLength <= 2 * 1024 * 1024
1137
- ? {
1138
- size: content.byteLength,
1139
- sha256: createHash("sha256").update(content).digest("hex"),
1140
- }
1141
- : undefined;
1142
- })()
1143
- : undefined;
1144
- const dispatchResult = await dispatchLintWithResult(filePath, cwd, piApi, ctx.modifiedRanges, {
1145
- model: ctx.telemetry?.model ?? "unknown",
1146
- sessionId: ctx.telemetry?.sessionId ?? "unknown",
1147
- turnIndex: ctx.telemetry?.turnIndex ?? 0,
1148
- writeIndex: ctx.telemetry?.writeIndex ?? 0,
1149
- }, {
1150
- projectRoot: ctx.projectRoot,
1151
- writeIndex: ctx.telemetry?.writeIndex,
1152
- telemetryModel: ctx.telemetry?.modelId,
1153
- telemetryProvider: ctx.telemetry?.provider,
1154
- });
1155
- recordDiagnostics(filePath, dispatchResult.diagnostics, ctx.telemetry?.writeIndex);
1156
- // #502: emit the write batch's FINAL diagnostic state immediately after
1157
- // recordDiagnostics commits it — this call site runs after format,
1158
- // autofix, and dispatch have all completed for this batch (see the phase
1159
- // order above), so dispatchResult.diagnostics IS the latest post-batch
1160
- // picture, not an intermediate runner result. Full-replace semantics: we
1161
- // always pass the complete current set for this file, including an
1162
- // explicit `[]` on the transition from a previously-reported-dirty state
1163
- // to clean (see clients/diagnostics-publish.ts for the contract).
1164
- {
1165
- const absPath = path.resolve(filePath);
1166
- const wasDirty = wasPreviouslyReportedDirty(absPath);
1167
- if (dispatchResult.diagnostics.length > 0 || wasDirty) {
1168
- publishDiagnostics({
1169
- cwd,
1170
- files: [
1171
- {
1172
- path: absPath,
1173
- diagnostics: dispatchResult.diagnostics.map(toPilensDiagnosticEntry),
1174
- },
1175
- ],
1176
- dbg,
1177
- });
1178
- }
1179
- }
1180
- // Opaque recovery is a valid analysis input, but its bytes are not an agent
1181
- // authored surface. Keep diagnostics and advisory output visible while
1182
- // withholding the edit-directed channels that would tell the agent to mutate
1183
- // the observed artifact (#3226).
1184
- const hasBlockers = allowAutonomousWriters && dispatchResult.hasBlockers;
1185
- const actionableWarnings = allowAutonomousWriters
1186
- ? dispatchResult.warnings
1187
- .map((diagnostic) => recordFromDispatchDiagnostic(diagnostic, cwd))
1188
- .filter((warning) => Boolean(warning))
1189
- : [];
1190
- const codeQualityWarnings = dispatchResult.warnings
1191
- .map((diagnostic) => recordFromCodeQualityDiagnostic(diagnostic, cwd))
1192
- .filter((warning) => Boolean(warning));
1193
- if (dispatchResult.diagnostics.length > 0) {
1194
- const tracker = getDiagnosticTracker();
1195
- tracker.trackShown(dispatchResult.diagnostics);
1196
- }
1197
- if (fixedCount > 0)
1198
- getDiagnosticTracker().trackAutoFixed(fixedCount);
1199
- if (dispatchResult.resolvedCount > 0)
1200
- getDiagnosticTracker().trackAgentFixed(dispatchResult.resolvedCount);
1201
- let output = "";
1202
- // #2028: the 🔴 STOP block is an agent-facing delivery surface
1203
- // (finding-delivery-gate.ts's `tool-call:stop-blocker`), so it routes through
1204
- // the shared deleted-path gate before rendering: a blocker whose cited file
1205
- // no longer exists has no remediation the agent can perform (the finding IS
1206
- // the deleted file's content), so it is dropped here rather than re-asserted.
1207
- // One bounded stat per unique cited path, only when blockers exist — zero
1208
- // cost on the clean/fast paths.
1209
- const deliverableBlockers = hasBlockers
1210
- ? dropFindingsForMissingPaths({
1211
- store: "stop-blocker",
1212
- findings: dispatchResult.blockers,
1213
- cwd,
1214
- citedPath: (b) => b.filePath || undefined,
1215
- })
1216
- : [];
1217
- // #3190: ONE gate, applied once here, for EVERY surface. The durable record
1218
- // this function hands `runtime.recordInlineBlockers` (and through it the
1219
- // turn-end `Unresolved from this turn` re-serve) used to be built from the
1220
- // UNGATED `dispatchResult.blockerOutput`/`blockers`, so a blocker retracted
1221
- // from the tool result above was re-delivered at turn end — and the ungated
1222
- // verdict still latched the commit gate through `updateGitGuardStatus`. It is
1223
- // exactly `hasBlockers && nothing-survived-the-gate` inverted: the gate
1224
- // returns `[]` whenever `hasBlockers` is false, so this one expression is the
1225
- // whole condition.
1226
- const hasDeliverableBlockers = deliverableBlockers.length > 0;
1227
- if (dispatchResult.diagnostics.length > 0) {
1228
- const logger = getDiagnosticLogger();
1229
- const toKey = (d) => [
1230
- d.tool || "",
1231
- d.id || "",
1232
- d.rule || "",
1233
- d.filePath || "",
1234
- d.line || 0,
1235
- d.column || 0,
1236
- ].join("|");
1237
- const inlineKeys = new Set([...deliverableBlockers, ...dispatchResult.fixed].map(toKey));
1238
- for (const d of dispatchResult.diagnostics) {
1239
- logger.logCaught(d, {
1240
- model: ctx.telemetry?.model ?? "unknown",
1241
- sessionId: ctx.telemetry?.sessionId ?? "unknown",
1242
- turnIndex: ctx.telemetry?.turnIndex ?? 0,
1243
- writeIndex: ctx.telemetry?.writeIndex ?? 0,
1244
- }, inlineKeys.has(toKey(d)));
1245
- }
1246
- }
1247
- const deliveredOutput = !allowAutonomousWriters && dispatchResult.hasBlockers
1248
- ? dispatchResult.output.slice(dispatchResult.blockerOutput.length)
1249
- : dispatchResult.output;
1250
- if (hasBlockers && fileContent) {
1251
- // Enrich blocker output with a code snippet so the agent can see the
1252
- // exact line it wrote that caused each violation — no re-read needed.
1253
- if (deliverableBlockers.length > 0) {
1254
- output += buildEnrichedBlockerOutput(deliverableBlockers, fileContent);
1255
- }
1256
- // Append fixed/coverage parts from the original output (slice off the
1257
- // blocker section we're replacing).
1258
- const rest = dispatchResult.output.slice(dispatchResult.blockerOutput.length);
1259
- if (rest)
1260
- output += rest;
1261
- }
1262
- else if (deliveredOutput) {
1263
- // #2028 review P3: this path fires when readback FAILED - raw output
1264
- // still cites possibly-deleted files. Re-render from the gated set
1265
- // (no snippets without fileContent) and keep the post-blocker slice.
1266
- if (hasBlockers &&
1267
- deliverableBlockers.length !== dispatchResult.blockers.length) {
1268
- let gatedOut = buildEnrichedBlockerOutput(deliverableBlockers);
1269
- const rest = dispatchResult.output.slice(dispatchResult.blockerOutput.length);
1270
- if (rest)
1271
- gatedOut += rest;
1272
- // #3188: every blocker retracted and nothing after the blocker
1273
- // section leaves nothing to say — appending the separator alone would
1274
- // still deliver a (whitespace-only) tool-result block, because
1275
- // `handleToolResult` decides delivery on `output` truthiness.
1276
- if (gatedOut)
1277
- output += `\n\n${gatedOut}`;
1278
- }
1279
- else {
1280
- output += `\n\n${deliveredOutput}`;
1281
- }
1282
- }
1283
- if (fixedCount > 0) {
1284
- const detail = autofixTools.length > 0 ? ` (${autofixTools.join(", ")})` : "";
1285
- output += `\n\n✅ Auto-fixed ${fixedCount} issue(s)${detail}`;
1286
- }
1287
- if (formatFailures.length > 0) {
1288
- const details = formatFailures.slice(0, 3).join("; ");
1289
- const suffix = formatFailures.length > 3
1290
- ? `; ... and ${formatFailures.length - 3} more`
1291
- : "";
1292
- output += `\n\n⚠️ Auto-format failed: ${details}${suffix}`;
1293
- }
1294
- // #1590: the notice sentence itself is NOT rendered here. This layer cannot
1295
- // see the attachment cap or the aggregate budget, so it hands the display
1296
- // paths to `handleToolResult`, which owns the decision and renders the one
1297
- // sentence with `renderPostAutofixNotice`.
1298
- const postAutofixNotice = formatChanged || fixedCount > 0
1299
- ? {
1300
- targetPath: toRunnerDisplayPath(cwd, filePath),
1301
- changedFiles: [...piChangedFiles].map((changedFile) => toRunnerDisplayPath(cwd, changedFile)),
1302
- }
1303
- : undefined;
1304
- phase.end("dispatch_lint", {
1305
- hasOutput: !!dispatchResult.output,
1306
- diagnosticCount: dispatchResult.diagnostics.length,
1307
- });
1308
- // --- 6. Cascade diagnostics (LSP only) ---
1309
- // Kicked off UNAWAITED so the graph rebuild + neighbor LSP pulls run
1310
- // concurrently after the edit returns rather than blocking it (#450). The
1311
- // result is never shown inline — settled (bounded) and surfaced at turn_end.
1312
- // The stored promise must never reject: an unhandled rejection is fatal, so a
1313
- // failing compute resolves to an "error" skip-run instead.
1314
- const cascadeOrigin = {
1315
- turnSeq: ctx.telemetry?.turnIndex,
1316
- writeSeq: ctx.telemetry?.writeIndex,
1317
- projectSeq: ctx.seqState?.projectSeq(),
1318
- };
1319
- const cascadePromise = getFlag("no-lsp")
1320
- ? undefined
1321
- : computeCascadeForFile(filePath, cwd, {
1322
- hasBlockers,
1323
- dbg,
1324
- turnSeq: ctx.telemetry?.turnIndex,
1325
- writeSeq: ctx.telemetry?.writeIndex,
1326
- // #3157: `cwd` here is the LANGUAGE root. The cascade's display
1327
- // filter reads the disposition store and the `.pi-lens.json` rule
1328
- // policy, both written under the PROJECT root (#1030) — the same
1329
- // pair the dispatcher is handed above.
1330
- projectRoot: ctx.projectRoot,
1331
- seqState: ctx.seqState,
1332
- turnEndCascadeSettleStart: ctx.turnEndCascadeSettleStart,
1333
- fileContent,
1334
- wordIndex: ctx.wordIndex,
1335
- onWordIndexUpdated: ctx.onWordIndexUpdated,
1336
- })
1337
- .then((run) => ({ ...run, origin: cascadeOrigin }))
1338
- .catch((err) => {
1339
- dbg(`cascade compute failed for ${filePath}: ${err}`);
1340
- return {
1341
- filePath,
1342
- origin: cascadeOrigin,
1343
- result: undefined,
1344
- neighborCount: 0,
1345
- diagnosticCount: 0,
1346
- skipReason: "error",
1347
- // #1023: a thrown compute is ALSO "couldn't compute downstream
1348
- // impact" — carry an indeterminate marker so turn_end surfaces an
1349
- // honest advisory instead of the error skip being logs-only.
1350
- indeterminate: {
1351
- reason: "error",
1352
- detail: "cascade computation failed",
1353
- },
1354
- };
1355
- });
1356
- // --- Final timing + all-clear ---
1357
- const elapsed = Date.now() - pipelineStart;
1358
- // #1590: `postAutofixNotice` is output this run produces, just rendered a
1359
- // layer up. Treat it as output here, or a format-only change would newly
1360
- // gain an "all clear" line it never had while the sentence lived inline.
1361
- if (!output && !postAutofixNotice) {
1362
- output = buildAllClearOutput(dispatchResult, elapsed, filePath);
1363
- }
1364
- phase.end("total", { hasOutput: !!output });
1365
- const changedFiles = [...piChangedFiles];
1366
- emitLensAnalysisComplete({
1367
- cwd,
1368
- filePath,
1369
- toolName,
1370
- model: ctx.telemetry?.model ?? "unknown",
1371
- sessionId: ctx.telemetry?.sessionId ?? "unknown",
1372
- turnIndex: ctx.telemetry?.turnIndex ?? 0,
1373
- writeIndex: ctx.telemetry?.writeIndex ?? 0,
1374
- diagnostics: dispatchResult.diagnostics,
1375
- blockers: hasBlockers ? dispatchResult.blockers : [],
1376
- warnings: dispatchResult.warnings,
1377
- fixed: dispatchResult.fixed,
1378
- resolvedCount: dispatchResult.resolvedCount,
1379
- hasBlockers,
1380
- fileModified,
1381
- changedFiles,
1382
- durationMs: elapsed,
1383
- });
1384
- return {
1385
- output,
1386
- // #3190: the verdict every consumer reads — `updateGitGuardStatus`'s commit
1387
- // latch above all — is what was DELIVERED, not what the dispatch found. The
1388
- // two disagreed whenever the deleted-path gate retracted every blocker: the
1389
- // tool result said `✓ clean` while the commit gate stayed latched, its
1390
- // summary falling back to that very all-clear line. `emitLensAnalysisComplete`
1391
- // above deliberately keeps the raw dispatch counts — it records what was
1392
- // observed, not what was served.
1393
- hasBlockers: hasDeliverableBlockers,
1394
- cascadePromise,
1395
- isError: false,
1396
- fileModified,
1397
- postWriteStateHash,
1398
- changedFiles,
1399
- // #3190: re-rendered from the GATED set with `formatDiagnostics(...,
1400
- // "blocking")` — the very expression `dispatcher.ts:1409` builds
1401
- // `blockerOutput` with, over a subset of the very array it rendered. When
1402
- // the gate drops nothing the two are byte-identical (same renderer, same
1403
- // array, same display cap), so an unretracted record is unchanged; when it
1404
- // drops something the stored text can no longer disagree with the tool
1405
- // result, and `inline-blocker-dispositions.ts`' re-render at turn end still
1406
- // matches this string exactly.
1407
- inlineBlockerSummary: hasDeliverableBlockers
1408
- ? formatDiagnostics(deliverableBlockers, "blocking").trim() || undefined
1409
- : undefined,
1410
- // #1561 F1: taken from the very diagnostics `blockerOutput` was rendered
1411
- // from, so the provenance can never disagree with the text it guards. An
1412
- // untagged diagnostic contributes the literal "unknown", which no verdict
1413
- // claims coverage for — it pins the entry rather than silently widening
1414
- // what an LSP check is allowed to clear.
1415
- inlineBlockerSources: hasDeliverableBlockers
1416
- ? [
1417
- ...new Set(deliverableBlockers.map((d) => d.tool?.trim() || "unknown")),
1418
- ]
1419
- : undefined,
1420
- inlineBlockerLines: hasDeliverableBlockers
1421
- ? deliverableBlockers
1422
- // #1641 review F2: the blocker array (#3190: the deliverable subset
1423
- // of `dispatchResult.blockers`) is NOT guaranteed to be
1424
- // scoped to THIS file — a chart-wide runner (helm-lint, helm-render)
1425
- // reports blocking diagnostics against other files in the chart
1426
- // (e.g. `values.yaml`) alongside `ctx.filePath`. The precedent every
1427
- // per-file runner already follows (dotnet-build.ts, javac.ts) is to
1428
- // drop cross-file rows before they reach a per-file record; this is
1429
- // that same filter applied at the aggregation point instead, since
1430
- // `blockers` is pooled across every runner dispatched for this file.
1431
- // Without it, a cross-file line count gets attributed to THIS
1432
- // file's past-EOF check and can demote an in-bounds, fully valid
1433
- // blocker for content the diagnostic never described.
1434
- //
1435
- // #1641 review round 2 (LOW): `path.resolve` equality doesn't fold
1436
- // case, and an LSP-sourced diagnostic's `filePath` is stamped with
1437
- // realpath canonical casing (dispatch/runners/lsp.ts ->
1438
- // normalizeMapKey) while `ctx.filePath` can arrive lowercase-drive
1439
- // on Windows — the drive-letter class from #1139/#1150. A bare
1440
- // `path.resolve` equality then drops EVERY LSP blocker line and
1441
- // this record silently skips the past-EOF gate (fail-open, but
1442
- // exactly the pre-fix behavior on the surface #1641 targets).
1443
- // `normalizeEphemeralMapKey` slash-folds and (on win32)
1444
- // lowercase-folds both sides with no filesystem I/O — cheap enough
1445
- // for this per-blocker hot-path filter. `pathsEqual` was
1446
- // deliberately NOT used here: it calls `realpathSync` per
1447
- // comparison, which this filter cannot afford per blocker.
1448
- .filter((d) => normalizeEphemeralMapKey(d.filePath) ===
1449
- normalizeEphemeralMapKey(filePath))
1450
- .map((d) => d.line)
1451
- .filter((line) => typeof line === "number")
1452
- : undefined,
1453
- actionableWarnings,
1454
- codeQualityWarnings,
1455
- diagnostics: dispatchResult.diagnostics,
1456
- formattersUsed,
1457
- fixedCount,
1458
- autofixTools,
1459
- postAutofixNotice,
1460
- postMutation: ctx.autofixMode !== "deferred" &&
1461
- autofixChangedFiles.some((f) => path.resolve(f) === path.resolve(filePath)) &&
1462
- fileContent !== undefined
1463
- ? {
1464
- filePath: path.resolve(filePath),
1465
- content: fileContent,
1466
- source: "autofix",
1467
- }
1468
- : undefined,
1469
- inlineBlockerFileContent: hasBlockers
1470
- ? inlineBlockerFileContent
1471
- : undefined,
1472
- // #3246: the very array `blockerOutput` above was rendered from, so the
1473
- // turn-end policy filter and its re-render can never disagree with the
1474
- // text they guard — the same provenance argument `inlineBlockerSources`
1475
- // makes one field up.
1476
- inlineBlockerDiagnostics: hasDeliverableBlockers
1477
- ? deliverableBlockers
1478
- : undefined,
1479
- };
1480
- }