@herbertgao/pi-extensions 2026.9.0 → 2026.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +13 -1
  2. package/node_modules/@czottmann/pi-automode/README.md +20 -0
  3. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +1 -1
  4. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +2 -1
  5. package/node_modules/@czottmann/pi-automode/docs/defaults.md +3 -2
  6. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +38 -2
  7. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +5 -3
  8. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +11 -3
  9. package/node_modules/@czottmann/pi-automode/package.json +1 -1
  10. package/node_modules/@narumitw/pi-btw/README.md +57 -141
  11. package/node_modules/@narumitw/pi-btw/dist/index.ts +466 -41
  12. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  13. package/node_modules/@narumitw/pi-btw/docs/workflows.md +74 -0
  14. package/node_modules/@narumitw/pi-btw/package.json +2 -1
  15. package/node_modules/@narumitw/pi-btw/src/btw.ts +15 -1
  16. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +32 -2
  17. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +330 -0
  18. package/node_modules/@narumitw/pi-btw/src/menu.ts +136 -19
  19. package/node_modules/@narumitw/pi-btw/src/settings.ts +32 -0
  20. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +57 -5
  21. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +52 -30
  22. package/node_modules/@pi-plugins/fast-mode/README.md +5 -3
  23. package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
  24. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20 -55
  25. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  26. package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
  27. package/node_modules/pi-lens/CHANGELOG.md +1072 -0
  28. package/node_modules/pi-lens/README.md +3 -0
  29. package/node_modules/pi-lens/config/biome/core.jsonc +11 -2
  30. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +13 -1
  31. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +2 -9
  32. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +927 -80
  33. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +1 -1
  34. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +13 -4
  35. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +123 -3
  36. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +60 -5
  37. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +2 -9
  38. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +2 -0
  39. package/node_modules/pi-lens/dist/clients/bash-file-access.js +133 -3
  40. package/node_modules/pi-lens/dist/clients/biome-client.js +9 -2
  41. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +14 -6
  42. package/node_modules/pi-lens/dist/clients/bootstrap.js +509 -73
  43. package/node_modules/pi-lens/dist/clients/bounded-cache.js +152 -12
  44. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +1 -1
  45. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +59 -9
  46. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +113 -0
  47. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +3 -11
  48. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +31 -4
  49. package/node_modules/pi-lens/dist/clients/cache-manager.js +105 -6
  50. package/node_modules/pi-lens/dist/clients/cache-observability.js +40 -19
  51. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +422 -0
  52. package/node_modules/pi-lens/dist/clients/cascade-logger.js +2 -9
  53. package/node_modules/pi-lens/dist/clients/child-unref.js +1 -19
  54. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +13 -3
  55. package/node_modules/pi-lens/dist/clients/complexity-client.js +16 -5
  56. package/node_modules/pi-lens/dist/clients/config-core/deny.js +221 -0
  57. package/node_modules/pi-lens/dist/clients/config-core/index.js +50 -0
  58. package/node_modules/pi-lens/dist/clients/config-core/merge.js +357 -0
  59. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +340 -0
  60. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +248 -0
  61. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +164 -0
  62. package/node_modules/pi-lens/dist/clients/config-core/records.js +218 -0
  63. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +125 -0
  64. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +78 -0
  65. package/node_modules/pi-lens/dist/clients/config-core/schema.js +165 -0
  66. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +281 -0
  67. package/node_modules/pi-lens/dist/clients/config-locations.js +160 -0
  68. package/node_modules/pi-lens/dist/clients/config-resolve.js +789 -0
  69. package/node_modules/pi-lens/dist/clients/config-schema.js +234 -0
  70. package/node_modules/pi-lens/dist/clients/config-warn.js +407 -0
  71. package/node_modules/pi-lens/dist/clients/dead-code-client.js +19 -14
  72. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +2 -6
  73. package/node_modules/pi-lens/dist/clients/deadline-utils.js +178 -0
  74. package/node_modules/pi-lens/dist/clients/debug-handles.js +2 -2
  75. package/node_modules/pi-lens/dist/clients/debug-heap.js +3 -3
  76. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +106 -0
  77. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +113 -7
  78. package/node_modules/pi-lens/dist/clients/dependency-checker.js +8 -19
  79. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +1 -1
  80. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +12 -9
  81. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +15 -5
  82. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -4
  83. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +162 -44
  84. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +97 -0
  85. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +27 -8
  86. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -6
  87. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +1 -1
  88. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -3
  89. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +2 -2
  90. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +1 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +383 -27
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +4 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -1
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -1
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -1
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -1
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -1
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -1
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +1 -3
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -1
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -1
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +1 -2
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -1
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -1
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -1
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -1
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -1
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -1
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +14 -18
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -1
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -1
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -1
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +6 -3
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +7 -4
  122. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -1
  123. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +1 -2
  124. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +1 -3
  125. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +1 -2
  126. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -1
  127. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +4 -3
  128. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -1
  129. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -1
  130. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -1
  131. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -1
  132. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -1
  133. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -1
  134. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -1
  135. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -1
  136. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -1
  137. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
  138. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -67
  139. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +194 -80
  140. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +84 -15
  141. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -21
  142. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -1
  143. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +2 -20
  144. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +1 -1
  145. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -1
  146. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +2 -2
  147. package/node_modules/pi-lens/dist/clients/disposition-logger.js +3 -10
  148. package/node_modules/pi-lens/dist/clients/effective-config.js +403 -0
  149. package/node_modules/pi-lens/dist/clients/error-class.js +23 -0
  150. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +274 -0
  151. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +2 -2
  152. package/node_modules/pi-lens/dist/clients/extension-log.js +2 -2
  153. package/node_modules/pi-lens/dist/clients/feature-hints.js +2 -1
  154. package/node_modules/pi-lens/dist/clients/file-kinds.js +1 -38
  155. package/node_modules/pi-lens/dist/clients/file-time.js +7 -2
  156. package/node_modules/pi-lens/dist/clients/file-utils.js +117 -10
  157. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +50 -13
  158. package/node_modules/pi-lens/dist/clients/format-service.js +6 -2
  159. package/node_modules/pi-lens/dist/clients/formatters.js +385 -104
  160. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +17 -0
  161. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +116 -22
  162. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -9
  163. package/node_modules/pi-lens/dist/clients/git-guard.js +6 -1
  164. package/node_modules/pi-lens/dist/clients/go-client.js +39 -0
  165. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +252 -0
  166. package/node_modules/pi-lens/dist/clients/grammar-source.js +3 -3
  167. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +424 -0
  168. package/node_modules/pi-lens/dist/clients/hook-budgets.js +76 -0
  169. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +5 -2
  170. package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
  171. package/node_modules/pi-lens/dist/clients/installer/index.js +289 -59
  172. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +84 -18
  173. package/node_modules/pi-lens/dist/clients/instance-reaper.js +110 -236
  174. package/node_modules/pi-lens/dist/clients/instance-registry.js +6 -3
  175. package/node_modules/pi-lens/dist/clients/language-profile.js +0 -9
  176. package/node_modules/pi-lens/dist/clients/language-registry.js +599 -0
  177. package/node_modules/pi-lens/dist/clients/latency-logger.js +165 -9
  178. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +34 -1
  179. package/node_modules/pi-lens/dist/clients/lens-config.js +160 -30
  180. package/node_modules/pi-lens/dist/clients/lens-engine.js +33 -41
  181. package/node_modules/pi-lens/dist/clients/lens-events.js +1 -1
  182. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +77 -6
  183. package/node_modules/pi-lens/dist/clients/lens-map.js +1 -1
  184. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -38
  185. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +3 -1
  186. package/node_modules/pi-lens/dist/clients/lsp/client.js +172 -33
  187. package/node_modules/pi-lens/dist/clients/lsp/config.js +484 -158
  188. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +1 -1
  189. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +1 -1
  190. package/node_modules/pi-lens/dist/clients/lsp/edits.js +18 -5
  191. package/node_modules/pi-lens/dist/clients/lsp/index.js +892 -96
  192. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +1 -1
  193. package/node_modules/pi-lens/dist/clients/lsp/language.js +18 -177
  194. package/node_modules/pi-lens/dist/clients/lsp/launch.js +2 -1
  195. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  196. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +5 -18
  197. package/node_modules/pi-lens/dist/clients/lsp/server.js +284 -147
  198. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +116 -23
  199. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +2 -7
  200. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -1
  201. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +14 -7
  202. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +6 -0
  203. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +2 -1
  204. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +1 -1
  205. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +1 -1
  206. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +163 -21
  207. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +36 -0
  208. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +33 -4
  209. package/node_modules/pi-lens/dist/clients/mcp/session.js +5 -16
  210. package/node_modules/pi-lens/dist/clients/memory-sampler.js +8 -3
  211. package/node_modules/pi-lens/dist/clients/metrics-history.js +28 -107
  212. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +3 -5
  213. package/node_modules/pi-lens/dist/clients/module-report.js +21 -46
  214. package/node_modules/pi-lens/dist/clients/mutating-tool.js +651 -0
  215. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +368 -0
  216. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +240 -0
  217. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +247 -24
  218. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +101 -0
  219. package/node_modules/pi-lens/dist/clients/observed-mutation.js +1215 -0
  220. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +70 -28
  221. package/node_modules/pi-lens/dist/clients/opengrep-config.js +5 -1
  222. package/node_modules/pi-lens/dist/clients/package-manager.js +195 -26
  223. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +359 -80
  224. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +23 -7
  225. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +21 -2
  226. package/node_modules/pi-lens/dist/clients/path-utils.js +451 -15
  227. package/node_modules/pi-lens/dist/clients/performance-report.js +2 -2
  228. package/node_modules/pi-lens/dist/clients/persist-debounce.js +8 -1
  229. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +114 -0
  230. package/node_modules/pi-lens/dist/clients/pipeline.js +79 -18
  231. package/node_modules/pi-lens/dist/clients/probe-home-state.js +228 -0
  232. package/node_modules/pi-lens/dist/clients/process-bridge.js +66 -0
  233. package/node_modules/pi-lens/dist/clients/process-snapshot.js +68 -0
  234. package/node_modules/pi-lens/dist/clients/project-changes.js +1 -1
  235. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +1 -1
  236. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
  237. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +1 -1
  238. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +1 -1
  239. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +1 -1
  240. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +1 -1
  241. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +28 -4
  242. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +51 -20
  243. package/node_modules/pi-lens/dist/clients/project-lens-config.js +438 -122
  244. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +3 -51
  245. package/node_modules/pi-lens/dist/clients/project-snapshot.js +52 -24
  246. package/node_modules/pi-lens/dist/clients/python-environment.js +217 -0
  247. package/node_modules/pi-lens/dist/clients/python-provenance.js +639 -0
  248. package/node_modules/pi-lens/dist/clients/quiet-window.js +6 -1
  249. package/node_modules/pi-lens/dist/clients/read-bridge.js +13 -21
  250. package/node_modules/pi-lens/dist/clients/read-expansion.js +9 -46
  251. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +16 -5
  252. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +364 -212
  253. package/node_modules/pi-lens/dist/clients/read-guard.js +26 -2
  254. package/node_modules/pi-lens/dist/clients/recent-touches.js +2 -2
  255. package/node_modules/pi-lens/dist/clients/resource-sampler.js +195 -96
  256. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +155 -114
  257. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +1 -1
  258. package/node_modules/pi-lens/dist/clients/review-graph/service.js +55 -3
  259. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +7 -15
  260. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +10 -74
  261. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +9 -3
  262. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +19 -3
  263. package/node_modules/pi-lens/dist/clients/runtime-context.js +49 -16
  264. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +25 -6
  265. package/node_modules/pi-lens/dist/clients/runtime-session.js +184 -32
  266. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +179 -36
  267. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +797 -243
  268. package/node_modules/pi-lens/dist/clients/runtime-turn.js +1028 -53
  269. package/node_modules/pi-lens/dist/clients/rust-client.js +31 -0
  270. package/node_modules/pi-lens/dist/clients/safe-spawn.js +33 -10
  271. package/node_modules/pi-lens/dist/clients/sanitize.js +7 -12
  272. package/node_modules/pi-lens/dist/clients/scan-utils.js +1 -56
  273. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +1 -3
  274. package/node_modules/pi-lens/dist/clients/security-scan-client.js +3 -0
  275. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +1 -1
  276. package/node_modules/pi-lens/dist/clients/session-start-observability.js +79 -0
  277. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -24
  278. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +12 -3
  279. package/node_modules/pi-lens/dist/clients/sgconfig.js +7 -6
  280. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +1 -1
  281. package/node_modules/pi-lens/dist/clients/skills-resolver.js +105 -0
  282. package/node_modules/pi-lens/dist/clients/smells-rollup.js +4 -4
  283. package/node_modules/pi-lens/dist/clients/startup-scan.js +2 -10
  284. package/node_modules/pi-lens/dist/clients/string-utils.js +13 -0
  285. package/node_modules/pi-lens/dist/clients/subagent-mode.js +17 -4
  286. package/node_modules/pi-lens/dist/clients/test-runner-client.js +176 -39
  287. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +244 -0
  288. package/node_modules/pi-lens/dist/clients/tool-definition.js +41 -1
  289. package/node_modules/pi-lens/dist/clients/tool-policy.js +92 -22
  290. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +40 -40
  291. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +87 -100
  292. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +2 -2
  293. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +81 -1
  294. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +51 -46
  295. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +28 -0
  296. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -4
  297. package/node_modules/pi-lens/dist/clients/typos-config.js +5 -0
  298. package/node_modules/pi-lens/dist/clients/user-notify.js +6 -6
  299. package/node_modules/pi-lens/dist/clients/widget-state.js +191 -26
  300. package/node_modules/pi-lens/dist/clients/word-index-logger.js +2 -9
  301. package/node_modules/pi-lens/dist/clients/word-index-store.js +23 -12
  302. package/node_modules/pi-lens/dist/clients/word-index.js +252 -47
  303. package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -1
  304. package/node_modules/pi-lens/dist/clients/zizmor-config.js +4 -1
  305. package/node_modules/pi-lens/dist/index.js +65899 -55340
  306. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -1
  307. package/node_modules/pi-lens/dist/mcp/build-staleness.js +1 -1
  308. package/node_modules/pi-lens/dist/mcp/server.js +127 -5
  309. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +583 -0
  310. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +129 -0
  311. package/node_modules/pi-lens/dist/tools/effective-config.js +89 -0
  312. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +96 -25
  313. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +104 -86
  314. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +37 -16
  315. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +3 -3
  316. package/node_modules/pi-lens/dist/tools/render-compact.js +1 -1
  317. package/node_modules/pi-lens/dist/tools/shared.js +0 -1
  318. package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -2
  319. package/node_modules/pi-lens/docs/agent-guide.md +64 -1
  320. package/node_modules/pi-lens/docs/configuration.md +222 -0
  321. package/node_modules/pi-lens/docs/dependencies.md +3 -3
  322. package/node_modules/pi-lens/docs/features.md +44 -5
  323. package/node_modules/pi-lens/docs/globalconfig.md +20 -0
  324. package/node_modules/pi-lens/docs/language-coverage.md +2 -2
  325. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +9 -1
  326. package/node_modules/pi-lens/docs/pi-lens-fixer.md +25 -0
  327. package/node_modules/pi-lens/docs/pi-lens-investigator.md +25 -0
  328. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +33 -0
  329. package/node_modules/pi-lens/docs/pi-lens-subagent.md +49 -0
  330. package/node_modules/pi-lens/docs/pi-lens-warden.md +55 -0
  331. package/node_modules/pi-lens/docs/public-api-stability.md +359 -0
  332. package/node_modules/pi-lens/docs/release-qa-baseline.md +190 -0
  333. package/node_modules/pi-lens/docs/servercapabilities.md +7 -3
  334. package/node_modules/pi-lens/docs/subagent-compat.md +110 -30
  335. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +2 -2
  336. package/node_modules/pi-lens/docs/word-index.md +1 -1
  337. package/node_modules/pi-lens/package.json +18 -35
  338. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
  339. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
  340. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
  341. package/node_modules/pi-lens/rules/typos/_typos.toml +4 -2
  342. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +192 -1
  343. package/node_modules/pi-lens/scripts/install-selftest.mjs +58 -2
  344. package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +129 -0
  345. package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +1 -1
  346. package/node_modules/pi-lens/scripts/rpc-load-check.mjs +3 -1
  347. package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +4 -2
  348. package/node_modules/pi-web-access/CHANGELOG.md +28 -0
  349. package/node_modules/pi-web-access/README.md +62 -15
  350. package/node_modules/pi-web-access/curator-page.ts +3 -1
  351. package/node_modules/pi-web-access/curator-server.ts +5 -1
  352. package/node_modules/pi-web-access/gemini-search.ts +8 -4
  353. package/node_modules/pi-web-access/github-extract.ts +242 -1
  354. package/node_modules/pi-web-access/index.ts +118 -64
  355. package/node_modules/pi-web-access/mistral-search.ts +281 -0
  356. package/node_modules/pi-web-access/package.json +2 -2
  357. package/node_modules/pi-web-access/perplexity.ts +14 -1
  358. package/node_modules/pi-web-access/utils.ts +23 -6
  359. package/node_modules/pi-web-access/xai-search.ts +96 -33
  360. package/package.json +6 -6
