pi-lens 4.2.1 → 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 (671) hide show
  1. package/CHANGELOG.md +1562 -0
  2. package/README.md +157 -24
  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 +19039 -8956
  125. package/dist/mcp/analyze-cli.js +186 -228
  126. package/dist/mcp/cli.js +137 -98
  127. package/dist/mcp/server.js +22592 -1564
  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/adr/0001-stale-advisory-live-arm.md +29 -0
  133. package/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
  134. package/docs/adr/0003-git-guard-latch-writer.md +203 -0
  135. package/docs/adr/0004-disposition-policy-seam.md +38 -0
  136. package/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
  137. package/docs/adr/0006-derived-state-benchmark-first.md +31 -0
  138. package/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
  139. package/docs/adr/0008-turn-end-lane-interface.md +165 -0
  140. package/docs/adr/0009-reported-path-attribution.md +114 -0
  141. package/docs/adr/README.md +26 -0
  142. package/docs/agent-guide.md +13 -4
  143. package/docs/agent-tools.md +60 -1
  144. package/docs/ast-grep_rules_catalog.md +6 -4
  145. package/docs/configuration.md +75 -2
  146. package/docs/engineering-principles.md +202 -0
  147. package/docs/environment-variables.md +113 -6
  148. package/docs/features.md +10 -6
  149. package/docs/globalconfig.md +7 -1
  150. package/docs/io-bridge-v2-migration.md +94 -0
  151. package/docs/language-coverage.md +17 -0
  152. package/docs/lsp-capability-matrix.md +131 -32
  153. package/docs/lsp-idle-eviction.md +124 -0
  154. package/docs/mcp.md +21 -5
  155. package/docs/merge-queue-rollout.md +77 -0
  156. package/docs/pi-lens-fixer.md +161 -39
  157. package/docs/pi-lens-investigator.md +77 -20
  158. package/docs/pi-lens-merge-policy.md +694 -0
  159. package/docs/pi-lens-monitor.md +73 -7
  160. package/docs/pi-lens-retro.md +6 -4
  161. package/docs/pi-lens-reviewer.md +190 -34
  162. package/docs/pi-lens-subagent.md +247 -37
  163. package/docs/pi-lens-warden.md +18 -7
  164. package/docs/public-api-stability.md +56 -0
  165. package/docs/release-qa-baseline.md +86 -17
  166. package/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  167. package/docs/servercapabilities.md +66 -52
  168. package/docs/settings.md +13 -3
  169. package/docs/subagent-compat.md +1 -1
  170. package/docs/usage.md +5 -1
  171. package/grammars/tree-sitter-bash.wasm +0 -0
  172. package/grammars/tree-sitter-bash.wasm.json +3 -3
  173. package/package.json +23 -9
  174. package/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  175. package/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
  176. package/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
  177. package/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
  178. package/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
  179. package/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
  180. package/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  181. package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
  182. package/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
  183. package/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
  184. package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
  185. package/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
  186. package/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
  187. package/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
  188. package/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
  189. package/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
  190. package/scripts/analyze-pi-lens-logs.mjs +1275 -62
  191. package/scripts/download-grammars.js +5 -0
  192. package/scripts/grammars.lock.json +6 -1
  193. package/scripts/install-selftest.mjs +38 -35
  194. package/scripts/lib/host-provided-deps.mjs +13 -4
  195. package/scripts/lib/web-tree-sitter-dir.mjs +141 -0
  196. package/dist/clients/actionable-warnings-logger.js +0 -35
  197. package/dist/clients/actionable-warnings.js +0 -1594
  198. package/dist/clients/advisory-provenance.js +0 -457
  199. package/dist/clients/agent-behavior-client.js +0 -147
  200. package/dist/clients/agent-nudge.js +0 -325
  201. package/dist/clients/analysed-root.js +0 -1
  202. package/dist/clients/ast-grep-client.js +0 -731
  203. package/dist/clients/ast-grep-rule-manager.js +0 -146
  204. package/dist/clients/ast-grep-tool-logger.js +0 -118
  205. package/dist/clients/ast-grep-types.js +0 -10
  206. package/dist/clients/ast-grep-yaml-synth.js +0 -154
  207. package/dist/clients/atomic-write-staging.js +0 -75
  208. package/dist/clients/atomic-write.js +0 -189
  209. package/dist/clients/availability-probe-flight.js +0 -12
  210. package/dist/clients/bash-file-access.js +0 -1056
  211. package/dist/clients/biome-client.js +0 -357
  212. package/dist/clients/blocker-freshness.js +0 -837
  213. package/dist/clients/blocker-past-eof.js +0 -129
  214. package/dist/clients/bootstrap.js +0 -569
  215. package/dist/clients/bounded-cache.js +0 -190
  216. package/dist/clients/bounded-pid-file-lock.js +0 -202
  217. package/dist/clients/bounded-telemetry.js +0 -295
  218. package/dist/clients/build-identity.js +0 -95
  219. package/dist/clients/bundled-resource-health.js +0 -113
  220. package/dist/clients/bus-events-logger.js +0 -105
  221. package/dist/clients/bus-publish.js +0 -171
  222. package/dist/clients/cache/rule-cache.js +0 -190
  223. package/dist/clients/cache-manager.js +0 -465
  224. package/dist/clients/cache-observability.js +0 -1313
  225. package/dist/clients/call-graph.js +0 -852
  226. package/dist/clients/cargo-manifest.js +0 -422
  227. package/dist/clients/cascade-budget.js +0 -196
  228. package/dist/clients/cascade-format.js +0 -315
  229. package/dist/clients/cascade-logger.js +0 -31
  230. package/dist/clients/cascade-types.js +0 -9
  231. package/dist/clients/child-unref.js +0 -115
  232. package/dist/clients/code-quality-warnings.js +0 -286
  233. package/dist/clients/codebase-model.js +0 -173
  234. package/dist/clients/collateral-test-role.js +0 -40
  235. package/dist/clients/complexity-client.js +0 -462
  236. package/dist/clients/config-core/deny.js +0 -221
  237. package/dist/clients/config-core/index.js +0 -50
  238. package/dist/clients/config-core/merge.js +0 -357
  239. package/dist/clients/config-core/normalize.js +0 -340
  240. package/dist/clients/config-core/process-spec.js +0 -248
  241. package/dist/clients/config-core/provenance.js +0 -164
  242. package/dist/clients/config-core/records.js +0 -218
  243. package/dist/clients/config-core/resolve.js +0 -125
  244. package/dist/clients/config-core/safe-object.js +0 -78
  245. package/dist/clients/config-core/schema.js +0 -165
  246. package/dist/clients/config-diagnostic-codes.js +0 -283
  247. package/dist/clients/config-locations.js +0 -160
  248. package/dist/clients/config-resolve.js +0 -792
  249. package/dist/clients/config-schema.js +0 -251
  250. package/dist/clients/config-warn.js +0 -407
  251. package/dist/clients/console-guard-install.js +0 -26
  252. package/dist/clients/cooperative-budget.js +0 -42
  253. package/dist/clients/dead-code-client.js +0 -510
  254. package/dist/clients/dead-code-logger.js +0 -30
  255. package/dist/clients/deadline-utils.js +0 -280
  256. package/dist/clients/debug-handles.js +0 -211
  257. package/dist/clients/debug-heap.js +0 -174
  258. package/dist/clients/deferred-lsp-work.js +0 -106
  259. package/dist/clients/degradation-ledger.js +0 -396
  260. package/dist/clients/demoted-finding-render.js +0 -169
  261. package/dist/clients/dependency-checker.js +0 -862
  262. package/dist/clients/deps/ast-grep-napi.js +0 -22
  263. package/dist/clients/deps/js-yaml.js +0 -9
  264. package/dist/clients/deps/minimatch.js +0 -4
  265. package/dist/clients/deps/pi-tui.js +0 -14
  266. package/dist/clients/deps/typebox.js +0 -14
  267. package/dist/clients/deps/vscode-jsonrpc.js +0 -1
  268. package/dist/clients/deps/web-tree-sitter.js +0 -24
  269. package/dist/clients/diagnostic-dispositions.js +0 -594
  270. package/dist/clients/diagnostic-line-freshness.js +0 -305
  271. package/dist/clients/diagnostic-logger.js +0 -73
  272. package/dist/clients/diagnostic-tracker.js +0 -105
  273. package/dist/clients/diagnostics-publish.js +0 -230
  274. package/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  275. package/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  276. package/dist/clients/dispatch/collect-later-tier.js +0 -35
  277. package/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  278. package/dist/clients/dispatch/dispatcher.js +0 -1269
  279. package/dist/clients/dispatch/fact-provider-types.js +0 -1
  280. package/dist/clients/dispatch/fact-rule-runner.js +0 -17
  281. package/dist/clients/dispatch/fact-runner.js +0 -30
  282. package/dist/clients/dispatch/fact-scheduler.js +0 -68
  283. package/dist/clients/dispatch/fact-store.js +0 -377
  284. package/dist/clients/dispatch/facts/comment-facts.js +0 -22
  285. package/dist/clients/dispatch/facts/file-content.js +0 -19
  286. package/dist/clients/dispatch/facts/function-facts.js +0 -348
  287. package/dist/clients/dispatch/facts/import-facts.js +0 -204
  288. package/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  289. package/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  290. package/dist/clients/dispatch/finding-policy.js +0 -222
  291. package/dist/clients/dispatch/indent-detect.js +0 -411
  292. package/dist/clients/dispatch/inline-suppressions.js +0 -70
  293. package/dist/clients/dispatch/integration.js +0 -2377
  294. package/dist/clients/dispatch/lazy.js +0 -11
  295. package/dist/clients/dispatch/pending-runner-findings.js +0 -87
  296. package/dist/clients/dispatch/plan.js +0 -278
  297. package/dist/clients/dispatch/priorities.js +0 -20
  298. package/dist/clients/dispatch/rule-id-normalize.js +0 -50
  299. package/dist/clients/dispatch/rule-ignores.js +0 -62
  300. package/dist/clients/dispatch/rule-policy.js +0 -135
  301. package/dist/clients/dispatch/rules/async-noise.js +0 -40
  302. package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  303. package/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  304. package/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  305. package/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  306. package/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  307. package/dist/clients/dispatch/rules/high-complexity.js +0 -72
  308. package/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  309. package/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  310. package/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  311. package/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  312. package/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  313. package/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  314. package/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  315. package/dist/clients/dispatch/runner-context.js +0 -52
  316. package/dist/clients/dispatch/runners/actionlint.js +0 -113
  317. package/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  318. package/dist/clients/dispatch/runners/biome-check.js +0 -283
  319. package/dist/clients/dispatch/runners/cpp-check.js +0 -236
  320. package/dist/clients/dispatch/runners/credo.js +0 -80
  321. package/dist/clients/dispatch/runners/cue-vet.js +0 -398
  322. package/dist/clients/dispatch/runners/dart-analyze.js +0 -202
  323. package/dist/clients/dispatch/runners/detekt.js +0 -169
  324. package/dist/clients/dispatch/runners/dotnet-build.js +0 -159
  325. package/dist/clients/dispatch/runners/elixir-check.js +0 -210
  326. package/dist/clients/dispatch/runners/eslint.js +0 -154
  327. package/dist/clients/dispatch/runners/fact-rules.js +0 -29
  328. package/dist/clients/dispatch/runners/fish-indent.js +0 -69
  329. package/dist/clients/dispatch/runners/gleam-check.js +0 -87
  330. package/dist/clients/dispatch/runners/go-vet.js +0 -90
  331. package/dist/clients/dispatch/runners/golangci-lint.js +0 -120
  332. package/dist/clients/dispatch/runners/hadolint.js +0 -79
  333. package/dist/clients/dispatch/runners/helm-lint.js +0 -171
  334. package/dist/clients/dispatch/runners/helm-render.js +0 -940
  335. package/dist/clients/dispatch/runners/htmlhint.js +0 -108
  336. package/dist/clients/dispatch/runners/index.js +0 -108
  337. package/dist/clients/dispatch/runners/javac.js +0 -89
  338. package/dist/clients/dispatch/runners/ktlint.js +0 -136
  339. package/dist/clients/dispatch/runners/lsp.js +0 -378
  340. package/dist/clients/dispatch/runners/markdownlint.js +0 -160
  341. package/dist/clients/dispatch/runners/mypy.js +0 -101
  342. package/dist/clients/dispatch/runners/oxlint.js +0 -398
  343. package/dist/clients/dispatch/runners/php-lint.js +0 -62
  344. package/dist/clients/dispatch/runners/phpstan.js +0 -120
  345. package/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  346. package/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  347. package/dist/clients/dispatch/runners/pyright.js +0 -169
  348. package/dist/clients/dispatch/runners/rubocop.js +0 -90
  349. package/dist/clients/dispatch/runners/ruff.js +0 -89
  350. package/dist/clients/dispatch/runners/rust-clippy.js +0 -251
  351. package/dist/clients/dispatch/runners/shellcheck.js +0 -169
  352. package/dist/clients/dispatch/runners/shfmt.js +0 -102
  353. package/dist/clients/dispatch/runners/spellcheck.js +0 -121
  354. package/dist/clients/dispatch/runners/spotbugs.js +0 -259
  355. package/dist/clients/dispatch/runners/sqlfluff.js +0 -174
  356. package/dist/clients/dispatch/runners/stylelint.js +0 -158
  357. package/dist/clients/dispatch/runners/swiftlint.js +0 -154
  358. package/dist/clients/dispatch/runners/taplo.js +0 -168
  359. package/dist/clients/dispatch/runners/terragrunt.js +0 -168
  360. package/dist/clients/dispatch/runners/tflint.js +0 -110
  361. package/dist/clients/dispatch/runners/tree-sitter.js +0 -620
  362. package/dist/clients/dispatch/runners/trivy-config.js +0 -247
  363. package/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  364. package/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  365. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -85
  366. package/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  367. package/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  368. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  369. package/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  370. package/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  371. package/dist/clients/dispatch/runners/utils.js +0 -15
  372. package/dist/clients/dispatch/runners/vale.js +0 -116
  373. package/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  374. package/dist/clients/dispatch/runners/yamllint.js +0 -85
  375. package/dist/clients/dispatch/runners/zig-check.js +0 -91
  376. package/dist/clients/dispatch/suppress-writer.js +0 -69
  377. package/dist/clients/dispatch/tool-profile.js +0 -30
  378. package/dist/clients/dispatch/types.js +0 -21
  379. package/dist/clients/dispatch/utils/format-utils.js +0 -45
  380. package/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  381. package/dist/clients/disposition-logger.js +0 -32
  382. package/dist/clients/disposition-publish.js +0 -124
  383. package/dist/clients/durable-store.js +0 -97
  384. package/dist/clients/effective-config.js +0 -413
  385. package/dist/clients/env-utils.js +0 -83
  386. package/dist/clients/error-class.js +0 -23
  387. package/dist/clients/eval-timestamp.js +0 -17
  388. package/dist/clients/event-loop-hold.js +0 -274
  389. package/dist/clients/event-loop-monitor.js +0 -250
  390. package/dist/clients/extension-log.js +0 -504
  391. package/dist/clients/extension-mode.js +0 -73
  392. package/dist/clients/feature-hints.js +0 -103
  393. package/dist/clients/file-kinds.js +0 -412
  394. package/dist/clients/file-role.js +0 -134
  395. package/dist/clients/file-time.js +0 -160
  396. package/dist/clients/file-utils.js +0 -1032
  397. package/dist/clients/finding-delivery-gate.js +0 -463
  398. package/dist/clients/finding-identity.js +0 -88
  399. package/dist/clients/fix-worklog.js +0 -103
  400. package/dist/clients/format-events-publish.js +0 -343
  401. package/dist/clients/format-service.js +0 -222
  402. package/dist/clients/formatters-lazy.js +0 -9
  403. package/dist/clients/formatters.js +0 -1994
  404. package/dist/clients/freshness-cadence.js +0 -17
  405. package/dist/clients/freshness.js +0 -38
  406. package/dist/clients/generated-artifacts.js +0 -394
  407. package/dist/clients/generation-guard.js +0 -215
  408. package/dist/clients/git-guard.js +0 -923
  409. package/dist/clients/git-tracked-ignore.js +0 -243
  410. package/dist/clients/gitleaks-client.js +0 -509
  411. package/dist/clients/go-client.js +0 -104
  412. package/dist/clients/govulncheck-client.js +0 -605
  413. package/dist/clients/gradle-ktfmt-style.js +0 -252
  414. package/dist/clients/grammar-source.js +0 -465
  415. package/dist/clients/gzip-stage-write.js +0 -135
  416. package/dist/clients/hashline-anchor.js +0 -424
  417. package/dist/clients/hook-budgets.js +0 -76
  418. package/dist/clients/host-edit-normalize.js +0 -140
  419. package/dist/clients/host-ports.js +0 -39
  420. package/dist/clients/indent-retarget.js +0 -154
  421. package/dist/clients/install-diagnostics.js +0 -143
  422. package/dist/clients/installer/index.js +0 -5167
  423. package/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  424. package/dist/clients/installer/managed-tool-refresh.js +0 -783
  425. package/dist/clients/instance-reaper-state.js +0 -7
  426. package/dist/clients/instance-reaper.js +0 -1137
  427. package/dist/clients/instance-registry-lock.js +0 -145
  428. package/dist/clients/instance-registry.js +0 -773
  429. package/dist/clients/jscpd-client.js +0 -370
  430. package/dist/clients/json-cache-read.js +0 -65
  431. package/dist/clients/knip-client.js +0 -788
  432. package/dist/clients/language-policy.js +0 -252
  433. package/dist/clients/language-profile.js +0 -361
  434. package/dist/clients/language-registry.js +0 -603
  435. package/dist/clients/latency-logger.js +0 -719
  436. package/dist/clients/lazy-import.js +0 -21
  437. package/dist/clients/ledger-bounds.js +0 -54
  438. package/dist/clients/lens-config.js +0 -346
  439. package/dist/clients/lens-engine.js +0 -358
  440. package/dist/clients/lens-events.js +0 -125
  441. package/dist/clients/lens-flag-registry.js +0 -467
  442. package/dist/clients/lens-map.js +0 -1156
  443. package/dist/clients/live-bus-emitter.js +0 -74
  444. package/dist/clients/log-cleanup.js +0 -222
  445. package/dist/clients/lsp/aggregation.js +0 -178
  446. package/dist/clients/lsp/cascade-tier.js +0 -421
  447. package/dist/clients/lsp/client.js +0 -4465
  448. package/dist/clients/lsp/config.js +0 -689
  449. package/dist/clients/lsp/diagnostic-binding.js +0 -225
  450. package/dist/clients/lsp/document-drift.js +0 -467
  451. package/dist/clients/lsp/edits.js +0 -1219
  452. package/dist/clients/lsp/index.js +0 -8305
  453. package/dist/clients/lsp/inferred-project.js +0 -216
  454. package/dist/clients/lsp/jvm-runtime.js +0 -298
  455. package/dist/clients/lsp/language.js +0 -63
  456. package/dist/clients/lsp/launch.js +0 -725
  457. package/dist/clients/lsp/lombok.js +0 -158
  458. package/dist/clients/lsp/path-utils.js +0 -5
  459. package/dist/clients/lsp/pending-aux-coverage.js +0 -251
  460. package/dist/clients/lsp/position-encoding.js +0 -61
  461. package/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  462. package/dist/clients/lsp/server.js +0 -3209
  463. package/dist/clients/lsp/session-roots.js +0 -192
  464. package/dist/clients/lsp/spawn-history.js +0 -17
  465. package/dist/clients/lsp/sync-kind.js +0 -42
  466. package/dist/clients/lsp/tsserver-sync.js +0 -332
  467. package/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  468. package/dist/clients/lsp/wait-policy/classification.js +0 -85
  469. package/dist/clients/lsp/wait-policy/index.js +0 -10
  470. package/dist/clients/lsp/wait-policy/strategies.js +0 -303
  471. package/dist/clients/lsp/watch-queue.js +0 -61
  472. package/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  473. package/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  474. package/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  475. package/dist/clients/lsp-budget.js +0 -186
  476. package/dist/clients/lsp-document-symbols.js +0 -110
  477. package/dist/clients/lsp-lazy.js +0 -9
  478. package/dist/clients/lsp-mutation.js +0 -396
  479. package/dist/clients/lsp-status.js +0 -59
  480. package/dist/clients/map-with-concurrency.js +0 -40
  481. package/dist/clients/mcp/analyze.js +0 -420
  482. package/dist/clients/mcp/host-shim.js +0 -31
  483. package/dist/clients/mcp/ipc.js +0 -335
  484. package/dist/clients/mcp/review.js +0 -151
  485. package/dist/clients/mcp/session.js +0 -412
  486. package/dist/clients/memory-sampler.js +0 -208
  487. package/dist/clients/message-end-attribution.js +0 -30
  488. package/dist/clients/metrics-client.js +0 -107
  489. package/dist/clients/metrics-history.js +0 -327
  490. package/dist/clients/middle-man-analysis.js +0 -263
  491. package/dist/clients/model-provider.js +0 -53
  492. package/dist/clients/module-report-lsp.js +0 -189
  493. package/dist/clients/module-report.js +0 -2150
  494. package/dist/clients/mutating-tool.js +0 -651
  495. package/dist/clients/mutation-attribution.js +0 -368
  496. package/dist/clients/mutation-bridge.js +0 -240
  497. package/dist/clients/ndjson-logger.js +0 -734
  498. package/dist/clients/observed-mutation-sources.js +0 -101
  499. package/dist/clients/observed-mutation.js +0 -1253
  500. package/dist/clients/oldtext-autopatch.js +0 -128
  501. package/dist/clients/opaque-mutation-scan.js +0 -496
  502. package/dist/clients/opengrep-client.js +0 -328
  503. package/dist/clients/opengrep-config.js +0 -76
  504. package/dist/clients/package-manager.js +0 -607
  505. package/dist/clients/package-root.js +0 -38
  506. package/dist/clients/partial-edit-apply.js +0 -439
  507. package/dist/clients/path-attribution-telemetry.js +0 -43
  508. package/dist/clients/path-keyed-map.js +0 -108
  509. package/dist/clients/path-utils.js +0 -1244
  510. package/dist/clients/performance-report.js +0 -331
  511. package/dist/clients/persist-debounce.js +0 -70
  512. package/dist/clients/persistent-reverify.js +0 -271
  513. package/dist/clients/php-cs-fixer-config.js +0 -119
  514. package/dist/clients/pipeline.js +0 -1425
  515. package/dist/clients/post-autofix-notice.js +0 -37
  516. package/dist/clients/print-mode.js +0 -21
  517. package/dist/clients/probe-home-state.js +0 -228
  518. package/dist/clients/process-bridge.js +0 -66
  519. package/dist/clients/process-singletons.js +0 -210
  520. package/dist/clients/process-snapshot.js +0 -68
  521. package/dist/clients/project-changes.js +0 -181
  522. package/dist/clients/project-conventions.js +0 -171
  523. package/dist/clients/project-diagnostics/cache.js +0 -137
  524. package/dist/clients/project-diagnostics/extractors.js +0 -109
  525. package/dist/clients/project-diagnostics/fresh-fetch.js +0 -650
  526. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  527. package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  528. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  529. package/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  530. package/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  531. package/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  532. package/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  533. package/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  534. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  535. package/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  536. package/dist/clients/project-diagnostics/scanner.js +0 -520
  537. package/dist/clients/project-diagnostics/types.js +0 -1
  538. package/dist/clients/project-lens-config.js +0 -799
  539. package/dist/clients/project-report.js +0 -701
  540. package/dist/clients/project-scale.js +0 -237
  541. package/dist/clients/project-scan-policy.js +0 -11
  542. package/dist/clients/project-snapshot-fingerprint.js +0 -48
  543. package/dist/clients/project-snapshot-persist-worker.js +0 -20
  544. package/dist/clients/project-snapshot.js +0 -1653
  545. package/dist/clients/project-trust.js +0 -181
  546. package/dist/clients/python-environment.js +0 -217
  547. package/dist/clients/python-provenance.js +0 -639
  548. package/dist/clients/quiet-window-config.js +0 -47
  549. package/dist/clients/quiet-window.js +0 -236
  550. package/dist/clients/read-bridge.js +0 -139
  551. package/dist/clients/read-expansion.js +0 -266
  552. package/dist/clients/read-guard-logger.js +0 -251
  553. package/dist/clients/read-guard-tool-lines.js +0 -1306
  554. package/dist/clients/read-guard.js +0 -1561
  555. package/dist/clients/recent-touches.js +0 -240
  556. package/dist/clients/redact/secrets.js +0 -449
  557. package/dist/clients/resource-sampler.js +0 -628
  558. package/dist/clients/reverse-deps.js +0 -270
  559. package/dist/clients/review-graph/builder.js +0 -5118
  560. package/dist/clients/review-graph/format.js +0 -33
  561. package/dist/clients/review-graph/git-identity.js +0 -157
  562. package/dist/clients/review-graph/import-resolvers.js +0 -481
  563. package/dist/clients/review-graph/persist-worker.js +0 -10
  564. package/dist/clients/review-graph/query.js +0 -244
  565. package/dist/clients/review-graph/revision-drift.js +0 -21
  566. package/dist/clients/review-graph/service.js +0 -96
  567. package/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  568. package/dist/clients/review-graph/symbol-id.js +0 -103
  569. package/dist/clients/review-graph/tsconfig-paths.js +0 -299
  570. package/dist/clients/review-graph/types.js +0 -1
  571. package/dist/clients/review-graph/workspace-modules.js +0 -449
  572. package/dist/clients/review-graph-logger.js +0 -81
  573. package/dist/clients/ruff-client.js +0 -152
  574. package/dist/clients/rules-scanner.js +0 -131
  575. package/dist/clients/run-duration.js +0 -55
  576. package/dist/clients/runtime-agent-end.js +0 -746
  577. package/dist/clients/runtime-config.js +0 -77
  578. package/dist/clients/runtime-context.js +0 -211
  579. package/dist/clients/runtime-coordinator.js +0 -1290
  580. package/dist/clients/runtime-session.js +0 -2485
  581. package/dist/clients/runtime-tool-call.js +0 -1284
  582. package/dist/clients/runtime-tool-result.js +0 -2034
  583. package/dist/clients/runtime-turn.js +0 -3600
  584. package/dist/clients/rust-client.js +0 -97
  585. package/dist/clients/safe-spawn.js +0 -2166
  586. package/dist/clients/sanitize.js +0 -286
  587. package/dist/clients/scan-utils.js +0 -25
  588. package/dist/clients/scratch-tree-policy.js +0 -165
  589. package/dist/clients/search-read-registration.js +0 -94
  590. package/dist/clients/secret-findings.js +0 -114
  591. package/dist/clients/security-scan-client.js +0 -352
  592. package/dist/clients/session-event-guard.js +0 -297
  593. package/dist/clients/session-lifecycle.js +0 -484
  594. package/dist/clients/session-start-observability.js +0 -94
  595. package/dist/clients/session-state-store.js +0 -101
  596. package/dist/clients/session-summary.js +0 -15
  597. package/dist/clients/sessionstart-logger.js +0 -27
  598. package/dist/clients/sg-runner.js +0 -876
  599. package/dist/clients/sgconfig.js +0 -347
  600. package/dist/clients/shared-checkout-guard.js +0 -338
  601. package/dist/clients/single-flight.js +0 -158
  602. package/dist/clients/situational-tool-telemetry.js +0 -108
  603. package/dist/clients/skills-resolver.js +0 -141
  604. package/dist/clients/slow-fs.js +0 -138
  605. package/dist/clients/smells-rollup.js +0 -231
  606. package/dist/clients/source-filter.js +0 -596
  607. package/dist/clients/source-walker.js +0 -211
  608. package/dist/clients/spawn-output-cap.js +0 -37
  609. package/dist/clients/spawn-timeout-cooldown.js +0 -110
  610. package/dist/clients/stale-marker.js +0 -17
  611. package/dist/clients/startup-scan.js +0 -400
  612. package/dist/clients/startup-timing.js +0 -130
  613. package/dist/clients/string-utils.js +0 -33
  614. package/dist/clients/subagent-mode.js +0 -111
  615. package/dist/clients/symbol-containment.js +0 -24
  616. package/dist/clients/symbol-types.js +0 -5
  617. package/dist/clients/test-runner-client.js +0 -2507
  618. package/dist/clients/test-runner-delivery.js +0 -287
  619. package/dist/clients/todo-scanner.js +0 -229
  620. package/dist/clients/tool-agreement.js +0 -423
  621. package/dist/clients/tool-config.js +0 -249
  622. package/dist/clients/tool-cwd.js +0 -270
  623. package/dist/clients/tool-definition.js +0 -47
  624. package/dist/clients/tool-event.js +0 -105
  625. package/dist/clients/tool-policy.js +0 -2755
  626. package/dist/clients/tool-probe.js +0 -53
  627. package/dist/clients/tool-render.js +0 -156
  628. package/dist/clients/tool-set-policy.js +0 -118
  629. package/dist/clients/tree-sitter-cache.js +0 -364
  630. package/dist/clients/tree-sitter-client.js +0 -4134
  631. package/dist/clients/tree-sitter-logger.js +0 -100
  632. package/dist/clients/tree-sitter-navigator.js +0 -269
  633. package/dist/clients/tree-sitter-query-loader.js +0 -569
  634. package/dist/clients/tree-sitter-shared.js +0 -158
  635. package/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  636. package/dist/clients/trivy-client.js +0 -467
  637. package/dist/clients/tui-fit.js +0 -50
  638. package/dist/clients/turn-context.js +0 -52
  639. package/dist/clients/turn-summary-render.js +0 -72
  640. package/dist/clients/turn-summary.js +0 -132
  641. package/dist/clients/typos-config.js +0 -29
  642. package/dist/clients/user-notify.js +0 -51
  643. package/dist/clients/vanished-instance-marker.js +0 -57
  644. package/dist/clients/warm-attach.js +0 -246
  645. package/dist/clients/widget-state.js +0 -1677
  646. package/dist/clients/word-index-logger.js +0 -49
  647. package/dist/clients/word-index-store.js +0 -459
  648. package/dist/clients/word-index.js +0 -2080
  649. package/dist/clients/workspace-topology.js +0 -389
  650. package/dist/clients/write-ordering-guard.js +0 -65
  651. package/dist/clients/zizmor-config.js +0 -327
  652. package/dist/i18n.js +0 -69
  653. package/dist/mcp/build-staleness.js +0 -124
  654. package/dist/scripts/lib/process-scan.mjs +0 -583
  655. package/dist/scripts/lib/skills-predicate.mjs +0 -129
  656. package/dist/tools/activate-tools.js +0 -87
  657. package/dist/tools/ast-grep-outline.js +0 -137
  658. package/dist/tools/ast-grep-replace.js +0 -244
  659. package/dist/tools/ast-grep-search.js +0 -762
  660. package/dist/tools/effective-config.js +0 -90
  661. package/dist/tools/lens-diagnostic-mark.js +0 -375
  662. package/dist/tools/lens-diagnostics.js +0 -2544
  663. package/dist/tools/lsp-diagnostics.js +0 -1634
  664. package/dist/tools/lsp-navigation.js +0 -1367
  665. package/dist/tools/lsp-structured-output.js +0 -294
  666. package/dist/tools/module-report.js +0 -382
  667. package/dist/tools/project-report.js +0 -86
  668. package/dist/tools/render-compact.js +0 -374
  669. package/dist/tools/scan-progress.js +0 -53
  670. package/dist/tools/shared.js +0 -29
  671. package/dist/tools/symbol-search.js +0 -155
