@herbertgao/pi-extensions 2026.8.6 → 2026.8.7

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 (343) hide show
  1. package/README.md +5 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
  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 +272 -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 +1 -1
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +14 -0
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -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 +1 -0
  35. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +26 -10
  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 +2 -1
  39. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +3 -1
  40. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +2 -2
  41. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/zh.json +2 -2
  42. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  43. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +10 -0
  44. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +3 -1
  45. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +30 -7
  46. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
  47. package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +30 -4
  48. package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +26 -5
  49. package/node_modules/@narumitw/pi-btw/README.md +120 -111
  50. package/node_modules/@narumitw/pi-btw/dist/index.ts +2898 -0
  51. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +7 -0
  52. package/node_modules/@narumitw/pi-btw/package.json +10 -6
  53. package/node_modules/@narumitw/pi-btw/src/btw.ts +36 -40
  54. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +55 -10
  55. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +390 -0
  56. package/node_modules/@narumitw/pi-btw/src/menu.ts +45 -2
  57. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
  58. package/node_modules/pi-lens/CHANGELOG.md +909 -0
  59. package/node_modules/pi-lens/README.md +11 -3
  60. package/node_modules/pi-lens/config/ruff/core.toml +42 -0
  61. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +130 -27
  62. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +126 -13
  63. package/node_modules/pi-lens/dist/clients/agent-nudge.js +67 -11
  64. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +9 -3
  65. package/node_modules/pi-lens/dist/clients/atomic-write.js +9 -3
  66. package/node_modules/pi-lens/dist/clients/bash-file-access.js +285 -8
  67. package/node_modules/pi-lens/dist/clients/biome-client.js +61 -30
  68. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
  69. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +119 -0
  70. package/node_modules/pi-lens/dist/clients/bootstrap.js +12 -1
  71. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +12 -3
  72. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +192 -0
  73. package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
  74. package/node_modules/pi-lens/dist/clients/cascade-format.js +20 -1
  75. package/node_modules/pi-lens/dist/clients/cascade-types.js +9 -1
  76. package/node_modules/pi-lens/dist/clients/child-unref.js +54 -6
  77. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +76 -24
  78. package/node_modules/pi-lens/dist/clients/dead-code-client.js +123 -31
  79. package/node_modules/pi-lens/dist/clients/deadline-utils.js +8 -1
  80. package/node_modules/pi-lens/dist/clients/debug-handles.js +5 -0
  81. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +62 -13
  82. package/node_modules/pi-lens/dist/clients/dependency-checker.js +25 -3
  83. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +147 -33
  84. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
  85. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +52 -2
  86. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +256 -88
  87. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +7 -4
  88. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +5 -0
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +234 -18
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +187 -19
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +6 -3
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +11 -9
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +398 -0
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +11 -3
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +12 -7
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +18 -0
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +895 -0
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +4 -0
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +16 -3
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +60 -53
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -0
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +23 -0
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +331 -55
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +38 -5
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +97 -19
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +14 -0
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +20 -0
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +22 -0
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +10 -0
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +18 -6
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +73 -12
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +275 -13
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +21 -6
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +260 -27
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +734 -64
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +86 -11
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +72 -0
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +9 -0
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +9 -0
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -1
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
  122. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
  123. package/node_modules/pi-lens/dist/clients/extension-log.js +22 -1
  124. package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
  125. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +294 -0
  126. package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
  127. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +3 -2
  128. package/node_modules/pi-lens/dist/clients/formatters.js +500 -115
  129. package/node_modules/pi-lens/dist/clients/generation-guard.js +216 -0
  130. package/node_modules/pi-lens/dist/clients/git-guard.js +31 -0
  131. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
  132. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +109 -9
  133. package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
  134. package/node_modules/pi-lens/dist/clients/host-ports.js +8 -0
  135. package/node_modules/pi-lens/dist/clients/installer/index.js +1114 -100
  136. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +58 -0
  137. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +715 -0
  138. package/node_modules/pi-lens/dist/clients/instance-reaper.js +643 -97
  139. package/node_modules/pi-lens/dist/clients/instance-registry.js +87 -17
  140. package/node_modules/pi-lens/dist/clients/jscpd-client.js +82 -6
  141. package/node_modules/pi-lens/dist/clients/knip-client.js +310 -10
  142. package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
  143. package/node_modules/pi-lens/dist/clients/language-profile.js +2 -0
  144. package/node_modules/pi-lens/dist/clients/latency-logger.js +440 -11
  145. package/node_modules/pi-lens/dist/clients/lazy-import.js +21 -0
  146. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -0
  147. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  148. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +102 -7
  149. package/node_modules/pi-lens/dist/clients/lsp/client.js +1516 -159
  150. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +83 -1
  151. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
  152. package/node_modules/pi-lens/dist/clients/lsp/index.js +2274 -147
  153. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +215 -0
  154. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +146 -14
  155. package/node_modules/pi-lens/dist/clients/lsp/language.js +48 -4
  156. package/node_modules/pi-lens/dist/clients/lsp/server.js +277 -16
  157. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +22 -0
  158. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +43 -0
  159. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +79 -3
  160. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +43 -0
  161. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +422 -28
  162. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +30 -0
  163. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +175 -0
  164. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +3 -2
  165. package/node_modules/pi-lens/dist/clients/mcp/session.js +10 -2
  166. package/node_modules/pi-lens/dist/clients/module-report.js +8 -0
  167. package/node_modules/pi-lens/dist/clients/opengrep-client.js +15 -1
  168. package/node_modules/pi-lens/dist/clients/package-manager.js +170 -14
  169. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +27 -0
  170. package/node_modules/pi-lens/dist/clients/path-utils.js +160 -0
  171. package/node_modules/pi-lens/dist/clients/pipeline.js +154 -33
  172. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +37 -0
  173. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +17 -3
  174. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +89 -22
  175. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +84 -14
  176. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +15 -3
  177. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +50 -2
  178. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +5 -0
  179. package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
  180. package/node_modules/pi-lens/dist/clients/project-snapshot.js +299 -0
  181. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +47 -0
  182. package/node_modules/pi-lens/dist/clients/quiet-window.js +11 -25
  183. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +1 -1
  184. package/node_modules/pi-lens/dist/clients/read-guard.js +184 -10
  185. package/node_modules/pi-lens/dist/clients/resource-sampler.js +49 -17
  186. package/node_modules/pi-lens/dist/clients/reverse-deps.js +50 -11
  187. package/node_modules/pi-lens/dist/clients/review-graph/query.js +52 -4
  188. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +96 -7
  189. package/node_modules/pi-lens/dist/clients/runtime-context.js +8 -1
  190. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +308 -45
  191. package/node_modules/pi-lens/dist/clients/runtime-session.js +332 -9
  192. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
  193. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +212 -79
  194. package/node_modules/pi-lens/dist/clients/runtime-turn.js +575 -67
  195. package/node_modules/pi-lens/dist/clients/safe-spawn.js +220 -7
  196. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +161 -0
  197. package/node_modules/pi-lens/dist/clients/search-read-registration.js +34 -6
  198. package/node_modules/pi-lens/dist/clients/security-scan-client.js +157 -24
  199. package/node_modules/pi-lens/dist/clients/sg-runner.js +116 -22
  200. package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
  201. package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
  202. package/node_modules/pi-lens/dist/clients/startup-timing.js +19 -1
  203. package/node_modules/pi-lens/dist/clients/test-runner-client.js +151 -16
  204. package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
  205. package/node_modules/pi-lens/dist/clients/tool-policy.js +125 -20
  206. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +75 -1
  207. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +769 -64
  208. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +11 -0
  209. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +90 -0
  210. package/node_modules/pi-lens/dist/clients/trivy-client.js +19 -1
  211. package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
  212. package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
  213. package/node_modules/pi-lens/dist/index.js +67738 -55739
  214. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +466 -54
  215. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +132 -43
  216. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
  217. package/node_modules/pi-lens/docs/agent-guide.md +6 -1
  218. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +515 -506
  219. package/node_modules/pi-lens/docs/dispositions.md +21 -0
  220. package/node_modules/pi-lens/docs/features.md +19 -5
  221. package/node_modules/pi-lens/docs/globalconfig.md +3 -0
  222. package/node_modules/pi-lens/docs/language-coverage.md +71 -5
  223. package/node_modules/pi-lens/docs/mcp.md +2 -2
  224. package/node_modules/pi-lens/docs/servercapabilities.md +4 -1
  225. package/node_modules/pi-lens/docs/settings.md +19 -1
  226. package/node_modules/pi-lens/docs/usage.md +1 -0
  227. package/node_modules/pi-lens/package.json +10 -6
  228. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-js-snapshot.yml +23 -0
  229. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-snapshot.yml +23 -0
  230. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/ts-void-zero-snapshot.yml +23 -0
  231. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-object-param-test.yml +16 -0
  232. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +15 -1
  233. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-compile-call-test.yml +29 -1
  234. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-dupe-class-members-test.yml +33 -0
  235. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-js-test.yml +7 -1
  236. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-test.yml +7 -1
  237. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-known-value-widening-test.yml +9 -0
  238. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-non-null-assertion-test.yml +216 -0
  239. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +2 -0
  240. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +2 -0
  241. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +2 -1
  242. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +2 -1
  243. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-return-value-in-generator-test.yml +12 -0
  244. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-runtime-typeof-test.yml +33 -0
  245. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-server-bind-wildcard-test.yml +9 -0
  246. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-shape-in-symbol-names-test.yml +13 -0
  247. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-js-test.yml +5 -1
  248. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-test.yml +5 -1
  249. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-parameters-test.yml +12 -0
  250. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-returns-test.yml +16 -0
  251. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-any-test.yml +10 -0
  252. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-unknown-test.yml +8 -0
  253. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/redundant-unsafe-function-test.yml +35 -0
  254. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/require-safety-comment-for-as-unknown-as-test.yml +177 -0
  255. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ts-void-zero-test.yml +1 -0
  256. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member-js.yml +13 -2
  257. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member.yml +13 -2
  258. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +2 -0
  259. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-object-param.yml +60 -0
  260. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +57 -12
  261. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-compile-call.yml +63 -5
  262. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-dupe-class-members.yml +52 -8
  263. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html-js.yml +27 -2
  264. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html.yml +27 -2
  265. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-known-value-widening.yml +71 -0
  266. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-non-null-assertion.yml +316 -0
  267. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +7 -0
  268. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +22 -2
  269. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +23 -2
  270. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +33 -5
  271. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +34 -8
  272. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-return-value-in-generator.yml +22 -5
  273. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-runtime-typeof.yml +121 -0
  274. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-server-bind-wildcard.yml +23 -7
  275. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-shape-in-symbol-names.yml +68 -0
  276. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +41 -2
  277. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code.yml +41 -2
  278. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +6 -1
  279. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-parameters.yml +67 -0
  280. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-returns.yml +106 -0
  281. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-any.yml +50 -0
  282. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-unknown.yml +56 -0
  283. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +2 -0
  284. package/node_modules/pi-lens/rules/ast-grep-rules/rules/redundant-unsafe-function.yml +32 -0
  285. package/node_modules/pi-lens/rules/ast-grep-rules/rules/require-safety-comment-for-as-unknown-as.yml +184 -0
  286. package/node_modules/pi-lens/rules/ast-grep-rules/rules/ts-void-zero.yml +5 -0
  287. package/node_modules/pi-lens/rules/ast-grep-rules/self-scan-baseline.json +5 -0
  288. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +53 -1
  289. package/node_modules/pi-lens/scripts/download-grammars.js +25 -0
  290. package/node_modules/pi-lens/scripts/grammars.lock.json +9 -0
  291. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +2 -0
  292. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +80 -3
  293. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +41 -0
  294. package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
  295. package/node_modules/pi-mcp-adapter/CHANGELOG.md +36 -0
  296. package/node_modules/pi-mcp-adapter/README.md +47 -5
  297. package/node_modules/pi-mcp-adapter/cli.js +163 -1
  298. package/node_modules/pi-mcp-adapter/commands.ts +128 -22
  299. package/node_modules/pi-mcp-adapter/config.ts +25 -7
  300. package/node_modules/pi-mcp-adapter/direct-tools.ts +5 -4
  301. package/node_modules/pi-mcp-adapter/index.ts +219 -32
  302. package/node_modules/pi-mcp-adapter/init.ts +36 -4
  303. package/node_modules/pi-mcp-adapter/lifecycle.ts +315 -24
  304. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +39 -6
  305. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +387 -0
  306. package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +27 -3
  307. package/node_modules/pi-mcp-adapter/mcp-code.ts +4 -0
  308. package/node_modules/pi-mcp-adapter/mcp-panel.ts +42 -13
  309. package/node_modules/pi-mcp-adapter/metadata-cache.ts +32 -21
  310. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
  311. package/node_modules/pi-mcp-adapter/package.json +3 -1
  312. package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
  313. package/node_modules/pi-mcp-adapter/request-headers-command.ts +31 -37
  314. package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
  315. package/node_modules/pi-mcp-adapter/server-manager.ts +140 -2
  316. package/node_modules/pi-mcp-adapter/session-recovery.ts +7 -0
  317. package/node_modules/pi-mcp-adapter/tool-approval.ts +15 -2
  318. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
  319. package/node_modules/pi-mcp-adapter/types.ts +2 -0
  320. package/node_modules/pi-mcp-adapter/ui-server.ts +18 -16
  321. package/node_modules/pi-mcp-adapter/utils.ts +70 -0
  322. package/node_modules/pi-web-access/CHANGELOG.md +23 -0
  323. package/node_modules/pi-web-access/README.md +27 -9
  324. package/node_modules/pi-web-access/anysearch.ts +4 -2
  325. package/node_modules/pi-web-access/brave.ts +16 -4
  326. package/node_modules/pi-web-access/curator-page.ts +6 -3
  327. package/node_modules/pi-web-access/curator-server.ts +4 -1
  328. package/node_modules/pi-web-access/exa.ts +18 -7
  329. package/node_modules/pi-web-access/extract.ts +28 -3
  330. package/node_modules/pi-web-access/gemini-search.ts +16 -4
  331. package/node_modules/pi-web-access/index.ts +41 -57
  332. package/node_modules/pi-web-access/package.json +1 -1
  333. package/node_modules/pi-web-access/page-query.ts +4 -2
  334. package/node_modules/pi-web-access/parallel-mcp.ts +249 -0
  335. package/node_modules/pi-web-access/parallel.ts +2 -2
  336. package/node_modules/pi-web-access/query-rewrite.ts +51 -0
  337. package/node_modules/pi-web-access/serper.ts +211 -0
  338. package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
  339. package/node_modules/pi-web-access/summary-review.ts +44 -22
  340. package/node_modules/pi-web-access/tavily.ts +16 -4
  341. package/node_modules/pi-web-access/utils.ts +87 -0
  342. package/node_modules/pi-web-access/valyu.ts +199 -0
  343. package/package.json +13 -13
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Quiet-window kill switch and wait budget, split out of `quiet-window.ts` so a
3
+ * caller that only needs the NUMBERS does not have to import the scheduler.
4
+ *
5
+ * `cascade-budget.ts` is the caller that forced it (#1462): it reads both knobs
6
+ * to decide how much drain is really left, and depending on `quiet-window.ts`
7
+ * for that would point a pure arithmetic module at a task registry — module
8
+ * state (`_tasks`, `_inProgress`), a heartbeat sampler, latency logging, none
9
+ * of which it wants. Config belongs below the scheduler, not beside it.
10
+ *
11
+ * NOT a load-time win, despite what the first version of this comment claimed:
12
+ * `cascade-budget.ts` imports `runtime-config.ts` for `cascadeMaxFiles`, and
13
+ * that reaches `pidusage` through `lens-config.ts` regardless — probed, and
14
+ * `dispatch/integration.ts` already imported `runtime-config.ts` before #1462.
15
+ * The split is a dependency-direction fix and nothing more.
16
+ *
17
+ * `quiet-window.ts` re-exports all three, so every existing importer keeps
18
+ * working and there is still exactly one memo behind `isQuietWindowEnabled`.
19
+ */
20
+ import { toPositiveFinite } from "./env-utils.js";
21
+ let _enabledCache;
22
+ /** `PI_LENS_QUIET_WINDOW=0` disables the whole scheduler (no-op, no logging). */
23
+ export function isQuietWindowEnabled() {
24
+ if (_enabledCache !== undefined)
25
+ return _enabledCache;
26
+ _enabledCache = process.env.PI_LENS_QUIET_WINDOW !== "0";
27
+ return _enabledCache;
28
+ }
29
+ /** Test-only: clear the memoized kill-switch read. */
30
+ export function _resetQuietWindowEnabledForTests() {
31
+ _enabledCache = undefined;
32
+ }
33
+ const DEFAULT_QUIET_WINDOW_WAIT_MS = 15_000;
34
+ /**
35
+ * Bounded wait for the quiet-window's own settle attempts (currently just
36
+ * the carried-over cascade drain). Lazy env read, `Number.isFinite`-guarded
37
+ * so a malformed value falls back to the default instead of poisoning
38
+ * `Math.max`/`setTimeout` with `NaN` (see PR #109).
39
+ *
40
+ * This is the task's BUDGET, not a statement that the task runs — it keeps
41
+ * returning the default when `isQuietWindowEnabled()` is false. Callers asking
42
+ * "how long will the pipeline actually keep working on this" must check both.
43
+ */
44
+ export function quietWindowWaitMs() {
45
+ const raw = toPositiveFinite(process.env.PI_LENS_QUIET_WINDOW_WAIT_MS);
46
+ return raw > 0 ? raw : DEFAULT_QUIET_WINDOW_WAIT_MS;
47
+ }
@@ -32,7 +32,7 @@
32
32
  import { readInstanceRegistry, updateHeartbeat, } from "./instance-registry.js";
33
33
  import { logLatency } from "./latency-logger.js";
34
34
  import { sampleProcesses } from "./resource-sampler.js";
35
- import { toPositiveFinite } from "./env-utils.js";
35
+ import { _resetQuietWindowEnabledForTests, isQuietWindowEnabled, quietWindowWaitMs, } from "./quiet-window-config.js";
36
36
  // Module-level registry so future work (#458 Tier-3 reconcile, #236
37
37
  // enrichment) can plug into the same scheduler without touching it.
38
38
  const _tasks = [];
@@ -48,30 +48,14 @@ export function registerQuietWindowTask(name, fn) {
48
48
  export function _resetQuietWindowTasksForTests() {
49
49
  _tasks.length = 0;
50
50
  }
51
- // --- Kill switch (lazy, memoized house style per clients/runtime-config.ts) ---
52
- let _enabledCache;
53
- /** `PI_LENS_QUIET_WINDOW=0` disables the whole scheduler (no-op, no logging). */
54
- export function isQuietWindowEnabled() {
55
- if (_enabledCache !== undefined)
56
- return _enabledCache;
57
- _enabledCache = process.env.PI_LENS_QUIET_WINDOW !== "0";
58
- return _enabledCache;
59
- }
60
- /** Test-only: clear the memoized kill-switch read. */
61
- export function _resetQuietWindowEnabledForTests() {
62
- _enabledCache = undefined;
63
- }
64
- const DEFAULT_QUIET_WINDOW_WAIT_MS = 15_000;
65
- /**
66
- * Bounded wait for the quiet-window's own settle attempts (currently just
67
- * the carried-over cascade drain). Lazy env read, `Number.isFinite`-guarded
68
- * so a malformed value falls back to the default instead of poisoning
69
- * `Math.max`/`setTimeout` with `NaN` (see PR #109).
70
- */
71
- export function quietWindowWaitMs() {
72
- const raw = toPositiveFinite(process.env.PI_LENS_QUIET_WINDOW_WAIT_MS);
73
- return raw > 0 ? raw : DEFAULT_QUIET_WINDOW_WAIT_MS;
74
- }
51
+ // --- Kill switch and wait budget ---
52
+ //
53
+ // #1462 review (N4): both live in `quiet-window-config.ts` now, so a caller
54
+ // that needs only the numbers (`cascade-budget.ts`, on the dispatch load path)
55
+ // does not drag this module's `resource-sampler` → `pidusage` import in with
56
+ // them. Re-exported here so every existing importer is unaffected and there is
57
+ // still one memo behind `isQuietWindowEnabled`.
58
+ export { isQuietWindowEnabled, quietWindowWaitMs, _resetQuietWindowEnabledForTests, };
75
59
  // Re-entrancy guard: agent_settled can fire multiple times per session
76
60
  // (once per completed/aborted run). If a previous quiet-window run is still
77
61
  // in flight when the next fires, skip rather than queue/overlap.
@@ -224,6 +208,8 @@ async function buildHeartbeatResourcePatch() {
224
208
  const self = instances.find((instance) => instance.pid === process.pid);
225
209
  const childPids = self?.lspChildren.map((child) => child.pid) ?? [];
226
210
  const usage = await sampleProcesses([process.pid, ...childPids]);
211
+ if (usage === null)
212
+ return {};
227
213
  const childUsage = {};
228
214
  for (const pid of childPids) {
229
215
  const sample = usage.get(pid);
@@ -19,7 +19,7 @@ const SNAPSHOT_LOG_SETTING = (process.env.PI_LENS_READ_GUARD_LOG_SNAPSHOTS ?? "1
19
19
  const LOG_SNAPSHOT_VALIDATION = !["0", "false", "off"].includes(SNAPSHOT_LOG_SETTING);
20
20
  export const MAX_EDIT_BATCH_ITEMS = 100;
21
21
  let generatedCorrelationCounter = 0;
22
- function sanitizeCorrelationId(value) {
22
+ export function sanitizeCorrelationId(value) {
23
23
  if (typeof value !== "string" && typeof value !== "number")
24
24
  return undefined;
25
25
  const sanitized = String(value)
@@ -12,7 +12,7 @@
12
12
  import * as fs from "node:fs";
13
13
  import { createFileTime } from "./file-time.js";
14
14
  import { hashDiagnosticContent } from "./lsp/diagnostic-binding.js";
15
- import { normalizeFilePath } from "./path-utils.js";
15
+ import { normalizeEphemeralMapKey, normalizeFilePath } from "./path-utils.js";
16
16
  import { logReadGuardEvent } from "./read-guard-logger.js";
17
17
  export const READ_GUARD_STATE_VERSION = 1;
18
18
  // --- Constants ---
@@ -35,6 +35,61 @@ const READ_HASH_MAX_LINES = Math.max(0, Number.parseInt(process.env.PI_LENS_READ
35
35
  */
36
36
  const READ_BINDING_MAX_BYTES = 4 * 1024 * 1024;
37
37
  const READ_GUARD_MAX_FILES = 256;
38
+ /**
39
+ * #1904 item 3: `enforceFileCap` bounds how many FILES the store holds, not how
40
+ * many records each file holds. One hot file reached 268 hash-bearing records in
41
+ * 75 minutes, and every record carries a `lineHashes` map, so a long session on
42
+ * a few files grows without bound. Cap records per file and drop oldest-first.
43
+ *
44
+ * Eviction is safe in the blocking direction: it narrows the staleness-rescue
45
+ * path — a stale edit rescued by an older snapshot that still matches — and can
46
+ * turn an allow into a "re-read and retry" block. It never turns a block into
47
+ * an allow. See `enforceRecordCapForFile` for which records go first, and why
48
+ * age alone is the wrong order.
49
+ */
50
+ const READ_GUARD_MAX_RECORDS_PER_FILE = 128;
51
+ /**
52
+ * Trim a single file's read list to the per-file cap. Returns how many records
53
+ * were dropped so the ledger can show the trim.
54
+ *
55
+ * Eviction is NOT purely by age. The shape that overflows this cap is
56
+ * read-once-then-grep-often: one whole-file read followed by hundreds of cheap
57
+ * search credits. Pure age order evicts that whole-file read first, and it is
58
+ * the record `canIgnoreStalenessByHashes` needs to rescue an edit after an
59
+ * unrelated mtime touch. So spend the search credits first, oldest among them
60
+ * first, and only fall through to genuine reads when the credits run out.
61
+ * Genuine reads are then evicted oldest-first as before.
62
+ *
63
+ * Records are trimmed IN PLACE: `EditRecord.precedingReads` holds a reference
64
+ * to this same array, so a replacement array would silently detach it.
65
+ */
66
+ function enforceRecordCapForFile(records) {
67
+ if (records.length <= READ_GUARD_MAX_RECORDS_PER_FILE)
68
+ return 0;
69
+ const excess = records.length - READ_GUARD_MAX_RECORDS_PER_FILE;
70
+ const evicted = new Set();
71
+ for (let i = 0; i < records.length && evicted.size < excess; i++) {
72
+ if (records[i].searchCredit !== undefined)
73
+ evicted.add(i);
74
+ }
75
+ for (let i = 0; i < records.length && evicted.size < excess; i++) {
76
+ evicted.add(i);
77
+ }
78
+ const kept = records.filter((_, i) => !evicted.has(i));
79
+ records.length = 0;
80
+ for (const record of kept)
81
+ records.push(record);
82
+ return excess;
83
+ }
84
+ /**
85
+ * #1904 class sweep: `this.edits` is the same shape as `this.reads` — a
86
+ * per-file array that only ever grows. Its cap sits far above every consumer's
87
+ * reach, so trimming is inert: `canTreatStalenessAsOwnPriorEdit` reads only the
88
+ * last record, and `findRelocation`'s window saturates at
89
+ * RELOCATION_WINDOW_MAX / RELOCATION_WINDOW_PER_EDIT (20) applied edits. Only
90
+ * `getStats`, a debug surface, sees the older records at all.
91
+ */
92
+ const READ_GUARD_MAX_EDITS_PER_FILE = 256;
38
93
  // Unconsumed reads remain valid until edit or session end, but this high
39
94
  // sanity cap prevents a read-only session from growing without bound.
40
95
  const READ_GUARD_MAX_UNCONSUMED_FILES = 4096;
@@ -213,6 +268,23 @@ export class ReadGuard {
213
268
  // pi Write tool authored, independent of filesystem mtime granularity
214
269
  // or clock skew (NFS, FAT32, etc.).
215
270
  writtenThisSession = new Set();
271
+ // Existence-independent index for hasKnownPath/forgetPath (#1668 review
272
+ // F1). `this.key()` (normalizeFilePath) branches on whether `filePath`
273
+ // currently exists on disk: an existing file resolves to realpathSync
274
+ // canonical casing, a missing one to a lowercased tail. recordRead/
275
+ // recordWritten always key while the file is still on disk (real
276
+ // casing); hasKnownPath/forgetPath are queried AFTER an external delete
277
+ // already landed, when the path no longer exists — recomputing
278
+ // `this.key()` at that point returns a DIFFERENT string for any
279
+ // mixed-case basename (`MyModule.ts` → `mymodule.ts`), so a lookup
280
+ // against `reads`/`writtenThisSession` silently misses. This index maps
281
+ // a purely syntactic key (`normalizeEphemeralMapKey` — slash-fold +
282
+ // lowercase, no filesystem access, so it never depends on current disk
283
+ // state) to the REAL key `reads`/`writtenThisSession` used at record
284
+ // time, so a post-delete lookup finds the same entry regardless of what
285
+ // happened to the file since. Pruned inside `evictFile` so it never
286
+ // outlives the record it points at.
287
+ knownPathIndex = 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 evictedRecordCount = enforceRecordCapForFile(arr);
314
403
  this.touchFile(storedRecord.filePath);
315
404
  this.enforceFileCap();
316
405
  logReadGuardEvent({
@@ -334,6 +423,15 @@ 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
  });
339
437
  // Also update FileTime stamp for this file
@@ -442,9 +540,17 @@ export class ReadGuard {
442
540
  // 2. FileTime check (actual staleness)
443
541
  let ignoredOwnEditStaleness = false;
444
542
  let ignoredHashStaleness = false;
543
+ let ignoredOldTextResolvedStaleness = false;
445
544
  if (this.fileTime.hasChanged(filePath)) {
446
545
  const lastRead = fileReads[fileReads.length - 1];
447
- if (this.canTreatStalenessAsOwnPriorEdit(filePath, lastRead.timestamp)) {
546
+ if (options?.oldTextResolved === true) {
547
+ // The host-facing preflight sets this only after oldText resolves to
548
+ // exactly one span in the live bytes. That content evidence is newer
549
+ // and stronger than FileTime's coarse external-write signal, matching
550
+ // the skipSnapshotCheck exception at the later range-stale gate.
551
+ ignoredOldTextResolvedStaleness = true;
552
+ }
553
+ else if (this.canTreatStalenessAsOwnPriorEdit(filePath, lastRead.timestamp)) {
448
554
  ignoredOwnEditStaleness = true;
449
555
  }
450
556
  else if (this.canIgnoreStalenessByHashes(filePath, fileReads, touchedLines, editRanges)) {
@@ -474,7 +580,7 @@ export class ReadGuard {
474
580
  const rangesToCheck = editRanges && editRanges.length > 1 ? editRanges : [touchedLines];
475
581
  let viaSymbol = false;
476
582
  for (const range of rangesToCheck) {
477
- const snapshotValidation = this.validateRangeSnapshot(filePath, range);
583
+ const snapshotValidation = this.validateRangeSnapshot(filePath, range, !!options?.skipSnapshotCheck);
478
584
  const coverage = this.checkCoverage(filePath, range);
479
585
  if (!coverage.covered) {
480
586
  const lastRead = fileReads[fileReads.length - 1];
@@ -568,10 +674,15 @@ export class ReadGuard {
568
674
  }
569
675
  const verdict = this.allow();
570
676
  this.recordVerdict(filePath, "edit", touchedLines, verdict, {
571
- reasonKind: viaSymbol ? "symbol_coverage" : "range_coverage",
677
+ reasonKind: ignoredOldTextResolvedStaleness
678
+ ? "file_modified_oldtext_unique"
679
+ : viaSymbol
680
+ ? "symbol_coverage"
681
+ : "range_coverage",
572
682
  viaSymbol,
573
683
  ignoredOwnEditStaleness,
574
684
  ignoredHashStaleness,
685
+ oldTextResolved: ignoredOldTextResolvedStaleness,
575
686
  });
576
687
  return verdict;
577
688
  }
@@ -587,6 +698,44 @@ export class ReadGuard {
587
698
  return true; // Assume new if we can't stat
588
699
  }
589
700
  }
701
+ /**
702
+ * Whether pi-lens has any record of this path from a read or write this
703
+ * session (#1668). Gates external-delete detection: a path pi-lens never
704
+ * read or wrote is one no LSP server's cache was ever told about through
705
+ * us, so an `rm` naming it carries no signal worth checking disk for —
706
+ * this is a lookup against state already being tracked, never a fresh
707
+ * filesystem stat over an unbounded path set.
708
+ *
709
+ * MUST go through `knownPathIndex`, not `this.key(filePath)` directly
710
+ * (#1668 review F1): this is called AFTER a bash delete has already
711
+ * landed, when `filePath` no longer exists — `this.key()` at that point
712
+ * returns a lowercased-tail key, not the real-casing key `recordRead`/
713
+ * `recordWritten` stored while the file was still on disk. For a
714
+ * mixed-case basename (`MyModule.ts`) the two keys differ and a direct
715
+ * `this.key()` lookup always misses.
716
+ */
717
+ hasKnownPath(filePath) {
718
+ const stored = this.knownPathIndex.get(normalizeEphemeralMapKey(filePath));
719
+ return (stored !== undefined &&
720
+ (this.reads.has(stored) || this.writtenThisSession.has(stored)));
721
+ }
722
+ /**
723
+ * Drop all record of a path pi-lens confirmed no longer exists on disk
724
+ * (#1668, external delete). Without this a later write reusing the same
725
+ * path would inherit a stale writtenThisSession/reads entry from before
726
+ * the delete, and a repeat `rm` of the same already-gone path would keep
727
+ * matching {@link hasKnownPath} and re-emitting a type-3 notification.
728
+ *
729
+ * MUST evict through `knownPathIndex`, not `this.key(filePath)` directly
730
+ * (#1668 review F1) — same reasoning as {@link hasKnownPath}: `filePath`
731
+ * is already gone from disk by the time this runs, so recomputing
732
+ * `this.key()` targets the wrong (lowercased-tail) key for a mixed-case
733
+ * basename and leaves the real entry behind un-evicted.
734
+ */
735
+ forgetPath(filePath) {
736
+ const stored = this.knownPathIndex.get(normalizeEphemeralMapKey(filePath));
737
+ this.evictFile(stored ?? this.key(filePath));
738
+ }
590
739
  /**
591
740
  * Mark a file as pending creation (Write tool to a non-existing file).
592
741
  * Must be called from the tool_call handler before the write lands so
@@ -601,8 +750,12 @@ export class ReadGuard {
601
750
  * Call this from the tool_result handler so the next checkEdit on the same
602
751
  * file doesn't see "file_modified" caused by our own previous edit.
603
752
  */
604
- recordWritten(filePath) {
605
- filePath = this.key(filePath);
753
+ recordWritten(rawFilePath) {
754
+ const filePath = this.key(rawFilePath);
755
+ // #1668 review F1: index by the existence-independent syntactic key
756
+ // (see `knownPathIndex`) so a later hasKnownPath/forgetPath lookup
757
+ // after an external delete can still find this entry's real key.
758
+ this.knownPathIndex.set(normalizeEphemeralMapKey(rawFilePath), filePath);
606
759
  this.fileTime.read(filePath);
607
760
  this.writtenThisSession.add(filePath);
608
761
  if (this.reads.has(filePath))
@@ -838,7 +991,13 @@ export class ReadGuard {
838
991
  return (read.enclosingSymbol.startLine <= editStart &&
839
992
  read.enclosingSymbol.endLine >= editEnd);
840
993
  }
841
- validateRangeSnapshot(filePath, range) {
994
+ validateRangeSnapshot(filePath, range,
995
+ /**
996
+ * What the CALLER will do with the verdict. `checkEdit` honours
997
+ * `skipSnapshotCheck` for content-validated (oldText) edits, so a
998
+ * `shouldBlock` verdict may never reach the gate (#1904 item 1).
999
+ */
1000
+ snapshotCheckSkipped) {
842
1001
  const reads = this.reads.get(filePath) ?? [];
843
1002
  const candidates = reads.filter((read) => this.readCoversRange(read, range));
844
1003
  let status = "unavailable";
@@ -886,6 +1045,17 @@ export class ReadGuard {
886
1045
  lastUnavailableTimestamp <= lastMismatchTimestamp &&
887
1046
  checkedCandidateCount > 0 &&
888
1047
  hashUnavailableCandidateCount === 0;
1048
+ // `enforced` below states INTENT — whether this validation reached a
1049
+ // decidable verdict. It says nothing about what the caller did with it.
1050
+ // `outcome` states the caller's actual behaviour, which is what an
1051
+ // enforcement rate must be computed from (#1904 item 1).
1052
+ const outcome = shouldBlock
1053
+ ? snapshotCheckSkipped
1054
+ ? "bypassed-content-match"
1055
+ : "enforced-block"
1056
+ : status === "match"
1057
+ ? "enforced-pass"
1058
+ : "not-decidable";
889
1059
  logReadGuardEvent({
890
1060
  event: "range_snapshot_validation",
891
1061
  sessionId: this.sessionId,
@@ -903,6 +1073,7 @@ export class ReadGuard {
903
1073
  missingLines: missingLines.slice(0, 20),
904
1074
  mismatchedLines: mismatchedLines.slice(0, 20),
905
1075
  enforced: shouldBlock || status === "match",
1076
+ outcome,
906
1077
  },
907
1078
  });
908
1079
  return {
@@ -1117,6 +1288,9 @@ export class ReadGuard {
1117
1288
  reason: verdict.reason,
1118
1289
  timestamp: Date.now(),
1119
1290
  });
1291
+ if (arr.length > READ_GUARD_MAX_EDITS_PER_FILE) {
1292
+ arr.splice(0, arr.length - READ_GUARD_MAX_EDITS_PER_FILE);
1293
+ }
1120
1294
  this.edits.set(filePath, arr);
1121
1295
  }
1122
1296
  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
  }