@@ -822,6 +822,27 @@ export class ReadGuard {
822
822
  return (stored !== undefined &&
823
823
  (this.reads.has(stored) || this.writtenThisSession.has(stored)));
824
824
  }
825
+ /**
826
+ * Every path this guard has a read or a write record for, as normalized keys
827
+ * (#2430).
828
+ *
829
+ * This is the read-guard half of the observational net's TRACKED-FILE SET:
830
+ * the files pi-lens has actually seen this session, which the `agent_settled`
831
+ * sweep hash-checks instead of walking the workspace. It reads state already
832
+ * held in memory and stats nothing; both backing containers are capped
833
+ * (`READ_GUARD_MAX_UNCONSUMED_FILES`), so the result is bounded by
834
+ * construction.
835
+ *
836
+ * `touchFile` is deliberately NOT called: enumerating is not using, and
837
+ * refreshing every entry's idle clock on each sweep would make the idle
838
+ * eviction unreachable.
839
+ */
840
+ getTrackedPaths() {
841
+ const tracked = new Set(this.reads.keys());
842
+ for (const written of this.writtenThisSession)
843
+ tracked.add(written);
844
+ return [...tracked];
845
+ }
825
846
  /**
826
847
  * Drop all record of a path pi-lens confirmed no longer exists on disk
827
848
  * (#1668, external delete). Without this a later write reusing the same
@@ -1375,8 +1396,11 @@ export class ReadGuard {
1375
1396
  return filePath.endsWith(suffix);
1376
1397
  }
1377
1398
  if (pattern.includes("*")) {
1378
- // Convert glob to regex
1379
- const regex = new RegExp(`^${pattern.replace(/\\/g, "\\\\").replace(/\./g, "\\.").replace(/\*/g, ".*")}$`);
1399
+ // Adjacent stars are equivalent to one star in this dialect. Collapse
1400
+ // them before compiling so a non-match cannot backtrack over every
1401
+ // nullable `.*` group (#2622).
1402
+ const collapsedPattern = pattern.replace(/\*+/g, "*");
1403
+ const regex = new RegExp(`^${collapsedPattern.replace(/\\/g, "\\\\").replace(/\./g, "\\.").replace(/\*/g, ".*")}$`);
1380
1404
  return regex.test(filePath);
1381
1405
  }
1382
1406
  return filePath === pattern;
@@ -37,7 +37,7 @@ import { getProjectDataDir } from "./file-utils.js";
37
37
  import { normalizeFilePath } from "./path-utils.js";
38
38
  export const RECENT_TOUCHES_MAX_ENTRIES = 50;
39
39
  /** Freshness window for the CHILD session_start consumer (15 minutes). */
40
- export const RECENT_TOUCHES_FRESHNESS_MS = 15 * 60 * 1000;
40
+ const RECENT_TOUCHES_FRESHNESS_MS = 15 * 60 * 1000;
41
41
  function recentTouchesPath(cwd) {
42
42
  return path.join(getProjectDataDir(cwd), "recent-touches.json");
43
43
  }
@@ -46,7 +46,7 @@ function recentTouchesPath(cwd) {
46
46
  // INTO the same nudge pipeline, so one switch governs the whole path
47
47
  // (producer, both consumers) as specified in the issue.
48
48
  let _enabledCache;
49
- export function isRecentTouchesEnabled() {
49
+ function isRecentTouchesEnabled() {
50
50
  if (_enabledCache === undefined) {
51
51
  _enabledCache = process.env.PI_LENS_AGENT_NUDGE !== "0";
52
52
  }
@@ -25,11 +25,11 @@
25
25
  * **synchronously in that detached callback**, which no `try { await pidusage }