@@ -1,1284 +0,0 @@
1
- import * as nodeFs from "node:fs";
2
- import * as path from "node:path";
3
- import { loadBootstrapClients, requestBootstrapClients } from "./bootstrap.js";
4
- import { recordDegradationOnce } from "./degradation-ledger.js";
5
- import { detectFileKind } from "./file-kinds.js";
6
- import { isPathIgnoredByProject } from "./file-utils.js";
7
- import { evaluateGitGuard, isGitCommitOrPushAttempt } from "./git-guard.js";
8
- import { dropHashlineAnchorMemo } from "./hashline-anchor.js";
9
- import { evaluateSharedCheckoutGuard } from "./shared-checkout-guard.js";
10
- import { logLatency } from "./latency-logger.js";
11
- import { normalizeMapKey } from "./path-utils.js";
12
- import { captureFileStats, getOpaqueBaselineStore, isGitWorktree, } from "./opaque-mutation-scan.js";
13
- import { normalizeForGuardMatch } from "./host-edit-normalize.js";
14
- import { retargetReplacementIndentation } from "./indent-retarget.js";
15
- import { LANGUAGE_POLICY } from "./language-policy.js";
16
- import { isComplexitySupportedFile } from "./tree-sitter-shared.js";
17
- import { classifyMutatingTool, readMutationPathField, } from "./mutating-tool.js";
18
- import { isProvisionalLearnedAttribution } from "./mutation-attribution.js";
19
- import { armObservedMutation } from "./observed-mutation.js";
20
- import { getLSPService } from "./lsp/index.js";
21
- import { findDocumentSymbolAtLine, getOpenDocumentSymbols, lspSymbolKindName, qualifiedLspSymbolName, } from "./lsp-document-symbols.js";
22
- import { computeTrailingWhitespaceOldTextPatch, findUniqueMatchLineRange, } from "./oldtext-autopatch.js";
23
- import { applyPartiallyApplicableEdits } from "./partial-edit-apply.js";
24
- import { isExternalOrVendorFile, resolveHostPathVariants, resolveHostToolPath, } from "./path-utils.js";
25
- import { EXPANSION_BUDGET_MS, EXPANSION_LIMIT_LINES, tryExpandRead, } from "./read-expansion.js";
26
- import { boundedIndexesForCount, createReadGuardEditBatchSummary, getReadGuardCorrelationId, logReadGuardEvent, } from "./read-guard-logger.js";
27
- import { countFileLines, formatAlreadyAppliedNotes, getTouchedLinesForGuard, relocateEditRange, tryCorrectIndentationMismatch, tryCorrectIndentationMismatchFromContent, } from "./read-guard-tool-lines.js";
28
- import { handleToolResult } from "./runtime-tool-result.js";
29
- import { resolveToolCallCorrelationId } from "./tool-event.js";
30
- import { getSharedTreeSitterClient } from "./tree-sitter-shared.js";
31
- const LSP_TOOLCALL_NAV_TOUCH_BUDGET_MS = Math.max(0, Number.parseInt(process.env.PI_LENS_TOOLCALL_NAV_TOUCH_MS ??
32
- process.env.PI_LENS_LSP_NAV_CLIENT_WAIT_MS ??
33
- "1500", 10) || 1500);
34
- const LSP_TOOLCALL_TOUCH_BUDGET_MS = Math.max(0, Number.parseInt(process.env.PI_LENS_TOOLCALL_TOUCH_MS ?? "750", 10) || 750);
35
- /**
36
- * Composes the final block reason for a partial apply whose commit landed
37
- * (#2402). The committed bytes lead the message; the preflight's per-edit
38
- * failure bodies follow UNCHANGED, so they still describe exactly the edits
39
- * that were NOT applied. The preflight header (🔄/🛑) is never reused here —
40
- * relabelling committed edits as a retryable oldText miss is the #2402 defect.
41
- */
42
- function composePartialApplyReason(args) {
43
- const parts = [];
44
- const editSuffix = args.appliedCount === 1 ? "" : "s";
45
- parts.push(args.postEditStatus === "failed"
46
- ? `⚠️ PARTIAL APPLY — ${args.appliedCount} edit${editSuffix} committed (${args.appliedIndices}). Post-edit analysis failed after the commit; the committed bytes stand and were not reverted. Do NOT resubmit the applied edits.`
47
- : `⚠️ PARTIAL APPLY — ${args.appliedCount} edit${editSuffix} committed (${args.appliedIndices}). Do NOT resubmit the applied edits.`);
48
- if (args.preflightDetails && args.preflightDetails.length > 0) {
49
- parts.push(args.preflightDetails.join("\n\n"));
50
- }
51
- const alreadyNotes = formatAlreadyAppliedNotes(args.alreadyAppliedEdits);
52
- if (alreadyNotes)
53
- parts.push(alreadyNotes.trimStart());
54
- if (args.postEditOutput && args.postEditStatus === "succeeded") {
55
- parts.push(`Post-apply analysis:\n${args.postEditOutput}`);
56
- }
57
- return parts.join("\n\n");
58
- }
59
- function getToolCallRawFilePath(toolName, event) {
60
- const inputObj = (event.input ?? {});
61
- // #2423: the seam owns "does this tool target a file it mutates". No ctx is
62
- // passed — the path is not resolved yet, so adapters must not log here.
63
- const mutation = classifyMutatingTool({ ...event, toolName });
64
- if (mutation)
65
- return mutation.path;
66
- if (toolName === "read") {
67
- if (typeof inputObj.path === "string")
68
- return inputObj.path;
69
- if (typeof inputObj.filePath === "string")
70
- return inputObj.filePath;
71
- return undefined;
72
- }
73
- if (toolName === "lsp_navigation") {
74
- return typeof inputObj.filePath === "string"
75
- ? inputObj.filePath
76
- : undefined;
77
- }
78
- return undefined;
79
- }
80
- /**
81
- * Resolve a tool_call's raw path to the file pi will actually touch.
82
- *
83
- * The `cwd` basis is the host `ctx.cwd`, which for the pinned host IS the same
84
- * value the tools were constructed with — `AgentSession` passes its one `_cwd`
85
- * to both `createAllToolDefinitions`
86
- * (`@earendil-works/pi-coding-agent/dist/core/agent-session.js:2026`, source
87
- * `src/core/tools/index.ts:99-124`) and the `ExtensionRunner` behind `ctx.cwd`
88
- * (`dist/core/agent-session.js:2037`; `runner.js:154`, `:476-479`). Neither is
89
- * reassigned after construction, so there is no drift TODAY (#1655 item 4).
90
- * `tests/clients/pi-host-contract.test.ts` pins that, because the day a host
91
- * lets one move independently, every path here silently retargets.
92
- *
93
- * The path is produced the way pi produces it, in pi's two stages (#1655 item
94
- * 5, plus review F1):
95
- *
96
- * 1. `resolveHostToolPath` mirrors pi's BASE resolution, `resolveToCwd`
97
- * (`dist/core/tools/path-utils.js:42-44`, source
98
- * `src/core/tools/path-utils.ts:~44-46`) — unicode-space folding, `@`
99
- * prefix stripping, `~` expansion, `file://` conversion, and Git-Bash
100
- * drive paths, all BEFORE anything is probed.
101
- * 2. `resolveHostPathVariants` mirrors pi's fallback LADDER
102
- * (`dist/core/tools/path-utils.js:45-70`, source
103
- * `src/core/tools/path-utils.ts:52-83`).
104
- *
105
- * Stage 1 was missing in the first cut of this fix, and the ladder cannot
106
- * stand in for it: an `@`-prefixed path, a `file://` URL, or a non-breaking
107
- * space are all resolved by pi at stage 1 and are untouched by any of the four
108
- * stage-2 candidates. A plain `path.resolve` therefore produced a path pi
109
- * never opens, and the miss looked identical to "file genuinely absent".
110
- */
111
- function resolveToolCallFilePath(rawFilePath, cwd, projectRoot) {
112
- if (!rawFilePath)
113
- return undefined;
114
- return resolveHostPathVariants(resolveHostToolPath(rawFilePath, cwd ?? projectRoot));
115
- }
116
- function getReadToolInput(toolName, input) {
117
- if (toolName !== "read")
118
- return undefined;
119
- return input;
120
- }
121
- function getEffectiveReadLimit(filePath, readInput) {
122
- if (!filePath || !readInput)
123
- return undefined;
124
- const requestedOffset = readInput.offset ?? 1;
125
- const requestedLimit = readInput.limit;
126
- return (requestedLimit ??
127
- Math.max(1, countFileLines(filePath) - requestedOffset + 1));
128
- }
129
- function isLspCapableFile(filePath) {
130
- const kind = detectFileKind(filePath);
131
- if (!kind)
132
- return false;
133
- return LANGUAGE_POLICY[kind]?.lspCapable !== false;
134
- }
135
- function shouldSkipLspAutoTouch(filePath, projectRoot) {
136
- const normalized = path.resolve(filePath).replace(/\\/g, "/").toLowerCase();
137
- const base = path.basename(filePath).toLowerCase();
138
- if (normalized.includes("/.pi-lens/"))
139
- return true;
140
- if (normalized.includes("/.harness/"))
141
- return true;
142
- if (isExternalOrVendorFile(filePath, projectRoot))
143
- return true;
144
- if (base === "stdout.jsonl" ||
145
- base === "stderr.txt" ||
146
- base === "prompt.txt") {
147
- return true;
148
- }
149
- if (base === "case.json" && normalized.includes("/cases/")) {
150
- return true;
151
- }
152
- return false;
153
- }
154
- // Kept in lockstep with the gate's normalizeContent + oldtext-autopatch's
155
- // normalizeOldTextForMatch: the host edit tool's full fuzzy-match space, so the
156
- // autopatch passes count/locate oldText exactly where the host applies it (#257).
157
- function normalizeOldTextForMatch(text) {
158
- return normalizeForGuardMatch(text);
159
- }
160
- function countTextOccurrences(haystack, needle) {
161
- if (!needle)
162
- return 0;
163
- let count = 0;
164
- let pos = 0;
165
- while (pos < haystack.length) {
166
- const idx = haystack.indexOf(needle, pos);
167
- if (idx === -1)
168
- break;
169
- count += 1;
170
- pos = idx + needle.length;
171
- }
172
- return count;
173
- }
174
- function countOldTextMatches(filePath, oldText, cachedNormalizedContent) {
175
- try {
176
- const content = cachedNormalizedContent ??
177
- normalizeOldTextForMatch(nodeFs.readFileSync(filePath, "utf-8"));
178
- return countTextOccurrences(content, normalizeOldTextForMatch(oldText));
179
- }
180
- catch {
181
- return 0;
182
- }
183
- }
184
- function isIndentationOnlyChange(before, after) {
185
- const beforeLines = before.replace(/\r\n/g, "\n").split("\n");
186
- const afterLines = after.replace(/\r\n/g, "\n").split("\n");
187
- if (beforeLines.length !== afterLines.length)
188
- return false;
189
- // Strip both leading and trailing whitespace: consistent with
190
- // findIndentationInsensitiveCandidate which matches via .trimEnd(), so a
191
- // candidate that differs only in trailing whitespace is still indentation-only.
192
- return beforeLines.every((line, index) => line.trim() === afterLines[index].trim());
193
- }
194
- function getNewContentFromToolCall(event) {
195
- const mutation = classifyMutatingTool(event);
196
- if (mutation?.kind === "write") {
197
- return event.input
198
- .content;
199
- }
200
- if (mutation?.kind === "edit") {
201
- const edits = event.input.edits;
202
- return edits?.map((edit) => edit.newText ?? "").join("\n");
203
- }
204
- return undefined;
205
- }
206
- /**
207
- * Total guard around the `tool_call` handler body (#1655 item 1).
208
- *
209
- * `tool_call` is the ONE pi emit path with no per-handler `try`/`catch`. Every
210
- * sibling wraps each handler and routes a throw to `emitError`
211
- * (`emitToolResult` at
212
- * `@earendil-works/pi-coding-agent/dist/core/extensions/runner.js:649-707`,
213
- * source `src/core/extensions/runner.ts:877-930`; the generic `emit` at
214
- * source `runner.ts:801-832`). `emitToolCall`
215
- * (`dist/core/extensions/runner.js:701-717`, source `runner.ts:932-953`) calls
216
- * `await handler(event, ctx)` bare, and its caller
217
- * `AgentSession._installAgentToolHooks`'s `beforeToolCall`
218
- * (`dist/core/agent-session.js:229-241`, source `agent-session.ts:~228-242`)
219
- * rethrows: `throw new Error("Extension failed, blocking execution: ...")`.
220
- *
221
- * So an unguarded throw anywhere in pi-lens's `tool_call` handler does not
222
- * degrade pi-lens — it BLOCKS the user's tool call outright. Advisory
223
- * instrumentation must never be able to do that. Every failure degrades to
224
- * "pi-lens did nothing for this call" (`undefined`, which pi reads as
225
- * "no extension opinion") and lands one ledger entry per tool name.
226
- *
227
- * Returning `undefined` on a throw is deliberate: a partially-executed handler
228
- * cannot have earned a `{ block: true }` verdict, and inventing one would turn
229
- * a pi-lens bug into a refused user tool call — the exact outcome this guard
230
- * exists to prevent.
231
- *
232
- * #1642 F2 (inside the guard): a BLOCKED call (git-guard, read-guard
233
- * preflight, the duplicate-export check) never gets a paired `tool_result` —
234
- * the host never lets the tool execute. Any path attribution recorded for it
235
- * below would otherwise sit in the correlation cache forever (bounded by its
236
- * LRU cap, but pure garbage that can crowd out a live in-flight record under
237
- * enough blocked-edit volume — a reviewer-reproduced leak). Clear it eagerly
238
- * on the way out whenever the call was blocked; a no-op if nothing was
239
- * recorded (a gitignored SKIP is not a block, so it is unaffected). This
240
- * cleanup runs INSIDE the try, so a throw out of it is absorbed too.
241
- */
242
- export async function handleToolCall(deps) {
243
- try {
244
- const result = await handleToolCallImpl(deps);
245
- if (result && result.block) {
246
- // Review F2 — this cleanup gets its OWN catch, and it is not optional
247
- // tidiness. By the time it runs, `result` is a FINAL `{ block: true }`
248
- // verdict: the read guard has already refused an edit-without-read, or
249
- // the git guard has already refused a commit. Letting a throw out of
250
- // bookkeeping reach the outer catch would convert that verdict into
251
- // `undefined` — "pi-lens has no opinion" — and the refused edit would
252
- // execute. A guard that fails OPEN because its telemetry broke is
253
- // worse than no guard, so the verdict is returned regardless of what
254
- // the cleanup does.
255
- try {
256
- const toolCallId = resolveToolCallCorrelationId(deps.event);
257
- if (toolCallId !== undefined) {
258
- deps.runtime.takeToolCallAttribution(toolCallId);
259
- }
260
- }
261
- catch (cleanupError) {
262
- const reason = cleanupError instanceof Error
263
- ? cleanupError.message
264
- : String(cleanupError);
265
- recordDegradationOnce({
266
- kind: "tool-call-handler-throw",
267
- subject: `attribution-cleanup:${deps.event?.toolName ?? "unknown"}`,
268
- reason,
269
- });
270
- try {
271
- deps.dbg?.(`tool_call blocked-attribution cleanup threw (block verdict preserved): ${reason}`);
272
- }
273
- catch {
274
- // A broken `dbg` must not resurrect the throw just absorbed.
275
- }
276
- }
277
- }
278
- return result;
279
- }
280
- catch (error) {
281
- const toolName = deps.event?.toolName ?? "unknown";
282
- const reason = error instanceof Error ? error.message : String(error);
283
- // Once per tool name per session: a wedged handler fires on every call of
284
- // that tool, and the ledger is a health surface, not a log.
285
- recordDegradationOnce({
286
- kind: "tool-call-handler-throw",
287
- subject: toolName,
288
- reason,
289
- });
290
- try {
291
- deps.dbg?.(`tool_call handler threw for ${toolName} (degraded, not blocking): ${reason}`);
292
- if (error instanceof Error && error.stack) {
293
- deps.dbg?.(`tool_call handler stack: ${error.stack}`);
294
- }
295
- }
296
- catch {
297
- // A broken `dbg` must not resurrect the throw this guard just absorbed.
298
- }
299
- // Any attribution already recorded is deliberately LEFT in place: the
300
- // tool now executes (this returns "no opinion"), so its paired
301
- // `tool_result` still arrives and claims the record exactly once.
302
- return undefined;
303
- }
304
- }
305
- async function handleToolCallImpl(deps) {
306
- const { event, ctx, lensEnabled, getFlag, dbg, runtime, cacheManager, ensureLSPConfigInitialized, updateLspStatus, resetLSPService, getTreeSitterClient = getSharedTreeSitterClient, } = deps;
307
- // #2423 review round 4, finding F5: the hashline anchor memo is keyed by
308
- // mtime+size, which a same-size rewrite inside one mtime tick cannot
309
- // distinguish from stale content. It only ever needs to survive the
310
- // several `classifyMutatingTool` asks WITHIN this one tool_call, so drop
311
- // it here at the boundary rather than trusting mtime+size across calls.
312
- dropHashlineAnchorMemo();
313
- const readGuardCorrelationId = getReadGuardCorrelationId(event);
314
- let filePath;
315
- const logToolReadGuardEvent = (entry) => logReadGuardEvent({ ...entry, correlationId: readGuardCorrelationId });
316
- const toolName = event.toolName ?? "";
317
- // #2423: one classification per tool_call, reused by every branch below that
318
- // used to compare `toolName` to the `"write"` / `"edit"` literals. No ctx —
319
- // `filePath` is not resolved yet, and adapters stay silent without one.
320
- const mutation = classifyMutatingTool(event);
321
- const editInputForTelemetry = event.input;
322
- // #1655 item 3: SNAPSHOT the requested batch width here, at handler entry.
323
- // `input` is a live, mutable, extension-ordered object — pi types it
324
- // `Record<string, unknown>` and hands the SAME reference to every handler in
325
- // turn (`@earendil-works/pi-coding-agent/dist/core/extensions/runner.js:701-716`
326
- // passes `event` unchanged through the loop; source
327
- // `src/core/extensions/types.ts:914-919`), and pi-lens itself rewrites it in
328
- // place further down (indent correction, hashline resolution, range
329
- // relocation). `requestedEditIndexes` was already a snapshot; the totals
330
- // below used to RE-READ `editInputForTelemetry.edits.length` from the live
331
- // object hundreds of lines and several `await`s later, so a batch that
332
- // changed width in between produced an internally inconsistent summary —
333
- // indexes from the call-time array, totals from the mutated one.
334
- const requestedEditIndexes = mutation?.kind === "write"
335
- ? [0]
336
- : Array.isArray(editInputForTelemetry?.edits)
337
- ? boundedIndexesForCount(editInputForTelemetry.edits.length)
338
- : [0];
339
- const requestedEditTotal = mutation?.kind === "write"
340
- ? 1
341
- : Array.isArray(editInputForTelemetry?.edits)
342
- ? editInputForTelemetry.edits.length
343
- : 1;
344
- const logBlockedEditSummary = (source) => logToolReadGuardEvent({
345
- event: "edit_batch_summary",
346
- filePath: filePath ?? "",
347
- metadata: {
348
- tool: toolName,
349
- source,
350
- editBatchSummary: createReadGuardEditBatchSummary({
351
- requestedIndexes: requestedEditIndexes,
352
- requestedTotal: requestedEditTotal,
353
- rejectedReasons: requestedEditIndexes.map((index) => ({
354
- index,
355
- code: "preflight_blocked",
356
- })),
357
- rejectedTotal: requestedEditTotal,
358
- terminalStatus: "blocked",
359
- }),
360
- },
361
- });
362
- if (!lensEnabled)
363
- return;
364
- // #2000 phase 2: opaque-command pre-snapshot. A bash command whose writes
365
- // the extractor will not recognize gets a bounded stat snapshot of the
366
- // project source universe BEFORE it runs; the tool_result side diffs it.
367
- // Fire-and-forget capture is WRONG here (the child may finish first), but
368
- // the capture is stat-only and budgeted, so awaiting it is bounded work on
369
- // an already-second-scale bash path.
370
- if (toolName === "bash") {
371
- const commandInput = event.input;
372
- if (typeof commandInput?.command === "string" && commandInput.command) {
373
- const scanRoot = ctx.cwd ?? runtime.projectRoot;
374
- if (scanRoot) {
375
- const started = Date.now();
376
- const rootKey = `${normalizeMapKey(path.resolve(scanRoot))}:${runtime.sessionGeneration}`;
377
- let baseline;
378
- let resultNote;
379
- if (await isGitWorktree(scanRoot)) {
380
- // Git-first: the timestamp IS the baseline; git answers what
381
- // changed, at any repo size, with no file-universe cap.
382
- baseline = { startedAt: started, strategy: "git" };
383
- resultNote = "git";
384
- }
385
- else {
386
- const outcome = await captureFileStats(scanRoot, {
387
- withHashes: true,
388
- });
389
- baseline = outcome.snapshot
390
- ? {
391
- startedAt: started,
392
- strategy: "stat-diff",
393
- stats: outcome.snapshot,
394
- }
395
- : {
396
- startedAt: started,
397
- strategy: "stat-diff",
398
- statsUnknownReason: outcome.unknownReason ?? "walk-failed",
399
- };
400
- resultNote =
401
- outcome.unknownReason ?? `scanned:${outcome.scannedCount}`;
402
- }
403
- // Session-stamped key: a concurrent-secondary session (#473)
404
- // replacing this slot must yield a no-pending-snapshot UNKNOWN
405
- // for us - never a diff against another session's baseline.
406
- getOpaqueBaselineStore().record(rootKey, baseline);
407
- logLatency({
408
- type: "phase",
409
- phase: "opaque_mutation_prescan",
410
- filePath: commandInput.command.slice(0, 80),
411
- durationMs: Date.now() - started,
412
- result: resultNote,
413
- });
414
- }
415
- }
416
- }
417
- // #2430: the observational net. A call the seam could NOT classify, whose
418
- // input still names a file, gets a bounded pre-snapshot so the tool_result
419
- // side can see whether it actually wrote anything. `bash` is excluded
420
- // because it already has its own (wider, git-first) baseline above, and a
421
- // classified mutation never reaches here at all — that is what keeps the
422
- // cost of a plain write/edit at zero.
423
- //
424
- // `isProvisionalLearnedAttribution` is the ONE exception (#2449 review round
425
- // 2, F2): a tool attributed from a single observation IS classified from
426
- // here on, but its attribution has not earned persistence yet, and the only
427
- // thing that can earn it is a second real disk diff. Without this clause the
428
- // tool is classified on call two and never observed again, so
429
- // `PERSIST_AFTER_OBSERVATIONS = 2` is unreachable and nothing is ever
430
- // written to disk for the next session to learn from.
431
- //
432
- // The eligibility check inside `armObservedMutation` is a map lookup, so an
433
- // ineligible tool (durably attributed, or twice observed clean) pays that
434
- // and nothing else. The universe is the TARGET PATH alone, so an armed call
435
- // costs one stat plus one hash of the file it named — not a directory walk.
436
- if ((mutation === undefined || isProvisionalLearnedAttribution(toolName)) &&
437
- toolName !== "bash" &&
438
- !getFlag("no-read-guard")) {
439
- const observedRawPath = readMutationPathField(event);
440
- const observedPath = observedRawPath
441
- ? resolveToolCallFilePath(observedRawPath, ctx.cwd, runtime.projectRoot)
442
- ?.path
443
- : undefined;
444
- if (observedPath) {
445
- await armObservedMutation({
446
- toolCallId: resolveToolCallCorrelationId(event),
447
- toolName,
448
- targetPath: observedPath,
449
- cwd: ctx.cwd ?? runtime.projectRoot,
450
- sessionGeneration: runtime.sessionGeneration,
451
- turnIndex: runtime.turnIndex,
452
- signal: ctx.signal,
453
- dbg,
454
- });
455
- }
456
- }
457
- if (getFlag("lens-guard") &&
458
- isGitCommitOrPushAttempt(toolName, event.input)) {
459
- const guard = evaluateGitGuard(runtime, cacheManager, ctx.cwd ?? runtime.projectRoot, ctx.host ?? "pi");
460
- if (guard.block) {
461
- return {
462
- block: true,
463
- reason: guard.reason,
464
- };
465
- }
466
- }
467
- // #2007: a sibling session's branch switch destroys uncommitted work in a
468
- // shared checkout. Independent of `lens-guard`: that gate is about blocker
469
- // hygiene before a commit, this one is about not deleting a peer's WIP.
470
- // The evaluator short-circuits on a pure string classification, so a
471
- // non-git bash command pays no I/O here.
472
- if (getFlag("lens-checkout-guard")) {
473
- const sharedCheckout = await evaluateSharedCheckoutGuard(toolName, event.input, ctx.cwd ?? runtime.projectRoot ?? process.cwd());
474
- if (sharedCheckout.block) {
475
- return {
476
- block: true,
477
- reason: sharedCheckout.reason,
478
- };
479
- }
480
- }
481
- const rawFilePath = getToolCallRawFilePath(toolName, event);
482
- const pathResolution = resolveToolCallFilePath(rawFilePath, ctx.cwd, runtime.projectRoot);
483
- filePath = pathResolution?.path;
484
- if (pathResolution?.variant) {
485
- dbg(`tool_call: path resolved via host ${pathResolution.variant} variant → ${filePath}`);
486
- }
487
- if (!getFlag("no-lsp")) {
488
- try {
489
- const configCwd = filePath
490
- ? path.dirname(filePath)
491
- : (ctx.cwd ?? runtime.projectRoot ?? process.cwd());
492
- await ensureLSPConfigInitialized(configCwd);
493
- }
494
- catch (cfgErr) {
495
- dbg(`lsp config init failed during tool_call: ${cfgErr}`);
496
- }
497
- }
498
- if (!filePath)
499
- return;
500
- dbg(`tool_call fired for: ${filePath} (exists: ${nodeFs.existsSync(filePath)})`);
501
- const toolCallId = resolveToolCallCorrelationId(event);
502
- const attributesMutationTarget = toolCallId !== undefined && mutation !== undefined;
503
- const targetMissing = !nodeFs.existsSync(filePath);
504
- // #1642 F1: a brand-new file's WRITE is never a "skip" — `tool_call`
505
- // fires PRE-execution, so `existsSync` is false for every path a write
506
- // is about to CREATE. Gitignore is a pure pattern match (no existence
507
- // requirement), so it is checked regardless of `targetMissing`; only an
508
- // actual ignore verdict is a real "refuse". Folding `targetMissing` into
509
- // `skipped` (the pre-fix-round-2 shape of this file) made EVERY new-file
510
- // write's paired tool_result refuse to run diagnostics/autofix/format at
511
- // all — a full pipeline regression worse than the bug #1642 fixes.
512
- const targetIgnored = isPathIgnoredByProject(filePath, runtime.projectRoot, false);
513
- if (attributesMutationTarget) {
514
- // #1642: record the canonical target BY TOOL-CALL IDENTITY — including
515
- // (especially) when it is being skipped here. The paired tool_result
516
- // must take this exact verdict rather than re-deriving its own path
517
- // from relative diff metadata, which is how a gitignored worktree
518
- // edit got re-attributed onto a same-relative-path parent file.
519
- runtime.recordToolCallAttribution(toolCallId, {
520
- resolvedPath: filePath,
521
- skipped: targetIgnored,
522
- originCwd: ctx.cwd ?? runtime.projectRoot,
523
- });
524
- }
525
- if (targetMissing) {
526
- // #1655 item 5: this early return used to be the whole story — pi-lens
527
- // did nothing, said nothing, and the file pi went on to read stayed
528
- // invisible to the read guard, the LSP warm, and the dispatch. The
529
- // variant ladder above now finds the macOS-shaped cases; when it probed
530
- // variants and STILL found nothing, say so rather than returning an
531
- // indistinguishable silence (defect shape 10). `write` is exempt for the
532
- // same reason #1642 F1 gives just above: a write's target legitimately
533
- // does not exist yet.
534
- //
535
- // Review F1: this used to also require `triedVariants.length > 0`, which
536
- // silenced exactly the misses the base-normalization gap produced —
537
- // those paths have no quote and no AM/PM, so every stage-2 candidate
538
- // collapses onto the base path and nothing is "tried". `unresolved` is
539
- // the whole condition; the tried list is detail for the reason string.
540
- if (mutation?.kind !== "write" && pathResolution?.unresolved) {
541
- const tried = pathResolution.triedVariants.length > 0
542
- ? `tried ${pathResolution.triedVariants.join(", ")}`
543
- : "no variant differed from the resolved path";
544
- recordDegradationOnce({
545
- kind: "path-variant-unresolved",
546
- subject: filePath,
547
- reason: `${toolName}: no file at the resolved path, and none of pi's variants matched (${tried})`,
548
- });
549
- }
550
- return;
551
- }
552
- if (targetIgnored) {
553
- dbg(`tool_call: skipping gitignored file ${filePath}`);
554
- return;
555
- }
556
- const isExternalOrVendor = isExternalOrVendorFile(filePath, runtime.projectRoot);
557
- const lspCapableFile = isLspCapableFile(filePath);
558
- const lspAutoTouchSkipped = shouldSkipLspAutoTouch(filePath, runtime.projectRoot);
559
- const lspAutoTouchEligible = lspCapableFile && !lspAutoTouchSkipped;
560
- const shouldWarmReadLsp = toolName === "read" &&
561
- lspAutoTouchEligible &&
562
- runtime.shouldWarmLspOnRead(filePath);
563
- const shouldAutoTouch = (mutation !== undefined ||
564
- toolName === "lsp_navigation" ||
565
- shouldWarmReadLsp) &&
566
- !getFlag("no-lsp") &&
567
- lspAutoTouchEligible;
568
- if (!lspCapableFile && !getFlag("no-lsp")) {
569
- dbg(`lsp auto-touch skipped: ${path.basename(filePath)} (file kind not LSP-capable)`);
570
- }
571
- else if (lspAutoTouchSkipped && !getFlag("no-lsp")) {
572
- dbg(`lsp auto-touch skipped: ${path.basename(filePath)} (internal/support artifact)`);
573
- }
574
- if (toolName === "read" && !getFlag("no-lsp") && !shouldWarmReadLsp) {
575
- const readSkipReason = !lspAutoTouchEligible
576
- ? "file not eligible for LSP warm"
577
- : "already warming or warmed recently";
578
- dbg(`lsp read warm skipped: ${path.basename(filePath)} (${readSkipReason})`);
579
- }
580
- if (shouldAutoTouch) {
581
- try {
582
- const fileContent = nodeFs.readFileSync(filePath, "utf-8");
583
- const maxClientWaitMs = toolName === "lsp_navigation"
584
- ? LSP_TOOLCALL_NAV_TOUCH_BUDGET_MS
585
- : LSP_TOOLCALL_TOUCH_BUDGET_MS;
586
- if (toolName === "read") {
587
- runtime.markLspReadWarmStarted(filePath);
588
- dbg(`lsp read warm started: ${path.basename(filePath)}`);
589
- }
590
- void getLSPService()
591
- .touchFile(filePath, fileContent, {
592
- diagnostics: "none",
593
- source: `tool_call:${toolName}`,
594
- clientScope: "primary",
595
- maxClientWaitMs,
596
- })
597
- .then((result) => {
598
- if (toolName === "read") {
599
- if (result === undefined) {
600
- runtime.clearLspReadWarmState(filePath);
601
- dbg(`lsp read warm unavailable: ${path.basename(filePath)} (no LSP client ready)`);
602
- }
603
- else {
604
- runtime.markLspReadWarmCompleted(filePath);
605
- dbg(`lsp read warm completed: ${path.basename(filePath)}`);
606
- }
607
- }
608
- if (ctx.ui) {
609
- updateLspStatus(ctx.ui.setStatus, ctx.ui.theme);
610
- }
611
- })
612
- .catch((err) => {
613
- if (toolName === "read") {
614
- runtime.clearLspReadWarmState(filePath);
615
- }
616
- dbg(`lsp auto-touch failed for ${filePath}: ${err}`);
617
- });
618
- }
619
- catch {
620
- if (toolName === "read") {
621
- runtime.clearLspReadWarmState(filePath);
622
- }
623
- // Best effort only; never block tool calls.
624
- }
625
- }
626
- const readInput = getReadToolInput(toolName, event.input);
627
- const requestedReadOffset = readInput?.offset ?? 1;
628
- const requestedReadLimit = readInput?.limit;
629
- let effectiveReadOffset = requestedReadOffset;
630
- let effectiveReadLimit = getEffectiveReadLimit(filePath, readInput);
631
- // --- Opportunistic read expansion via tree-sitter ---
632
- // For partial reads (small limit, not from line 1), find the enclosing
633
- // symbol and expand the read range to cover it. This gives the read guard
634
- // accurate symbol-level coverage without requiring an LSP server.
635
- let enclosingSymbol;
636
- const readExpansionClient = toolName === "read" &&
637
- !getFlag("no-lsp") &&
638
- !isExternalOrVendor &&
639
- filePath &&
640
- readInput &&
641
- requestedReadLimit != null &&
642
- requestedReadLimit <= EXPANSION_LIMIT_LINES
643
- ? getTreeSitterClient()
644
- : null;
645
- if (readExpansionClient &&
646
- filePath &&
647
- readInput &&
648
- requestedReadLimit != null) {
649
- const totalLines = effectiveReadLimit != null && requestedReadLimit == null
650
- ? effectiveReadLimit
651
- : countFileLines(filePath);
652
- try {
653
- const expansion = await tryExpandRead(filePath, requestedReadOffset, requestedReadLimit, totalLines, readExpansionClient);
654
- if (expansion) {
655
- readInput.offset = expansion.newOffset;
656
- readInput.limit = expansion.newLimit;
657
- effectiveReadOffset = expansion.newOffset;
658
- effectiveReadLimit = expansion.newLimit;
659
- let enriched = false;
660
- let enrichedAncestry = expansion.ancestry;
661
- const lspSymbols = await getOpenDocumentSymbols(filePath);
662
- const located = lspSymbols
663
- ? findDocumentSymbolAtLine(lspSymbols, expansion.enclosingSymbol.startLine)
664
- : undefined;
665
- if (located) {
666
- enclosingSymbol = {
667
- ...expansion.enclosingSymbol,
668
- name: qualifiedLspSymbolName(located, lspSymbols),
669
- kind: lspSymbolKindName(located.symbol.kind),
670
- };
671
- // Flat SymbolInformation results (native-ts7's real shape) carry
672
- // no hierarchy — an empty LSP ancestry must NOT wipe the real
673
- // tree-sitter chain (#951 review finding 1). Enrich only when
674
- // the LSP actually provided one.
675
- if (located.ancestry.length > 0) {
676
- enrichedAncestry = located.ancestry.map((entry) => ({
677
- name: entry.name,
678
- kind: lspSymbolKindName(entry.kind),
679
- startLine: ((entry.range ?? entry.location?.range)?.start.line ?? 0) + 1,
680
- endLine: ((entry.range ?? entry.location?.range)?.end.line ?? 0) + 1,
681
- }));
682
- }
683
- enriched = true;
684
- }
685
- else {
686
- enclosingSymbol = expansion.enclosingSymbol;
687
- }
688
- logToolReadGuardEvent({
689
- event: "ts_range_expanded",
690
- sessionId: runtime.telemetrySessionId,
691
- filePath,
692
- requestedOffset: requestedReadOffset,
693
- requestedLimit: requestedReadLimit,
694
- effectiveOffset: expansion.newOffset,
695
- effectiveLimit: expansion.newLimit,
696
- symbol: enclosingSymbol.name,
697
- symbolKind: enclosingSymbol.kind,
698
- symbolStartLine: expansion.enclosingSymbol.startLine,
699
- symbolEndLine: expansion.enclosingSymbol.endLine,
700
- metadata: {
701
- enriched,
702
- durationMs: expansion.durationMs,
703
- budgetMs: EXPANSION_BUDGET_MS,
704
- },
705
- });
706
- const symbolPath = [
707
- ...(enrichedAncestry ?? []).map((a) => a.name),
708
- enclosingSymbol.name,
709
- ].join(" → ");
710
- dbg(`ts expanded read: ${path.basename(filePath)} ` +
711
- `lines ${requestedReadOffset}–${requestedReadOffset + requestedReadLimit - 1} ` +
712
- `→ ${symbolPath} ` +
713
- `(${expansion.newOffset}–${expansion.newOffset + expansion.newLimit - 1})`);
714
- }
715
- }
716
- catch {
717
- // Best-effort only.
718
- }
719
- }
720
- // Register the host-resolved path at tool_call. This path is load-bearing for
721
- // the read guard and the observed-mutation settled sweep, including under
722
- // --no-read-guard. The paired tool_result adds the authoritative delivered
723
- // range after the host applies EOF and output-cap clipping (#2802 probe 3).
724
- if (toolName === "read" && filePath && !isExternalOrVendor) {
725
- const totalLines = countFileLines(filePath);
726
- const deliveredLimit = effectiveReadLimit ?? 1;
727
- logToolReadGuardEvent({
728
- event: "read_pattern",
729
- sessionId: runtime.telemetrySessionId,
730
- filePath,
731
- requestedOffset: requestedReadOffset,
732
- requestedLimit: requestedReadLimit ?? deliveredLimit,
733
- effectiveOffset: effectiveReadOffset,
734
- effectiveLimit: deliveredLimit,
735
- metadata: {
736
- totalLines,
737
- isPartial: requestedReadLimit != null && requestedReadLimit < totalLines,
738
- fileKind: detectFileKind(filePath) ?? "unknown",
739
- fractionRead: totalLines > 0
740
- ? Math.round((deliveredLimit / totalLines) * 100) / 100
741
- : 1,
742
- expandedByTs: enclosingSymbol !== undefined,
743
- },
744
- });
745
- runtime.readGuard.recordRead({
746
- filePath,
747
- requestedOffset: requestedReadOffset,
748
- requestedLimit: requestedReadLimit ?? deliveredLimit,
749
- effectiveOffset: effectiveReadOffset,
750
- effectiveLimit: deliveredLimit,
751
- expandedByLsp: enclosingSymbol !== undefined,
752
- ...(enclosingSymbol !== undefined && { enclosingSymbol }),
753
- turnIndex: runtime.turnIndex,
754
- writeIndex: runtime.peekWriteIndex(),
755
- timestamp: Date.now(),
756
- provisional: true,
757
- ...(resolveToolCallCorrelationId(event) !== undefined && {
758
- source: `native-read:${resolveToolCallCorrelationId(event)}:provisional`,
759
- }),
760
- });
761
- }
762
- // Record complexity baseline for historical tracking (booboo/tdi).
763
- // Not shown inline - just captured for delta analysis.
764
- //
765
- // #2467: every client-free guard runs FIRST, so a tool call that cannot
766
- // produce a baseline never loads the analyzer graph. This await used to sit
767
- // above them and fire on EVERY tool_call — a bash command, a grep, a read
768
- // of a vendored file, a second read of an already-baselined file all paid
769
- // the seventeen-module load.
770
- //
771
- // `isComplexitySupportedFile` is the LAST of those guards and the one that
772
- // matters most in a real repo: complexity has node mappings for six
773
- // grammars, so every .md/.json/.yaml/.css/.java/.sh read is unsupported.
774
- // Asking the CLIENT (`isSupportedFile`) would mean loading the graph to
775
- // learn the answer is no — and because only a produced baseline memoizes
776
- // the file, the same read would pay it again on every repeat. The predicate
777
- // is the client's own answer, hoisted into `tree-sitter-shared.ts` where it
778
- // needs nothing but the extension registry; `ComplexityClient.isSupportedFile`
779
- // delegates to it, so this is not a second copy.
780
- if (!getFlag("no-complexity") &&
781
- !isExternalOrVendor &&
782
- filePath &&
783
- !runtime.complexityBaselines.has(filePath) &&
784
- isComplexitySupportedFile(filePath)) {
785
- // Fail open: no clients means no baseline for this call, counted once in
786
- // the ledger. A complexity baseline is delta-analysis bookkeeping — it
787
- // must never be the reason a user's tool call fails or stalls.
788
- const complexityClient = (await requestBootstrapClients({
789
- reason: "tool-call-complexity-baseline",
790
- // The pre-dispatch hook. It carries no wall budget in #2523's
791
- // contract, which is why `LedgerHookKey` names it alongside the
792
- // budgeted families rather than leaving this site to spell its
793
- // own axis value (#2557 review F7).
794
- hook: "tool_call",
795
- // The ambient slot is populated by tool_result, after this hook has
796
- // already run. Use the live tool_call signal so Escape can release
797
- // this await (#2523 AC4).
798
- signal: deps.ctx.signal,
799
- }))?.complexityClient;
800
- const baseline = await complexityClient?.analyzeFile(filePath);
801
- if (baseline) {
802
- runtime.complexityBaselines.set(filePath, baseline);
803
- const { captureSnapshot } = await import("./metrics-history.js");
804
- captureSnapshot(filePath, {
805
- maintainabilityIndex: baseline.maintainabilityIndex,
806
- cognitiveComplexity: baseline.cognitiveComplexity,
807
- maxNestingDepth: baseline.maxNestingDepth,
808
- linesOfCode: baseline.linesOfCode,
809
- maxCyclomatic: baseline.maxCyclomaticComplexity,
810
- entropy: baseline.codeEntropy,
811
- });
812
- }
813
- }
814
- else if (getFlag("no-complexity") &&
815
- filePath &&
816
- isComplexitySupportedFile(filePath)) {
817
- recordDegradationOnce({
818
- kind: "startup-analyzer-disabled",
819
- subject: "complexity",
820
- reason: "skipped (disabled by config)",
821
- });
822
- }
823
- // --- Read-Before-Edit Guard: check edits ---
824
- // write = full replacement; no prior read needed (you're starting fresh).
825
- // edit = partial modification; guard enforced to prevent blind overwrites.
826
- const isEditOnly = mutation?.kind === "edit";
827
- const isWriteOrEdit = mutation !== undefined;
828
- // Track any Write so recordWritten can inject a synthetic read afterward.
829
- // The agent authored the content (new or overwritten), so it trivially "knows" the file.
830
- if (!isEditOnly &&
831
- isWriteOrEdit &&
832
- event.toolName !== "bash" &&
833
- filePath &&
834
- !getFlag("no-read-guard")) {
835
- runtime.readGuard.noteCreatedFile(filePath, runtime.turnIndex, runtime.peekWriteIndex());
836
- }
837
- // --- Indentation mismatch correction ---
838
- // Some models output spaces in oldText when the file uses tabs (or vice versa).
839
- // Detect this before the read guard runs so a recoverable mismatch does not
840
- // degrade into a no-line-info allow path.
841
- if (isEditOnly && filePath) {
842
- const editInput = event.input;
843
- const oldTexts = editInput.oldText
844
- ? [
845
- {
846
- label: "oldText",
847
- value: editInput.oldText,
848
- newText: editInput.newText,
849
- apply: (corrected) => {
850
- editInput.oldText = corrected;
851
- },
852
- applyNewText: (corrected) => {
853
- editInput.newText = corrected;
854
- },
855
- },
856
- ]
857
- : (editInput.edits ?? [])
858
- .map((e, i) => e.oldText
859
- ? {
860
- label: `edits[${i}].oldText`,
861
- value: e.oldText,
862
- newText: e.newText,
863
- apply: (corrected) => {
864
- e.oldText = corrected;
865
- },
866
- applyNewText: (corrected) => {
867
- e.newText = corrected;
868
- },
869
- }
870
- : null)
871
- .filter((entry) => entry !== null);
872
- // Read the file once; derive the two normalized forms needed by
873
- // tryCorrectIndentationMismatchFromContent (CRLF->LF only) and
874
- // countOldTextMatches / the autopatch bridge (host fuzzy-match space).
875
- let crlfContent;
876
- let matchNormalizedContent;
877
- try {
878
- const raw = nodeFs.readFileSync(filePath, "utf-8");
879
- crlfContent = raw.replace(/\r\n/g, "\n");
880
- matchNormalizedContent = normalizeOldTextForMatch(raw);
881
- }
882
- catch {
883
- // File unreadable — corrections will be skipped gracefully below.
884
- }
885
- // --- Pass 0: escaped control-char correction ---
886
- // Models may write literal \n or \t in oldText (JSON interprets them as actual
887
- // newline/tab) when the file has the two-character escape sequences (e.g. inside
888
- // a regex or string literal). Safety gates: original must not match at all;
889
- // escaped version must match exactly once.
890
- if (matchNormalizedContent !== undefined) {
891
- for (const entry of oldTexts) {
892
- const v = entry.value;
893
- if (!v.includes("\t") && !v.includes("\n"))
894
- continue;
895
- if (countOldTextMatches(filePath, v, matchNormalizedContent) !== 0)
896
- continue;
897
- const escaped = v.replace(/\t/g, "\\t").replace(/\n/g, "\\n");
898
- if (escaped === v)
899
- continue;
900
- if (countOldTextMatches(filePath, escaped, matchNormalizedContent) !== 1)
901
- continue;
902
- entry.apply(escaped);
903
- entry.value = escaped;
904
- logToolReadGuardEvent({
905
- event: "oldtext_escape_autopatched",
906
- sessionId: runtime.telemetrySessionId,
907
- filePath,
908
- metadata: { tool: "edit", label: entry.label },
909
- });
910
- }
911
- }
912
- // --- Pass 1: trailing whitespace correction ---
913
- // Editors strip trailing whitespace on save; the model may copy content
914
- // that had it. Safety gates: the original raw oldText must not already
915
- // match, and the stripped raw candidate must match exactly once. When
916
- // trailing empty lines are stripped from oldText, strip the equivalent
917
- // suffix from newText so the replacement span is not accidentally widened.
918
- if (crlfContent !== undefined) {
919
- for (const entry of oldTexts) {
920
- const patch = computeTrailingWhitespaceOldTextPatch({
921
- oldText: entry.value,
922
- newText: entry.newText,
923
- fileContent: crlfContent,
924
- });
925
- if (!patch)
926
- continue;
927
- entry.apply(patch.oldText);
928
- entry.value = patch.oldText;
929
- const newTextPatched = patch.newText !== undefined && patch.newText !== entry.newText;
930
- if (newTextPatched) {
931
- entry.applyNewText(patch.newText);
932
- entry.newText = patch.newText;
933
- }
934
- logToolReadGuardEvent({
935
- event: "oldtext_trailing_ws_autopatched",
936
- sessionId: runtime.telemetrySessionId,
937
- filePath,
938
- metadata: {
939
- tool: "edit",
940
- label: entry.label,
941
- removedLineTrailingWhitespace: patch.removedLineTrailingWhitespace,
942
- removedTrailingEmptyLineCount: patch.removedTrailingEmptyLineCount,
943
- newTextTrailingEmptyLinesPatched: newTextPatched,
944
- },
945
- });
946
- // Bridge: same rationale as the indent autopatch — the
947
- // trailing-ws patcher only applies when the stripped oldText
948
- // matches exactly once against the file, so the agent's text
949
- // reflects real content at the matched span. Register a
950
- // synthetic read covering it so the read-guard downstream
951
- // doesn't fire a zero_read block after the verification.
952
- if (matchNormalizedContent !== undefined && runtime.readGuard) {
953
- const range = findUniqueMatchLineRange(matchNormalizedContent, patch.oldText);
954
- if (range) {
955
- runtime.readGuard.recordRead({
956
- filePath,
957
- requestedOffset: range.startLine,
958
- requestedLimit: range.endLine - range.startLine + 1,
959
- effectiveOffset: range.startLine,
960
- effectiveLimit: range.endLine - range.startLine + 1,
961
- expandedByLsp: false,
962
- turnIndex: runtime.turnIndex,
963
- writeIndex: 0,
964
- timestamp: Date.now(),
965
- });
966
- }
967
- }
968
- }
969
- }
970
- const correctedOldTexts = oldTexts
971
- .map(({ label, value, newText, apply, applyNewText }) => {
972
- const corrected = crlfContent !== undefined
973
- ? tryCorrectIndentationMismatchFromContent(value, crlfContent)
974
- : tryCorrectIndentationMismatch(value, filePath);
975
- return corrected === undefined
976
- ? undefined
977
- : {
978
- label,
979
- value,
980
- newText,
981
- corrected,
982
- apply,
983
- applyNewText,
984
- currentMatchCount: countOldTextMatches(filePath, value, matchNormalizedContent),
985
- correctedMatchCount: countOldTextMatches(filePath, corrected, matchNormalizedContent),
986
- indentationOnly: isIndentationOnlyChange(value, corrected),
987
- };
988
- })
989
- .filter((entry) => entry !== undefined);
990
- // Apply safe corrections individually — each edit stands alone.
991
- // Unsafe corrections (non-indentation-only or ambiguous) fall through
992
- // to resolveOldTextEdits, which handles them per-edit with proper
993
- // oldtext_duplicate / oldtext_not_found reporting and partial apply.
994
- for (const entry of correctedOldTexts) {
995
- if (entry.indentationOnly &&
996
- entry.currentMatchCount === 0 &&
997
- entry.correctedMatchCount === 1) {
998
- entry.apply(entry.corrected);
999
- // The same unique-match span the synthetic-read bridge below
1000
- // resolves, computed once and reused: it also anchors the
1001
- // interior-mask lexer to the real file (#3116 review round 2,
1002
- // F1) so a template literal the oldText fragment crosses the
1003
- // boundary of is read correctly instead of ambiguously — a
1004
- // fragment-only lexer can misread which side of a boundary a
1005
- // line falls on when the fragment doesn't carry the opener or
1006
- // closer that resolves it. Falls back to fragment-only masking
1007
- // inside retargetReplacementIndentation when no unique span is
1008
- // found (file unreadable, or the corrected text isn't unique in
1009
- // the host's fuzzy-match space).
1010
- const matchedRange = matchNormalizedContent !== undefined
1011
- ? findUniqueMatchLineRange(matchNormalizedContent, entry.corrected)
1012
- : undefined;
1013
- const correctedNewText = entry.newText
1014
- ? retargetReplacementIndentation(entry.newText, entry.value, entry.corrected, matchNormalizedContent !== undefined && matchedRange
1015
- ? {
1016
- content: matchNormalizedContent,
1017
- startLine: matchedRange.startLine,
1018
- }
1019
- : undefined)
1020
- : undefined;
1021
- if (correctedNewText !== undefined) {
1022
- entry.applyNewText(correctedNewText);
1023
- }
1024
- logToolReadGuardEvent({
1025
- event: "oldtext_indent_autopatched",
1026
- sessionId: runtime.telemetrySessionId,
1027
- filePath,
1028
- metadata: {
1029
- tool: "edit",
1030
- label: entry.label,
1031
- correctedMatchCount: entry.correctedMatchCount,
1032
- newTextIndentationPatched: correctedNewText !== undefined,
1033
- },
1034
- });
1035
- // Bridge: a unique-match autopatch proves the agent's oldText
1036
- // reflects real content at this span. Register a synthetic read
1037
- // for the matched range so a zero_read block downstream isn't
1038
- // thrown after the autopatch already verified the content.
1039
- if (matchNormalizedContent !== undefined && runtime.readGuard) {
1040
- const range = matchedRange;
1041
- if (range) {
1042
- runtime.readGuard.recordRead({
1043
- filePath,
1044
- requestedOffset: range.startLine,
1045
- requestedLimit: range.endLine - range.startLine + 1,
1046
- effectiveOffset: range.startLine,
1047
- effectiveLimit: range.endLine - range.startLine + 1,
1048
- expandedByLsp: false,
1049
- turnIndex: runtime.turnIndex,
1050
- writeIndex: 0,
1051
- timestamp: Date.now(),
1052
- });
1053
- }
1054
- }
1055
- }
1056
- }
1057
- }
1058
- if (isEditOnly && filePath && !getFlag("no-read-guard")) {
1059
- const readGuard = runtime.readGuard;
1060
- const isExistingFile = typeof readGuard?.isNewFile !== "function" ||
1061
- !readGuard.isNewFile(filePath);
1062
- if (readGuard && isExistingFile && !isExternalOrVendor) {
1063
- const { touchedLines, editRanges, preflightError, preflightDetails, alreadyAppliedEdits, partiallyApplicable, contentMatchValidated, editBatchSummary, } = getTouchedLinesForGuard(event, filePath, runtime.telemetrySessionId, readGuardCorrelationId, runtime.partialApplyRecords);
1064
- if (preflightError) {
1065
- if (partiallyApplicable && partiallyApplicable.length > 0) {
1066
- try {
1067
- const partial = await applyPartiallyApplicableEdits({
1068
- filePath,
1069
- edits: partiallyApplicable,
1070
- summary: editBatchSummary,
1071
- correlationId: readGuardCorrelationId,
1072
- recordStore: runtime.partialApplyRecords,
1073
- afterWrite: async () => {
1074
- const { biomeClient, ruffClient, metricsClient, agentBehaviorClient, } = await loadBootstrapClients();
1075
- const result = await handleToolResult({
1076
- signal: deps.ctx.signal,
1077
- event: {
1078
- toolName: "write",
1079
- input: { path: filePath },
1080
- details: {
1081
- piLensPartialApply: true,
1082
- readGuardCorrelationId,
1083
- },
1084
- content: [],
1085
- // #1655 item 2: `provider`/`model`/`sessionId`/`session`
1086
- // used to be forwarded here from the tool_call event.
1087
- // pi builds a `tool_call` with exactly
1088
- // `type`/`toolName`/`toolCallId`/`input`
1089
- // (`@earendil-works/pi-coding-agent/dist/core/agent-session.js:230-234`,
1090
- // source `src/core/agent-session.ts:~228-236`), so all
1091
- // four were always `undefined` and the tool_result
1092
- // branch they fed was unreachable.
1093
- },
1094
- getFlag: (name) => getFlag(name),
1095
- dbg,
1096
- runtime,
1097
- cacheManager,
1098
- biomeClient,
1099
- ruffClient,
1100
- metricsClient,
1101
- resetLSPService,
1102
- readGuard: runtime.readGuard,
1103
- agentBehaviorRecord: (toolName, analyzedPath) => agentBehaviorClient.recordToolCall(toolName, analyzedPath),
1104
- formatBehaviorWarnings: (warnings) => agentBehaviorClient.formatWarnings(warnings),
1105
- });
1106
- if (result?.isError) {
1107
- throw new Error("post-edit pipeline rejected synthetic partial apply");
1108
- }
1109
- return result?.content
1110
- ?.map((item) => item.text)
1111
- .filter((text) => !!text)
1112
- .join("\n\n");
1113
- },
1114
- });
1115
- if (partial.rejected) {
1116
- // Nothing was written and no post-edit dispatch ran. The
1117
- // preflight verdict still describes the whole batch; append
1118
- // the structured rejection so the agent rebuilds from a
1119
- // fresh read instead of retrying the stale payload.
1120
- if (!editBatchSummary)
1121
- logBlockedEditSummary("preflight");
1122
- return {
1123
- block: true,
1124
- reason: `${preflightError}\n\n🛑 Batch not applied: ${partial.rejected.detail}`,
1125
- };
1126
- }
1127
- if (partial.postEditStatus === "failed") {
1128
- // The commit stands; the post-edit analysis does not. The
1129
- // committed bytes must never be re-labelled by the preflight
1130
- // header (a RETRYABLE/RE-READ line here sends the agent into
1131
- // the #2402 retry loop against already-applied edits).
1132
- return {
1133
- block: true,
1134
- reason: composePartialApplyReason({
1135
- appliedCount: partial.appliedCount,
1136
- appliedIndices: partial.appliedIndices,
1137
- postEditStatus: "failed",
1138
- preflightDetails,
1139
- alreadyAppliedEdits,
1140
- }),
1141
- };
1142
- }
1143
- if (partial.appliedCount > 0) {
1144
- logToolReadGuardEvent({
1145
- event: "edit_partial_apply",
1146
- sessionId: runtime.telemetrySessionId,
1147
- filePath,
1148
- metadata: {
1149
- appliedCount: partial.appliedCount,
1150
- appliedTotal: partial.appliedTotal,
1151
- appliedIndices: partial.appliedIndices,
1152
- editBatchSummary: partial.summary,
1153
- routedThroughPostEditPipeline: true,
1154
- },
1155
- });
1156
- return {
1157
- block: true,
1158
- reason: composePartialApplyReason({
1159
- appliedCount: partial.appliedCount,
1160
- appliedIndices: partial.appliedIndices,
1161
- postEditStatus: "succeeded",
1162
- postEditOutput: partial.postEditOutput,
1163
- preflightDetails,
1164
- alreadyAppliedEdits,
1165
- }),
1166
- };
1167
- }
1168
- }
1169
- catch {
1170
- // commit write failure — fall through to the full preflight block
1171
- }
1172
- }
1173
- if (!editBatchSummary)
1174
- logBlockedEditSummary("preflight");
1175
- return { block: true, reason: preflightError };
1176
- }
1177
- logToolReadGuardEvent({
1178
- event: "edit_check_started",
1179
- sessionId: runtime.telemetrySessionId,
1180
- filePath,
1181
- metadata: {
1182
- tool: mutation?.kind ?? "edit",
1183
- touchedLines: touchedLines ?? null,
1184
- isExistingFile,
1185
- },
1186
- });
1187
- const verdict = typeof readGuard.checkEdit === "function"
1188
- ? readGuard.checkEdit(filePath, touchedLines, editRanges, {
1189
- skipSnapshotCheck: !!contentMatchValidated,
1190
- oldTextResolved: !!contentMatchValidated,
1191
- })
1192
- : { action: "allow" };
1193
- // Content-verified range-stale relocation: the lines the agent meant
1194
- // to edit moved (read-time line hashes uniquely match the new spot),
1195
- // so re-target the positional edit to where the content now lives
1196
- // instead of dead-ending. Safe because the hashes prove the new span
1197
- // IS the intended content — the same guarantee that lets
1198
- // pi-hashline-readmap auto-apply. Single-range only (set by the guard).
1199
- if (verdict.relocation) {
1200
- const relocated = relocateEditRange(event.input, verdict.relocation.from, verdict.relocation.to);
1201
- if (relocated) {
1202
- const [toStart, toEnd] = verdict.relocation.to;
1203
- runtime.readGuard?.recordRead({
1204
- filePath,
1205
- requestedOffset: toStart,
1206
- requestedLimit: toEnd - toStart + 1,
1207
- effectiveOffset: toStart,
1208
- effectiveLimit: toEnd - toStart + 1,
1209
- expandedByLsp: false,
1210
- turnIndex: runtime.turnIndex,
1211
- writeIndex: 0,
1212
- timestamp: Date.now(),
1213
- });
1214
- logToolReadGuardEvent({
1215
- event: "edit_range_relocated",
1216
- sessionId: runtime.telemetrySessionId,
1217
- filePath,
1218
- metadata: {
1219
- tool: "edit",
1220
- from: verdict.relocation.from,
1221
- to: verdict.relocation.to,
1222
- },
1223
- });
1224
- // Relocation applied — let the re-targeted edit proceed.
1225
- }
1226
- else if (verdict.action === "block") {
1227
- logBlockedEditSummary("range_relocated_blocked");
1228
- return { block: true, reason: verdict.reason };
1229
- }
1230
- }
1231
- else if (verdict.action === "block") {
1232
- logBlockedEditSummary("read_guard_blocked");
1233
- return {
1234
- block: true,
1235
- reason: verdict.reason,
1236
- };
1237
- }
1238
- }
1239
- }
1240
- // --- Pre-write duplicate detection ---
1241
- // Check if new content redefines functions that already exist elsewhere.
1242
- // Uses cachedExports (populated at session_start via ast-grep scan).
1243
- if (isWriteOrEdit && runtime.cachedExports.size > 0) {
1244
- const newContent = getNewContentFromToolCall(event);
1245
- if (newContent) {
1246
- const dupeWarnings = [];
1247
- const exportRe = /export\s+(?:async\s+)?(?:function|class|const|let|type|interface)\s+(\w+)/g;
1248
- // Read current on-disk content once so we can check whether the file
1249
- // being written already owns a given export (e.g. it IS the source and
1250
- // another file merely re-exports from it). cachedExports only tracks one
1251
- // file per name — whichever was scanned first — so a re-exporter can
1252
- // win the slot and incorrectly shadow the original definition.
1253
- let currentFileExports;
1254
- if (filePath && nodeFs.existsSync(filePath)) {
1255
- try {
1256
- const currentContent = nodeFs.readFileSync(filePath, "utf-8");
1257
- currentFileExports = new Set();
1258
- for (const m of currentContent.matchAll(exportRe)) {
1259
- currentFileExports.add(m[1]);
1260
- }
1261
- }
1262
- catch {
1263
- // non-fatal — fall back to no current-export knowledge
1264
- }
1265
- }
1266
- for (const match of newContent.matchAll(exportRe)) {
1267
- const name = match[1];
1268
- const existingFile = runtime.cachedExports.get(name);
1269
- if (existingFile &&
1270
- path.resolve(existingFile) !== path.resolve(filePath) &&
1271
- !currentFileExports?.has(name)) {
1272
- dupeWarnings.push(`\`${name}\` already exists in ${path.relative(runtime.projectRoot, existingFile)}`);
1273
- }
1274
- }
1275
- if (dupeWarnings.length > 0) {
1276
- return {
1277
- block: true,
1278
- reason: "🔴 STOP - Redefining existing export(s). Import instead:\n" +
1279
- dupeWarnings.map((w) => " • " + w).join("\n"),
1280
- };
1281
- }
1282
- }
1283
- }
1284
- }