@herbertgao/pi-extensions 2026.10.0 → 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 (769) hide show
  1. package/README.md +16 -12
  2. package/THIRD_PARTY_NOTICES.md +3 -3
  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/.opencode/plugins/index.js +4 -0
  10. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +43 -3
  11. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +142 -59
  12. package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
  13. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  14. package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
  15. package/node_modules/@dietrichgebert/ponytail/README.md +52 -242
  16. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +3 -3
  17. package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +4 -4
  18. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +70 -16
  19. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +8 -7
  20. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
  21. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
  22. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +46 -23
  23. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +39 -7
  24. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +20 -3
  25. package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +3 -3
  26. package/node_modules/@dietrichgebert/ponytail/package.json +3 -3
  27. package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +47 -17
  28. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -7
  29. package/node_modules/@dietrichgebert/ponytail/skills/plugin.json +5 -0
  30. package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
  31. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -27
  32. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
  33. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +21 -18
  34. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +12 -13
  35. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -52
  36. package/node_modules/@herbertgao/pi-agency/LICENSE +21 -0
  37. package/node_modules/@herbertgao/pi-agency/README.md +40 -0
  38. package/node_modules/@herbertgao/pi-agency/package.json +53 -0
  39. package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
  43. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
  44. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
  45. package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
  46. package/node_modules/@narumitw/pi-caffeinate/package.json +4 -4
  47. package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
  48. package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
  49. package/node_modules/@tifan/pi-recap/package.json +1 -1
  50. package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
  51. package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
  52. package/node_modules/pi-antigravity/README.md +23 -2
  53. package/node_modules/pi-antigravity/package.json +5 -3
  54. package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
  55. package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
  56. package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
  57. package/node_modules/pi-antigravity/src/image/image.ts +86 -47
  58. package/node_modules/pi-antigravity/src/image/native.ts +69 -0
  59. package/node_modules/pi-antigravity/src/index.ts +52 -137
  60. package/node_modules/pi-antigravity/src/search/search.ts +130 -31
  61. package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
  62. package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
  63. package/node_modules/pi-antigravity/src/types/types.ts +1 -1
  64. package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
  65. package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
  66. package/node_modules/pi-lens/CHANGELOG.md +1064 -0
  67. package/node_modules/pi-lens/README.md +115 -25
  68. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
  69. package/node_modules/pi-lens/dist/chunk-22AVIGXS.js +560 -0
  70. package/node_modules/pi-lens/dist/chunk-27BBHSZC.js +106 -0
  71. package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
  72. package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
  73. package/node_modules/pi-lens/dist/chunk-34U7PLGE.js +23 -0
  74. package/node_modules/pi-lens/dist/chunk-3I5QFT3N.js +181 -0
  75. package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
  76. package/node_modules/pi-lens/dist/chunk-47D2BO4S.js +87 -0
  77. package/node_modules/pi-lens/dist/chunk-4RERDEXR.js +348 -0
  78. package/node_modules/pi-lens/dist/chunk-4W2PLC4J.js +27 -0
  79. package/node_modules/pi-lens/dist/chunk-52H5B5TC.js +202 -0
  80. package/node_modules/pi-lens/dist/chunk-54B42YRX.js +27 -0
  81. package/node_modules/pi-lens/dist/chunk-5RXBNSW2.js +266 -0
  82. package/node_modules/pi-lens/dist/chunk-5THXD6X5.js +201 -0
  83. package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
  84. package/node_modules/pi-lens/dist/chunk-5ZEJHV35.js +341 -0
  85. package/node_modules/pi-lens/dist/chunk-6P62QVEA.js +118 -0
  86. package/node_modules/pi-lens/dist/chunk-6QIUX4EP.js +459 -0
  87. package/node_modules/pi-lens/dist/chunk-6VCD4VKZ.js +78 -0
  88. package/node_modules/pi-lens/dist/chunk-7F72PXY2.js +326 -0
  89. package/node_modules/pi-lens/dist/chunk-7FBF7NPR.js +998 -0
  90. package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
  91. package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
  92. package/node_modules/pi-lens/dist/chunk-AHDW3GPE.js +50 -0
  93. package/node_modules/pi-lens/dist/chunk-AJ3QXTRQ.js +756 -0
  94. package/node_modules/pi-lens/dist/chunk-AJVLWNFN.js +1610 -0
  95. package/node_modules/pi-lens/dist/chunk-AJXODZY6.js +112 -0
  96. package/node_modules/pi-lens/dist/chunk-ARFCXG7S.js +84 -0
  97. package/node_modules/pi-lens/dist/chunk-AS44CFQ4.js +708 -0
  98. package/node_modules/pi-lens/dist/chunk-AWE5TMPO.js +38 -0
  99. package/node_modules/pi-lens/dist/chunk-B3I2UUT4.js +4692 -0
  100. package/node_modules/pi-lens/dist/chunk-BRJKOBZP.js +331 -0
  101. package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
  102. package/node_modules/pi-lens/dist/chunk-C2AUYXQH.js +149 -0
  103. package/node_modules/pi-lens/dist/chunk-CGGAIP7J.js +29 -0
  104. package/node_modules/pi-lens/dist/chunk-D6QS3U6M.js +30 -0
  105. package/node_modules/pi-lens/dist/chunk-DFVMIKMQ.js +349 -0
  106. package/node_modules/pi-lens/dist/chunk-DIULIWKU.js +169 -0
  107. package/node_modules/pi-lens/dist/chunk-DQ2NZ7C4.js +2317 -0
  108. package/node_modules/pi-lens/dist/chunk-E2WFBRHM.js +213 -0
  109. package/node_modules/pi-lens/dist/chunk-E4MAEDPM.js +50 -0
  110. package/node_modules/pi-lens/dist/chunk-EOKRQ3SA.js +30 -0
  111. package/node_modules/pi-lens/dist/chunk-F3AU2TMM.js +2626 -0
  112. package/node_modules/pi-lens/dist/chunk-F3P7VQ7H.js +170 -0
  113. package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
  114. package/node_modules/pi-lens/dist/chunk-FJNKANOS.js +32 -0
  115. package/node_modules/pi-lens/dist/chunk-GUOGVTCQ.js +146 -0
  116. package/node_modules/pi-lens/dist/chunk-GVXA6CCE.js +18 -0
  117. package/node_modules/pi-lens/dist/chunk-HHYAELLJ.js +2334 -0
  118. package/node_modules/pi-lens/dist/chunk-HMFXILPV.js +83 -0
  119. package/node_modules/pi-lens/dist/chunk-HVUZXSWK.js +421 -0
  120. package/node_modules/pi-lens/dist/chunk-HWBZIGMJ.js +62 -0
  121. package/node_modules/pi-lens/dist/chunk-I3R7GKDR.js +17 -0
  122. package/node_modules/pi-lens/dist/chunk-I7SPGOWI.js +55 -0
  123. package/node_modules/pi-lens/dist/chunk-IIXOAGHR.js +38 -0
  124. package/node_modules/pi-lens/dist/chunk-IVP5KAB7.js +2250 -0
  125. package/node_modules/pi-lens/dist/chunk-IXEBJSUE.js +22 -0
  126. package/node_modules/pi-lens/dist/chunk-JOLITGCC.js +27 -0
  127. package/node_modules/pi-lens/dist/chunk-K6VYLB2U.js +145 -0
  128. package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
  129. package/node_modules/pi-lens/dist/chunk-LNAKEO63.js +78 -0
  130. package/node_modules/pi-lens/dist/chunk-LSMJ6KBE.js +4139 -0
  131. package/node_modules/pi-lens/dist/chunk-LW6XKDZQ.js +5565 -0
  132. package/node_modules/pi-lens/dist/chunk-N3YQJI6O.js +899 -0
  133. package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
  134. package/node_modules/pi-lens/dist/chunk-NKOUFPQF.js +659 -0
  135. package/node_modules/pi-lens/dist/chunk-NL5GLMFI.js +211 -0
  136. package/node_modules/pi-lens/dist/chunk-NMABWBZN.js +327 -0
  137. package/node_modules/pi-lens/dist/chunk-NTHH2J74.js +27 -0
  138. package/node_modules/pi-lens/dist/chunk-NTUJEGDZ.js +59 -0
  139. package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
  140. package/node_modules/pi-lens/dist/chunk-NXPS2L6U.js +466 -0
  141. package/node_modules/pi-lens/dist/chunk-O5WILGMI.js +42 -0
  142. package/node_modules/pi-lens/dist/chunk-O6TQT6RI.js +48 -0
  143. package/node_modules/pi-lens/dist/chunk-OAXNDZJ7.js +323 -0
  144. package/node_modules/pi-lens/dist/chunk-ODEEOXPH.js +44 -0
  145. package/node_modules/pi-lens/dist/chunk-OQGFCZJ4.js +4533 -0
  146. package/node_modules/pi-lens/dist/chunk-ORTMY4Q4.js +282 -0
  147. package/node_modules/pi-lens/dist/chunk-OS32RNR2.js +442 -0
  148. package/node_modules/pi-lens/dist/chunk-PAK6IDME.js +189 -0
  149. package/node_modules/pi-lens/dist/chunk-PWFHBTSF.js +406 -0
  150. package/node_modules/pi-lens/dist/chunk-Q5U6FMDI.js +2352 -0
  151. package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
  152. package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
  153. package/node_modules/pi-lens/dist/chunk-RIISMHKN.js +36 -0
  154. package/node_modules/pi-lens/dist/chunk-RPD4ZSB7.js +25 -0
  155. package/node_modules/pi-lens/dist/chunk-RPD5K5NC.js +79 -0
  156. package/node_modules/pi-lens/dist/chunk-RUOG5D2C.js +262 -0
  157. package/node_modules/pi-lens/dist/chunk-T4ERT73Y.js +599 -0
  158. package/node_modules/pi-lens/dist/chunk-T6D4Q4AQ.js +54 -0
  159. package/node_modules/pi-lens/dist/chunk-TGPXLY2J.js +391 -0
  160. package/node_modules/pi-lens/dist/chunk-TLGFBM3F.js +12116 -0
  161. package/node_modules/pi-lens/dist/chunk-TO6WL3GM.js +88 -0
  162. package/node_modules/pi-lens/dist/chunk-TT22N74J.js +517 -0
  163. package/node_modules/pi-lens/dist/chunk-TVN5N6GY.js +21216 -0
  164. package/node_modules/pi-lens/dist/chunk-U3VGR4VU.js +308 -0
  165. package/node_modules/pi-lens/dist/chunk-UK3CMEAL.js +1250 -0
  166. package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
  167. package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
  168. package/node_modules/pi-lens/dist/chunk-UY752QSN.js +45 -0
  169. package/node_modules/pi-lens/dist/chunk-V5PXFSZK.js +188 -0
  170. package/node_modules/pi-lens/dist/chunk-V7R425XG.js +398 -0
  171. package/node_modules/pi-lens/dist/chunk-VCSRSYLF.js +81 -0
  172. package/node_modules/pi-lens/dist/chunk-VD4JQCQC.js +162 -0
  173. package/node_modules/pi-lens/dist/chunk-VDBPVIKD.js +62 -0
  174. package/node_modules/pi-lens/dist/chunk-VHQLPIRJ.js +114 -0
  175. package/node_modules/pi-lens/dist/chunk-VN2AXLEX.js +5414 -0
  176. package/node_modules/pi-lens/dist/chunk-VNMT7C64.js +478 -0
  177. package/node_modules/pi-lens/dist/chunk-WONERJTP.js +309 -0
  178. package/node_modules/pi-lens/dist/chunk-XECLWQZN.js +85 -0
  179. package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
  180. package/node_modules/pi-lens/dist/chunk-XK4CIILI.js +144 -0
  181. package/node_modules/pi-lens/dist/chunk-Y4T27COV.js +562 -0
  182. package/node_modules/pi-lens/dist/chunk-YGVYKNSN.js +1860 -0
  183. package/node_modules/pi-lens/dist/chunk-YLF3PPT5.js +308 -0
  184. package/node_modules/pi-lens/dist/chunk-YSNFMJKN.js +14 -0
  185. package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
  186. package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
  187. package/node_modules/pi-lens/dist/chunk-ZAWW3AF6.js +97 -0
  188. package/node_modules/pi-lens/dist/chunk-ZMOL6RQC.js +167 -0
  189. package/node_modules/pi-lens/dist/index.js +16456 -8335
  190. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
  191. package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
  192. package/node_modules/pi-lens/dist/mcp/server.js +22589 -1573
  193. package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
  194. package/node_modules/pi-lens/dist/probes/installer.js +135 -0
  195. package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
  196. package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
  197. package/node_modules/pi-lens/docs/agent-guide.md +13 -4
  198. package/node_modules/pi-lens/docs/agent-tools.md +42 -0
  199. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +3 -2
  200. package/node_modules/pi-lens/docs/configuration.md +20 -1
  201. package/node_modules/pi-lens/docs/environment-variables.md +52 -2
  202. package/node_modules/pi-lens/docs/features.md +8 -4
  203. package/node_modules/pi-lens/docs/globalconfig.md +1 -0
  204. package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
  205. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +36 -4
  206. package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
  207. package/node_modules/pi-lens/docs/mcp.md +21 -5
  208. package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
  209. package/node_modules/pi-lens/docs/pi-lens-fixer.md +156 -88
  210. package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
  211. package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +694 -0
  212. package/node_modules/pi-lens/docs/pi-lens-monitor.md +73 -7
  213. package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
  214. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +188 -48
  215. package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
  216. package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
  217. package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
  218. package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
  219. package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  220. package/node_modules/pi-lens/docs/servercapabilities.md +53 -49
  221. package/node_modules/pi-lens/docs/settings.md +1 -0
  222. package/node_modules/pi-lens/docs/usage.md +5 -1
  223. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
  224. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
  225. package/node_modules/pi-lens/package.json +18 -6
  226. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  227. package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  228. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
  229. package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
  230. package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
  231. package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
  232. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
  233. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
  234. package/node_modules/pi-typesafe/README.md +1 -1
  235. package/node_modules/pi-typesafe/dist/backends.d.ts +15 -5
  236. package/node_modules/pi-typesafe/dist/backends.js +18 -0
  237. package/node_modules/pi-typesafe/dist/client.d.ts +4 -3
  238. package/node_modules/pi-typesafe/dist/client.js +22 -7
  239. package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
  240. package/node_modules/pi-typesafe/dist/index.js +1 -1
  241. package/node_modules/pi-typesafe/dist/usage.d.ts +14 -4
  242. package/node_modules/pi-typesafe/dist/usage.js +147 -42
  243. package/node_modules/pi-typesafe/package.json +1 -1
  244. package/node_modules/pi-web-access/CHANGELOG.md +48 -0
  245. package/node_modules/pi-web-access/README.md +57 -11
  246. package/node_modules/pi-web-access/brave-rate-limit.ts +8 -2
  247. package/node_modules/pi-web-access/curator-page.ts +5 -1
  248. package/node_modules/pi-web-access/curator-server.ts +2 -0
  249. package/node_modules/pi-web-access/declared-web-links.ts +2 -2
  250. package/node_modules/pi-web-access/degoog.ts +323 -0
  251. package/node_modules/pi-web-access/dist/index.js +3483 -2745
  252. package/node_modules/pi-web-access/dist/mcp-cli.js +27443 -0
  253. package/node_modules/pi-web-access/exa.ts +5 -1
  254. package/node_modules/pi-web-access/extract.ts +41 -3
  255. package/node_modules/pi-web-access/firecrawl.ts +35 -4
  256. package/node_modules/pi-web-access/gemini-search.ts +34 -11
  257. package/node_modules/pi-web-access/gemini-web.ts +14 -9
  258. package/node_modules/pi-web-access/index.ts +153 -1202
  259. package/node_modules/pi-web-access/keenable.ts +221 -0
  260. package/node_modules/pi-web-access/mcp-cli.ts +40 -0
  261. package/node_modules/pi-web-access/mcp-server.ts +81 -0
  262. package/node_modules/pi-web-access/mcp-tool-definitions.ts +117 -0
  263. package/node_modules/pi-web-access/openai-search.ts +34 -1
  264. package/node_modules/pi-web-access/package.json +23 -5
  265. package/node_modules/pi-web-access/perplexity.ts +64 -56
  266. package/node_modules/pi-web-access/tool-arguments.ts +20 -0
  267. package/node_modules/pi-web-access/video-extract.ts +1 -1
  268. package/node_modules/pi-web-access/web-tool-contract.ts +86 -0
  269. package/node_modules/pi-web-access/web-tool-core.ts +1502 -0
  270. package/node_modules/pi-web-access/youtube-extract.ts +2 -5
  271. package/package.json +17 -12
  272. package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -329
  273. package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -346
  274. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +0 -21
  275. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +0 -62
  276. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
  277. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +0 -115
  278. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +0 -27
  279. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +0 -27
  280. package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
  281. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  282. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +0 -4
  283. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +0 -4
  284. package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
  285. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
  286. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
  287. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
  288. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
  289. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
  290. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
  291. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
  292. package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
  293. package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
  294. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
  295. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
  296. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
  297. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
  298. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
  299. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
  300. package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
  301. package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
  302. package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
  303. package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
  304. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
  305. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
  306. package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
  307. package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
  308. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
  309. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
  310. package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
  311. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
  312. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
  313. package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
  314. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
  315. package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
  316. package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
  317. package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
  318. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
  319. package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
  320. package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
  321. package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
  322. package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
  323. package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
  324. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
  325. package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
  326. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
  327. package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
  328. package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
  329. package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
  330. package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
  331. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
  332. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
  333. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
  334. package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
  335. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
  336. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
  337. package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
  338. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
  339. package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
  340. package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
  341. package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
  342. package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
  343. package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
  344. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
  345. package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
  346. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
  347. package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
  348. package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
  349. package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
  350. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
  351. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
  352. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
  353. package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
  354. package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
  355. package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
  356. package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
  357. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
  358. package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
  359. package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
  360. package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
  361. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
  362. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
  363. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
  364. package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
  365. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
  366. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  367. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  368. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
  369. package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  370. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
  371. package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
  372. package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
  373. package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
  374. package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
  375. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
  376. package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
  377. package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
  378. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
  379. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
  380. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  381. package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  382. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
  383. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
  384. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
  385. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
  386. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
  387. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
  388. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
  389. package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
  390. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
  391. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
  392. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
  393. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
  394. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  395. package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  396. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  397. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  398. package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  399. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
  400. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  401. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  402. package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  403. package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  404. package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  405. package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  406. package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  407. package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
  408. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
  409. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  410. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
  411. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
  412. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
  413. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
  414. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
  415. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
  416. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
  417. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
  418. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
  419. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
  420. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
  421. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
  422. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
  423. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
  424. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
  425. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
  426. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
  427. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
  428. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
  429. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
  430. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
  431. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
  432. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
  433. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
  434. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
  435. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
  436. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
  437. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  438. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  439. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
  440. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
  441. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
  442. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
  443. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
  444. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
  445. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
  446. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
  447. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
  448. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
  449. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
  450. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
  451. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
  452. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
  453. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
  454. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
  455. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  456. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  457. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
  458. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  459. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  460. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  461. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  462. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  463. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
  464. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
  465. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  466. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
  467. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
  468. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
  469. package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
  470. package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
  471. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
  472. package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  473. package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
  474. package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
  475. package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
  476. package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
  477. package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
  478. package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
  479. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
  480. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
  481. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
  482. package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
  483. package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
  484. package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
  485. package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
  486. package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
  487. package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
  488. package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
  489. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
  490. package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
  491. package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
  492. package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
  493. package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
  494. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
  495. package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
  496. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
  497. package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
  498. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
  499. package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
  500. package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
  501. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
  502. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
  503. package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
  504. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
  505. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
  506. package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
  507. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
  508. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
  509. package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
  510. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
  511. package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
  512. package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
  513. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
  514. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
  515. package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
  516. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  517. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
  518. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
  519. package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
  520. package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
  521. package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
  522. package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
  523. package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
  524. package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
  525. package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
  526. package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
  527. package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
  528. package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
  529. package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
  530. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
  531. package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
  532. package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
  533. package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
  534. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
  535. package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
  536. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
  537. package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
  538. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
  539. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
  540. package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
  541. package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
  542. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
  543. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
  544. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
  545. package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
  546. package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
  547. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
  548. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
  549. package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
  550. package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
  551. package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
  552. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
  553. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
  554. package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
  555. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  556. package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
  557. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
  558. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
  559. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
  560. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
  561. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  562. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
  563. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
  564. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
  565. package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
  566. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  567. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  568. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  569. package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
  570. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
  571. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
  572. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
  573. package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
  574. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
  575. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
  576. package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
  577. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
  578. package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
  579. package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
  580. package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
  581. package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
  582. package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
  583. package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
  584. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
  585. package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
  586. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
  587. package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
  588. package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
  589. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
  590. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
  591. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
  592. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
  593. package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
  594. package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
  595. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
  596. package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
  597. package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
  598. package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
  599. package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
  600. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
  601. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
  602. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
  603. package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
  604. package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
  605. package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
  606. package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
  607. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
  608. package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
  609. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
  610. package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
  611. package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
  612. package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
  613. package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
  614. package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
  615. package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
  616. package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
  617. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
  618. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
  619. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
  620. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  621. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  622. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  623. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  624. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  625. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  626. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  627. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  628. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  629. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  630. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
  631. package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
  632. package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
  633. package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
  634. package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
  635. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
  636. package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
  637. package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
  638. package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
  639. package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
  640. package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
  641. package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
  642. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
  643. package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
  644. package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
  645. package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
  646. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
  647. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
  648. package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
  649. package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
  650. package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
  651. package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
  652. package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
  653. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
  654. package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
  655. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
  656. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
  657. package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
  658. package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
  659. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
  660. package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
  661. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  662. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
  663. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
  664. package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
  665. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
  666. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
  667. package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
  668. package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
  669. package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
  670. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
  671. package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
  672. package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
  673. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
  674. package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
  675. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
  676. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
  677. package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
  678. package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
  679. package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
  680. package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
  681. package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
  682. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
  683. package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
  684. package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
  685. package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
  686. package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
  687. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
  688. package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
  689. package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
  690. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
  691. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
  692. package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
  693. package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
  694. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
  695. package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
  696. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
  697. package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
  698. package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
  699. package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
  700. package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
  701. package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
  702. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
  703. package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
  704. package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
  705. package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
  706. package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
  707. package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
  708. package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
  709. package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
  710. package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
  711. package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
  712. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
  713. package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
  714. package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
  715. package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
  716. package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
  717. package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
  718. package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
  719. package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
  720. package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
  721. package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
  722. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
  723. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
  724. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
  725. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
  726. package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
  727. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
  728. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
  729. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  730. package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
  731. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
  732. package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
  733. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
  734. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
  735. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
  736. package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
  737. package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
  738. package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
  739. package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
  740. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
  741. package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
  742. package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
  743. package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
  744. package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
  745. package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
  746. package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
  747. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
  748. package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
  749. package/node_modules/pi-lens/dist/i18n.js +0 -69
  750. package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
  751. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
  752. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
  753. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
  754. package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
  755. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
  756. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
  757. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
  758. package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
  759. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
  760. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
  761. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
  762. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
  763. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
  764. package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
  765. package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
  766. package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
  767. package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
  768. package/node_modules/pi-lens/dist/tools/shared.js +0 -29
  769. 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
- }