26
26
  * catch {}` at the call site can catch → uncaughtException → the pi host
27
27
  * crashes (#620, #533). pidusage 4.0.1 exposes no option to avoid the gwmi
28
- * path. So on Windows this module runs its OWN fully guarded
29
- * `Get-CimInstance Win32_Process` query (see `sampleProcessesWindows`),
30
- * mirroring `findDescendantPidsWindows`'s guard pattern, and computes CPU%
31
- * from the same KernelModeTime/UserModeTime delta-over-elapsed formula gwmi
32
- * uses — so a spawn failure can only ever lose a data point, never throw.
28
+ * path. So on Windows this module asks the shared, fully guarded process-table
29
+ * seam (`clients/process-snapshot.ts` over `scripts/lib/process-scan.mjs`,
30
+ * #2443) for the CPU/RSS columns instead, and computes CPU% from the same
31
+ * KernelModeTime/UserModeTime delta-over-elapsed formula gwmi uses — so a
32
+ * spawn failure can only ever lose a data point, never throw.
33
33
  *
34
34
  * Every export here is best-effort: a sampling failure (pid already exited,
35
35
  * `pidusage` throwing, permission denied, etc.) must never throw into the
@@ -43,11 +43,10 @@
43
43
  * process/pidusage involvement — mirrors the pure/impure split in
44
44
  * clients/instance-reaper.ts (`decideOrphanReaping` vs `sweepOrphans`).
45
45
  */
46
- import * as path from "node:path";
47
46
  import pidusage from "pidusage";
48
- import { spawnCollectStdoutResult } from "./child-unref.js";
49
47
  import { recordDegradationOnce } from "./degradation-ledger.js";
50
48
  import { terminateScannerChild } from "./instance-reaper.js";
49
+ import { queryProcessTable } from "./process-snapshot.js";
51
50
  export const RESOURCE_SAMPLE_QUERY_TIMEOUT_MS = 2_000;
52
51
  function recordQueryFailure(subject, status, exitCode) {
53
52
  const exitReason = status === "exit-error" ? ` (exit code ${exitCode ?? "unknown"})` : "";
@@ -112,41 +111,31 @@ export function walkDescendantPids(rootPid, pairs) {
112
111
  async function findDescendantPidsWindows(rootPid) {
113
112
  if (!runningOnWindows() || !Number.isFinite(rootPid) || rootPid <= 0)
114
113
  return [];
115
- // One WQL query pulls every process's (pid, parentPid) pair; walk the BFS
116
- // in JS rather than issuing N queries for N tree levels.
117
- const psScript = "Get-CimInstance Win32_Process " +
118
- "| Select-Object -Property ProcessId,ParentProcessId " +
119
- '| ForEach-Object { "$($_.ProcessId),$($_.ParentProcessId)" }';
120
- const powershell = path.join(process.env.SystemRoot ?? String.raw `C:\Windows`, "WindowsPowerShell", "v1.0", "powershell.exe");
121
- // Fire-and-forget, per-poll-tick spawn (#1155): `spawnCollectStdout` unrefs
122
- // the child AND its piped stdout so this one-shot CIM query can never keep
123
- // a settled `pi --print` process alive past its own close — mirrors the
124
- // reaper's identical spawn→collect plumbing (#1153/#1160). Sampling still
125
- // works normally in an interactive/long-lived session: unref only means
126
- // "don't hold the loop open FOR this alone," the collected stdout is still
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"] }, {
114
+ // One query pulls every process's (pid, parentPid) pair; walk the BFS in
115
+ // JS rather than issuing N queries for N tree levels. The listing itself is
116
+ // the shared seam (#2443), which also supplies the fire-and-forget spawn
117
+ // rails this call has always needed (#1155): the child AND its piped stdout
118
+ // are unref'd, so this one-shot query can never keep a settled
119
+ // `pi --print` process alive past its own close, and a scanner child that
120
+ // blows the timeout is tree-killed and verified rather than abandoned. The
121
+ // result status keeps a failed query distinct from a successful empty
122
+ // process table.
123
+ const result = await queryProcessTable({ fields: ["pid", "ppid"] }, {
130
124
  timeoutMs: RESOURCE_SAMPLE_QUERY_TIMEOUT_MS,
131
- onTimeout: (child) => terminateScannerChild(child, {}),
125
+ onTimeout: (child) => terminateScannerChild(child, {
126
+ kind: "resource-sampler-scanner-escalated",
127
+ timeoutMs: RESOURCE_SAMPLE_QUERY_TIMEOUT_MS,
128
+ }),
132
129
  });
133
130
  if (result.status !== "ok") {
134
131
  recordQueryFailure("windows-descendant-process-table", result.status, result.exitCode);
135
132
  return null;
136
133
  }
137
- const pairs = [];
138
- for (const line of result.stdout.split(/\r?\n/)) {
139
- const [pidStr, ppidStr] = line.split(",");
140
- const pid = Number(pidStr);
141
- const ppid = Number(ppidStr);
142
- if (Number.isFinite(pid) && Number.isFinite(ppid)) {
143
- pairs.push([pid, ppid]);
144
- }
145
- }
146
- return walkDescendantPids(rootPid, pairs);
134
+ return walkDescendantPids(rootPid, result.rows.map((row) => [row.pid, row.ppid]));
147
135
  }
148
136
  const windowsCpuHistory = new Map();
149
137
  const CPU_HISTORY_MAX_AGE_MS = 60_000;
138
+ const CPU_HISTORY_MAX_ENTRIES = 4_096;
150
139
  /**
151
140
  * TEST-ONLY: clear the Windows CPU%-history so a test's two-sample CPU%
152
141
  * assertion starts from a known-empty state (module-level state otherwise
@@ -155,43 +144,57 @@ const CPU_HISTORY_MAX_AGE_MS = 60_000;
155
144
  export function __resetWindowsCpuHistoryForTests() {
156
145
  windowsCpuHistory.clear();
157
146
  }
147
+ export function __windowsCpuHistorySizeForTests() {
148
+ return windowsCpuHistory.size;
149
+ }
150
+ export function __windowsCpuHistoryHasForTests(pid, processIdentity) {
151
+ return windowsCpuHistory.has(`${pid}:${processIdentity}`);
152
+ }
158
153
  /**
159
- * Windows-only CPU%/RSS sampling via a FULLY GUARDED `Get-CimInstance
160
- * Win32_Process` query (mirrors `findDescendantPidsWindows`): a synchronous
161
- * throw from `spawn` (the `spawn UNKNOWN` crash vector, #620), a `child`
162
- * `error` event, or a non-zero/garbage exit all resolve to an errored/absent
163
- * map — this function can NEVER throw or reject. Deliberately does NOT call
164
- * `pidusage`, whose unguarded internal `gwmi` spawn is the crash we're fixing.
154
+ * Windows-only CPU%/RSS sampling through the FULLY GUARDED process-table
155
+ * seam (mirrors `findDescendantPidsWindows`): a synchronous throw from
156
+ * `spawn` (the `spawn UNKNOWN` crash vector, #620), a `child` `error` event,
157
+ * or a non-zero/garbage exit all resolve to an errored/absent map — this
158
+ * function can NEVER throw or reject. Deliberately does NOT call `pidusage`,
159
+ * whose unguarded internal `gwmi` spawn is the crash we're fixing.
165
160
  *
166
- * RSS comes from `WorkingSetSize`; CPU% from `KernelModeTime`+`UserModeTime`
167
- * (both in 100 ns units → ms via `/1e4`) differenced against this pid's prior
168
- * sample over the elapsed wall time — the same computation pidusage's gwmi
169
- * path uses. The first time a pid is seen it has no prior sample, so CPU% is
170
- * reported as 0 for that tick and a real rate lands on the next one.
161
+ * RSS comes from `WorkingSetSize` (`rssBytes`); CPU% from `KernelModeTime`
162
+ * plus `UserModeTime` (both in 100 ns units → ms via `/1e4`) differenced
163
+ * against this pid's prior sample over the elapsed wall time — the same
164
+ * computation pidusage's gwmi path uses. The first time a pid is seen it has
165
+ * no prior sample, so CPU% is reported as 0 for that tick and a real rate
166
+ * lands on the next one. The process creation date (`startedAt`) is the
167
+ * pid-reuse discriminator: a recycled pid must not inherit the previous
168
+ * process's CPU baseline.
171
169
  */
172
170
  async function sampleProcessesWindows(valid) {
173
171
  const samples = new Map();
174
172
  if (valid.length === 0)
175
173
  return samples;
176
- // pids are pre-validated finite positive integers, so this WQL filter is
177
- // injection-safe. One line per pid: "pid,workingSet,kernel100ns,user100ns".
178
- const filter = valid.map((p) => `ProcessId=${p}`).join(" or ");
179
- const psScript = `Get-CimInstance Win32_Process -Filter "${filter}" ` +
180
- "| Select-Object -Property ProcessId,WorkingSetSize,KernelModeTime,UserModeTime " +
181
- '| ForEach-Object { "$($_.ProcessId),$($_.WorkingSetSize),$($_.KernelModeTime),$($_.UserModeTime)" }';
182
- const powershell = path.join(process.env.SystemRoot ?? String.raw `C:\Windows`, "WindowsPowerShell", "v1.0", "powershell.exe");
183
- // Fire-and-forget, per-poll-tick spawn (#1155): `spawnCollectStdout` unrefs
184
- // the child AND its piped stdout so this one-shot CIM query can never keep
185
- // a settled `pi --print` process alive past its own close — mirrors the
186
- // reaper's identical spawn→collect plumbing (#1153/#1160). It also absorbs
187
- // both failure modes this function used to guard inline — a synchronous
188
- // `spawn` throw (the `spawn UNKNOWN` crash vector, #620) and an async
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
191
- // session: unref only means "don't hold the loop open FOR this alone."
192
- const query = await spawnCollectStdoutResult(powershell, ["-NoProfile", "-NonInteractive", "-Command", psScript], { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] }, {
174
+ // pids are pre-validated finite positive integers, and the seam validates
175
+ // them again before they reach the query text, so the filter is
176
+ // injection-safe. The seam also supplies the fire-and-forget spawn rails
177
+ // (#1155: the child and its piped stdout are unref'd, so this one-shot
178
+ // query cannot keep a settled `pi --print` alive past its own close) and
179
+ // absorbs every failure mode this function used to guard inline — a
180
+ // synchronous `spawn` throw (the `spawn UNKNOWN` crash vector, #620), an
181
+ // async `error` event, a timeout, or a non-zero exit — reporting each
182
+ // through `status` rather than as an indistinguishable empty table.
183
+ const query = await queryProcessTable({
184
+ fields: [
185
+ "pid",
186
+ "rssBytes",
187
+ "cpuKernel100ns",
188
+ "cpuUser100ns",
189
+ "startedAt",
190
+ ],
191
+ filter: { column: "ProcessId", op: "eq", values: valid },
192
+ }, {
193
193
  timeoutMs: RESOURCE_SAMPLE_QUERY_TIMEOUT_MS,
194
- onTimeout: (child) => terminateScannerChild(child, {}),
194
+ onTimeout: (child) => terminateScannerChild(child, {
195
+ kind: "resource-sampler-scanner-escalated",
196
+ timeoutMs: RESOURCE_SAMPLE_QUERY_TIMEOUT_MS,
197
+ }),
195
198
  });
196
199
  if (query.status !== "ok") {
197
200
  recordQueryFailure("windows-process-table", query.status, query.exitCode);
@@ -200,20 +203,37 @@ async function sampleProcessesWindows(valid) {
200
203
  try {
201
204
  const now = Date.now();
202
205
  const seen = new Set();
203
- for (const line of query.stdout.split(/\r?\n/)) {
204
- const parts = line.split(",");
205
- if (parts.length < 4)
206
- continue;
207
- const pid = Number(parts[0]);
208
- const workingSet = Number(parts[1]);
209
- const kernel100ns = Number(parts[2]);
210
- const user100ns = Number(parts[3]);
211
- if (!Number.isFinite(pid) || pid <= 0)
212
- continue;
213
- if (!Number.isFinite(workingSet))
206
+ for (const row of query.rows) {
207
+ const pid = row.pid;
208
+ const workingSet = row.rssBytes;
209
+ const kernel100ns = row.cpuKernel100ns;
210
+ const user100ns = row.cpuUser100ns;
211
+ const processIdentity = row.startedAt ?? "";
212
+ // The seam already rejects a non-integer or negative column as
213
+ // UNKNOWN (undefined), so an absent value here means the row cannot
214
+ // be sampled — never that the process used zero.
215
+ if (workingSet === undefined ||
216
+ kernel100ns === undefined ||
217
+ user100ns === undefined ||
218
+ processIdentity.length === 0)
214
219
  continue;
215
220
  const cpuMs = Math.round(kernel100ns / 1e4) + Math.round(user100ns / 1e4);
216
- const prev = windowsCpuHistory.get(pid);
221
+ const historyKey = `${pid}:${processIdentity}`;
222
+ // A reused PID must start a fresh rate window. Drop every prior identity
223
+ // for this PID before looking up the current one.
224
+ for (const [key, prior] of windowsCpuHistory) {
225
+ if (prior.processIdentity !== processIdentity &&
226
+ key.startsWith(`${pid}:`)) {
227
+ windowsCpuHistory.delete(key);
228
+ }
229
+ }
230
+ const prev = windowsCpuHistory.get(historyKey);
231
+ if (prev && cpuMs < prev.cpuMs) {
232
+ // A counter reset is not a flat sample. Retire the baseline so the
233
+ // next valid observation starts a new rate window.
234
+ windowsCpuHistory.delete(historyKey);
235
+ continue;
236
+ }
217
237
  let cpuPercent = 0;
218
238
  if (prev) {
219
239
  const wallMs = now - prev.ts;
@@ -223,16 +243,30 @@ async function sampleProcessesWindows(valid) {
223
243
  cpuPercent = 0;
224
244
  }
225
245
  }
226
- windowsCpuHistory.set(pid, { cpuMs, ts: now });
246
+ windowsCpuHistory.set(historyKey, { processIdentity, cpuMs, ts: now });
227
247
  seen.add(pid);
228
248
  samples.set(pid, { rssBytes: workingSet, cpuPercent });
229
249
  }
230
250
  // Prune stale history so pids that have gone away don't accumulate.
231
- for (const [pid, entry] of windowsCpuHistory) {
251
+ for (const [key, entry] of windowsCpuHistory) {
252
+ const pid = Number(key.slice(0, key.indexOf(":")));
232
253
  if (!seen.has(pid) && now - entry.ts > CPU_HISTORY_MAX_AGE_MS) {
233
- windowsCpuHistory.delete(pid);
254
+ windowsCpuHistory.delete(key);
234
255
  }
235
256
  }
257
+ while (windowsCpuHistory.size > CPU_HISTORY_MAX_ENTRIES) {
258
+ let oldestKey;
259
+ let oldestTs = Number.POSITIVE_INFINITY;
260
+ for (const [key, entry] of windowsCpuHistory) {
261
+ if (entry.ts < oldestTs) {
262
+ oldestKey = key;
263
+ oldestTs = entry.ts;
264
+ }
265
+ }
266
+ if (oldestKey === undefined)
267
+ break;
268
+ windowsCpuHistory.delete(oldestKey);
269
+ }
236
270
  }
237
271
  catch {
238
272
  // Parsing must never throw into the caller; best-effort.
@@ -264,6 +298,11 @@ export async function sampleProcesses(pids) {
264
298
  const stat = stats[String(pid)];
265
299
  if (!stat)
266
300
  continue; // pidusage couldn't resolve this pid — leave absent
301
+ if (!Number.isFinite(stat.cpu) ||
302
+ stat.cpu < 0 ||
303
+ !Number.isFinite(stat.memory) ||
304
+ stat.memory < 0)
305
+ continue;
267
306
  result.set(pid, {
268
307
  rssBytes: stat.memory,
269
308
  cpuPercent: stat.cpu,
@@ -313,28 +352,88 @@ export class UsageAccumulator {
313
352
  }
314
353
  }
315
354
  /**
316
- * Brackets one transient spawn with a short-interval poll. Usage:
355
+ * #2358: sample a live process tree twice across a short window and answer
356
+ * whether it burned CPU above `floorPercent`.
317
357
  *
318
- * const sampler = startSpawnUsageSampler(child.pid);
319
- * child.on("close", () => {
320
- * const usage = sampler.stop(); // null if never got a single sample
321
- * });
358
+ * The notify-stall breaker uses this to tell a BUSY server (burning a core
359
+ * while it drains a burst) from a genuinely DEAD input path (flat CPU), and
360
+ * only tears the latter down. It reuses the same platform machinery as
361
+ * `startSpawnUsageSampler`: on Windows the direct child may be a `cmd`/`.cmd`
362
+ * shim that does no work itself, so the live descendant tree resolves once per
363
+ * read and CPU% is summed across it.
322
364
  *
323
- * `intervalMs` defaults to 750ms — inside the issue's suggested 500ms-1s
324
- * band, cheap enough not to become a new source of measurable overhead for
325
- * the (usually sub-few-second) analyzer children this brackets. Best-effort:
326
- * a poll tick that throws (pid already gone, sampling error) is silently
327
- * skipped — it never stops the timer or the spawn early, and `stop()` is
328
- * always safe to call even if zero samples ever landed.
329
- *
330
- * Windows note: `clients/safe-spawn.ts` spawns with `shell: true` on Windows,
331
- * so `pid` here is `cmd.exe`'s pid, not the real tool's — sampling it alone
332
- * would report near-zero usage for the whole invocation. Each Windows tick
333
- * resolves `pid`'s live descendant tree (`findDescendantPidsWindows`) and
334
- * sums usage across `pid` + every descendant, so a `node`/`npx`-wrapped tool
335
- * (or one that re-execs itself) is actually captured. POSIX spawns are
336
- * unwrapped (`shell: false`), so `pid` there is already the real tool.
365
+ * Best-effort like every other export here: a failed query loses a data point,
366
+ * it never throws, and it never blocks for longer than the query timeouts plus
367
+ * `windowMs`. The target itself must resolve for `measured` to be true;
368
+ * missing descendants are partial but valid evidence. An unmeasurable target
369
+ * answers `{ busy: false, measured: false }` for the caller to classify.
337
370
  */
371
+ export async function sampleProcessTreeCpuPercent(pid, windowMs = 1200, floorPercent = 10) {
372
+ if (!Number.isFinite(pid) || pid <= 0) {
373
+ return { busy: false, measured: false, cpuPercent: null };
374
+ }
375
+ const targetPid = pid;
376
+ const readOnce = async () => {
377
+ try {
378
+ const descendants = runningOnWindows()
379
+ ? await findDescendantPidsWindows(targetPid)
380
+ : [];
381
+ if (descendants === null)
382
+ return null;
383
+ const pids = runningOnWindows()
384
+ ? [targetPid, ...descendants]
385
+ : [targetPid];
386
+ const usageByPid = await sampleProcesses(pids);
387
+ if (usageByPid === null)
388
+ return null;
389
+ const targetUsage = usageByPid.get(targetPid);
390
+ if (!targetUsage)
391
+ return { cpuPercent: 0, measured: false };
392
+ let cpuPercent = 0;
393
+ for (const usage of usageByPid.values())
394
+ cpuPercent += usage.cpuPercent;
395
+ return { cpuPercent, measured: true };
396
+ }
397
+ catch {
398
+ return null;
399
+ }
400
+ };
401
+ // The FIRST read is a BASELINE, not evidence: it re-anchors this pid's CPU
402
+ // history (Windows' own map, pidusage's on POSIX) so the second read is a
403
+ // rate over `windowMs` and nothing else. Both reads must retain the target;
404
+ // disappearance or query failure is explicitly unmeasured, never flat.
405
+ const first = await readOnce();
406
+ const second = await new Promise((resolve) => {
407
+ const timer = setTimeout(() => {
408
+ void readOnce().then(resolve);
409
+ }, windowMs);
410
+ timer.unref?.();
411
+ });
412
+ if (first === null && second === null) {
413
+ return { busy: false, measured: false, cpuPercent: null };
414
+ }
415
+ if (first === null ||
416
+ second === null ||
417
+ !first.measured ||
418
+ !second.measured) {
419
+ return { busy: false, measured: false, cpuPercent: null };
420
+ }
421
+ // #2358 (post-#2382): the verdict is the WINDOW read alone. Whatever the
422
+ // baseline read reports is a rate since the LAST caller's observation —
423
+ // clients/quiet-window.ts's heartbeat samples every recorded LSP child once
424
+ // per tick, and pidusage keeps 60 s of per-pid history — so folding it in
425
+ // (`Math.max(first, second)`) let CPU the process had already stopped
426
+ // burning vote "busy". A scanner that drained its burst and then wedged (the
427
+ // issue's own opengrep evidence) was therefore deferred as progressing and
428
+ // died at the hard cap, misrecorded as `cap-exceeded`, instead of being torn
429
+ // down on its budget as `budget-exceeded-cpu-flat`.
430
+ const observed = second.cpuPercent;
431
+ return {
432
+ busy: observed > floorPercent,
433
+ measured: true,
434
+ cpuPercent: observed,
435
+ };
436
+ }
338
437
  export function startSpawnUsageSampler(pid, intervalMs = 750) {
339
438
  if (!Number.isFinite(pid) || pid <= 0) {
340
439
  return { stop: () => null };