@herbertgao/pi-extensions 2026.10.1 → 2026.10.2

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 (707) hide show
  1. package/README.md +13 -9
  2. package/THIRD_PARTY_NOTICES.md +2 -2
  3. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +2 -2
  4. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +2 -2
  5. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
  6. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +1 -1
  7. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +2 -2
  8. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
  9. package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
  10. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  11. package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
  12. package/node_modules/@dietrichgebert/ponytail/README.md +68 -72
  13. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +7 -0
  14. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
  15. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
  16. package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
  17. package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
  18. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -32
  19. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
  20. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +17 -16
  21. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +9 -10
  22. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -57
  23. package/node_modules/@herbertgao/pi-agency/LICENSE +21 -0
  24. package/node_modules/@herbertgao/pi-agency/README.md +40 -0
  25. package/node_modules/@herbertgao/pi-agency/package.json +53 -0
  26. package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
  32. package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
  33. package/node_modules/@narumitw/pi-caffeinate/package.json +3 -3
  34. package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
  35. package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
  36. package/node_modules/@tifan/pi-recap/package.json +1 -1
  37. package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
  38. package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
  39. package/node_modules/pi-antigravity/README.md +23 -2
  40. package/node_modules/pi-antigravity/package.json +5 -3
  41. package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
  42. package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
  43. package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
  44. package/node_modules/pi-antigravity/src/image/image.ts +86 -47
  45. package/node_modules/pi-antigravity/src/image/native.ts +69 -0
  46. package/node_modules/pi-antigravity/src/index.ts +52 -137
  47. package/node_modules/pi-antigravity/src/search/search.ts +130 -31
  48. package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
  49. package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
  50. package/node_modules/pi-antigravity/src/types/types.ts +1 -1
  51. package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
  52. package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
  53. package/node_modules/pi-lens/CHANGELOG.md +1064 -0
  54. package/node_modules/pi-lens/README.md +115 -25
  55. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
  56. package/node_modules/pi-lens/dist/chunk-22AVIGXS.js +560 -0
  57. package/node_modules/pi-lens/dist/chunk-27BBHSZC.js +106 -0
  58. package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
  59. package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
  60. package/node_modules/pi-lens/dist/chunk-34U7PLGE.js +23 -0
  61. package/node_modules/pi-lens/dist/chunk-3I5QFT3N.js +181 -0
  62. package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
  63. package/node_modules/pi-lens/dist/chunk-47D2BO4S.js +87 -0
  64. package/node_modules/pi-lens/dist/chunk-4RERDEXR.js +348 -0
  65. package/node_modules/pi-lens/dist/chunk-4W2PLC4J.js +27 -0
  66. package/node_modules/pi-lens/dist/chunk-52H5B5TC.js +202 -0
  67. package/node_modules/pi-lens/dist/chunk-54B42YRX.js +27 -0
  68. package/node_modules/pi-lens/dist/chunk-5RXBNSW2.js +266 -0
  69. package/node_modules/pi-lens/dist/chunk-5THXD6X5.js +201 -0
  70. package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
  71. package/node_modules/pi-lens/dist/chunk-5ZEJHV35.js +341 -0
  72. package/node_modules/pi-lens/dist/chunk-6P62QVEA.js +118 -0
  73. package/node_modules/pi-lens/dist/chunk-6QIUX4EP.js +459 -0
  74. package/node_modules/pi-lens/dist/chunk-6VCD4VKZ.js +78 -0
  75. package/node_modules/pi-lens/dist/chunk-7F72PXY2.js +326 -0
  76. package/node_modules/pi-lens/dist/chunk-7FBF7NPR.js +998 -0
  77. package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
  78. package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
  79. package/node_modules/pi-lens/dist/chunk-AHDW3GPE.js +50 -0
  80. package/node_modules/pi-lens/dist/chunk-AJ3QXTRQ.js +756 -0
  81. package/node_modules/pi-lens/dist/chunk-AJVLWNFN.js +1610 -0
  82. package/node_modules/pi-lens/dist/chunk-AJXODZY6.js +112 -0
  83. package/node_modules/pi-lens/dist/chunk-ARFCXG7S.js +84 -0
  84. package/node_modules/pi-lens/dist/chunk-AS44CFQ4.js +708 -0
  85. package/node_modules/pi-lens/dist/chunk-AWE5TMPO.js +38 -0
  86. package/node_modules/pi-lens/dist/chunk-B3I2UUT4.js +4692 -0
  87. package/node_modules/pi-lens/dist/chunk-BRJKOBZP.js +331 -0
  88. package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
  89. package/node_modules/pi-lens/dist/chunk-C2AUYXQH.js +149 -0
  90. package/node_modules/pi-lens/dist/chunk-CGGAIP7J.js +29 -0
  91. package/node_modules/pi-lens/dist/chunk-D6QS3U6M.js +30 -0
  92. package/node_modules/pi-lens/dist/chunk-DFVMIKMQ.js +349 -0
  93. package/node_modules/pi-lens/dist/chunk-DIULIWKU.js +169 -0
  94. package/node_modules/pi-lens/dist/chunk-DQ2NZ7C4.js +2317 -0
  95. package/node_modules/pi-lens/dist/chunk-E2WFBRHM.js +213 -0
  96. package/node_modules/pi-lens/dist/chunk-E4MAEDPM.js +50 -0
  97. package/node_modules/pi-lens/dist/chunk-EOKRQ3SA.js +30 -0
  98. package/node_modules/pi-lens/dist/chunk-F3AU2TMM.js +2626 -0
  99. package/node_modules/pi-lens/dist/chunk-F3P7VQ7H.js +170 -0
  100. package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
  101. package/node_modules/pi-lens/dist/chunk-FJNKANOS.js +32 -0
  102. package/node_modules/pi-lens/dist/chunk-GUOGVTCQ.js +146 -0
  103. package/node_modules/pi-lens/dist/chunk-GVXA6CCE.js +18 -0
  104. package/node_modules/pi-lens/dist/chunk-HHYAELLJ.js +2334 -0
  105. package/node_modules/pi-lens/dist/chunk-HMFXILPV.js +83 -0
  106. package/node_modules/pi-lens/dist/chunk-HVUZXSWK.js +421 -0
  107. package/node_modules/pi-lens/dist/chunk-HWBZIGMJ.js +62 -0
  108. package/node_modules/pi-lens/dist/chunk-I3R7GKDR.js +17 -0
  109. package/node_modules/pi-lens/dist/chunk-I7SPGOWI.js +55 -0
  110. package/node_modules/pi-lens/dist/chunk-IIXOAGHR.js +38 -0
  111. package/node_modules/pi-lens/dist/chunk-IVP5KAB7.js +2250 -0
  112. package/node_modules/pi-lens/dist/chunk-IXEBJSUE.js +22 -0
  113. package/node_modules/pi-lens/dist/chunk-JOLITGCC.js +27 -0
  114. package/node_modules/pi-lens/dist/chunk-K6VYLB2U.js +145 -0
  115. package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
  116. package/node_modules/pi-lens/dist/chunk-LNAKEO63.js +78 -0
  117. package/node_modules/pi-lens/dist/chunk-LSMJ6KBE.js +4139 -0
  118. package/node_modules/pi-lens/dist/chunk-LW6XKDZQ.js +5565 -0
  119. package/node_modules/pi-lens/dist/chunk-N3YQJI6O.js +899 -0
  120. package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
  121. package/node_modules/pi-lens/dist/chunk-NKOUFPQF.js +659 -0
  122. package/node_modules/pi-lens/dist/chunk-NL5GLMFI.js +211 -0
  123. package/node_modules/pi-lens/dist/chunk-NMABWBZN.js +327 -0
  124. package/node_modules/pi-lens/dist/chunk-NTHH2J74.js +27 -0
  125. package/node_modules/pi-lens/dist/chunk-NTUJEGDZ.js +59 -0
  126. package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
  127. package/node_modules/pi-lens/dist/chunk-NXPS2L6U.js +466 -0
  128. package/node_modules/pi-lens/dist/chunk-O5WILGMI.js +42 -0
  129. package/node_modules/pi-lens/dist/chunk-O6TQT6RI.js +48 -0
  130. package/node_modules/pi-lens/dist/chunk-OAXNDZJ7.js +323 -0
  131. package/node_modules/pi-lens/dist/chunk-ODEEOXPH.js +44 -0
  132. package/node_modules/pi-lens/dist/chunk-OQGFCZJ4.js +4533 -0
  133. package/node_modules/pi-lens/dist/chunk-ORTMY4Q4.js +282 -0
  134. package/node_modules/pi-lens/dist/chunk-OS32RNR2.js +442 -0
  135. package/node_modules/pi-lens/dist/chunk-PAK6IDME.js +189 -0
  136. package/node_modules/pi-lens/dist/chunk-PWFHBTSF.js +406 -0
  137. package/node_modules/pi-lens/dist/chunk-Q5U6FMDI.js +2352 -0
  138. package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
  139. package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
  140. package/node_modules/pi-lens/dist/chunk-RIISMHKN.js +36 -0
  141. package/node_modules/pi-lens/dist/chunk-RPD4ZSB7.js +25 -0
  142. package/node_modules/pi-lens/dist/chunk-RPD5K5NC.js +79 -0
  143. package/node_modules/pi-lens/dist/chunk-RUOG5D2C.js +262 -0
  144. package/node_modules/pi-lens/dist/chunk-T4ERT73Y.js +599 -0
  145. package/node_modules/pi-lens/dist/chunk-T6D4Q4AQ.js +54 -0
  146. package/node_modules/pi-lens/dist/chunk-TGPXLY2J.js +391 -0
  147. package/node_modules/pi-lens/dist/chunk-TLGFBM3F.js +12116 -0
  148. package/node_modules/pi-lens/dist/chunk-TO6WL3GM.js +88 -0
  149. package/node_modules/pi-lens/dist/chunk-TT22N74J.js +517 -0
  150. package/node_modules/pi-lens/dist/chunk-TVN5N6GY.js +21216 -0
  151. package/node_modules/pi-lens/dist/chunk-U3VGR4VU.js +308 -0
  152. package/node_modules/pi-lens/dist/chunk-UK3CMEAL.js +1250 -0
  153. package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
  154. package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
  155. package/node_modules/pi-lens/dist/chunk-UY752QSN.js +45 -0
  156. package/node_modules/pi-lens/dist/chunk-V5PXFSZK.js +188 -0
  157. package/node_modules/pi-lens/dist/chunk-V7R425XG.js +398 -0
  158. package/node_modules/pi-lens/dist/chunk-VCSRSYLF.js +81 -0
  159. package/node_modules/pi-lens/dist/chunk-VD4JQCQC.js +162 -0
  160. package/node_modules/pi-lens/dist/chunk-VDBPVIKD.js +62 -0
  161. package/node_modules/pi-lens/dist/chunk-VHQLPIRJ.js +114 -0
  162. package/node_modules/pi-lens/dist/chunk-VN2AXLEX.js +5414 -0
  163. package/node_modules/pi-lens/dist/chunk-VNMT7C64.js +478 -0
  164. package/node_modules/pi-lens/dist/chunk-WONERJTP.js +309 -0
  165. package/node_modules/pi-lens/dist/chunk-XECLWQZN.js +85 -0
  166. package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
  167. package/node_modules/pi-lens/dist/chunk-XK4CIILI.js +144 -0
  168. package/node_modules/pi-lens/dist/chunk-Y4T27COV.js +562 -0
  169. package/node_modules/pi-lens/dist/chunk-YGVYKNSN.js +1860 -0
  170. package/node_modules/pi-lens/dist/chunk-YLF3PPT5.js +308 -0
  171. package/node_modules/pi-lens/dist/chunk-YSNFMJKN.js +14 -0
  172. package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
  173. package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
  174. package/node_modules/pi-lens/dist/chunk-ZAWW3AF6.js +97 -0
  175. package/node_modules/pi-lens/dist/chunk-ZMOL6RQC.js +167 -0
  176. package/node_modules/pi-lens/dist/index.js +16456 -8335
  177. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
  178. package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
  179. package/node_modules/pi-lens/dist/mcp/server.js +22589 -1573
  180. package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
  181. package/node_modules/pi-lens/dist/probes/installer.js +135 -0
  182. package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
  183. package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
  184. package/node_modules/pi-lens/docs/agent-guide.md +13 -4
  185. package/node_modules/pi-lens/docs/agent-tools.md +42 -0
  186. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +3 -2
  187. package/node_modules/pi-lens/docs/configuration.md +20 -1
  188. package/node_modules/pi-lens/docs/environment-variables.md +52 -2
  189. package/node_modules/pi-lens/docs/features.md +8 -4
  190. package/node_modules/pi-lens/docs/globalconfig.md +1 -0
  191. package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
  192. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +36 -4
  193. package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
  194. package/node_modules/pi-lens/docs/mcp.md +21 -5
  195. package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
  196. package/node_modules/pi-lens/docs/pi-lens-fixer.md +156 -88
  197. package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
  198. package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +694 -0
  199. package/node_modules/pi-lens/docs/pi-lens-monitor.md +73 -7
  200. package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
  201. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +188 -48
  202. package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
  203. package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
  204. package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
  205. package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
  206. package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  207. package/node_modules/pi-lens/docs/servercapabilities.md +53 -49
  208. package/node_modules/pi-lens/docs/settings.md +1 -0
  209. package/node_modules/pi-lens/docs/usage.md +5 -1
  210. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
  211. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
  212. package/node_modules/pi-lens/package.json +18 -6
  213. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  214. package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  215. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
  216. package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
  217. package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
  218. package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
  219. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
  220. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
  221. package/package.json +15 -10
  222. package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -212
  223. package/node_modules/@dietrichgebert/ponytail/README.ja.md +0 -212
  224. package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -212
  225. package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +0 -212
  226. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
  227. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
  228. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
  229. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
  230. package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
  231. package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
  232. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
  233. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
  234. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
  235. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
  236. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
  237. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
  238. package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
  239. package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
  240. package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
  241. package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
  242. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
  243. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
  244. package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
  245. package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
  246. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
  247. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
  248. package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
  249. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
  250. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
  251. package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
  252. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
  253. package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
  254. package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
  255. package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
  256. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
  257. package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
  258. package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
  259. package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
  260. package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
  261. package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
  262. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
  263. package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
  264. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
  265. package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
  266. package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
  267. package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
  268. package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
  269. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
  270. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
  271. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
  272. package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
  273. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
  274. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
  275. package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
  276. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
  277. package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
  278. package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
  279. package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
  280. package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
  281. package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
  282. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
  283. package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
  284. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
  285. package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
  286. package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
  287. package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
  288. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
  289. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
  290. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
  291. package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
  292. package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
  293. package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
  294. package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
  295. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
  296. package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
  297. package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
  298. package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
  299. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
  300. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
  301. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
  302. package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
  303. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
  304. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  305. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  306. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
  307. package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  308. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
  309. package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
  310. package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
  311. package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
  312. package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
  313. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
  314. package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
  315. package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
  316. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
  317. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
  318. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  319. package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  320. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
  321. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
  322. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
  323. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
  324. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
  325. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
  326. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
  327. package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
  328. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
  329. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
  330. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
  331. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
  332. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  333. package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  334. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  335. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  336. package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  337. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
  338. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  339. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  340. package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  341. package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  342. package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  343. package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  344. package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  345. package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
  346. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
  347. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  348. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
  349. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
  350. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
  351. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
  352. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
  353. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
  354. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
  355. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
  356. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
  357. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
  358. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
  359. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
  360. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
  361. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
  362. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
  363. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
  364. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
  365. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
  366. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
  367. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
  368. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
  369. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
  370. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
  371. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
  372. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
  373. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
  374. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
  375. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  376. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  377. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
  378. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
  379. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
  380. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
  381. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
  382. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
  383. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
  384. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
  385. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
  386. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
  387. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
  388. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
  389. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
  390. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
  391. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
  392. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
  393. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  394. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  395. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
  396. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  397. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  398. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  399. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  400. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  401. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
  402. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
  403. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  404. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
  405. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
  406. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
  407. package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
  408. package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
  409. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
  410. package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  411. package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
  412. package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
  413. package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
  414. package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
  415. package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
  416. package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
  417. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
  418. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
  419. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
  420. package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
  421. package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
  422. package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
  423. package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
  424. package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
  425. package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
  426. package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
  427. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
  428. package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
  429. package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
  430. package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
  431. package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
  432. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
  433. package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
  434. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
  435. package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
  436. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
  437. package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
  438. package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
  439. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
  440. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
  441. package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
  442. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
  443. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
  444. package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
  445. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
  446. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
  447. package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
  448. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
  449. package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
  450. package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
  451. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
  452. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
  453. package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
  454. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  455. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
  456. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
  457. package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
  458. package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
  459. package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
  460. package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
  461. package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
  462. package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
  463. package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
  464. package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
  465. package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
  466. package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
  467. package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
  468. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
  469. package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
  470. package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
  471. package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
  472. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
  473. package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
  474. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
  475. package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
  476. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
  477. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
  478. package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
  479. package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
  480. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
  481. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
  482. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
  483. package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
  484. package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
  485. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
  486. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
  487. package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
  488. package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
  489. package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
  490. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
  491. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
  492. package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
  493. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  494. package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
  495. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
  496. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
  497. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
  498. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
  499. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  500. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
  501. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
  502. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
  503. package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
  504. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  505. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  506. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  507. package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
  508. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
  509. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
  510. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
  511. package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
  512. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
  513. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
  514. package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
  515. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
  516. package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
  517. package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
  518. package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
  519. package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
  520. package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
  521. package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
  522. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
  523. package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
  524. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
  525. package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
  526. package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
  527. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
  528. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
  529. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
  530. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
  531. package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
  532. package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
  533. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
  534. package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
  535. package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
  536. package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
  537. package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
  538. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
  539. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
  540. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
  541. package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
  542. package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
  543. package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
  544. package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
  545. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
  546. package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
  547. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
  548. package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
  549. package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
  550. package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
  551. package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
  552. package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
  553. package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
  554. package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
  555. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
  556. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
  557. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
  558. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  559. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  560. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  561. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  562. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  563. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  564. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  565. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  566. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  567. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  568. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
  569. package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
  570. package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
  571. package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
  572. package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
  573. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
  574. package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
  575. package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
  576. package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
  577. package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
  578. package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
  579. package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
  580. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
  581. package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
  582. package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
  583. package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
  584. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
  585. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
  586. package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
  587. package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
  588. package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
  589. package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
  590. package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
  591. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
  592. package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
  593. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
  594. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
  595. package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
  596. package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
  597. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
  598. package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
  599. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  600. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
  601. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
  602. package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
  603. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
  604. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
  605. package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
  606. package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
  607. package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
  608. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
  609. package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
  610. package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
  611. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
  612. package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
  613. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
  614. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
  615. package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
  616. package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
  617. package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
  618. package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
  619. package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
  620. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
  621. package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
  622. package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
  623. package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
  624. package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
  625. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
  626. package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
  627. package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
  628. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
  629. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
  630. package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
  631. package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
  632. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
  633. package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
  634. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
  635. package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
  636. package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
  637. package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
  638. package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
  639. package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
  640. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
  641. package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
  642. package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
  643. package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
  644. package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
  645. package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
  646. package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
  647. package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
  648. package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
  649. package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
  650. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
  651. package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
  652. package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
  653. package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
  654. package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
  655. package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
  656. package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
  657. package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
  658. package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
  659. package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
  660. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
  661. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
  662. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
  663. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
  664. package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
  665. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
  666. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
  667. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  668. package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
  669. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
  670. package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
  671. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
  672. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
  673. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
  674. package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
  675. package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
  676. package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
  677. package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
  678. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
  679. package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
  680. package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
  681. package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
  682. package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
  683. package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
  684. package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
  685. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
  686. package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
  687. package/node_modules/pi-lens/dist/i18n.js +0 -69
  688. package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
  689. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
  690. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
  691. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
  692. package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
  693. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
  694. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
  695. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
  696. package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
  697. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
  698. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
  699. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
  700. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
  701. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
  702. package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
  703. package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
  704. package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
  705. package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
  706. package/node_modules/pi-lens/dist/tools/shared.js +0 -29
  707. package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
