@herbertgao/pi-extensions 2026.8.6 → 2026.8.8

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 (385) hide show
  1. package/README.md +8 -6
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +19 -7
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +30 -9
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +320 -22
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +94 -77
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +79 -17
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +27 -0
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +66 -17
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +41 -2
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +75 -6
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +7 -3
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +64 -1
  15. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  16. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  17. package/node_modules/@herbertgao/pi-subagents/README.md +30 -22
  18. package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +5 -5
  19. package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +7 -1
  21. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +48 -19
  22. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +17 -13
  23. package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +26 -2
  24. package/node_modules/@herbertgao/pi-subagents/src/index.ts +219 -40
  25. package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +101 -3
  26. package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +18 -4
  27. package/node_modules/@herbertgao/pi-subagents/src/settings.ts +99 -0
  28. package/node_modules/@herbertgao/pi-subagents/src/types.ts +22 -3
  29. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +51 -3
  30. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +20 -2
  31. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +21 -6
  32. package/node_modules/@herbertgao/pi-subagents/src/usage.ts +129 -1
  33. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +20 -0
  34. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +5 -4
  35. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +174 -100
  36. package/node_modules/@juicesharp/rpiv-ask-user-question/config.ts +25 -0
  37. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/configuration.md +13 -4
  38. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +5 -1
  39. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
  40. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +10 -6
  41. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
  42. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +5 -2
  43. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/zh.json +2 -2
  44. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  45. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +10 -0
  46. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +140 -97
  47. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +63 -28
  48. package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
  49. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
  50. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
  51. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
  52. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
  53. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
  54. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
  55. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
  56. package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +86 -38
  57. package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +76 -9
  58. package/node_modules/@narumitw/pi-btw/README.md +120 -111
  59. package/node_modules/@narumitw/pi-btw/dist/index.ts +2898 -0
  60. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +7 -0
  61. package/node_modules/@narumitw/pi-btw/package.json +10 -6
  62. package/node_modules/@narumitw/pi-btw/src/btw.ts +36 -40
  63. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +55 -10
  64. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +390 -0
  65. package/node_modules/@narumitw/pi-btw/src/menu.ts +45 -2
  66. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
  67. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
  68. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  69. package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
  70. package/node_modules/pi-lens/CHANGELOG.md +999 -0
  71. package/node_modules/pi-lens/README.md +117 -3
  72. package/node_modules/pi-lens/config/ruff/core.toml +42 -0
  73. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +130 -27
  74. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +126 -13
  75. package/node_modules/pi-lens/dist/clients/agent-nudge.js +67 -11
  76. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +9 -3
  77. package/node_modules/pi-lens/dist/clients/atomic-write.js +9 -3
  78. package/node_modules/pi-lens/dist/clients/bash-file-access.js +381 -13
  79. package/node_modules/pi-lens/dist/clients/biome-client.js +61 -30
  80. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
  81. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +129 -0
  82. package/node_modules/pi-lens/dist/clients/bootstrap.js +12 -1
  83. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +12 -3
  84. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +206 -0
  85. package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
  86. package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
  87. package/node_modules/pi-lens/dist/clients/cascade-format.js +20 -1
  88. package/node_modules/pi-lens/dist/clients/cascade-types.js +9 -1
  89. package/node_modules/pi-lens/dist/clients/child-unref.js +54 -6
  90. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +76 -24
  91. package/node_modules/pi-lens/dist/clients/dead-code-client.js +123 -31
  92. package/node_modules/pi-lens/dist/clients/deadline-utils.js +8 -1
  93. package/node_modules/pi-lens/dist/clients/debug-handles.js +5 -0
  94. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +84 -14
  95. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
  96. package/node_modules/pi-lens/dist/clients/dependency-checker.js +25 -3
  97. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
  98. package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
  99. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +147 -33
  100. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
  101. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +52 -2
  102. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +256 -88
  103. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +7 -4
  104. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +5 -0
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +234 -18
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +187 -19
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +6 -3
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +11 -9
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +398 -0
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +11 -3
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +12 -7
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +18 -0
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +895 -0
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +4 -0
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +16 -3
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +60 -53
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -2
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +25 -2
  122. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
  123. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
  124. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +331 -55
  125. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +38 -5
  126. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +97 -19
  127. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +17 -8
  128. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +32 -4
  129. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +47 -3
  130. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +14 -4
  131. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +127 -28
  132. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
  133. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +73 -12
  134. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +275 -13
  135. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +21 -6
  136. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +260 -27
  137. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +734 -64
  138. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +86 -11
  139. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +188 -0
  140. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +9 -0
  141. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +21 -10
  142. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -2
  143. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
  144. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
  145. package/node_modules/pi-lens/dist/clients/extension-log.js +22 -1
  146. package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
  147. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +316 -0
  148. package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
  149. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +3 -2
  150. package/node_modules/pi-lens/dist/clients/formatters.js +500 -115
  151. package/node_modules/pi-lens/dist/clients/generation-guard.js +216 -0
  152. package/node_modules/pi-lens/dist/clients/git-guard.js +31 -0
  153. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
  154. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +109 -9
  155. package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
  156. package/node_modules/pi-lens/dist/clients/host-ports.js +8 -0
  157. package/node_modules/pi-lens/dist/clients/installer/index.js +1114 -100
  158. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +58 -0
  159. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +715 -0
  160. package/node_modules/pi-lens/dist/clients/instance-reaper.js +643 -97
  161. package/node_modules/pi-lens/dist/clients/instance-registry.js +87 -17
  162. package/node_modules/pi-lens/dist/clients/jscpd-client.js +87 -8
  163. package/node_modules/pi-lens/dist/clients/knip-client.js +310 -10
  164. package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
  165. package/node_modules/pi-lens/dist/clients/language-profile.js +9 -2
  166. package/node_modules/pi-lens/dist/clients/latency-logger.js +440 -11
  167. package/node_modules/pi-lens/dist/clients/lazy-import.js +21 -0
  168. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -0
  169. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  170. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +128 -8
  171. package/node_modules/pi-lens/dist/clients/lsp/client.js +1660 -164
  172. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +83 -1
  173. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
  174. package/node_modules/pi-lens/dist/clients/lsp/index.js +2423 -151
  175. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +215 -0
  176. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +146 -14
  177. package/node_modules/pi-lens/dist/clients/lsp/language.js +48 -4
  178. package/node_modules/pi-lens/dist/clients/lsp/server.js +277 -16
  179. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +22 -0
  180. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +43 -0
  181. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +79 -3
  182. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +43 -0
  183. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +422 -28
  184. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +30 -0
  185. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +175 -0
  186. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +3 -2
  187. package/node_modules/pi-lens/dist/clients/mcp/session.js +10 -2
  188. package/node_modules/pi-lens/dist/clients/module-report.js +22 -1
  189. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
  190. package/node_modules/pi-lens/dist/clients/opengrep-client.js +15 -1
  191. package/node_modules/pi-lens/dist/clients/package-manager.js +170 -14
  192. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +27 -0
  193. package/node_modules/pi-lens/dist/clients/path-utils.js +160 -0
  194. package/node_modules/pi-lens/dist/clients/pipeline.js +154 -33
  195. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +37 -0
  196. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +17 -3
  197. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +89 -22
  198. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +84 -14
  199. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +15 -3
  200. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +50 -2
  201. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +23 -9
  202. package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
  203. package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
  204. package/node_modules/pi-lens/dist/clients/project-snapshot.js +299 -0
  205. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +47 -0
  206. package/node_modules/pi-lens/dist/clients/quiet-window.js +11 -25
  207. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +8 -3
  208. package/node_modules/pi-lens/dist/clients/read-guard.js +253 -10
  209. package/node_modules/pi-lens/dist/clients/resource-sampler.js +49 -17
  210. package/node_modules/pi-lens/dist/clients/reverse-deps.js +50 -11
  211. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
  212. package/node_modules/pi-lens/dist/clients/review-graph/query.js +52 -4
  213. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
  214. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
  215. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +96 -7
  216. package/node_modules/pi-lens/dist/clients/runtime-context.js +8 -1
  217. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +352 -45
  218. package/node_modules/pi-lens/dist/clients/runtime-session.js +373 -9
  219. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
  220. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +212 -79
  221. package/node_modules/pi-lens/dist/clients/runtime-turn.js +620 -67
  222. package/node_modules/pi-lens/dist/clients/safe-spawn.js +220 -7
  223. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +161 -0
  224. package/node_modules/pi-lens/dist/clients/search-read-registration.js +34 -6
  225. package/node_modules/pi-lens/dist/clients/security-scan-client.js +157 -24
  226. package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
  227. package/node_modules/pi-lens/dist/clients/sg-runner.js +116 -22
  228. package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
  229. package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
  230. package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
  231. package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
  232. package/node_modules/pi-lens/dist/clients/startup-timing.js +19 -1
  233. package/node_modules/pi-lens/dist/clients/test-runner-client.js +151 -16
  234. package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
  235. package/node_modules/pi-lens/dist/clients/tool-policy.js +125 -20
  236. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +75 -1
  237. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +769 -64
  238. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
  239. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +11 -0
  240. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +90 -0
  241. package/node_modules/pi-lens/dist/clients/trivy-client.js +19 -1
  242. package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
  243. package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
  244. package/node_modules/pi-lens/dist/index.js +61407 -48577
  245. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +476 -57
  246. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +132 -43
  247. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
  248. package/node_modules/pi-lens/docs/agent-guide.md +6 -1
  249. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +515 -506
  250. package/node_modules/pi-lens/docs/dispositions.md +21 -0
  251. package/node_modules/pi-lens/docs/features.md +19 -5
  252. package/node_modules/pi-lens/docs/globalconfig.md +3 -0
  253. package/node_modules/pi-lens/docs/language-coverage.md +71 -5
  254. package/node_modules/pi-lens/docs/mcp.md +2 -2
  255. package/node_modules/pi-lens/docs/servercapabilities.md +4 -1
  256. package/node_modules/pi-lens/docs/settings.md +19 -1
  257. package/node_modules/pi-lens/docs/usage.md +1 -0
  258. package/node_modules/pi-lens/package.json +27 -10
  259. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-js-snapshot.yml +23 -0
  260. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-snapshot.yml +23 -0
  261. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/ts-void-zero-snapshot.yml +23 -0
  262. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-object-param-test.yml +16 -0
  263. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +15 -1
  264. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-compile-call-test.yml +29 -1
  265. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-dupe-class-members-test.yml +33 -0
  266. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-js-test.yml +7 -1
  267. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-test.yml +7 -1
  268. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-known-value-widening-test.yml +9 -0
  269. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-non-null-assertion-test.yml +216 -0
  270. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +2 -0
  271. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +2 -0
  272. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +2 -1
  273. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +2 -1
  274. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-return-value-in-generator-test.yml +12 -0
  275. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-runtime-typeof-test.yml +33 -0
  276. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-server-bind-wildcard-test.yml +9 -0
  277. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-shape-in-symbol-names-test.yml +13 -0
  278. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-js-test.yml +5 -1
  279. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-test.yml +5 -1
  280. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-parameters-test.yml +12 -0
  281. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-returns-test.yml +16 -0
  282. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-any-test.yml +10 -0
  283. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-unknown-test.yml +8 -0
  284. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/redundant-unsafe-function-test.yml +35 -0
  285. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/require-safety-comment-for-as-unknown-as-test.yml +177 -0
  286. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ts-void-zero-test.yml +1 -0
  287. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member-js.yml +13 -2
  288. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member.yml +13 -2
  289. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +2 -0
  290. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-object-param.yml +60 -0
  291. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +57 -12
  292. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-compile-call.yml +63 -5
  293. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-dupe-class-members.yml +52 -8
  294. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html-js.yml +27 -2
  295. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html.yml +27 -2
  296. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-known-value-widening.yml +71 -0
  297. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-non-null-assertion.yml +316 -0
  298. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +7 -0
  299. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +22 -2
  300. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +23 -2
  301. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +33 -5
  302. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +34 -8
  303. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-return-value-in-generator.yml +22 -5
  304. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-runtime-typeof.yml +121 -0
  305. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-server-bind-wildcard.yml +23 -7
  306. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-shape-in-symbol-names.yml +68 -0
  307. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +41 -2
  308. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code.yml +41 -2
  309. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +6 -1
  310. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-parameters.yml +67 -0
  311. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-returns.yml +106 -0
  312. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-any.yml +50 -0
  313. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-unknown.yml +56 -0
  314. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +2 -0
  315. package/node_modules/pi-lens/rules/ast-grep-rules/rules/redundant-unsafe-function.yml +32 -0
  316. package/node_modules/pi-lens/rules/ast-grep-rules/rules/require-safety-comment-for-as-unknown-as.yml +184 -0
  317. package/node_modules/pi-lens/rules/ast-grep-rules/rules/ts-void-zero.yml +5 -0
  318. package/node_modules/pi-lens/rules/ast-grep-rules/self-scan-baseline.json +5 -0
  319. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +53 -1
  320. package/node_modules/pi-lens/scripts/download-grammars.js +25 -0
  321. package/node_modules/pi-lens/scripts/grammars.lock.json +9 -0
  322. package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
  323. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
  324. package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
  325. package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
  326. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +2 -0
  327. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +80 -3
  328. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +41 -0
  329. package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
  330. package/node_modules/pi-mcp-adapter/CHANGELOG.md +36 -0
  331. package/node_modules/pi-mcp-adapter/README.md +47 -5
  332. package/node_modules/pi-mcp-adapter/cli.js +163 -1
  333. package/node_modules/pi-mcp-adapter/commands.ts +128 -22
  334. package/node_modules/pi-mcp-adapter/config.ts +25 -7
  335. package/node_modules/pi-mcp-adapter/direct-tools.ts +5 -4
  336. package/node_modules/pi-mcp-adapter/index.ts +219 -32
  337. package/node_modules/pi-mcp-adapter/init.ts +36 -4
  338. package/node_modules/pi-mcp-adapter/lifecycle.ts +315 -24
  339. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +39 -6
  340. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +387 -0
  341. package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +27 -3
  342. package/node_modules/pi-mcp-adapter/mcp-code.ts +4 -0
  343. package/node_modules/pi-mcp-adapter/mcp-panel.ts +42 -13
  344. package/node_modules/pi-mcp-adapter/metadata-cache.ts +32 -21
  345. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
  346. package/node_modules/pi-mcp-adapter/package.json +3 -1
  347. package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
  348. package/node_modules/pi-mcp-adapter/request-headers-command.ts +31 -37
  349. package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
  350. package/node_modules/pi-mcp-adapter/server-manager.ts +140 -2
  351. package/node_modules/pi-mcp-adapter/session-recovery.ts +7 -0
  352. package/node_modules/pi-mcp-adapter/tool-approval.ts +15 -2
  353. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
  354. package/node_modules/pi-mcp-adapter/types.ts +2 -0
  355. package/node_modules/pi-mcp-adapter/ui-server.ts +18 -16
  356. package/node_modules/pi-mcp-adapter/utils.ts +70 -0
  357. package/node_modules/pi-web-access/CHANGELOG.md +37 -0
  358. package/node_modules/pi-web-access/README.md +32 -13
  359. package/node_modules/pi-web-access/anysearch.ts +4 -2
  360. package/node_modules/pi-web-access/brave.ts +16 -4
  361. package/node_modules/pi-web-access/chrome-cookies.ts +86 -15
  362. package/node_modules/pi-web-access/curator-page.ts +6 -3
  363. package/node_modules/pi-web-access/curator-server.ts +4 -1
  364. package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
  365. package/node_modules/pi-web-access/exa.ts +18 -7
  366. package/node_modules/pi-web-access/extract.ts +40 -5
  367. package/node_modules/pi-web-access/firecrawl.ts +18 -2
  368. package/node_modules/pi-web-access/gemini-search.ts +16 -4
  369. package/node_modules/pi-web-access/github-extract.ts +65 -36
  370. package/node_modules/pi-web-access/index.ts +60 -61
  371. package/node_modules/pi-web-access/openai-search.ts +17 -6
  372. package/node_modules/pi-web-access/package.json +1 -1
  373. package/node_modules/pi-web-access/page-query.ts +4 -2
  374. package/node_modules/pi-web-access/parallel-mcp.ts +249 -0
  375. package/node_modules/pi-web-access/parallel.ts +2 -2
  376. package/node_modules/pi-web-access/pdf-extract.ts +18 -5
  377. package/node_modules/pi-web-access/query-rewrite.ts +51 -0
  378. package/node_modules/pi-web-access/serper.ts +211 -0
  379. package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
  380. package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
  381. package/node_modules/pi-web-access/summary-review.ts +44 -22
  382. package/node_modules/pi-web-access/tavily.ts +16 -4
  383. package/node_modules/pi-web-access/utils.ts +87 -0
  384. package/node_modules/pi-web-access/valyu.ts +199 -0
  385. package/package.json +14 -14