@@ -1,1561 +0,0 @@
1
- /**
2
- * Read-Before-Edit Guard for pi-lens
3
- *
4
- * Blocks edits that lack adequate prior reading:
5
- * 1. Zero-read edit: never read this file in this branch
6
- * 2. File modified since read: disk content changed (FileTime)
7
- * 3. Out-of-range edit: edit target not covered by any previous read
8
- * 4. LSP expansion exemption: single-line read expanded to full symbol counts
9
- *
10
- * Falls back safely when LSP is unavailable.
11
- */
12
- import * as fs from "node:fs";
13
- import { incrementDegradationCount } from "./degradation-ledger.js";
14
- import { createFileTime } from "./file-time.js";
15
- import { hashDiagnosticContent } from "./lsp/diagnostic-binding.js";
16
- import { normalizeEphemeralMapKey, normalizeFilePath } from "./path-utils.js";
17
- import { logReadGuardEvent } from "./read-guard-logger.js";
18
- export const READ_GUARD_STATE_VERSION = 1;
19
- // --- Constants ---
20
- const DEFAULT_CONFIG = {
21
- enabled: true,
22
- mode: "block",
23
- contextLines: 3,
24
- exemptions: [
25
- { pattern: "*.md", mode: "warn" },
26
- { pattern: "*.txt", mode: "allow" },
27
- { pattern: "*.log", mode: "allow" },
28
- ],
29
- };
30
- const OWN_EDIT_STALE_GRACE_MS = Math.max(0, Number.parseInt(process.env.PI_LENS_READ_GUARD_OWN_EDIT_GRACE_MS ?? "120000", 10) || 120000);
31
- /** Avoid hashing very large reads in the hot path. */
32
- const READ_HASH_MAX_LINES = Math.max(0, Number.parseInt(process.env.PI_LENS_READ_GUARD_HASH_MAX_LINES ?? "3000", 10) || 3000);
33
- /**
34
- * Content bindings are defense in depth on a read-adjacent hot path. Cap the
35
- * synchronous disk read so bridge registration never hashes an unbounded file.
36
- */
37
- const READ_BINDING_MAX_BYTES = 4 * 1024 * 1024;
38
- const READ_GUARD_MAX_FILES = 256;
39
- /**
40
- * #1904 item 3: `enforceFileCap` bounds how many FILES the store holds, not how
41
- * many records each file holds. One hot file reached 268 hash-bearing records in
42
- * 75 minutes, and every record carries a `lineHashes` map, so a long session on
43
- * a few files grows without bound. Cap records per file and drop oldest-first.
44
- *
45
- * Eviction is safe in the blocking direction: it narrows the staleness-rescue
46
- * path — a stale edit rescued by an older snapshot that still matches — and can
47
- * turn an allow into a "re-read and retry" block. It never turns a block into
48
- * an allow. See `enforceRecordCapForFile` for which records go first, and why
49
- * age alone is the wrong order.
50
- */
51
- const READ_GUARD_MAX_RECORDS_PER_FILE = 128;
52
- const NO_TRIM = {
53
- evictedCount: 0,
54
- evictedCreditCount: 0,
55
- evictedGenuineCount: 0,
56
- };
57
- function enforceRecordCapForFile(records) {
58
- if (records.length <= READ_GUARD_MAX_RECORDS_PER_FILE)
59
- return NO_TRIM;
60
- const excess = records.length - READ_GUARD_MAX_RECORDS_PER_FILE;
61
- const evicted = new Set();
62
- let evictedCreditCount = 0;
63
- for (let i = 0; i < records.length && evicted.size < excess; i++) {
64
- if (records[i].searchCredit !== undefined) {
65
- evicted.add(i);
66
- evictedCreditCount++;
67
- }
68
- }
69
- for (let i = 0; i < records.length && evicted.size < excess; i++) {
70
- evicted.add(i);
71
- }
72
- const kept = records.filter((_, i) => !evicted.has(i));
73
- records.length = 0;
74
- for (const record of kept)
75
- records.push(record);
76
- return {
77
- evictedCount: excess,
78
- evictedCreditCount,
79
- evictedGenuineCount: excess - evictedCreditCount,
80
- };
81
- }
82
- /**
83
- * #1904 class sweep: `this.edits` is the same shape as `this.reads` — a
84
- * per-file array that only ever grows. Its cap sits far above every consumer's
85
- * reach, so trimming is inert: `canTreatStalenessAsOwnPriorEdit` reads only the
86
- * last record, and `findRelocation`'s window saturates at
87
- * RELOCATION_WINDOW_MAX / RELOCATION_WINDOW_PER_EDIT (20) applied edits. Only
88
- * `getStats`, a debug surface, sees the older records at all.
89
- */
90
- const READ_GUARD_MAX_EDITS_PER_FILE = 256;
91
- // Unconsumed reads remain valid until edit or session end, but this high
92
- // sanity cap prevents a read-only session from growing without bound.
93
- const READ_GUARD_MAX_UNCONSUMED_FILES = 4096;
94
- const READ_GUARD_IDLE_EVICT_MS_DEFAULT = 30 * 60_000;
95
- export function captureReadContentBinding(filePath, offset, limit) {
96
- try {
97
- if (fs.statSync(filePath).size > READ_BINDING_MAX_BYTES)
98
- return undefined;
99
- const content = fs.readFileSync(filePath, "utf-8");
100
- // Allocate at most the classification prefix first. For range bindings,
101
- // splitting then stops at the requested range end rather than the EOF.
102
- const lines = splitLinesThrough(content, READ_HASH_MAX_LINES + 1);
103
- if (lines.length <= READ_HASH_MAX_LINES) {
104
- return {
105
- hash: hashDiagnosticContent(content),
106
- fullFile: true,
107
- offset: 1,
108
- limit: lines.length,
109
- };
110
- }
111
- const scopedOffset = Math.max(1, offset);
112
- const scopedLimit = Math.min(Math.max(0, limit), READ_HASH_MAX_LINES);
113
- if (scopedLimit === 0)
114
- return undefined;
115
- const rangeLines = splitLinesThrough(content, scopedOffset - 1 + scopedLimit);
116
- const scopedContent = rangeLines
117
- .slice(scopedOffset - 1, scopedOffset - 1 + scopedLimit)
118
- .join("\n");
119
- return {
120
- hash: hashDiagnosticContent(scopedContent),
121
- fullFile: false,
122
- offset: scopedOffset,
123
- limit: scopedLimit,
124
- };
125
- }
126
- catch {
127
- return undefined;
128
- }
129
- }
130
- export function _currentContentMatchesBindingForTests(filePath, binding) {
131
- try {
132
- const content = fs.readFileSync(filePath, "utf-8");
133
- const boundContent = binding.fullFile
134
- ? content
135
- : splitLines(content)
136
- .slice(binding.offset - 1, binding.offset - 1 + binding.limit)
137
- .join("\n");
138
- return hashDiagnosticContent(boundContent) === binding.hash;
139
- }
140
- catch {
141
- return false;
142
- }
143
- }
144
- const currentContentMatchesBinding = _currentContentMatchesBindingForTests;
145
- // Adaptive relocation window (findRelocation). A globally-unique hash-sequence
146
- // match always wins; when the content is duplicated elsewhere, we fall back to
147
- // a match that is unique WITHIN this window of the original position. The window
148
- // widens with edits already applied to the file (accumulated line drift) —
149
- // floor + per-edit growth, capped — the analog of pi-hashline-readmap's
150
- // edits-scaled relocation window.
151
- const RELOCATION_WINDOW_MIN = Math.max(1, Number.parseInt(process.env.PI_LENS_READ_GUARD_RELOCATION_WINDOW_MIN ?? "40", 10) || 40);
152
- const RELOCATION_WINDOW_PER_EDIT = Math.max(0, Number.parseInt(process.env.PI_LENS_READ_GUARD_RELOCATION_WINDOW_PER_EDIT ?? "20", 10) || 20);
153
- const RELOCATION_WINDOW_MAX = Math.max(RELOCATION_WINDOW_MIN, Number.parseInt(process.env.PI_LENS_READ_GUARD_RELOCATION_WINDOW_MAX ?? "400", 10) || 400);
154
- function splitLines(text) {
155
- return text.split(/\r?\n/);
156
- }
157
- /** `splitLines` semantics, but without scanning/allocating beyond `maxLines`. */
158
- function splitLinesThrough(text, maxLines) {
159
- if (maxLines <= 0)
160
- return [];
161
- const lines = [];
162
- let start = 0;
163
- while (lines.length < maxLines) {
164
- const newline = text.indexOf("\n", start);
165
- if (newline === -1) {
166
- lines.push(text.slice(start));
167
- break;
168
- }
169
- const end = newline > start && text[newline - 1] === "\r" ? newline - 1 : newline;
170
- lines.push(text.slice(start, end));
171
- start = newline + 1;
172
- if (start === text.length && lines.length < maxLines) {
173
- lines.push("");
174
- break;
175
- }
176
- }
177
- return lines;
178
- }
179
- export function lineContentHash(line) {
180
- // FNV-1a over whitespace-stripped content. This treats no-op formatter/touch
181
- // changes as still-valid context while detecting semantic line changes.
182
- const normalized = line.replace(/\s+/g, "");
183
- let hash = 2166136261;
184
- for (let i = 0; i < normalized.length; i++) {
185
- hash = Math.imul(hash ^ normalized.charCodeAt(i), 16777619);
186
- }
187
- return (hash >>> 0).toString(16).padStart(8, "0");
188
- }
189
- function readRangeCoversLine(read, lineNo) {
190
- return (lineNo >= read.effectiveOffset &&
191
- lineNo <= read.effectiveOffset + read.effectiveLimit - 1);
192
- }
193
- function readEffectiveRangeCoversRange(read, [startLine, endLine]) {
194
- return (readRangeCoversLine(read, startLine) && readRangeCoversLine(read, endLine));
195
- }
196
- function captureLineHashes(filePath, offset, limit) {
197
- if (limit <= 0 || limit > READ_HASH_MAX_LINES)
198
- return undefined;
199
- try {
200
- const lines = splitLines(fs.readFileSync(filePath, "utf-8"));
201
- const hashes = {};
202
- const end = Math.min(lines.length, offset + limit - 1);
203
- for (let lineNo = Math.max(1, offset); lineNo <= end; lineNo++) {
204
- hashes[lineNo] = lineContentHash(lines[lineNo - 1] ?? "");
205
- }
206
- return Object.keys(hashes).length > 0 ? hashes : undefined;
207
- }
208
- catch {
209
- return undefined;
210
- }
211
- }
212
- export function currentLinesMatchReadSnapshot(filePath, read, [startLine, endLine]) {
213
- const hashes = read.lineHashes ?? {};
214
- const missingLines = [];
215
- const mismatchedLines = [];
216
- for (let lineNo = startLine; lineNo <= endLine; lineNo += 1) {
217
- if (!readRangeCoversLine(read, lineNo) || hashes[lineNo] === undefined) {
218
- missingLines.push(lineNo);
219
- }
220
- }
221
- if (missingLines.length > 0) {
222
- return { checked: false, matches: false, missingLines, mismatchedLines };
223
- }
224
- let lines;
225
- try {
226
- lines = splitLines(fs.readFileSync(filePath, "utf-8"));
227
- }
228
- catch {
229
- return {
230
- checked: true,
231
- matches: false,
232
- missingLines,
233
- mismatchedLines: [...Array(endLine - startLine + 1)].map((_, index) => startLine + index),
234
- };
235
- }
236
- for (let lineNo = startLine; lineNo <= endLine; lineNo += 1) {
237
- if (lineNo < 1 || lineNo > lines.length) {
238
- mismatchedLines.push(lineNo);
239
- continue;
240
- }
241
- if (lineContentHash(lines[lineNo - 1] ?? "") !== hashes[lineNo]) {
242
- mismatchedLines.push(lineNo);
243
- }
244
- }
245
- return {
246
- checked: true,
247
- matches: mismatchedLines.length === 0,
248
- missingLines,
249
- mismatchedLines,
250
- };
251
- }
252
- // --- ReadGuard Class ---
253
- export class ReadGuard {
254
- config;
255
- reads = new Map();
256
- edits = new Map();
257
- fileLastUsed = new Map();
258
- fileIdleTimers = new Map();
259
- /** Reads remain behavior-gating until the corresponding edit is published. */
260
- consumedReadFiles = new Set();
261
- fileTime;
262
- exemptions = new Set(); // One-time exemptions via /lens-allow-edit
263
- pendingCreations = new Map();
264
- // Files that recordWritten() has fired on this session. Lets
265
- // wasWrittenThisSession() return a deterministic answer for files the
266
- // pi Write tool authored, independent of filesystem mtime granularity
267
- // or clock skew (NFS, FAT32, etc.).
268
- writtenThisSession = new Set();
269
- unchangedThisSession = new Set();
270
- // Existence-independent index for hasKnownPath/forgetPath (#1668 review
271
- // F1). `this.key()` (normalizeFilePath) branches on whether `filePath`
272
- // currently exists on disk, on EVERY platform since #3098 — an existing
273
- // file resolves to its on-disk casing (win32: `realpathSync.native`;
274
- // POSIX: the on-disk casing of the trailing segments), a missing one to a
275
- // lowercased tail on win32 and to the caller's own spelling on POSIX.
276
- // recordRead/recordWritten always key while the file is still on disk
277
- // (real casing); hasKnownPath/forgetPath are queried AFTER an external
278
- // delete already landed, when the path no longer exists — recomputing
279
- // `this.key()` at that point returns a DIFFERENT string for any
280
- // mixed-case basename (win32 `MyModule.ts` → `mymodule.ts`; POSIX
281
- // `Components/x.ts` held against an on-disk `components/`), so a lookup
282
- // against `reads`/`writtenThisSession` silently misses. This index maps
283
- // a purely syntactic key (`normalizeEphemeralMapKey` — slash-fold +
284
- // lowercase, no filesystem access, so it never depends on current disk
285
- // state) to the REAL key `reads`/`writtenThisSession` used at record
286
- // time, so a post-delete lookup finds the same entry regardless of what
287
- // happened to the file since. Pruned inside `evictFile` so it never
288
- // outlives the record it points at.
289
- //
290
- // ONLY hasKnownPath/forgetPath route through this index, because only they
291
- // need the real key BACK. `noteCreatedFile` (keys while the file is still
292
- // absent) and `recordWritten` (keys after it exists) straddle the same
293
- // state change, and used to do it through `this.key()` directly — a
294
- // spelling whose key moved orphaned the `pendingCreations` entry and the
295
- // creation read was never injected (#3163, pre-existing on win32 and
296
- // reachable on POSIX since #3098). They now key `pendingCreations` by the
297
- // same existence-independent `normalizeEphemeralMapKey` spelling this index
298
- // is keyed by; no index is needed there, since the real key is derived by
299
- // `recordWritten` itself at the moment it is used.
300
- knownPathIndex = new Map();
301
- /** Running per-file record-cap trim totals for this session (#1913 F1). */
302
- trimAccumulators = new Map();
303
- sessionId;
304
- sessionStartMs;
305
- constructor(sessionId, config = {}) {
306
- this.sessionId = sessionId;
307
- this.sessionStartMs = Date.now();
308
- this.config = { ...DEFAULT_CONFIG, ...config };
309
- this.fileTime = createFileTime(sessionId);
310
- }
311
- /**
312
- * Canonical Map key for a file path. Read sources arrive with mixed
313
- * separators/casing — the Read tool gives OS-native backslashes on Windows,
314
- * while LSP-expanded and search-tool reads arrive slash-normalized from URIs.
315
- * Keying the reads/edits/exemptions maps on the raw path made a read recorded
316
- * under one form invisible to an edit checked under another, producing a false
317
- * `zero_read` block despite the file having been read. `normalizeFilePath`
318
- * folds separators and Windows casing to one key, so record and lookup always
319
- * agree. Every map access in this class MUST key through here.
320
- */
321
- key(filePath) {
322
- return normalizeFilePath(filePath);
323
- }
324
- idleEvictMs() {
325
- const value = Number.parseInt(process.env.PI_LENS_READ_GUARD_IDLE_EVICT_MS ?? "", 10);
326
- return Number.isSafeInteger(value) && value > 0
327
- ? value
328
- : READ_GUARD_IDLE_EVICT_MS_DEFAULT;
329
- }
330
- clearFileTimer(filePath) {
331
- const timer = this.fileIdleTimers.get(filePath);
332
- if (timer)
333
- clearTimeout(timer);
334
- this.fileIdleTimers.delete(filePath);
335
- }
336
- /**
337
- * #1918: `enforceRecordCapForFile` (the per-file record cap) is the only
338
- * `read-guard.ts` eviction path #1913 gave a bounded record. This whole-file
339
- * evictor has four call sites — `enforceFileCap`'s consumed-file cap, its
340
- * unconsumed-file cap, `touchFile`'s idle timer, and `forgetPath`'s
341
- * external-delete cleanup — and none of them left any trace. A live
342
- * regression in any of the four (e.g. a cap set too low) dropped a file's
343
- * read/edit history with nothing in `~/.pi-lens/read-guard.log` to show it
344
- * happened.
345
- *
346
- * Follows #1913's aggregate-after-first pattern for the three PRESSURE
347
- * reasons (cap exhaustion, external delete): `incrementDegradationCount`
348
- * keeps the exact per-file eviction tally (and its own power-of-two
349
- * milestone rows in latency.log) while the read-guard.log line fires only
350
- * on the rising edge, so a file evicted repeatedly across a session can't
351
- * flood the log.
352
- *
353
- * `idle-timeout` is deliberately excluded from both the ledger tally and
354
- * the log line (#1918 review F2): idle eviction is designed housekeeping,
355
- * not a fault signal — a read-only session that idles out N files is
356
- * healthy, ordinary behavior, and N is unbounded (every distinct file path
357
- * touched that session is its own ledger subject). Recording it here would
358
- * grow the ledger's tally map without bound and spam `pilens_health` in
359
- * every healthy session, unlike the other three reasons, which are rare by
360
- * construction (only real pressure or an explicit delete reaches them).
361
- */
362
- evictFile(filePath, reason) {
363
- this.clearFileTimer(filePath);
364
- this.reads.delete(filePath);
365
- this.edits.delete(filePath);
366
- this.fileLastUsed.delete(filePath);
367
- this.consumedReadFiles.delete(filePath);
368
- this.writtenThisSession.delete(filePath);
369
- // #1668 review F1: prune the reverse-pointing knownPathIndex entries
370
- // too, so it never outlives the record it points at.
371
- for (const [syntacticKey, stored] of this.knownPathIndex) {
372
- if (stored === filePath)
373
- this.knownPathIndex.delete(syntacticKey);
374
- }
375
- if (reason === "idle-timeout")
376
- return;
377
- const isRisingEdge = incrementDegradationCount({
378
- kind: "read-guard-file-evicted",
379
- subject: filePath,
380
- reason: `evicted (${reason})`,
381
- });
382
- if (isRisingEdge) {
383
- logReadGuardEvent({
384
- event: "read_file_evicted",
385
- sessionId: this.sessionId,
386
- filePath,
387
- metadata: { reason },
388
- });
389
- }
390
- }
391
- touchFile(filePath) {
392
- const now = Date.now();
393
- this.fileLastUsed.set(filePath, now);
394
- this.clearFileTimer(filePath);
395
- // An outstanding read is enforcement state, not a rebuildable cache entry.
396
- // It must survive idle time and file-cap pressure until the edit consumes it.
397
- if (this.reads.has(filePath) && !this.consumedReadFiles.has(filePath))
398
- return;
399
- const stamp = now;
400
- const timer = setTimeout(() => {
401
- if (this.fileLastUsed.get(filePath) !== stamp)
402
- return;
403
- // Never turn an outstanding read into a zero-read block through idle
404
- // eviction. Only consumed reads and rebuildable edit history may expire.
405
- if (this.reads.has(filePath) && !this.consumedReadFiles.has(filePath))
406
- return;
407
- // #1918 review F2: this reason is intentionally silent inside
408
- // evictFile — idle eviction is routine housekeeping, not a fault, and
409
- // every distinct file idling out this session is its own ledger
410
- // subject, so recording it would grow unbounded and flood
411
- // pilens_health on every healthy session. See evictFile's doc comment.
412
- this.evictFile(filePath, "idle-timeout");
413
- }, this.idleEvictMs());
414
- timer.unref?.();
415
- this.fileIdleTimers.set(filePath, timer);
416
- }
417
- enforceFileCap() {
418
- while (this.reads.size > READ_GUARD_MAX_FILES) {
419
- const victim = [...this.reads.keys()]
420
- .filter((filePath) => this.consumedReadFiles.has(filePath))
421
- .sort((a, b) => (this.fileLastUsed.get(a) ?? 0) - (this.fileLastUsed.get(b) ?? 0))[0];
422
- if (!victim)
423
- break;
424
- this.evictFile(victim, "file-cap-consumed");
425
- }
426
- while (this.reads.size > READ_GUARD_MAX_UNCONSUMED_FILES) {
427
- const victim = [...this.reads.keys()]
428
- .filter((filePath) => !this.consumedReadFiles.has(filePath))
429
- .sort((a, b) => (this.fileLastUsed.get(a) ?? 0) - (this.fileLastUsed.get(b) ?? 0))[0];
430
- if (!victim)
431
- break;
432
- // This is a normal read miss: a later edit must require a fresh read,
433
- // never silently allow and never become a permanent hard-block.
434
- this.evictFile(victim, "file-cap-unconsumed");
435
- }
436
- }
437
- // --- Public API ---
438
- /**
439
- * Record that a file was read.
440
- * Call this from the tool_call handler after any LSP expansion.
441
- */
442
- recordRead(record, opts) {
443
- const filePath = this.key(record.filePath);
444
- if (opts?.supersedes) {
445
- const records = this.reads.get(filePath);
446
- const provisionalSource = `native-read:${opts.supersedes.toolCallId}:provisional`;
447
- const provisionalIndex = records?.findIndex((candidate) => candidate.source === provisionalSource);
448
- if (provisionalIndex !== undefined && provisionalIndex >= 0)
449
- records.splice(provisionalIndex, 1);
450
- }
451
- // #1668 review F1: index by the existence-independent syntactic key
452
- // while the file is (presumably) still on disk, so a later
453
- // hasKnownPath/forgetPath lookup after an external delete can still
454
- // find this entry's real key.
455
- this.knownPathIndex.set(normalizeEphemeralMapKey(record.filePath), filePath);
456
- const storedRecord = {
457
- ...record,
458
- filePath,
459
- lineHashes: record.lineHashes ??
460
- captureLineHashes(filePath, record.effectiveOffset, record.effectiveLimit),
461
- };
462
- const arr = this.reads.get(storedRecord.filePath) ?? [];
463
- this.consumedReadFiles.delete(storedRecord.filePath);
464
- arr.push(storedRecord);
465
- this.reads.set(storedRecord.filePath, arr);
466
- // Capture BEFORE the trim: `readCountForFile` saturates at the cap once
467
- // eviction starts, so it stops showing growth. `rawReadCountForFile` keeps
468
- // the real arrival count observable alongside `evictedRecordCount`.
469
- const rawReadCountForFile = arr.length;
470
- const { evictedCount: evictedRecordCount, evictedCreditCount, evictedGenuineCount, } = enforceRecordCapForFile(arr);
471
- this.touchFile(storedRecord.filePath);
472
- this.enforceFileCap();
473
- logReadGuardEvent({
474
- event: "read_recorded",
475
- sessionId: this.sessionId,
476
- filePath: storedRecord.filePath,
477
- requestedOffset: storedRecord.requestedOffset,
478
- requestedLimit: storedRecord.requestedLimit,
479
- effectiveOffset: storedRecord.effectiveOffset,
480
- effectiveLimit: storedRecord.effectiveLimit,
481
- symbol: storedRecord.enclosingSymbol?.name,
482
- symbolKind: storedRecord.enclosingSymbol?.kind,
483
- symbolStartLine: storedRecord.enclosingSymbol?.startLine,
484
- symbolEndLine: storedRecord.enclosingSymbol?.endLine,
485
- metadata: {
486
- expandedByLsp: storedRecord.expandedByLsp,
487
- turnIndex: storedRecord.turnIndex,
488
- writeIndex: storedRecord.writeIndex,
489
- readCountForFile: arr.length,
490
- hashLineCount: Object.keys(storedRecord.lineHashes ?? {}).length,
491
- ...(storedRecord.source !== undefined && {
492
- source: storedRecord.source,
493
- }),
494
- ...(storedRecord.searchCredit !== undefined && {
495
- searchCreditReason: storedRecord.searchCredit.reason,
496
- searchCreditMarginBefore: storedRecord.searchCredit.marginBefore,
497
- searchCreditMarginAfter: storedRecord.searchCredit.marginAfter,
498
- }),
499
- ...(evictedRecordCount > 0 && {
500
- evictedRecordCount,
501
- rawReadCountForFile,
502
- }),
503
- },
504
- });
505
- // #1913: the eviction counters above ride `read_recorded`, which is
506
- // gated behind PI_LENS_READ_GUARD_VERBOSE — off by default. That left a
507
- // live eviction regression with no trace at default verbosity.
508
- //
509
- // review F1: a naive "emit every trim" fix floods read-guard.log once a
510
- // hot file sits past the cap — every later push trims exactly 1 record
511
- // (the array is always AT the cap before the push, so `excess` is
512
- // always exactly 1), so 300 recordRead calls on one file is ~172
513
- // identical always-on lines, and can rotate `edit_blocked` records out
514
- // of the 1MB cap. `rawReadCountForFile` also freezes at cap+1 forever,
515
- // so a raw per-trim record can't even discriminate thrash severity.
516
- //
517
- // Fix: emit the read-guard.log line ONCE per file per session, on the
518
- // FIRST trim only. `trimAccumulators` still updates on every trim,
519
- // queryable via `getTrimStats` — the running totals a health surface or
520
- // a future emission point would need are never lost, even though
521
- // read-guard.log stops re-announcing them. "Have we already logged
522
- // this file's first trim" routes through the degradation ledger's own
523
- // rising-edge tally (`incrementDegradationCount` — the pattern
524
- // CLAUDE.md names for repeated degradations) instead of a hand-rolled
525
- // per-file Set; subsequent trims still call it, so the ledger's own
526
- // entry for this (kind, subject) keeps its reason text and count
527
- // current for the health/degradation summary even after read-guard.log
528
- // goes quiet.
529
- if (evictedRecordCount > 0) {
530
- const key = storedRecord.filePath;
531
- const acc = this.trimAccumulators.get(key) ?? {
532
- totalEvicted: 0,
533
- evictedCreditCount: 0,
534
- evictedGenuineCount: 0,
535
- trimEventCount: 0,
536
- };
537
- acc.totalEvicted += evictedRecordCount;
538
- acc.evictedCreditCount += evictedCreditCount;
539
- acc.evictedGenuineCount += evictedGenuineCount;
540
- acc.trimEventCount += 1;
541
- this.trimAccumulators.set(key, acc);
542
- const isRisingEdge = incrementDegradationCount({
543
- kind: "read-guard-record-cap-trim",
544
- subject: key,
545
- reason: `trim #${acc.trimEventCount}: evicted ${evictedRecordCount} (credit ${evictedCreditCount}, genuine ${evictedGenuineCount})`,
546
- });
547
- if (isRisingEdge) {
548
- logReadGuardEvent({
549
- event: "read_cap_trimmed",
550
- sessionId: this.sessionId,
551
- filePath: storedRecord.filePath,
552
- metadata: {
553
- trimEventCount: acc.trimEventCount,
554
- evictedRecordCount: acc.totalEvicted,
555
- evictedCreditCount: acc.evictedCreditCount,
556
- evictedGenuineCount: acc.evictedGenuineCount,
557
- rawReadCountForFile,
558
- },
559
- });
560
- }
561
- }
562
- // Also update FileTime stamp for this file
563
- this.fileTime.read(storedRecord.filePath);
564
- }
565
- /**
566
- * Record a structured symbol read (the `readSymbol` engine capability / its
567
- * MCP mirror) as a genuine read of that symbol's line range — the
568
- * read-substitute tie-in for #245. `readSymbol` returns the verbatim body, so
569
- * an edit within [startLine, endLine] is legitimately covered, exactly like a
570
- * TS/LSP-expanded read that delivered the whole enclosing symbol. Line hashes
571
- * for the range are captured by recordRead, so the edit is also
572
- * snapshot-verified (drift since the symbol read still blocks).
573
- *
574
- * Intentionally NOT offered for module *outlines*: an outline shows a symbol's
575
- * shape (name/signature/range), not its body, so granting edit coverage from
576
- * it would let the agent edit lines it never saw. Only a body-delivering read
577
- * (readSymbol / raw Read) records coverage.
578
- */
579
- recordSymbolRead(filePath, symbol, turnIndex, writeIndex) {
580
- const span = Math.max(1, symbol.endLine - symbol.startLine + 1);
581
- this.recordRead({
582
- filePath,
583
- requestedOffset: symbol.startLine,
584
- requestedLimit: span,
585
- effectiveOffset: symbol.startLine,
586
- effectiveLimit: span,
587
- expandedByLsp: false,
588
- enclosingSymbol: {
589
- name: symbol.name,
590
- kind: symbol.kind,
591
- startLine: symbol.startLine,
592
- endLine: symbol.endLine,
593
- },
594
- turnIndex,
595
- writeIndex,
596
- timestamp: Date.now(),
597
- });
598
- }
599
- /**
600
- * Check if an edit should be allowed.
601
- * Returns verdict with action and optional reason for blocking.
602
- */
603
- checkEdit(filePath, touchedLines, editRanges, options) {
604
- // Canonicalize once: every map lookup below (and every private helper this
605
- // passes filePath to) must agree with how recordRead keyed the read.
606
- filePath = this.key(filePath);
607
- if (this.reads.has(filePath) || this.edits.has(filePath))
608
- this.touchFile(filePath);
609
- // Check exemptions
610
- if (this.exemptions.has(filePath)) {
611
- this.exemptions.delete(filePath); // One-time use
612
- const verdict = this.allow();
613
- this.recordVerdict(filePath, "edit", touchedLines, verdict, {
614
- reasonKind: "manual_exemption",
615
- });
616
- return verdict;
617
- }
618
- // Check config exemptions by pattern
619
- const exemptionMode = this.getExemptionMode(filePath);
620
- if (exemptionMode === "allow") {
621
- const verdict = this.allow();
622
- this.recordVerdict(filePath, "edit", touchedLines, verdict, {
623
- reasonKind: "pattern_exemption",
624
- exemptionMode,
625
- });
626
- return verdict;
627
- }
628
- // "warn" pattern exemptions downgrade all blocking verdicts to warnings.
629
- const effectiveMode = exemptionMode === "warn" ? "warn" : undefined;
630
- // 1. Zero-read check
631
- const fileReads = this.reads.get(filePath);
632
- if (!fileReads || fileReads.length === 0) {
633
- // If the file was written after this session started, the agent authored
634
- // it in this session (via Write or any other mechanism). Allow the edit —
635
- // a synthetic read would have been injected for Write tool calls, but
636
- // this catches cases where the write bypassed the hook or the session
637
- // restarted between write and edit.
638
- if (this.wasWrittenThisSession(filePath)) {
639
- this.injectCreationRead(filePath, 0, 0);
640
- const verdict = this.allow();
641
- this.recordVerdict(filePath, "edit", touchedLines, verdict, {
642
- reasonKind: "session_authored",
643
- });
644
- return verdict;
645
- }
646
- const verdict = this.blockOrWarn("zero-read", `🔄 RETRYABLE — Edit without read: you have not read \`${filePath}\` in this conversation. Read it first, then retry: \`read path="${filePath}"\`.`, undefined, effectiveMode);
647
- this.recordVerdict(filePath, "edit", touchedLines, verdict, {
648
- reasonKind: "zero_read",
649
- });
650
- return verdict;
651
- }
652
- const lastBoundRead = [...fileReads]
653
- .reverse()
654
- .find((read) => read.contentBinding !== undefined);
655
- if (lastBoundRead?.contentBinding &&
656
- !currentContentMatchesBinding(filePath, lastBoundRead.contentBinding)) {
657
- const verdict = this.blockOrWarn("file-modified", `🔄 RETRYABLE — File modified since read\n\nYou last read \`${filePath}\` at ${new Date(lastBoundRead.timestamp).toISOString()}.\nThe file content no longer matches the bridge-recorded read.\n\nYour mental model is out of sync with the actual file content.\nTo proceed:\n 1. Re-read the file: \`read path="${filePath}"\``, undefined, effectiveMode);
658
- this.recordVerdict(filePath, "edit", touchedLines, verdict, {
659
- reasonKind: "file_modified",
660
- lastReadTimestamp: lastBoundRead.timestamp,
661
- contentBindingMismatch: true,
662
- });
663
- return verdict;
664
- }
665
- // 2. FileTime check (actual staleness)
666
- let ignoredOwnEditStaleness = false;
667
- let ignoredHashStaleness = false;
668
- let ignoredOldTextResolvedStaleness = false;
669
- if (this.fileTime.hasChanged(filePath)) {
670
- const lastRead = fileReads[fileReads.length - 1];
671
- if (options?.oldTextResolved === true) {
672
- // The host-facing preflight sets this only after oldText resolves to
673
- // exactly one span in the live bytes. That content evidence is newer
674
- // and stronger than FileTime's coarse external-write signal, matching
675
- // the skipSnapshotCheck exception at the later range-stale gate.
676
- ignoredOldTextResolvedStaleness = true;
677
- }
678
- else if (this.canTreatStalenessAsOwnPriorEdit(filePath, lastRead.timestamp)) {
679
- ignoredOwnEditStaleness = true;
680
- }
681
- else if (this.canIgnoreStalenessByHashes(filePath, fileReads, touchedLines, editRanges)) {
682
- ignoredHashStaleness = true;
683
- }
684
- else {
685
- const verdict = this.blockOrWarn("file-modified", `🔄 RETRYABLE — File modified since read\n\nYou last read \`${filePath}\` at ${new Date(lastRead.timestamp).toISOString()}.\nThe file has been modified on disk since then (auto-format, external tool, or previous edit).\n\nYour mental model is out of sync with the actual file content.\nTo proceed:\n 1. Re-read the file: \`read path="${filePath}"\``, undefined, effectiveMode);
686
- this.recordVerdict(filePath, "edit", touchedLines, verdict, {
687
- reasonKind: "file_modified",
688
- lastReadTimestamp: lastRead.timestamp,
689
- });
690
- return verdict;
691
- }
692
- }
693
- // If no line range specified, we can only check zero-read and FileTime
694
- if (!touchedLines) {
695
- const verdict = this.allow();
696
- this.recordVerdict(filePath, "edit", touchedLines, verdict, {
697
- reasonKind: "no_line_info",
698
- });
699
- return verdict;
700
- }
701
- // 3. Range coverage check
702
- // When the edit touches multiple disjoint spots (e.g. rename across 4 tool
703
- // registrations), check each spot independently. Collapsing to a bounding
704
- // box would falsely flag reads that cover exactly the right lines.
705
- const rangesToCheck = editRanges && editRanges.length > 1 ? editRanges : [touchedLines];
706
- let viaSymbol = false;
707
- for (const range of rangesToCheck) {
708
- const snapshotValidation = this.validateRangeSnapshot(filePath, range, !!options?.skipSnapshotCheck);
709
- const coverage = this.checkCoverage(filePath, range);
710
- if (!coverage.covered) {
711
- const lastRead = fileReads[fileReads.length - 1];
712
- const [editStart, editEnd] = range;
713
- const lastReadEnd = lastRead.effectiveOffset + lastRead.effectiveLimit - 1;
714
- // If oldText was resolved (content-verified), the model demonstrably
715
- // knew the content it's replacing — line drift from prior edits in
716
- // the session is the likely cause. Downgrade to warn rather than block.
717
- const outOfRangeMode = options?.oldTextResolved
718
- ? "warn"
719
- : effectiveMode;
720
- const verdict = this.blockOrWarn("out-of-range", `🔄 RETRYABLE — Edit outside read range\n\nYou read \`${filePath}\` lines ${lastRead.effectiveOffset}-${lastReadEnd}${lastRead.enclosingSymbol ? ` (${lastRead.enclosingSymbol.kind} \`${lastRead.enclosingSymbol.name}\`)` : ""}, but your edit touches lines ${editStart}-${editEnd}.\n\nRead the relevant section first, then retry the edit:\n \`read path="${filePath}" offset=${Math.max(1, editStart - 5)} limit=${Math.min(30, editEnd - editStart + 10)}\``, {
721
- editRange: range,
722
- readRanges: fileReads.map((r) => ({
723
- start: r.effectiveOffset,
724
- end: r.effectiveOffset + r.effectiveLimit - 1,
725
- })),
726
- symbolRanges: fileReads
727
- .filter((r) => r.enclosingSymbol)
728
- .map((r) => ({
729
- name: r.enclosingSymbol.name,
730
- start: r.enclosingSymbol.startLine,
731
- end: r.enclosingSymbol.endLine,
732
- })),
733
- }, outOfRangeMode);
734
- this.recordVerdict(filePath, "edit", touchedLines, verdict, {
735
- reasonKind: "out_of_range",
736
- oldTextResolved: options?.oldTextResolved ?? false,
737
- });
738
- return verdict;
739
- }
740
- if (snapshotValidation.shouldBlock && !options?.skipSnapshotCheck) {
741
- const [editStart, editEnd] = range;
742
- // Grace period: when the snapshot is stale because THIS session's own
743
- // earlier edit shifted line numbers (ignoredOwnEditStaleness), and
744
- // the agent read the file recently, downgrade to a warning rather
745
- // than blocking. The agent has fresh context — they just don't
746
- // know the exact new line numbers after the shift.
747
- const RANGE_STALE_GRACE_MS = 60_000;
748
- const lastRead = fileReads[fileReads.length - 1];
749
- const graceActive = ignoredOwnEditStaleness &&
750
- Date.now() - lastRead.timestamp < RANGE_STALE_GRACE_MS;
751
- // Content-verified relocation: if the lines the agent read have
752
- // merely shifted (same content, new offset), tell them exactly where
753
- // so they re-target in one turn. We hint rather than silently
754
- // re-apply: the host applies native range edits positionally and
755
- // can't re-verify, so an unverified auto-relocation could corrupt.
756
- const relocation = this.findRelocation(filePath, fileReads, range);
757
- const relocationNote = relocation
758
- ? `\n\n📍 The content you read at lines ${relocation.from[0]}-${relocation.from[1]} now appears unchanged at lines ${relocation.to[0]}-${relocation.to[1]} — it shifted position. Re-target your edit to lines ${relocation.to[0]}-${relocation.to[1]}.`
759
- : "";
760
- const verdict = this.blockOrWarn("range-stale", `🔄 RETRYABLE — Edit range changed since read\n\nYou are editing \`${filePath}\` lines ${editStart}-${editEnd}, but those lines no longer match the content you read earlier.${relocationNote}\n\nRe-read the relevant section, then retry the edit using the current line range/content:\n \`read path="${filePath}" offset=${Math.max(1, editStart - 5)} limit=${Math.min(30, editEnd - editStart + 10)}\``, {
761
- editRange: range,
762
- readRanges: fileReads.map((r) => ({
763
- start: r.effectiveOffset,
764
- end: r.effectiveOffset + r.effectiveLimit - 1,
765
- })),
766
- symbolRanges: fileReads
767
- .filter((r) => r.enclosingSymbol)
768
- .map((r) => ({
769
- name: r.enclosingSymbol.name,
770
- start: r.enclosingSymbol.startLine,
771
- end: r.enclosingSymbol.endLine,
772
- })),
773
- snapshot: {
774
- status: snapshotValidation.status,
775
- mismatchedLines: snapshotValidation.mismatchedLines,
776
- missingLines: snapshotValidation.missingLines,
777
- },
778
- ...(relocation ? { relocation } : {}),
779
- }, graceActive ? "warn" : effectiveMode);
780
- // Offer auto-apply only for a single-range edit: we relocated exactly
781
- // one range, so shifting it is the whole edit. A multi-range edit
782
- // could have other drifted spots we returned before checking, so it
783
- // stays a hint.
784
- if (relocation && rangesToCheck.length === 1) {
785
- verdict.relocation = relocation;
786
- }
787
- this.recordVerdict(filePath, "edit", touchedLines, verdict, {
788
- reasonKind: "range_stale",
789
- range,
790
- mismatchedLines: snapshotValidation.mismatchedLines.slice(0, 20),
791
- graceActive,
792
- relocatedTo: relocation?.to ?? null,
793
- relocationAutoApplyOffered: !!verdict.relocation,
794
- });
795
- return verdict;
796
- }
797
- if (coverage.viaSymbol)
798
- viaSymbol = true;
799
- }
800
- const verdict = this.allow();
801
- this.recordVerdict(filePath, "edit", touchedLines, verdict, {
802
- reasonKind: ignoredOldTextResolvedStaleness
803
- ? "file_modified_oldtext_unique"
804
- : viaSymbol
805
- ? "symbol_coverage"
806
- : "range_coverage",
807
- viaSymbol,
808
- ignoredOwnEditStaleness,
809
- ignoredHashStaleness,
810
- oldTextResolved: ignoredOldTextResolvedStaleness,
811
- });
812
- return verdict;
813
- }
814
- /**
815
- * Check if this is a new file (no existing file on disk).
816
- * New file writes are exempt from the guard.
817
- */
818
- isNewFile(filePath) {
819
- try {
820
- return !fs.existsSync(filePath);
821
- }
822
- catch {
823
- return true; // Assume new if we can't stat
824
- }
825
- }
826
- /**
827
- * Whether pi-lens has any record of this path from a read or write this
828
- * session (#1668). Gates external-delete detection: a path pi-lens never
829
- * read or wrote is one no LSP server's cache was ever told about through
830
- * us, so an `rm` naming it carries no signal worth checking disk for —
831
- * this is a lookup against state already being tracked, never a fresh
832
- * filesystem stat over an unbounded path set.
833
- *
834
- * MUST go through `knownPathIndex`, not `this.key(filePath)` directly
835
- * (#1668 review F1): this is called AFTER a bash delete has already
836
- * landed, when `filePath` no longer exists — `this.key()` at that point
837
- * returns a lowercased-tail key, not the real-casing key `recordRead`/
838
- * `recordWritten` stored while the file was still on disk. For a
839
- * mixed-case basename (`MyModule.ts`) the two keys differ and a direct
840
- * `this.key()` lookup always misses.
841
- */
842
- hasKnownPath(filePath) {
843
- const stored = this.knownPathIndex.get(normalizeEphemeralMapKey(filePath));
844
- return (stored !== undefined &&
845
- (this.reads.has(stored) || this.writtenThisSession.has(stored)));
846
- }
847
- /**
848
- * Every path this guard has a read or a write record for, as normalized keys
849
- * (#2430).
850
- *
851
- * This is the read-guard half of the observational net's TRACKED-FILE SET:
852
- * the files pi-lens has actually seen this session, which the `agent_settled`
853
- * sweep hash-checks instead of walking the workspace. It reads state already
854
- * held in memory and stats nothing; both backing containers are capped
855
- * (`READ_GUARD_MAX_UNCONSUMED_FILES`), so the result is bounded by
856
- * construction.
857
- *
858
- * `touchFile` is deliberately NOT called: enumerating is not using, and
859
- * refreshing every entry's idle clock on each sweep would make the idle
860
- * eviction unreachable.
861
- */
862
- getTrackedPaths() {
863
- const tracked = new Set(this.reads.keys());
864
- for (const written of this.writtenThisSession)
865
- tracked.add(written);
866
- return [...tracked];
867
- }
868
- /**
869
- * Drop all record of a path pi-lens confirmed no longer exists on disk
870
- * (#1668, external delete). Without this a later write reusing the same
871
- * path would inherit a stale writtenThisSession/reads entry from before
872
- * the delete, and a repeat `rm` of the same already-gone path would keep
873
- * matching {@link hasKnownPath} and re-emitting a type-3 notification.
874
- *
875
- * MUST evict through `knownPathIndex`, not `this.key(filePath)` directly
876
- * (#1668 review F1) — same reasoning as {@link hasKnownPath}: `filePath`
877
- * is already gone from disk by the time this runs, so recomputing
878
- * `this.key()` targets the wrong (lowercased-tail) key for a mixed-case
879
- * basename and leaves the real entry behind un-evicted.
880
- */
881
- forgetPath(filePath) {
882
- const stored = this.knownPathIndex.get(normalizeEphemeralMapKey(filePath));
883
- this.evictFile(stored ?? this.key(filePath), "external-delete");
884
- }
885
- /**
886
- * Mark a file as pending creation (Write tool to a non-existing file).
887
- * Must be called from the tool_call handler before the write lands so
888
- * isNewFile() still returns true. recordWritten will inject a synthetic
889
- * read so immediate follow-up edits are not blocked by zero_read.
890
- *
891
- * Keyed by the existence-INDEPENDENT syntactic spelling
892
- * (`normalizeEphemeralMapKey`, the same key `knownPathIndex` uses), NOT by
893
- * `this.key()` (#3163). This entry is written while the file is still
894
- * ABSENT and read back by `recordWritten` once it EXISTS — the one state
895
- * change `normalizeFilePath` itself branches on — so a `this.key()` on each
896
- * side answered with two different strings for any spelling whose key moves
897
- * when the file appears (win32: a mixed-case basename, lower-cased in the
898
- * absent branch by `resolveNonExisting`; POSIX since #3098: a parent whose
899
- * canonical casing is adopted once `realpathSync.native` succeeds). The
900
- * entry was then orphaned — nothing prunes `pendingCreations` — and the
901
- * creation read never injected. The syntactic key never touches the
902
- * filesystem, so it cannot move; the REAL key the injected read needs is
903
- * derived by `recordWritten` itself and is not stored here.
904
- */
905
- noteCreatedFile(filePath, turnIndex, writeIndex) {
906
- this.pendingCreations.set(normalizeEphemeralMapKey(filePath), {
907
- turnIndex,
908
- writeIndex,
909
- });
910
- }
911
- /**
912
- * Refresh the FileTime stamp after the model's own write lands on disk.
913
- * Call this from the tool_result handler so the next checkEdit on the same
914
- * file doesn't see "file_modified" caused by our own previous edit.
915
- */
916
- recordWritten(rawFilePath) {
917
- const filePath = this.key(rawFilePath);
918
- this.unchangedThisSession.delete(filePath);
919
- // #1668 review F1: index by the existence-independent syntactic key
920
- // (see `knownPathIndex`) so a later hasKnownPath/forgetPath lookup
921
- // after an external delete can still find this entry's real key.
922
- this.knownPathIndex.set(normalizeEphemeralMapKey(rawFilePath), filePath);
923
- this.fileTime.read(filePath);
924
- this.writtenThisSession.add(filePath);
925
- if (this.reads.has(filePath))
926
- this.consumedReadFiles.add(filePath);
927
- this.touchFile(filePath);
928
- this.enforceFileCap();
929
- // #3163: `noteCreatedFile` announced this creation while the file was
930
- // still absent, so the announcement is keyed by the syntactic spelling —
931
- // look it up the same way, never through the existence-dependent
932
- // `filePath` above. `injectCreationRead` still receives the real key.
933
- const creationKey = normalizeEphemeralMapKey(rawFilePath);
934
- const creation = this.pendingCreations.get(creationKey);
935
- if (creation) {
936
- this.pendingCreations.delete(creationKey);
937
- this.injectCreationRead(filePath, creation.turnIndex, creation.writeIndex);
938
- }
939
- }
940
- /** Record that a recognized mutation had complete evidence but changed no bytes. */
941
- recordUnchanged(rawFilePath) {
942
- const filePath = this.key(rawFilePath);
943
- // A no-op command is scoped to this command. It must not erase a
944
- // confirmed Write from earlier in the session.
945
- if (!this.writtenThisSession.has(filePath))
946
- this.unchangedThisSession.add(filePath);
947
- }
948
- /**
949
- * Add a one-time exemption for a file.
950
- * Called via /lens-allow-edit command.
951
- */
952
- addExemption(filePath) {
953
- this.exemptions.add(this.key(filePath));
954
- logReadGuardEvent({
955
- event: "exemption_added",
956
- sessionId: this.sessionId,
957
- filePath,
958
- metadata: {
959
- source: "lens-allow-edit",
960
- },
961
- });
962
- }
963
- /**
964
- * Get summary statistics for /lens-health.
965
- */
966
- getSummary() {
967
- let totalEdits = 0;
968
- let totalBlocks = 0;
969
- let lspExpansionsHelped = 0;
970
- const byReason = {};
971
- const byFile = {};
972
- for (const [filePath, records] of this.edits) {
973
- for (const record of records) {
974
- totalEdits++;
975
- byFile[filePath] = byFile[filePath] ?? { edits: 0, blocks: 0 };
976
- byFile[filePath].edits++;
977
- if (record.verdict === "blocked") {
978
- totalBlocks++;
979
- byFile[filePath].blocks++;
980
- }
981
- if (record.reason) {
982
- byReason[record.reason] = (byReason[record.reason] ?? 0) + 1;
983
- }
984
- // Count LSP expansions that allowed an edit
985
- if (record.precedingReads.some((r) => r.expandedByLsp) &&
986
- record.verdict === "allowed") {
987
- lspExpansionsHelped++;
988
- }
989
- }
990
- }
991
- return {
992
- totalEdits,
993
- totalBlocks,
994
- byReason,
995
- byFile,
996
- lspExpansionsHelped,
997
- };
998
- }
999
- /**
1000
- * Get all read records for a file (for debugging).
1001
- */
1002
- getReadHistory(filePath) {
1003
- const key = this.key(filePath);
1004
- if (this.reads.has(key))
1005
- this.touchFile(key);
1006
- return this.reads.get(key) ?? [];
1007
- }
1008
- /**
1009
- * Session-lifetime record-cap trim totals for a file (#1913 review F1).
1010
- * `recordRead` only writes ONE `read_cap_trimmed` read-guard.log line per
1011
- * file per session (on the first trim), so this is the running-totals
1012
- * surface for everything after that — the exact split this class already
1013
- * computes on every trim, not re-derivable from `getReadHistory` alone
1014
- * (which only shows the SURVIVING records, not what was evicted).
1015
- */
1016
- getTrimStats(filePath) {
1017
- const stats = this.trimAccumulators.get(this.key(filePath));
1018
- return stats ? { ...stats } : undefined;
1019
- }
1020
- /**
1021
- * Snapshot the read-set for persistence across a session resume (#1041).
1022
- * Mirrors widget-state's `exportWidgetState`: the Map is emitted as
1023
- * `[key, records]` tuples, keys already in `normalizeFilePath` form. Only
1024
- * `reads` is serialized — it is the payload `checkEdit`'s zero-read/coverage
1025
- * checks consult. Safe to call even when the guard is disabled (an empty or
1026
- * never-populated `reads` simply exports zero entries).
1027
- */
1028
- exportState() {
1029
- return {
1030
- version: READ_GUARD_STATE_VERSION,
1031
- reads: [...this.reads.entries()].map(([key, records]) => [
1032
- key,
1033
- records.map((record) => ({ ...record })),
1034
- ]),
1035
- };
1036
- }
1037
- /**
1038
- * Rehydrate a persisted read-set (#1041) into this (fresh, post-resume)
1039
- * guard, with mandatory staleness reconciliation: each read is re-verified
1040
- * against the CURRENT on-disk content via its recorded `lineHashes`, and any
1041
- * read whose file changed (or no longer exists, or that carries no verifiable
1042
- * hashes) is DROPPED. A rehydrated read must never mask a real staleness — a
1043
- * resume must not let the agent edit a file that changed on disk while it
1044
- * believed it held a fresh read. Kept reads are replayed through
1045
- * {@link recordRead}, which re-keys through {@link key} (idempotent — the
1046
- * exported keys are already normalized) and re-stamps FileTime so the next
1047
- * `checkEdit` sees a consistent baseline. Version-guarded and null-safe:
1048
- * `undefined` / a mismatched version / a missing field loads as "no prior
1049
- * reads". Returns a count of imported vs dropped reads for logging.
1050
- */
1051
- importState(state) {
1052
- const result = { imported: 0, dropped: 0 };
1053
- if (!state || state.version !== READ_GUARD_STATE_VERSION)
1054
- return result;
1055
- // A corrupt/hand-edited sidecar must degrade to "no prior reads", never
1056
- // throw: loadSessionState validates only version/widget, so a malformed
1057
- // `reads` reaches here. If importState threw, the session_start try/catch
1058
- // would abort the ENTIRE rehydration (incl. widget + mountLensWidget)
1059
- // rather than just skipping the read-set.
1060
- if (!Array.isArray(state.reads))
1061
- return result;
1062
- for (const entry of state.reads) {
1063
- // Skip anything that isn't a well-formed [key, records] tuple.
1064
- if (!Array.isArray(entry) || entry.length !== 2)
1065
- continue;
1066
- const [rawPath, records] = entry;
1067
- if (typeof rawPath !== "string")
1068
- continue;
1069
- if (!Array.isArray(records) || records.length === 0)
1070
- continue;
1071
- const filePath = this.key(rawPath);
1072
- let lines;
1073
- try {
1074
- lines = splitLines(fs.readFileSync(filePath, "utf-8"));
1075
- }
1076
- catch {
1077
- // File gone since it was read → drop every read for it.
1078
- result.dropped += records.length;
1079
- continue;
1080
- }
1081
- for (const record of records) {
1082
- const rehydrated = { ...record, filePath };
1083
- // readHashesStillMatch returns false when the recorded hashes no
1084
- // longer match disk OR when the read captured no hashes — both
1085
- // unverifiable, so both drop (safety over convenience).
1086
- if (this.readHashesStillMatch(rehydrated, lines)) {
1087
- this.recordRead(rehydrated);
1088
- result.imported += 1;
1089
- }
1090
- else {
1091
- result.dropped += 1;
1092
- }
1093
- }
1094
- }
1095
- return result;
1096
- }
1097
- /**
1098
- * Get all edit records for a file (for debugging).
1099
- */
1100
- getEditHistory(filePath) {
1101
- const key = this.key(filePath);
1102
- if (this.edits.has(key))
1103
- this.touchFile(key);
1104
- return this.edits.get(key) ?? [];
1105
- }
1106
- // --- Private helpers ---
1107
- injectCreationRead(filePath, turnIndex, writeIndex) {
1108
- let lineCount = 0;
1109
- try {
1110
- lineCount = splitLines(fs.readFileSync(filePath, "utf-8")).length;
1111
- }
1112
- catch {
1113
- return;
1114
- }
1115
- if (lineCount === 0)
1116
- return;
1117
- this.recordRead({
1118
- filePath,
1119
- requestedOffset: 1,
1120
- requestedLimit: lineCount,
1121
- effectiveOffset: 1,
1122
- effectiveLimit: lineCount,
1123
- expandedByLsp: false,
1124
- turnIndex,
1125
- writeIndex,
1126
- timestamp: Date.now(),
1127
- });
1128
- }
1129
- wasWrittenThisSession(filePath) {
1130
- if (this.unchangedThisSession.has(filePath))
1131
- return false;
1132
- // Authoritative path: we observed a write of this file via recordWritten.
1133
- // Survives mtime granularity (FAT32 ~2s), clock skew (NFS), and external
1134
- // tools that touch mtime backward.
1135
- if (this.writtenThisSession.has(filePath))
1136
- return true;
1137
- try {
1138
- return fs.statSync(filePath).mtimeMs >= this.sessionStartMs;
1139
- }
1140
- catch {
1141
- return false;
1142
- }
1143
- }
1144
- canTreatStalenessAsOwnPriorEdit(filePath, lastReadTimestamp) {
1145
- const edits = this.edits.get(filePath) ?? [];
1146
- const latest = edits.at(-1);
1147
- if (!latest)
1148
- return false;
1149
- if (latest.verdict !== "allowed" && latest.verdict !== "warned")
1150
- return false;
1151
- if (latest.timestamp < lastReadTimestamp)
1152
- return false;
1153
- return Date.now() - latest.timestamp <= OWN_EDIT_STALE_GRACE_MS;
1154
- }
1155
- canIgnoreStalenessByHashes(filePath, reads, touchedLines, editRanges) {
1156
- let lines;
1157
- try {
1158
- lines = splitLines(fs.readFileSync(filePath, "utf-8"));
1159
- }
1160
- catch {
1161
- return false;
1162
- }
1163
- const rangesToCheck = touchedLines
1164
- ? editRanges && editRanges.length > 1
1165
- ? editRanges
1166
- : [touchedLines]
1167
- : undefined;
1168
- if (!rangesToCheck) {
1169
- const lastRead = reads.at(-1);
1170
- return !!lastRead && this.readHashesStillMatch(lastRead, lines);
1171
- }
1172
- return rangesToCheck.every((range) => reads.some((read) => this.readCoversRange(read, range) &&
1173
- this.readRangeHashesStillMatch(read, lines, range)));
1174
- }
1175
- readCoversRange(read, [editStart, editEnd]) {
1176
- const readStart = Math.max(1, read.effectiveOffset - this.config.contextLines);
1177
- const readEnd = read.effectiveOffset + read.effectiveLimit - 1 + this.config.contextLines;
1178
- if (editStart >= readStart && editEnd <= readEnd)
1179
- return true;
1180
- if (!read.enclosingSymbol)
1181
- return false;
1182
- return (read.enclosingSymbol.startLine <= editStart &&
1183
- read.enclosingSymbol.endLine >= editEnd);
1184
- }
1185
- validateRangeSnapshot(filePath, range,
1186
- /**
1187
- * What the CALLER will do with the verdict. `checkEdit` honours
1188
- * `skipSnapshotCheck` for content-validated (oldText) edits, so a
1189
- * `shouldBlock` verdict may never reach the gate (#1904 item 1).
1190
- */
1191
- snapshotCheckSkipped) {
1192
- const reads = this.reads.get(filePath) ?? [];
1193
- const candidates = reads.filter((read) => this.readCoversRange(read, range));
1194
- let status = "unavailable";
1195
- let matchingReadIndex = -1;
1196
- let missingLines = [];
1197
- let mismatchedLines = [];
1198
- let checkedCandidateCount = 0;
1199
- let unavailableCandidateCount = 0;
1200
- let hashUnavailableCandidateCount = 0;
1201
- let lastMismatchTimestamp = -Infinity;
1202
- let lastUnavailableTimestamp = -Infinity;
1203
- for (let i = 0; i < candidates.length; i += 1) {
1204
- const validation = currentLinesMatchReadSnapshot(filePath, candidates[i], range);
1205
- if (!validation.checked) {
1206
- unavailableCandidateCount += 1;
1207
- if (readEffectiveRangeCoversRange(candidates[i], range)) {
1208
- hashUnavailableCandidateCount += 1;
1209
- }
1210
- if (status === "unavailable") {
1211
- missingLines = validation.missingLines;
1212
- }
1213
- lastUnavailableTimestamp = Math.max(lastUnavailableTimestamp, candidates[i].timestamp);
1214
- continue;
1215
- }
1216
- checkedCandidateCount += 1;
1217
- if (validation.matches) {
1218
- status = "match";
1219
- matchingReadIndex = i;
1220
- missingLines = [];
1221
- mismatchedLines = [];
1222
- break;
1223
- }
1224
- status = "mismatch";
1225
- missingLines = [];
1226
- mismatchedLines = validation.mismatchedLines;
1227
- lastMismatchTimestamp = Math.max(lastMismatchTimestamp, candidates[i].timestamp);
1228
- }
1229
- // Enforce only when no candidate that actually delivered the target range
1230
- // lacks hashes. Context-only/symbol-only coverage may be unavailable without
1231
- // weakening enforcement from another hash-checkable read of the same range.
1232
- // Also suppress when a re-read (unavailable only due to context-zone boundary)
1233
- // is more recent than the stale read that triggered the mismatch — the agent
1234
- // refreshed their view, and the re-read's edge lines fall within contextLines.
1235
- const shouldBlock = status === "mismatch" &&
1236
- lastUnavailableTimestamp <= lastMismatchTimestamp &&
1237
- checkedCandidateCount > 0 &&
1238
- hashUnavailableCandidateCount === 0;
1239
- // `enforced` below states INTENT — whether this validation reached a
1240
- // decidable verdict. It says nothing about what the caller did with it.
1241
- // `outcome` states the caller's actual behaviour, which is what an
1242
- // enforcement rate must be computed from (#1904 item 1).
1243
- const outcome = shouldBlock
1244
- ? snapshotCheckSkipped
1245
- ? "bypassed-content-match"
1246
- : "enforced-block"
1247
- : status === "match"
1248
- ? "enforced-pass"
1249
- : "not-decidable";
1250
- logReadGuardEvent({
1251
- event: "range_snapshot_validation",
1252
- sessionId: this.sessionId,
1253
- filePath,
1254
- metadata: {
1255
- range,
1256
- status,
1257
- candidateReadCount: candidates.length,
1258
- checkedCandidateCount,
1259
- unavailableCandidateCount,
1260
- hashUnavailableCandidateCount,
1261
- matchingReadIndex,
1262
- missingLineCount: missingLines.length,
1263
- mismatchedLineCount: mismatchedLines.length,
1264
- missingLines: missingLines.slice(0, 20),
1265
- mismatchedLines: mismatchedLines.slice(0, 20),
1266
- enforced: shouldBlock || status === "match",
1267
- outcome,
1268
- },
1269
- });
1270
- return {
1271
- status,
1272
- matchingReadIndex,
1273
- missingLines,
1274
- mismatchedLines,
1275
- candidateReadCount: candidates.length,
1276
- checkedCandidateCount,
1277
- unavailableCandidateCount,
1278
- shouldBlock,
1279
- };
1280
- }
1281
- readRangeHashesStillMatch(read, lines, [startLine, endLine]) {
1282
- const hashes = read.lineHashes ?? {};
1283
- for (let lineNo = startLine; lineNo <= endLine; lineNo += 1) {
1284
- if (!readRangeCoversLine(read, lineNo) || hashes[lineNo] === undefined) {
1285
- return false;
1286
- }
1287
- if (lineNo < 1 || lineNo > lines.length)
1288
- return false;
1289
- if (lineContentHash(lines[lineNo - 1] ?? "") !== hashes[lineNo]) {
1290
- return false;
1291
- }
1292
- }
1293
- return true;
1294
- }
1295
- /**
1296
- * Content-verified relocation. When a range the agent read has drifted, find
1297
- * where the read-time line-hash sequence for [startLine,endLine] now appears
1298
- * in the current file. Returns the unique new location, or undefined when:
1299
- * no recorded read captured hashes for the whole range; the sequence is too
1300
- * short to be collision-resistant (<2 lines); or it now matches zero or
1301
- * multiple spots. Powers a *hint* only — never a silent positional re-apply.
1302
- */
1303
- findRelocation(filePath, reads, [startLine, endLine]) {
1304
- const span = endLine - startLine + 1;
1305
- // A single line's hash collides too easily to relocate on confidently.
1306
- if (span < 2)
1307
- return undefined;
1308
- // Newest read that captured hashes for the entire target range wins.
1309
- let wanted;
1310
- for (let i = reads.length - 1; i >= 0; i -= 1) {
1311
- const hashes = reads[i].lineHashes;
1312
- if (!hashes)
1313
- continue;
1314
- const seq = [];
1315
- let complete = true;
1316
- for (let lineNo = startLine; lineNo <= endLine; lineNo += 1) {
1317
- const h = hashes[lineNo];
1318
- if (h === undefined) {
1319
- complete = false;
1320
- break;
1321
- }
1322
- seq.push(h);
1323
- }
1324
- if (complete) {
1325
- wanted = seq;
1326
- break;
1327
- }
1328
- }
1329
- if (!wanted)
1330
- return undefined;
1331
- let lines;
1332
- try {
1333
- lines = splitLines(fs.readFileSync(filePath, "utf-8"));
1334
- }
1335
- catch {
1336
- return undefined;
1337
- }
1338
- const currentHashes = lines.map((line) => lineContentHash(line));
1339
- const lastStart = currentHashes.length - span; // last valid 0-based start
1340
- const matchStarts = [];
1341
- for (let i = 0; i <= lastStart; i += 1) {
1342
- let ok = true;
1343
- for (let j = 0; j < span; j += 1) {
1344
- if (currentHashes[i + j] !== wanted[j]) {
1345
- ok = false;
1346
- break;
1347
- }
1348
- }
1349
- if (ok)
1350
- matchStarts.push(i + 1); // 1-indexed
1351
- }
1352
- let newStart;
1353
- if (matchStarts.length === 1) {
1354
- // Unique across the whole file → certainly the relocated span,
1355
- // regardless of how far it drifted (e.g. a large refactor moved it).
1356
- newStart = matchStarts[0];
1357
- }
1358
- else if (matchStarts.length > 1) {
1359
- // Duplicated elsewhere: fall back to locality. Lines rarely teleport,
1360
- // so accept a match unique WITHIN an adaptive window of the original
1361
- // position — out-of-window duplicates don't poison a locally
1362
- // unambiguous relocation. The window widens with the edits already
1363
- // applied to this file this session (each prior edit shifts line
1364
- // numbers, so accumulated drift grows).
1365
- const appliedEdits = (this.edits.get(filePath) ?? []).filter((record) => record.verdict !== "blocked").length;
1366
- const window = Math.min(RELOCATION_WINDOW_MAX, Math.max(RELOCATION_WINDOW_MIN, appliedEdits * RELOCATION_WINDOW_PER_EDIT));
1367
- const lo = startLine - window;
1368
- const hi = endLine + window;
1369
- const local = matchStarts.filter((start) => start >= lo && start <= hi);
1370
- if (local.length === 1)
1371
- newStart = local[0];
1372
- }
1373
- if (newStart === undefined || newStart === startLine)
1374
- return undefined;
1375
- return { from: [startLine, endLine], to: [newStart, newStart + span - 1] };
1376
- }
1377
- readHashesStillMatch(read, lines) {
1378
- const entries = Object.entries(read.lineHashes ?? {});
1379
- if (entries.length === 0)
1380
- return false;
1381
- for (const [lineText, expected] of entries) {
1382
- const lineNo = Number(lineText);
1383
- if (!Number.isInteger(lineNo) || lineNo < 1 || lineNo > lines.length) {
1384
- return false;
1385
- }
1386
- if (lineContentHash(lines[lineNo - 1] ?? "") !== expected)
1387
- return false;
1388
- }
1389
- return true;
1390
- }
1391
- checkCoverage(filePath, touchedLines) {
1392
- const [editStart, editEnd] = touchedLines;
1393
- const reads = this.reads.get(filePath) ?? [];
1394
- // First pass: check symbol coverage and any single read that covers the edit.
1395
- for (const read of reads) {
1396
- if (read.provisional === true)
1397
- continue;
1398
- const readStart = Math.max(1, read.effectiveOffset - this.config.contextLines);
1399
- const readEnd = read.effectiveOffset +
1400
- read.effectiveLimit -
1401
- 1 +
1402
- this.config.contextLines;
1403
- if (editStart >= readStart && editEnd <= readEnd) {
1404
- return { covered: true, viaSymbol: false };
1405
- }
1406
- if (read.enclosingSymbol) {
1407
- const symStart = read.enclosingSymbol.startLine;
1408
- const symEnd = read.enclosingSymbol.endLine;
1409
- if (symStart <= editStart && symEnd >= editEnd) {
1410
- return { covered: true, viaSymbol: true };
1411
- }
1412
- }
1413
- }
1414
- // Second pass: merge all read intervals and check if their union covers
1415
- // [editStart, editEnd]. Handles multi-chunk reads (e.g. 1-100 + 101-200).
1416
- const intervals = reads
1417
- .filter((read) => read.provisional !== true)
1418
- .map((read) => [
1419
- Math.max(1, read.effectiveOffset - this.config.contextLines),
1420
- read.effectiveOffset +
1421
- read.effectiveLimit -
1422
- 1 +
1423
- this.config.contextLines,
1424
- ]);
1425
- intervals.sort((a, b) => a[0] - b[0]);
1426
- // Merge overlapping/adjacent intervals
1427
- const merged = [];
1428
- for (const [s, e] of intervals) {
1429
- if (merged.length > 0 && s <= merged[merged.length - 1][1] + 1) {
1430
- merged[merged.length - 1][1] = Math.max(merged[merged.length - 1][1], e);
1431
- }
1432
- else {
1433
- merged.push([s, e]);
1434
- }
1435
- }
1436
- for (const [s, e] of merged) {
1437
- if (editStart >= s && editEnd <= e) {
1438
- return { covered: true, viaSymbol: false };
1439
- }
1440
- }
1441
- return { covered: false, viaSymbol: false };
1442
- }
1443
- getExemptionMode(filePath) {
1444
- for (const exemption of this.config.exemptions) {
1445
- if (this.matchesPattern(filePath, exemption.pattern)) {
1446
- return exemption.mode;
1447
- }
1448
- }
1449
- return null;
1450
- }
1451
- matchesPattern(filePath, pattern) {
1452
- // Simple glob matching — can be expanded
1453
- if (pattern.startsWith("*")) {
1454
- const suffix = pattern.slice(1);
1455
- return filePath.endsWith(suffix);
1456
- }
1457
- if (pattern.includes("*")) {
1458
- // Adjacent stars are equivalent to one star in this dialect. Collapse
1459
- // them before compiling so a non-match cannot backtrack over every
1460
- // nullable `.*` group (#2622).
1461
- const collapsedPattern = pattern.replace(/\*+/g, "*");
1462
- const regex = new RegExp(`^${collapsedPattern.replace(/\\/g, "\\\\").replace(/\./g, "\\.").replace(/\*/g, ".*")}$`);
1463
- return regex.test(filePath);
1464
- }
1465
- return filePath === pattern;
1466
- }
1467
- blockOrWarn(_reason, message, details, overrideMode) {
1468
- const mode = overrideMode ?? this.config.mode;
1469
- if (mode === "warn") {
1470
- return { action: "warn", reason: message, details };
1471
- }
1472
- return { action: "block", reason: message, details };
1473
- }
1474
- allow() {
1475
- return { action: "allow" };
1476
- }
1477
- recordEdit(filePath, tool, touchedLines, verdict) {
1478
- this.touchFile(filePath);
1479
- const arr = this.edits.get(filePath) ?? [];
1480
- arr.push({
1481
- filePath,
1482
- tool,
1483
- touchedLines,
1484
- precedingReads: this.reads.get(filePath) ?? [],
1485
- verdict: mapVerdictAction(verdict.action),
1486
- reason: verdict.reason,
1487
- timestamp: Date.now(),
1488
- });
1489
- if (arr.length > READ_GUARD_MAX_EDITS_PER_FILE) {
1490
- const trimmedCount = arr.length - READ_GUARD_MAX_EDITS_PER_FILE;
1491
- arr.splice(0, trimmedCount);
1492
- // #1918: the #282-289 doc comment argues this trim is inert in
1493
- // practice (every consumer reads only the last record or a
1494
- // bounded relocation window), but an argument is not a record —
1495
- // if that assumption ever breaks, this is the only signal. Same
1496
- // aggregate-after-first shape as `read_cap_trimmed` (#1913):
1497
- // `incrementDegradationCount` keeps the exact per-file tally,
1498
- // the read-guard.log line fires once on the rising edge.
1499
- const isRisingEdge = incrementDegradationCount({
1500
- kind: "read-guard-edits-cap-trim",
1501
- subject: filePath,
1502
- reason: `trimmed ${trimmedCount} edit record(s) past cap`,
1503
- });
1504
- if (isRisingEdge) {
1505
- logReadGuardEvent({
1506
- event: "edits_cap_trimmed",
1507
- sessionId: this.sessionId,
1508
- filePath,
1509
- metadata: { trimmedCount, cappedLength: arr.length },
1510
- });
1511
- }
1512
- }
1513
- this.edits.set(filePath, arr);
1514
- }
1515
- recordVerdict(filePath, tool, touchedLines, verdict, metadata = {}) {
1516
- const normalizedTouchedLines = touchedLines ?? [1, 1];
1517
- this.recordEdit(filePath, tool, normalizedTouchedLines, verdict);
1518
- const reads = this.reads.get(filePath) ?? [];
1519
- logReadGuardEvent({
1520
- event: verdict.action === "allow"
1521
- ? "edit_allowed"
1522
- : verdict.action === "warn"
1523
- ? "edit_warned"
1524
- : "edit_blocked",
1525
- sessionId: this.sessionId,
1526
- filePath,
1527
- metadata: {
1528
- tool,
1529
- touchedLines: touchedLines ?? null,
1530
- normalizedTouchedLines,
1531
- readCount: reads.length,
1532
- reads: reads.map((read) => ({
1533
- requestedOffset: read.requestedOffset,
1534
- requestedLimit: read.requestedLimit,
1535
- effectiveOffset: read.effectiveOffset,
1536
- effectiveLimit: read.effectiveLimit,
1537
- expandedByLsp: read.expandedByLsp,
1538
- enclosingSymbol: read.enclosingSymbol ?? null,
1539
- timestamp: read.timestamp,
1540
- })),
1541
- verdictAction: verdict.action,
1542
- details: verdict.details,
1543
- ...metadata,
1544
- },
1545
- });
1546
- }
1547
- }
1548
- // --- Factory ---
1549
- function mapVerdictAction(action) {
1550
- switch (action) {
1551
- case "allow":
1552
- return "allowed";
1553
- case "block":
1554
- return "blocked";
1555
- case "warn":
1556
- return "warned";
1557
- }
1558
- }
1559
- export function createReadGuard(sessionId, config) {
1560
- return new ReadGuard(sessionId, config);
1561
- }