@@ -10,9 +10,10 @@
10
10
  * Falls back safely when LSP is unavailable.
11
11
  */
12
12
  import * as fs from "node:fs";
13
+ import { incrementDegradationCount } from "./degradation-ledger.js";
13
14
  import { createFileTime } from "./file-time.js";
14
15
  import { hashDiagnosticContent } from "./lsp/diagnostic-binding.js";
15
- import { normalizeFilePath } from "./path-utils.js";
16
+ import { normalizeEphemeralMapKey, normalizeFilePath } from "./path-utils.js";
16
17
  import { logReadGuardEvent } from "./read-guard-logger.js";
17
18
  export const READ_GUARD_STATE_VERSION = 1;
18
19
  // --- Constants ---
@@ -35,6 +36,58 @@ const READ_HASH_MAX_LINES = Math.max(0, Number.parseInt(process.env.PI_LENS_READ
35
36
  */
36
37
  const READ_BINDING_MAX_BYTES = 4 * 1024 * 1024;
37
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;
38
91
  // Unconsumed reads remain valid until edit or session end, but this high
39
92
  // sanity cap prevents a read-only session from growing without bound.
40
93
  const READ_GUARD_MAX_UNCONSUMED_FILES = 4096;
@@ -213,6 +266,25 @@ export class ReadGuard {
213
266
  // pi Write tool authored, independent of filesystem mtime granularity
214
267
  // or clock skew (NFS, FAT32, etc.).
215
268
  writtenThisSession = new Set();
269
+ // Existence-independent index for hasKnownPath/forgetPath (#1668 review
270
+ // F1). `this.key()` (normalizeFilePath) branches on whether `filePath`
271
+ // currently exists on disk: an existing file resolves to realpathSync
272
+ // canonical casing, a missing one to a lowercased tail. recordRead/
273
+ // recordWritten always key while the file is still on disk (real
274
+ // casing); hasKnownPath/forgetPath are queried AFTER an external delete
275
+ // already landed, when the path no longer exists — recomputing
276
+ // `this.key()` at that point returns a DIFFERENT string for any
277
+ // mixed-case basename (`MyModule.ts` → `mymodule.ts`), so a lookup
278
+ // against `reads`/`writtenThisSession` silently misses. This index maps
279
+ // a purely syntactic key (`normalizeEphemeralMapKey` — slash-fold +
280
+ // lowercase, no filesystem access, so it never depends on current disk
281
+ // state) to the REAL key `reads`/`writtenThisSession` used at record
282
+ // time, so a post-delete lookup finds the same entry regardless of what
283
+ // happened to the file since. Pruned inside `evictFile` so it never
284
+ // outlives the record it points at.
285
+ knownPathIndex = new Map();
286
+ /** Running per-file record-cap trim totals for this session (#1913 F1). */
287
+ trimAccumulators = new Map();
216
288
  sessionId;
217
289
  sessionStartMs;
218
290
  constructor(sessionId, config = {}) {
@@ -236,7 +308,9 @@ export class ReadGuard {
236
308
  }
237
309
  idleEvictMs() {
238
310
  const value = Number.parseInt(process.env.PI_LENS_READ_GUARD_IDLE_EVICT_MS ?? "", 10);
239
- return Number.isSafeInteger(value) && value > 0 ? value : READ_GUARD_IDLE_EVICT_MS_DEFAULT;
311
+ return Number.isSafeInteger(value) && value > 0
312
+ ? value
313
+ : READ_GUARD_IDLE_EVICT_MS_DEFAULT;
240
314
  }
241
315
  clearFileTimer(filePath) {
242
316
  const timer = this.fileIdleTimers.get(filePath);
@@ -251,6 +325,12 @@ export class ReadGuard {
251
325
  this.fileLastUsed.delete(filePath);
252
326
  this.consumedReadFiles.delete(filePath);
253
327
  this.writtenThisSession.delete(filePath);
328
+ // #1668 review F1: prune the reverse-pointing knownPathIndex entries
329
+ // too, so it never outlives the record it points at.
330
+ for (const [syntacticKey, stored] of this.knownPathIndex) {
331
+ if (stored === filePath)
332
+ this.knownPathIndex.delete(syntacticKey);
333
+ }
254
334
  }
255
335
  touchFile(filePath) {
256
336
  const now = Date.now();
@@ -285,8 +365,7 @@ export class ReadGuard {
285
365
  while (this.reads.size > READ_GUARD_MAX_UNCONSUMED_FILES) {
286
366
  const victim = [...this.reads.keys()]
287
367
  .filter((filePath) => !this.consumedReadFiles.has(filePath))
288
- .sort((a, b) => (this.fileLastUsed.get(a) ?? 0) -
289
- (this.fileLastUsed.get(b) ?? 0))[0];
368
+ .sort((a, b) => (this.fileLastUsed.get(a) ?? 0) - (this.fileLastUsed.get(b) ?? 0))[0];
290
369
  if (!victim)
291
370
  break;
292
371
  // This is a normal read miss: a later edit must require a fresh read,
@@ -301,6 +380,11 @@ export class ReadGuard {
301
380
  */
302
381
  recordRead(record) {
303
382
  const filePath = this.key(record.filePath);
383
+ // #1668 review F1: index by the existence-independent syntactic key
384
+ // while the file is (presumably) still on disk, so a later
385
+ // hasKnownPath/forgetPath lookup after an external delete can still
386
+ // find this entry's real key.
387
+ this.knownPathIndex.set(normalizeEphemeralMapKey(record.filePath), filePath);
304
388
  const storedRecord = {
305
389
  ...record,
306
390
  filePath,
@@ -311,6 +395,11 @@ export class ReadGuard {
311
395
  this.consumedReadFiles.delete(storedRecord.filePath);
312
396
  arr.push(storedRecord);
313
397
  this.reads.set(storedRecord.filePath, arr);
398
+ // Capture BEFORE the trim: `readCountForFile` saturates at the cap once
399
+ // eviction starts, so it stops showing growth. `rawReadCountForFile` keeps
400
+ // the real arrival count observable alongside `evictedRecordCount`.
401
+ const rawReadCountForFile = arr.length;
402
+ const { evictedCount: evictedRecordCount, evictedCreditCount, evictedGenuineCount } = enforceRecordCapForFile(arr);
314
403
  this.touchFile(storedRecord.filePath);
315
404
  this.enforceFileCap();
316
405
  logReadGuardEvent({
@@ -334,8 +423,74 @@ export class ReadGuard {
334
423
  ...(storedRecord.source !== undefined && {
335
424
  source: storedRecord.source,
336
425
  }),
426
+ ...(storedRecord.searchCredit !== undefined && {
427
+ searchCreditReason: storedRecord.searchCredit.reason,
428
+ searchCreditMarginBefore: storedRecord.searchCredit.marginBefore,
429
+ searchCreditMarginAfter: storedRecord.searchCredit.marginAfter,
430
+ }),
431
+ ...(evictedRecordCount > 0 && {
432
+ evictedRecordCount,
433
+ rawReadCountForFile,
434
+ }),
337
435
  },
338
436
  });
437
+ // #1913: the eviction counters above ride `read_recorded`, which is
438
+ // gated behind PI_LENS_READ_GUARD_VERBOSE — off by default. That left a
439
+ // live eviction regression with no trace at default verbosity.
440
+ //
441
+ // review F1: a naive "emit every trim" fix floods read-guard.log once a
442
+ // hot file sits past the cap — every later push trims exactly 1 record
443
+ // (the array is always AT the cap before the push, so `excess` is
444
+ // always exactly 1), so 300 recordRead calls on one file is ~172
445
+ // identical always-on lines, and can rotate `edit_blocked` records out
446
+ // of the 1MB cap. `rawReadCountForFile` also freezes at cap+1 forever,
447
+ // so a raw per-trim record can't even discriminate thrash severity.
448
+ //
449
+ // Fix: emit the read-guard.log line ONCE per file per session, on the
450
+ // FIRST trim only. `trimAccumulators` still updates on every trim,
451
+ // queryable via `getTrimStats` — the running totals a health surface or
452
+ // a future emission point would need are never lost, even though
453
+ // read-guard.log stops re-announcing them. "Have we already logged
454
+ // this file's first trim" routes through the degradation ledger's own
455
+ // rising-edge tally (`incrementDegradationCount` — the pattern
456
+ // CLAUDE.md names for repeated degradations) instead of a hand-rolled
457
+ // per-file Set; subsequent trims still call it, so the ledger's own
458
+ // entry for this (kind, subject) keeps its reason text and count
459
+ // current for the health/degradation summary even after read-guard.log
460
+ // goes quiet.
461
+ if (evictedRecordCount > 0) {
462
+ const key = storedRecord.filePath;
463
+ const acc = this.trimAccumulators.get(key) ?? {
464
+ totalEvicted: 0,
465
+ evictedCreditCount: 0,
466
+ evictedGenuineCount: 0,
467
+ trimEventCount: 0,
468
+ };
469
+ acc.totalEvicted += evictedRecordCount;
470
+ acc.evictedCreditCount += evictedCreditCount;
471
+ acc.evictedGenuineCount += evictedGenuineCount;
472
+ acc.trimEventCount += 1;
473
+ this.trimAccumulators.set(key, acc);
474
+ const isRisingEdge = incrementDegradationCount({
475
+ kind: "read-guard-record-cap-trim",
476
+ subject: key,
477
+ reason: `trim #${acc.trimEventCount}: evicted ${evictedRecordCount} (credit ${evictedCreditCount}, genuine ${evictedGenuineCount})`,
478
+ });
479
+ if (isRisingEdge) {
480
+ logReadGuardEvent({
481
+ event: "read_cap_trimmed",
482
+ sessionId: this.sessionId,
483
+ filePath: storedRecord.filePath,
484
+ metadata: {
485
+ trimEventCount: acc.trimEventCount,
486
+ evictedRecordCount: acc.totalEvicted,
487
+ evictedCreditCount: acc.evictedCreditCount,
488
+ evictedGenuineCount: acc.evictedGenuineCount,
489
+ rawReadCountForFile,
490
+ },
491
+ });
492
+ }
493
+ }
339
494
  // Also update FileTime stamp for this file
340
495
  this.fileTime.read(storedRecord.filePath);
341
496
  }
@@ -442,9 +597,17 @@ export class ReadGuard {
442
597
  // 2. FileTime check (actual staleness)
443
598
  let ignoredOwnEditStaleness = false;
444
599
  let ignoredHashStaleness = false;
600
+ let ignoredOldTextResolvedStaleness = false;
445
601
  if (this.fileTime.hasChanged(filePath)) {
446
602
  const lastRead = fileReads[fileReads.length - 1];
447
- if (this.canTreatStalenessAsOwnPriorEdit(filePath, lastRead.timestamp)) {
603
+ if (options?.oldTextResolved === true) {
604
+ // The host-facing preflight sets this only after oldText resolves to
605
+ // exactly one span in the live bytes. That content evidence is newer
606
+ // and stronger than FileTime's coarse external-write signal, matching
607
+ // the skipSnapshotCheck exception at the later range-stale gate.
608
+ ignoredOldTextResolvedStaleness = true;
609
+ }
610
+ else if (this.canTreatStalenessAsOwnPriorEdit(filePath, lastRead.timestamp)) {
448
611
  ignoredOwnEditStaleness = true;
449
612
  }
450
613
  else if (this.canIgnoreStalenessByHashes(filePath, fileReads, touchedLines, editRanges)) {
@@ -474,7 +637,7 @@ export class ReadGuard {
474
637
  const rangesToCheck = editRanges && editRanges.length > 1 ? editRanges : [touchedLines];
475
638
  let viaSymbol = false;
476
639
  for (const range of rangesToCheck) {
477
- const snapshotValidation = this.validateRangeSnapshot(filePath, range);
640
+ const snapshotValidation = this.validateRangeSnapshot(filePath, range, !!options?.skipSnapshotCheck);
478
641
  const coverage = this.checkCoverage(filePath, range);
479
642
  if (!coverage.covered) {
480
643
  const lastRead = fileReads[fileReads.length - 1];
@@ -568,10 +731,15 @@ export class ReadGuard {
568
731
  }
569
732
  const verdict = this.allow();
570
733
  this.recordVerdict(filePath, "edit", touchedLines, verdict, {
571
- reasonKind: viaSymbol ? "symbol_coverage" : "range_coverage",
734
+ reasonKind: ignoredOldTextResolvedStaleness
735
+ ? "file_modified_oldtext_unique"
736
+ : viaSymbol
737
+ ? "symbol_coverage"
738
+ : "range_coverage",
572
739
  viaSymbol,
573
740
  ignoredOwnEditStaleness,
574
741
  ignoredHashStaleness,
742
+ oldTextResolved: ignoredOldTextResolvedStaleness,
575
743
  });
576
744
  return verdict;
577
745
  }
@@ -587,6 +755,44 @@ export class ReadGuard {
587
755
  return true; // Assume new if we can't stat
588
756
  }
589
757
  }
758
+ /**
759
+ * Whether pi-lens has any record of this path from a read or write this
760
+ * session (#1668). Gates external-delete detection: a path pi-lens never
761
+ * read or wrote is one no LSP server's cache was ever told about through
762
+ * us, so an `rm` naming it carries no signal worth checking disk for —
763
+ * this is a lookup against state already being tracked, never a fresh
764
+ * filesystem stat over an unbounded path set.
765
+ *
766
+ * MUST go through `knownPathIndex`, not `this.key(filePath)` directly
767
+ * (#1668 review F1): this is called AFTER a bash delete has already
768
+ * landed, when `filePath` no longer exists — `this.key()` at that point
769
+ * returns a lowercased-tail key, not the real-casing key `recordRead`/
770
+ * `recordWritten` stored while the file was still on disk. For a
771
+ * mixed-case basename (`MyModule.ts`) the two keys differ and a direct
772
+ * `this.key()` lookup always misses.
773
+ */
774
+ hasKnownPath(filePath) {
775
+ const stored = this.knownPathIndex.get(normalizeEphemeralMapKey(filePath));
776
+ return (stored !== undefined &&
777
+ (this.reads.has(stored) || this.writtenThisSession.has(stored)));
778
+ }
779
+ /**
780
+ * Drop all record of a path pi-lens confirmed no longer exists on disk
781
+ * (#1668, external delete). Without this a later write reusing the same
782
+ * path would inherit a stale writtenThisSession/reads entry from before
783
+ * the delete, and a repeat `rm` of the same already-gone path would keep
784
+ * matching {@link hasKnownPath} and re-emitting a type-3 notification.
785
+ *
786
+ * MUST evict through `knownPathIndex`, not `this.key(filePath)` directly
787
+ * (#1668 review F1) — same reasoning as {@link hasKnownPath}: `filePath`
788
+ * is already gone from disk by the time this runs, so recomputing
789
+ * `this.key()` targets the wrong (lowercased-tail) key for a mixed-case
790
+ * basename and leaves the real entry behind un-evicted.
791
+ */
792
+ forgetPath(filePath) {
793
+ const stored = this.knownPathIndex.get(normalizeEphemeralMapKey(filePath));
794
+ this.evictFile(stored ?? this.key(filePath));
795
+ }
590
796
  /**
591
797
  * Mark a file as pending creation (Write tool to a non-existing file).
592
798
  * Must be called from the tool_call handler before the write lands so
@@ -601,8 +807,12 @@ export class ReadGuard {
601
807
  * Call this from the tool_result handler so the next checkEdit on the same
602
808
  * file doesn't see "file_modified" caused by our own previous edit.
603
809
  */
604
- recordWritten(filePath) {
605
- filePath = this.key(filePath);
810
+ recordWritten(rawFilePath) {
811
+ const filePath = this.key(rawFilePath);
812
+ // #1668 review F1: index by the existence-independent syntactic key
813
+ // (see `knownPathIndex`) so a later hasKnownPath/forgetPath lookup
814
+ // after an external delete can still find this entry's real key.
815
+ this.knownPathIndex.set(normalizeEphemeralMapKey(rawFilePath), filePath);
606
816
  this.fileTime.read(filePath);
607
817
  this.writtenThisSession.add(filePath);
608
818
  if (this.reads.has(filePath))
@@ -675,6 +885,18 @@ export class ReadGuard {
675
885
  this.touchFile(key);
676
886
  return this.reads.get(key) ?? [];
677
887
  }
888
+ /**
889
+ * Session-lifetime record-cap trim totals for a file (#1913 review F1).
890
+ * `recordRead` only writes ONE `read_cap_trimmed` read-guard.log line per
891
+ * file per session (on the first trim), so this is the running-totals
892
+ * surface for everything after that — the exact split this class already
893
+ * computes on every trim, not re-derivable from `getReadHistory` alone
894
+ * (which only shows the SURVIVING records, not what was evicted).
895
+ */
896
+ getTrimStats(filePath) {
897
+ const stats = this.trimAccumulators.get(this.key(filePath));
898
+ return stats ? { ...stats } : undefined;
899
+ }
678
900
  /**
679
901
  * Snapshot the read-set for persistence across a session resume (#1041).
680
902
  * Mirrors widget-state's `exportWidgetState`: the Map is emitted as
@@ -838,7 +1060,13 @@ export class ReadGuard {
838
1060
  return (read.enclosingSymbol.startLine <= editStart &&
839
1061
  read.enclosingSymbol.endLine >= editEnd);
840
1062
  }
841
- validateRangeSnapshot(filePath, range) {
1063
+ validateRangeSnapshot(filePath, range,
1064
+ /**
1065
+ * What the CALLER will do with the verdict. `checkEdit` honours
1066
+ * `skipSnapshotCheck` for content-validated (oldText) edits, so a
1067
+ * `shouldBlock` verdict may never reach the gate (#1904 item 1).
1068
+ */
1069
+ snapshotCheckSkipped) {
842
1070
  const reads = this.reads.get(filePath) ?? [];
843
1071
  const candidates = reads.filter((read) => this.readCoversRange(read, range));
844
1072
  let status = "unavailable";
@@ -886,6 +1114,17 @@ export class ReadGuard {
886
1114
  lastUnavailableTimestamp <= lastMismatchTimestamp &&
887
1115
  checkedCandidateCount > 0 &&
888
1116
  hashUnavailableCandidateCount === 0;
1117
+ // `enforced` below states INTENT — whether this validation reached a
1118
+ // decidable verdict. It says nothing about what the caller did with it.
1119
+ // `outcome` states the caller's actual behaviour, which is what an
1120
+ // enforcement rate must be computed from (#1904 item 1).
1121
+ const outcome = shouldBlock
1122
+ ? snapshotCheckSkipped
1123
+ ? "bypassed-content-match"
1124
+ : "enforced-block"
1125
+ : status === "match"
1126
+ ? "enforced-pass"
1127
+ : "not-decidable";
889
1128
  logReadGuardEvent({
890
1129
  event: "range_snapshot_validation",
891
1130
  sessionId: this.sessionId,
@@ -903,6 +1142,7 @@ export class ReadGuard {
903
1142
  missingLines: missingLines.slice(0, 20),
904
1143
  mismatchedLines: mismatchedLines.slice(0, 20),
905
1144
  enforced: shouldBlock || status === "match",
1145
+ outcome,
906
1146
  },
907
1147
  });
908
1148
  return {
@@ -1117,6 +1357,9 @@ export class ReadGuard {
1117
1357
  reason: verdict.reason,
1118
1358
  timestamp: Date.now(),
1119
1359
  });
1360
+ if (arr.length > READ_GUARD_MAX_EDITS_PER_FILE) {
1361
+ arr.splice(0, arr.length - READ_GUARD_MAX_EDITS_PER_FILE);
1362
+ }
1120
1363
  this.edits.set(filePath, arr);
1121
1364
  }
1122
1365
  recordVerdict(filePath, tool, touchedLines, verdict, metadata = {}) {
@@ -45,7 +45,18 @@
45
45
  */
46
46
  import * as path from "node:path";
47
47
  import pidusage from "pidusage";
48
- import { spawnCollectStdout } from "./child-unref.js";
48
+ import { spawnCollectStdoutResult } from "./child-unref.js";
49
+ import { recordDegradationOnce } from "./degradation-ledger.js";
50
+ import { terminateScannerChild } from "./instance-reaper.js";
51
+ export const RESOURCE_SAMPLE_QUERY_TIMEOUT_MS = 2_000;
52
+ function recordQueryFailure(subject, status, exitCode) {
53
+ const exitReason = status === "exit-error" ? ` (exit code ${exitCode ?? "unknown"})` : "";
54
+ recordDegradationOnce({
55
+ kind: "resource-sampler-query-failed",
56
+ subject,
57
+ reason: `process-table query ${status}${exitReason}`,
58
+ });
59
+ }
49
60
  // Read the platform live (not a module-load const) so both the Windows and the
50
61
  // POSIX sampling paths are exercisable in unit tests regardless of the host OS.
51
62
  function runningOnWindows() {
@@ -113,12 +124,18 @@ async function findDescendantPidsWindows(rootPid) {
113
124
  // reaper's identical spawn→collect plumbing (#1153/#1160). Sampling still
114
125
  // works normally in an interactive/long-lived session: unref only means
115
126
  // "don't hold the loop open FOR this alone," the collected stdout is still
116
- // delivered whenever `close` fires. Resolves to `""` on any spawn/`error`
117
- // failure, which the parse below turns into an empty pairs list (same
118
- // result the old inline `resolve([])` error path produced).
119
- const out = await spawnCollectStdout(powershell, ["-NoProfile", "-NonInteractive", "-Command", psScript], { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
127
+ // delivered whenever `close` fires. The result status keeps a failed query
128
+ // distinct from a successful empty process table.
129
+ const result = await spawnCollectStdoutResult(powershell, ["-NoProfile", "-NonInteractive", "-Command", psScript], { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] }, {
130
+ timeoutMs: RESOURCE_SAMPLE_QUERY_TIMEOUT_MS,
131
+ onTimeout: (child) => terminateScannerChild(child, {}),
132
+ });
133
+ if (result.status !== "ok") {
134
+ recordQueryFailure("windows-descendant-process-table", result.status, result.exitCode);
135
+ return null;
136
+ }
120
137
  const pairs = [];
121
- for (const line of out.split(/\r?\n/)) {
138
+ for (const line of result.stdout.split(/\r?\n/)) {
122
139
  const [pidStr, ppidStr] = line.split(",");
123
140
  const pid = Number(pidStr);
124
141
  const ppid = Number(ppidStr);
@@ -142,7 +159,7 @@ export function __resetWindowsCpuHistoryForTests() {
142
159
  * Windows-only CPU%/RSS sampling via a FULLY GUARDED `Get-CimInstance
143
160
  * Win32_Process` query (mirrors `findDescendantPidsWindows`): a synchronous
144
161
  * throw from `spawn` (the `spawn UNKNOWN` crash vector, #620), a `child`
145
- * `error` event, or a non-zero/garbage exit all resolve to a partial/empty
162
+ * `error` event, or a non-zero/garbage exit all resolve to an errored/absent
146
163
  * map — this function can NEVER throw or reject. Deliberately does NOT call
147
164
  * `pidusage`, whose unguarded internal `gwmi` spawn is the crash we're fixing.
148
165
  *
@@ -153,9 +170,9 @@ export function __resetWindowsCpuHistoryForTests() {
153
170
  * reported as 0 for that tick and a real rate lands on the next one.
154
171
  */
155
172
  async function sampleProcessesWindows(valid) {
156
- const result = new Map();
173
+ const samples = new Map();
157
174
  if (valid.length === 0)
158
- return result;
175
+ return samples;
159
176
  // pids are pre-validated finite positive integers, so this WQL filter is
160
177
  // injection-safe. One line per pid: "pid,workingSet,kernel100ns,user100ns".
161
178
  const filter = valid.map((p) => `ProcessId=${p}`).join(" or ");
@@ -169,15 +186,21 @@ async function sampleProcessesWindows(valid) {
169
186
  // reaper's identical spawn→collect plumbing (#1153/#1160). It also absorbs
170
187
  // both failure modes this function used to guard inline — a synchronous
171
188
  // `spawn` throw (the `spawn UNKNOWN` crash vector, #620) and an async
172
- // `error` event — resolving to `""` either way, which the parse below
173
- // turns into the same empty/partial `result` map the old inline handlers
174
- // produced. Sampling still works normally in an interactive/long-lived
189
+ // `error` event, or a non-zero exit — the result status keeps failure distinct
190
+ // map. Sampling still works normally in an interactive/long-lived
175
191
  // session: unref only means "don't hold the loop open FOR this alone."
176
- const out = await spawnCollectStdout(powershell, ["-NoProfile", "-NonInteractive", "-Command", psScript], { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
192
+ const query = await spawnCollectStdoutResult(powershell, ["-NoProfile", "-NonInteractive", "-Command", psScript], { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] }, {
193
+ timeoutMs: RESOURCE_SAMPLE_QUERY_TIMEOUT_MS,
194
+ onTimeout: (child) => terminateScannerChild(child, {}),
195
+ });
196
+ if (query.status !== "ok") {
197
+ recordQueryFailure("windows-process-table", query.status, query.exitCode);
198
+ return null;
199
+ }
177
200
  try {
178
201
  const now = Date.now();
179
202
  const seen = new Set();
180
- for (const line of out.split(/\r?\n/)) {
203
+ for (const line of query.stdout.split(/\r?\n/)) {
181
204
  const parts = line.split(",");
182
205
  if (parts.length < 4)
183
206
  continue;
@@ -202,7 +225,7 @@ async function sampleProcessesWindows(valid) {
202
225
  }
203
226
  windowsCpuHistory.set(pid, { cpuMs, ts: now });
204
227
  seen.add(pid);
205
- result.set(pid, { rssBytes: workingSet, cpuPercent });
228
+ samples.set(pid, { rssBytes: workingSet, cpuPercent });
206
229
  }
207
230
  // Prune stale history so pids that have gone away don't accumulate.
208
231
  for (const [pid, entry] of windowsCpuHistory) {
@@ -214,7 +237,7 @@ async function sampleProcessesWindows(valid) {
214
237
  catch {
215
238
  // Parsing must never throw into the caller; best-effort.
216
239
  }
217
- return result;
240
+ return samples;
218
241
  }
219
242
  /**
220
243
  * Sample CPU%/RSS for a set of pids. Best-effort: a pid that can't be resolved
@@ -248,8 +271,10 @@ export async function sampleProcesses(pids) {
248
271
  }
249
272
  }
250
273
  catch {
274
+ recordQueryFailure("posix-pidusage-process-table", "spawn-error");
251
275
  // Best-effort: sampling failure loses this tick's data for every pid in
252
276
  // the batch, but must never throw into the heartbeat/spawn path.
277
+ return null;
253
278
  }
254
279
  return result;
255
280
  }
@@ -321,10 +346,17 @@ export function startSpawnUsageSampler(pid, intervalMs = 750) {
321
346
  if (stopped)
322
347
  return;
323
348
  try {
349
+ const descendants = runningOnWindows()
350
+ ? await findDescendantPidsWindows(targetPid)
351
+ : [];
352
+ if (descendants === null)
353
+ return;
324
354
  const pids = runningOnWindows()
325
- ? [targetPid, ...(await findDescendantPidsWindows(targetPid))]
355
+ ? [targetPid, ...descendants]
326
356
  : [targetPid];
327
357
  const usageByPid = await sampleProcesses(pids);
358
+ if (usageByPid === null)
359
+ return;
328
360
  if (stopped || usageByPid.size === 0)
329
361
  return;
330
362
  let rssBytes = 0;
@@ -33,14 +33,30 @@ function addEdge(index, fromFile, toFile) {
33
33
  (index.importedBy[to] ??= []).push(from);
34
34
  }
35
35
  function normalizeIndex(index) {
36
- const fileKeys = new Set([
37
- ...Object.keys(index.imports),
36
+ // #1814 F2: `imports[file]` asserts "we know file's complete import
37
+ // list" — true only when `index.imports` already has a raw entry for
38
+ // `file` (from a real per-file scan). `importedBy[file]` asserts "we
39
+ // know SOME importer of file", which holds regardless of whether `file`
40
+ // itself was ever scanned. Materializing `imports[file] = []` for every
41
+ // key in their UNION (the pre-fix behavior) fabricated the first claim
42
+ // for a file that is only an `importedBy` target — imported by
43
+ // something, never itself scanned — indistinguishable downstream from a
44
+ // file the scan confirmed has zero imports.
45
+ // `clients/lsp/workspace-diagnostics-cache.ts`'s `getImports` reads this
46
+ // map directly and relies on `undefined` meaning "not covered". Only
47
+ // `importedBy` keeps the union treatment: "who imports me" has no
48
+ // equivalent "was I scanned" precondition to fabricate.
49
+ const importFileKeys = [...new Set(Object.keys(index.imports))].sort((a, b) => a.localeCompare(b));
50
+ const allFileKeys = new Set([
51
+ ...importFileKeys,
38
52
  ...Object.keys(index.importedBy),
39
53
  ]);
40
54
  const imports = {};
41
- const importedBy = {};
42
- for (const file of [...fileKeys].sort((a, b) => a.localeCompare(b))) {
55
+ for (const file of importFileKeys) {
43
56
  imports[file] = sortedUnique(index.imports[file] ?? []);
57
+ }
58
+ const importedBy = {};
59
+ for (const file of [...allFileKeys].sort((a, b) => a.localeCompare(b))) {
44
60
  importedBy[file] = sortedUnique(index.importedBy[file] ?? []);
45
61
  }
46
62
  return { ...index, imports, importedBy };
@@ -120,12 +136,24 @@ export function buildReverseDependencyIndexFromSnapshot(snapshot) {
120
136
  };
121
137
  for (const [filePath, file] of fileEntries) {
122
138
  const normalized = normalizeMapKey(file.path || filePath);
123
- index.imports[normalized] = sortedUnique(file.imports ?? []);
124
- index.importedBy[normalized] ??= [];
125
- for (const imported of index.imports[normalized]) {
126
- index.importedBy[imported] ??= [];
127
- index.importedBy[imported].push(normalized);
139
+ // #1814 F2: only materialize `imports[normalized]` when this
140
+ // snapshot entry genuinely recorded an import list. `file.imports`
141
+ // absent means this file was captured in the snapshot (e.g. a
142
+ // lightweight touch) but never had its own imports scanned —
143
+ // defaulting to `[]` here fabricated the "covered, zero imports"
144
+ // claim `getImports` (workspace-diagnostics-cache.ts) distinguishes
145
+ // from "not covered". `hasFileImports` above already anticipates a
146
+ // mixed snapshot with entries in both states; this closes the gap
147
+ // per entry.
148
+ if (Array.isArray(file.imports)) {
149
+ const imports = sortedUnique(file.imports);
150
+ index.imports[normalized] = imports;
151
+ for (const imported of imports) {
152
+ index.importedBy[imported] ??= [];
153
+ index.importedBy[imported].push(normalized);
154
+ }
128
155
  }
156
+ index.importedBy[normalized] ??= [];
129
157
  }
130
158
  for (const [filePath, importers] of Object.entries(reverseDeps)) {
131
159
  const normalized = normalizeMapKey(filePath);
@@ -133,9 +161,20 @@ export function buildReverseDependencyIndexFromSnapshot(snapshot) {
133
161
  ...(index.importedBy[normalized] ?? []),
134
162
  ...importers,
135
163
  ]);
136
- index.imports[normalized] ??= [];
164
+ // #1814 F2 (class sweep): do NOT fabricate `imports[normalized] = []`
165
+ // here — `reverseDeps` records who imports `normalized`, not what
166
+ // `normalized` itself imports. Leave `imports[normalized]` exactly
167
+ // as the fileEntries loop above established it: present and real
168
+ // from a scan, or absent because this file was never scanned.
137
169
  for (const importer of index.importedBy[normalized]) {
138
- index.imports[importer] ??= [];
170
+ // #1814 F2: only record this edge on `importer`'s own imports
171
+ // list when that list is already genuinely known (a real
172
+ // per-file scan from the fileEntries loop above) — appending to
173
+ // an absent list would silently promote "we know ONE edge from
174
+ // reverseDeps" into "we know importer's COMPLETE import list",
175
+ // the same false-coverage claim closed above.
176
+ if (index.imports[importer] === undefined)
177
+ continue;
139
178
  if (!index.imports[importer].includes(normalized)) {
140
179
  index.imports[importer].push(normalized);
141
180
  }