@herbertgao/pi-extensions 2026.9.0 → 2026.9.1

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 (293) 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/@pi-plugins/fast-mode/README.md +5 -3
  11. package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
  12. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20 -55
  13. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  14. package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
  15. package/node_modules/pi-lens/CHANGELOG.md +988 -0
  16. package/node_modules/pi-lens/README.md +3 -0
  17. package/node_modules/pi-lens/config/biome/core.jsonc +11 -2
  18. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +13 -1
  19. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +2 -2
  20. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +927 -80
  21. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +13 -4
  22. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +123 -3
  23. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +60 -5
  24. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +2 -2
  25. package/node_modules/pi-lens/dist/clients/bash-file-access.js +1 -3
  26. package/node_modules/pi-lens/dist/clients/biome-client.js +9 -2
  27. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +14 -0
  28. package/node_modules/pi-lens/dist/clients/bootstrap.js +509 -73
  29. package/node_modules/pi-lens/dist/clients/bounded-cache.js +152 -12
  30. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +59 -4
  31. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +113 -0
  32. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +2 -2
  33. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +31 -4
  34. package/node_modules/pi-lens/dist/clients/cache-manager.js +105 -6
  35. package/node_modules/pi-lens/dist/clients/cache-observability.js +44 -18
  36. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +422 -0
  37. package/node_modules/pi-lens/dist/clients/cascade-logger.js +2 -2
  38. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +13 -3
  39. package/node_modules/pi-lens/dist/clients/complexity-client.js +16 -5
  40. package/node_modules/pi-lens/dist/clients/config-core/deny.js +221 -0
  41. package/node_modules/pi-lens/dist/clients/config-core/index.js +47 -0
  42. package/node_modules/pi-lens/dist/clients/config-core/merge.js +357 -0
  43. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +340 -0
  44. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +248 -0
  45. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +171 -0
  46. package/node_modules/pi-lens/dist/clients/config-core/records.js +218 -0
  47. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +125 -0
  48. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +78 -0
  49. package/node_modules/pi-lens/dist/clients/config-core/schema.js +167 -0
  50. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +281 -0
  51. package/node_modules/pi-lens/dist/clients/config-locations.js +160 -0
  52. package/node_modules/pi-lens/dist/clients/config-resolve.js +789 -0
  53. package/node_modules/pi-lens/dist/clients/config-schema.js +197 -0
  54. package/node_modules/pi-lens/dist/clients/config-warn.js +407 -0
  55. package/node_modules/pi-lens/dist/clients/dead-code-client.js +19 -14
  56. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +2 -2
  57. package/node_modules/pi-lens/dist/clients/deadline-utils.js +178 -0
  58. package/node_modules/pi-lens/dist/clients/debug-handles.js +2 -2
  59. package/node_modules/pi-lens/dist/clients/debug-heap.js +3 -3
  60. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +106 -0
  61. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +113 -7
  62. package/node_modules/pi-lens/dist/clients/dependency-checker.js +8 -19
  63. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +12 -9
  64. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +14 -4
  65. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +162 -18
  66. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +97 -0
  67. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +9 -7
  68. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -1
  69. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +380 -26
  70. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -1
  71. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -1
  72. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -1
  73. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -1
  74. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -1
  75. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -1
  76. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -1
  77. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -1
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -1
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -1
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -1
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -1
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +1 -3
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -1
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -1
  85. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -1
  86. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -1
  87. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -1
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -1
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -1
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -1
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +13 -18
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -1
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -1
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -1
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +7 -4
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +1 -3
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -1
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -1
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -1
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -1
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -1
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -1
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -1
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -1
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -1
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -1
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +194 -80
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +84 -15
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -1
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -1
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -1
  121. package/node_modules/pi-lens/dist/clients/disposition-logger.js +3 -3
  122. package/node_modules/pi-lens/dist/clients/effective-config.js +403 -0
  123. package/node_modules/pi-lens/dist/clients/error-class.js +23 -0
  124. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +274 -0
  125. package/node_modules/pi-lens/dist/clients/extension-log.js +2 -2
  126. package/node_modules/pi-lens/dist/clients/feature-hints.js +2 -1
  127. package/node_modules/pi-lens/dist/clients/file-time.js +6 -1
  128. package/node_modules/pi-lens/dist/clients/file-utils.js +112 -9
  129. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +50 -13
  130. package/node_modules/pi-lens/dist/clients/format-service.js +6 -1
  131. package/node_modules/pi-lens/dist/clients/formatters.js +276 -102
  132. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +17 -0
  133. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +114 -19
  134. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -9
  135. package/node_modules/pi-lens/dist/clients/git-guard.js +1 -1
  136. package/node_modules/pi-lens/dist/clients/go-client.js +39 -0
  137. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +252 -0
  138. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +424 -0
  139. package/node_modules/pi-lens/dist/clients/hook-budgets.js +76 -0
  140. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +5 -2
  141. package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
  142. package/node_modules/pi-lens/dist/clients/installer/index.js +71 -52
  143. package/node_modules/pi-lens/dist/clients/instance-reaper.js +104 -230
  144. package/node_modules/pi-lens/dist/clients/instance-registry.js +6 -3
  145. package/node_modules/pi-lens/dist/clients/language-registry.js +595 -0
  146. package/node_modules/pi-lens/dist/clients/latency-logger.js +160 -2
  147. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +13 -0
  148. package/node_modules/pi-lens/dist/clients/lens-config.js +141 -30
  149. package/node_modules/pi-lens/dist/clients/lens-engine.js +30 -37
  150. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +59 -6
  151. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -2
  152. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +3 -1
  153. package/node_modules/pi-lens/dist/clients/lsp/client.js +168 -30
  154. package/node_modules/pi-lens/dist/clients/lsp/config.js +483 -158
  155. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +1 -1
  156. package/node_modules/pi-lens/dist/clients/lsp/edits.js +15 -2
  157. package/node_modules/pi-lens/dist/clients/lsp/index.js +676 -71
  158. package/node_modules/pi-lens/dist/clients/lsp/language.js +18 -169
  159. package/node_modules/pi-lens/dist/clients/lsp/launch.js +2 -1
  160. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +5 -16
  161. package/node_modules/pi-lens/dist/clients/lsp/server.js +284 -147
  162. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +116 -23
  163. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +2 -7
  164. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +9 -2
  165. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +1 -1
  166. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +163 -21
  167. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +36 -0
  168. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +33 -4
  169. package/node_modules/pi-lens/dist/clients/mcp/session.js +5 -16
  170. package/node_modules/pi-lens/dist/clients/memory-sampler.js +8 -3
  171. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +2 -4
  172. package/node_modules/pi-lens/dist/clients/module-report.js +20 -45
  173. package/node_modules/pi-lens/dist/clients/mutating-tool.js +651 -0
  174. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +368 -0
  175. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +240 -0
  176. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +247 -20
  177. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +101 -0
  178. package/node_modules/pi-lens/dist/clients/observed-mutation.js +1215 -0
  179. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +68 -22
  180. package/node_modules/pi-lens/dist/clients/opengrep-config.js +4 -0
  181. package/node_modules/pi-lens/dist/clients/package-manager.js +195 -26
  182. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +359 -80
  183. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +23 -7
  184. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +21 -2
  185. package/node_modules/pi-lens/dist/clients/path-utils.js +428 -13
  186. package/node_modules/pi-lens/dist/clients/persist-debounce.js +8 -1
  187. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +114 -0
  188. package/node_modules/pi-lens/dist/clients/pipeline.js +77 -16
  189. package/node_modules/pi-lens/dist/clients/probe-home-state.js +228 -0
  190. package/node_modules/pi-lens/dist/clients/process-bridge.js +66 -0
  191. package/node_modules/pi-lens/dist/clients/process-snapshot.js +68 -0
  192. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
  193. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
  194. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +51 -20
  195. package/node_modules/pi-lens/dist/clients/project-lens-config.js +393 -122
  196. package/node_modules/pi-lens/dist/clients/project-snapshot.js +52 -24
  197. package/node_modules/pi-lens/dist/clients/python-environment.js +217 -0
  198. package/node_modules/pi-lens/dist/clients/python-provenance.js +639 -0
  199. package/node_modules/pi-lens/dist/clients/quiet-window.js +6 -1
  200. package/node_modules/pi-lens/dist/clients/read-bridge.js +13 -21
  201. package/node_modules/pi-lens/dist/clients/read-expansion.js +9 -46
  202. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +15 -4
  203. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +364 -212
  204. package/node_modules/pi-lens/dist/clients/read-guard.js +21 -0
  205. package/node_modules/pi-lens/dist/clients/resource-sampler.js +195 -96
  206. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +152 -109
  207. package/node_modules/pi-lens/dist/clients/review-graph/service.js +55 -3
  208. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +7 -15
  209. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +10 -74
  210. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +9 -3
  211. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +18 -2
  212. package/node_modules/pi-lens/dist/clients/runtime-context.js +49 -16
  213. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +25 -6
  214. package/node_modules/pi-lens/dist/clients/runtime-session.js +139 -22
  215. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +168 -35
  216. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +797 -243
  217. package/node_modules/pi-lens/dist/clients/runtime-turn.js +1028 -53
  218. package/node_modules/pi-lens/dist/clients/rust-client.js +31 -0
  219. package/node_modules/pi-lens/dist/clients/safe-spawn.js +33 -10
  220. package/node_modules/pi-lens/dist/clients/sanitize.js +7 -1
  221. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +1 -3
  222. package/node_modules/pi-lens/dist/clients/security-scan-client.js +3 -0
  223. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +1 -1
  224. package/node_modules/pi-lens/dist/clients/session-start-observability.js +79 -0
  225. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +12 -3
  226. package/node_modules/pi-lens/dist/clients/sgconfig.js +6 -1
  227. package/node_modules/pi-lens/dist/clients/skills-resolver.js +105 -0
  228. package/node_modules/pi-lens/dist/clients/smells-rollup.js +2 -2
  229. package/node_modules/pi-lens/dist/clients/string-utils.js +13 -0
  230. package/node_modules/pi-lens/dist/clients/subagent-mode.js +17 -4
  231. package/node_modules/pi-lens/dist/clients/test-runner-client.js +176 -39
  232. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +239 -0
  233. package/node_modules/pi-lens/dist/clients/tool-definition.js +41 -1
  234. package/node_modules/pi-lens/dist/clients/tool-policy.js +91 -21
  235. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +40 -40
  236. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +86 -48
  237. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +2 -2
  238. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +81 -1
  239. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +51 -46
  240. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +28 -0
  241. package/node_modules/pi-lens/dist/clients/typos-config.js +5 -0
  242. package/node_modules/pi-lens/dist/clients/user-notify.js +6 -2
  243. package/node_modules/pi-lens/dist/clients/widget-state.js +191 -26
  244. package/node_modules/pi-lens/dist/clients/word-index-logger.js +2 -2
  245. package/node_modules/pi-lens/dist/clients/word-index-store.js +20 -5
  246. package/node_modules/pi-lens/dist/clients/word-index.js +243 -31
  247. package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -1
  248. package/node_modules/pi-lens/dist/clients/zizmor-config.js +3 -0
  249. package/node_modules/pi-lens/dist/index.js +64335 -54300
  250. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -1
  251. package/node_modules/pi-lens/dist/mcp/server.js +127 -5
  252. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +583 -0
  253. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +129 -0
  254. package/node_modules/pi-lens/dist/tools/effective-config.js +89 -0
  255. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +95 -24
  256. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +67 -80
  257. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +37 -16
  258. package/node_modules/pi-lens/dist/tools/shared.js +0 -1
  259. package/node_modules/pi-lens/docs/agent-guide.md +64 -1
  260. package/node_modules/pi-lens/docs/configuration.md +222 -0
  261. package/node_modules/pi-lens/docs/dependencies.md +3 -3
  262. package/node_modules/pi-lens/docs/features.md +44 -5
  263. package/node_modules/pi-lens/docs/language-coverage.md +2 -2
  264. package/node_modules/pi-lens/docs/pi-lens-fixer.md +25 -0
  265. package/node_modules/pi-lens/docs/pi-lens-investigator.md +25 -0
  266. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +27 -0
  267. package/node_modules/pi-lens/docs/pi-lens-subagent.md +38 -0
  268. package/node_modules/pi-lens/docs/pi-lens-warden.md +55 -0
  269. package/node_modules/pi-lens/docs/public-api-stability.md +359 -0
  270. package/node_modules/pi-lens/docs/release-qa-baseline.md +182 -0
  271. package/node_modules/pi-lens/docs/subagent-compat.md +110 -30
  272. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +2 -2
  273. package/node_modules/pi-lens/package.json +15 -23
  274. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
  275. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
  276. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
  277. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +192 -1
  278. package/node_modules/pi-lens/scripts/install-selftest.mjs +58 -2
  279. package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +129 -0
  280. package/node_modules/pi-lens/scripts/rpc-load-check.mjs +3 -1
  281. package/node_modules/pi-web-access/CHANGELOG.md +28 -0
  282. package/node_modules/pi-web-access/README.md +62 -15
  283. package/node_modules/pi-web-access/curator-page.ts +3 -1
  284. package/node_modules/pi-web-access/curator-server.ts +5 -1
  285. package/node_modules/pi-web-access/gemini-search.ts +8 -4
  286. package/node_modules/pi-web-access/github-extract.ts +242 -1
  287. package/node_modules/pi-web-access/index.ts +118 -64
  288. package/node_modules/pi-web-access/mistral-search.ts +281 -0
  289. package/node_modules/pi-web-access/package.json +2 -2
  290. package/node_modules/pi-web-access/perplexity.ts +14 -1
  291. package/node_modules/pi-web-access/utils.ts +23 -6
  292. package/node_modules/pi-web-access/xai-search.ts +96 -33
  293. package/package.json +5 -5
@@ -29,16 +29,17 @@
29
29
  * - rust-analyzer (Rust LSP) [GitHub release]
30
30
  * - golangci-lint (Go linting) [GitHub release]
31
31
  *
32
- * Manual install required (25+ tools):
33
- * - yaml-language-server: npm install -g yaml-language-server
34
- * - vscode-json-languageserver: npm install -g vscode-langservers-extracted
35
- * - bash-language-server: npm install -g bash-language-server
36
- * - svelte-language-server: npm install -g svelte-language-server
37
- * - vscode-css-languageserver: npm install -g vscode-langservers-extracted
38
- * - @prisma/language-server: npm install -g @prisma/language-server
39
- * - dockerfile-language-server: npm install -g dockerfile-language-server-nodejs
40
- * - @vue/language-server: npm install -g @vue/language-server
41
- * - And all language-specific servers (gopls, rust-analyzer, etc.)
32
+ * Every other managed tool (including several the list above omits, e.g.
33
+ * bash-language-server, yaml-language-server, svelte-language-server,
34
+ * @prisma/language-server, @vue/language-server, dockerfile-language-server-nodejs,
35
+ * and vscode-css-languageserver — all of which ARE auto-installed) is
36
+ * documented by the `TOOLS` array below, not restated here. #2638: a
37
+ * hand-kept "manual install" list naming specific package specs used to live
38
+ * in this comment, drifted from `TOOLS` (it named a since-unpublished npm
39
+ * package for the CSS entry, and several tools it called "manual" were
40
+ * already auto-install above) and nothing caught it. `TOOLS` is the single
41
+ * source of truth for every id, package, and strategy; deleted rather than
42
+ * re-derived, since a comment cannot be generated from code at write time.
42
43
  *
43
44
  * Strategies:
44
45
  * - npm packages via npx/bun
@@ -234,7 +235,12 @@ export const TOOLS = [
234
235
  checkCommand: "typescript-language-server",
235
236
  checkArgs: ["--version"],
236
237
  installStrategy: "npm",
237
- packageName: "typescript-language-server",
238
+ // Pinned below the package's own floor: 6.0.0 declares
239
+ // engines.node >=22.22.2, above the pi host's own floor (pi-lens
240
+ // must never require more than pi does — #2633) — an unpinned
241
+ // install here resolves latest and prints EBADENGINE on any pi
242
+ // host's supported Node. 5.3.0's own floor is engines.node >=20.
243
+ packageName: "typescript-language-server@5.3.0",
238
244
  binaryName: "typescript-language-server",
239
245
  },
240
246
  {
@@ -440,7 +446,7 @@ export const TOOLS = [
440
446
  checkCommand: "vscode-html-language-server",
441
447
  checkArgs: ["--version"],
442
448
  installStrategy: "npm",
443
- packageName: "vscode-html-languageserver-bin",
449
+ packageName: "vscode-langservers-extracted",
444
450
  binaryName: "vscode-html-language-server",
445
451
  },
446
452
  {
@@ -529,7 +535,7 @@ export const TOOLS = [
529
535
  checkCommand: "vscode-css-language-server",
530
536
  checkArgs: ["--version"],
531
537
  installStrategy: "npm",
532
- packageName: "vscode-css-languageserver",
538
+ packageName: "vscode-langservers-extracted",
533
539
  binaryName: "vscode-css-language-server",
534
540
  },
535
541
  {
@@ -1823,7 +1829,7 @@ const lspTransportRequiredMatcher = /Connection (?:input|output) stream is not s
1823
1829
  * correctly reports "no pin" rather than mistaking the scope for a version.
1824
1830
  * Returns undefined when packageName has no explicit `@version` suffix.
1825
1831
  */
1826
- function parsePinnedVersion(packageName) {
1832
+ export function parsePinnedVersion(packageName) {
1827
1833
  const at = packageName.lastIndexOf("@");
1828
1834
  if (at <= 0)
1829
1835
  return undefined;
@@ -3303,10 +3309,10 @@ async function refreshPackageManagerManagedTool(tool) {
3303
3309
  const installed = tool.installStrategy === "pip"
3304
3310
  ? // `-U` is the whole fix: without it pip treats the installed copy as
3305
3311
  // satisfying the requirement and the day-one version never moves.
3306
- await installPipTool(tool.packageName, { upgrade: true })
3312
+ await installPipTool(tool.id, tool.packageName, { upgrade: true })
3307
3313
  : // `gem install` always fetches the newest version that satisfies the
3308
3314
  // requirement, so the install command IS the upgrade command.
3309
- await installGemTool(tool.packageName);
3315
+ await installGemTool(tool.id, tool.packageName);
3310
3316
  if (!installed) {
3311
3317
  return {
3312
3318
  ok: false,
@@ -3734,7 +3740,24 @@ async function installArchiveTool(tool) {
3734
3740
  return undefined;
3735
3741
  }
3736
3742
  }
3737
- async function installNpmTool(packageName, binaryName, verificationArgs = ["--version"], verificationTimeoutMs = 10_000) {
3743
+ /**
3744
+ * Record a genuine package-manager install exception against `toolId` and
3745
+ * log it — the one place `installNpmTool`/`installPipTool`/`installGemTool`'s
3746
+ * catch blocks funnel through (#2661 review F3/Sonar: the three catches were
3747
+ * byte-identical but for the strategy label, a duplication SonarCloud's new-
3748
+ * code gate correctly flagged). The real error (registry E404, EBADENGINE, a
3749
+ * network failure, "no version satisfies…") lands in `installFailureReasons`
3750
+ * so a caller that distinguishes a genuine installer defect from a policy
3751
+ * decline (the tool-smoke lane, #2638) sees the actual message, never the
3752
+ * generic fallback `finishInstallAttempt` uses when nothing set it.
3753
+ */
3754
+ function recordPackageManagerInstallException(toolId, strategyLabel, packageName, err) {
3755
+ const message = err.message;
3756
+ logSessionStart(`auto-install ${strategyLabel} ${packageName}: exception: ${message}`);
3757
+ installFailureReasons.set(toolId, message);
3758
+ return undefined;
3759
+ }
3760
+ async function installNpmTool(toolId, packageName, binaryName, verificationArgs = ["--version"], verificationTimeoutMs = 10_000) {
3738
3761
  try {
3739
3762
  // Ensure tools directory exists
3740
3763
  await fs.mkdir(TOOLS_DIR, { recursive: true });
@@ -3858,14 +3881,26 @@ async function installNpmTool(packageName, binaryName, verificationArgs = ["--ve
3858
3881
  return binPath;
3859
3882
  }
3860
3883
  catch (err) {
3861
- logSessionStart(`auto-install npm ${packageName}: exception: ${err.message}`);
3862
- return undefined;
3884
+ return recordPackageManagerInstallException(toolId, "npm", packageName, err);
3863
3885
  }
3864
3886
  }
3887
+ /**
3888
+ * The pip command ladder `installPipTool` tries, per platform, in priority
3889
+ * order — exported so a caller that needs to know "is ANY pip toolchain
3890
+ * reachable on this runner" (without running an install) probes the SAME
3891
+ * candidates rather than a second, narrower guess (#2661 review F4/S5: the
3892
+ * tool-smoke lane's own presence check tried bare `pip` only and missed a
3893
+ * `pip3`-only or python-module-only runner).
3894
+ */
3895
+ export function pipCommandCandidates() {
3896
+ return process.platform === "win32"
3897
+ ? ["pip", "py", "python"]
3898
+ : ["pip3", "pip", "python3", "python"];
3899
+ }
3865
3900
  /**
3866
3901
  * Install a pip package tool
3867
3902
  */
3868
- async function installPipTool(packageName,
3903
+ async function installPipTool(toolId, packageName,
3869
3904
  /**
3870
3905
  * Add `-U`, turning the install into an upgrade. Without it `pip install`
3871
3906
  * treats an already-present package as satisfied and leaves the day-one
@@ -3880,30 +3915,16 @@ options = {}) {
3880
3915
  const verb = options.upgrade
3881
3916
  ? ["install", "-U", "--user"]
3882
3917
  : ["install", "--user"];
3883
- const pipCandidates = isWindows
3884
- ? [
3885
- { command: "pip", args: [...verb, packageName] },
3886
- {
3887
- command: "py",
3888
- args: ["-m", "pip", ...verb, packageName],
3889
- },
3890
- {
3891
- command: "python",
3892
- args: ["-m", "pip", ...verb, packageName],
3893
- },
3894
- ]
3895
- : [
3896
- { command: "pip3", args: [...verb, packageName] },
3897
- { command: "pip", args: [...verb, packageName] },
3898
- {
3899
- command: "python3",
3900
- args: ["-m", "pip", ...verb, packageName],
3901
- },
3902
- {
3903
- command: "python",
3904
- args: ["-m", "pip", ...verb, packageName],
3905
- },
3906
- ];
3918
+ // Built from `pipCommandCandidates()` — the single source of truth this
3919
+ // module and any other caller (the tool-smoke lane's toolchain-presence
3920
+ // probe, #2661 review) share, rather than a second, independently
3921
+ // maintained ladder that can drift.
3922
+ const pipCandidates = pipCommandCandidates().map((command) => ({
3923
+ command,
3924
+ args: command === "pip" || command === "pip3"
3925
+ ? [...verb, packageName]
3926
+ : ["-m", "pip", ...verb, packageName],
3927
+ }));
3907
3928
  let lastError = "";
3908
3929
  for (const candidate of pipCandidates) {
3909
3930
  const pipResult = await safeSpawnAsync(candidate.command, candidate.args, {
@@ -4000,11 +4021,10 @@ options = {}) {
4000
4021
  throw new Error(`Failed to install ${packageName}: no usable pip command found (${lastError || "unknown error"})`);
4001
4022
  }
4002
4023
  catch (err) {
4003
- logSessionStart(`auto-install pip ${packageName}: exception: ${err.message}`);
4004
- return undefined;
4024
+ return recordPackageManagerInstallException(toolId, "pip", packageName, err);
4005
4025
  }
4006
4026
  }
4007
- async function installGemTool(packageName) {
4027
+ async function installGemTool(toolId, packageName) {
4008
4028
  try {
4009
4029
  const gemResult = await safeSpawnAsync("gem", ["install", packageName, "--no-document"], {
4010
4030
  timeout: 120_000,
@@ -4021,8 +4041,7 @@ async function installGemTool(packageName) {
4021
4041
  return packageName;
4022
4042
  }
4023
4043
  catch (err) {
4024
- logSessionStart(`auto-install gem ${packageName}: exception: ${err.message}`);
4025
- return undefined;
4044
+ return recordPackageManagerInstallException(toolId, "gem", packageName, err);
4026
4045
  }
4027
4046
  }
4028
4047
  /**
@@ -4113,7 +4132,7 @@ export async function installTool(toolId) {
4113
4132
  case "npm": {
4114
4133
  if (!tool.packageName || !tool.binaryName)
4115
4134
  return false;
4116
- const npmPath = await installNpmTool(tool.packageName, tool.binaryName, tool.checkArgs, getToolVerificationTimeout(tool));
4135
+ const npmPath = await installNpmTool(tool.id, tool.packageName, tool.binaryName, tool.checkArgs, getToolVerificationTimeout(tool));
4117
4136
  if (npmPath !== undefined) {
4118
4137
  // #1746 review F4: an install just resolved this package's range
4119
4138
  // against the registry, so record it as freshly checked. Otherwise a
@@ -4139,13 +4158,13 @@ export async function installTool(toolId) {
4139
4158
  case "pip": {
4140
4159
  if (!tool.packageName)
4141
4160
  return false;
4142
- const pipPath = await installPipTool(tool.packageName);
4161
+ const pipPath = await installPipTool(tool.id, tool.packageName);
4143
4162
  return finishInstallAttempt(tool.id, pipPath !== undefined, startedAt);
4144
4163
  }
4145
4164
  case "gem": {
4146
4165
  if (!tool.packageName)
4147
4166
  return false;
4148
- const gemPath = await installGemTool(tool.packageName);
4167
+ const gemPath = await installGemTool(tool.id, tool.packageName);
4149
4168
  return finishInstallAttempt(tool.id, gemPath !== undefined, startedAt);
4150
4169
  }
4151
4170
  case "github": {
@@ -67,11 +67,12 @@ import * as path from "node:path";
67
67
  import { writeFileAtomicAsync } from "./atomic-write.js";
68
68
  import { sweepOwnStagingFiles, } from "./atomic-write-staging.js";
69
69
  import { acquireQuarantinePidFileLock } from "./bounded-pid-file-lock.js";
70
- import { spawnCollectStdoutResult, unrefChildAndPipes, } from "./child-unref.js";
70
+ import { unrefChildAndPipes, } from "./child-unref.js";
71
71
  import { incrementDegradationCount, recordDegradationOnce, } from "./degradation-ledger.js";
72
72
  import { getGlobalPiLensDir } from "./file-utils.js";
73
73
  import { isInstanceRegistryEnabled, pruneDeadInstances as pruneDeadRegistryInstances, readInstanceRegistry, } from "./instance-registry.js";
74
74
  import { logLatency } from "./latency-logger.js";
75
+ import { queryProcessTable, windowsExe } from "./process-snapshot.js";
75
76
  const isWindows = process.platform === "win32";
76
77
  /**
77
78
  * KILL eligibility: pid-confirmed-dead ONLY. Heartbeat staleness must never
@@ -249,31 +250,14 @@ export async function sweepAtomicWriteStages(directories, options = {}) {
249
250
  const isPidAlive = options.isPidAlive ?? realIsPidAlive;
250
251
  return sweepOwnStagingFiles(directories, { maxEntries, isPidAlive });
251
252
  }
252
- function windowsExe(name) {
253
- return path.join(process.env.SystemRoot ?? String.raw `C:\Windows`, "System32", name);
254
- }
255
- /** Resolve an absolute path to `ps` (S4036: never spawn via bare PATH lookup).
256
- * Prefers `/bin/ps` (present on virtually every POSIX system), falls back to
257
- * `/usr/bin/ps`, and defaults back to `/bin/ps` if neither probe succeeds
258
- * (spawn will then fail closed rather than silently resolving via PATH). */
259
- function posixPsPath() {
260
- if (fs.existsSync("/bin/ps"))
261
- return "/bin/ps";
262
- if (fs.existsSync("/usr/bin/ps"))
263
- return "/usr/bin/ps";
264
- return "/bin/ps";
265
- }
266
- /** Escape a value for embedding in a WQL LIKE clause: WQL uses `'` as the
267
- * string delimiter (doubled to escape) and `%`/`_` as wildcards — the marker
268
- * is an opaque path string, so escape all three before interpolating. */
269
- function escapeWqlLikeValue(value) {
270
- return value.replaceAll("'", "''").replaceAll(/[%_]/g, (ch) => `[${ch}]`);
271
- }
272
- /** Escape a value for a WQL EQUALITY comparison: only the string delimiter
273
- * needs escaping — `%`/`_` are literal outside `LIKE`. */
274
- function escapeWqlStringValue(value) {
275
- return value.replaceAll("'", "''");
276
- }
253
+ /**
254
+ * Interpreter resolution, WQL escaping and the platform listing itself all
255
+ * live in the ONE process-table seam (#2443): `scripts/lib/process-scan.mjs`,
256
+ * reached from clients/ through `clients/process-snapshot.ts`. This file used
257
+ * to carry its own copy of each, which is how the three queries below drifted
258
+ * apart from the two in `clients/resource-sampler.ts` and the two in
259
+ * `scripts/`. `windowsExe` is re-exported by the seam and imported above.
260
+ */
277
261
  /** Search running processes whose command line contains `marker` (Windows,
278
262
  * via CIM/WQL). Returns matching pids. Best-effort: any failure ⇒ [], but a
279
263
  * failure is RECORDED (#1857 class sweep) — "the search found no orphan" and
@@ -282,14 +266,26 @@ function escapeWqlStringValue(value) {
282
266
  async function findPidsByMarkerWindows(marker) {
283
267
  if (!isWindows || !marker)
284
268
  return [];
285
- const escaped = escapeWqlLikeValue(marker);
286
- // $PID exclusion: the query's own powershell.exe command line embeds the
287
- // marker string, so it would match itself.
288
- const psScript = `Get-CimInstance Win32_Process -Filter "CommandLine LIKE '%${escaped}%'" ` +
289
- `| Where-Object { $_.ProcessId -ne $PID } ` +
290
- `| Select-Object -ExpandProperty ProcessId`;
291
- const powershell = windowsExe("WindowsPowerShell\\v1.0\\powershell.exe");
292
- const result = await spawnCollectStdoutResult(powershell, ["-NoProfile", "-NonInteractive", "-Command", psScript], { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] }, { timeoutMs: BACKSTOP_SCAN_TIMEOUT_MS });
269
+ const result = await queryProcessTable({
270
+ fields: ["pid"],
271
+ filter: { column: "CommandLine", op: "like", values: [marker] },
272
+ // The query's own powershell.exe command line embeds the marker, so
273
+ // without this exclusion the search matches itself.
274
+ excludeSelfPid: true,
275
+ }, {
276
+ timeoutMs: BACKSTOP_SCAN_TIMEOUT_MS,
277
+ // #2527 review F2: omitting `onTimeout` falls through to
278
+ // child-unref.ts's default — one bare, unverified `child.kill()`, no
279
+ // tree kill, no identity-carrying record — inside the orphan backstop
280
+ // path, the exact abandonment `terminateScannerChild`'s own doc
281
+ // comment says it exists to prevent. This query shares the backstop's
282
+ // scanner identity and budget (same producer as
283
+ // `enumerateManagedProcesses`), so it gets the same kind.
284
+ onTimeout: (child) => terminateScannerChild(child, {
285
+ kind: "orphan-backstop-scanner-escalated",
286
+ timeoutMs: BACKSTOP_SCAN_TIMEOUT_MS,
287
+ }),
288
+ });
293
289
  if (result.status !== "ok") {
294
290
  recordDegradationOnce({
295
291
  kind: "orphan-backstop-scan-failed",
@@ -297,10 +293,7 @@ async function findPidsByMarkerWindows(marker) {
297
293
  reason: `marker command-line search ${result.status}`,
298
294
  });
299
295
  }
300
- return result.stdout
301
- .split(/\r?\n/)
302
- .map((line) => Number(line.trim()))
303
- .filter((n) => Number.isFinite(n) && n > 0);
296
+ return result.rows.map((row) => row.pid);
304
297
  }
305
298
  /** Fetch command lines for a set of pids in one query (Windows: CIM; POSIX:
306
299
  * `ps`). Returns a pid → command-line map; pids that can't be resolved are
@@ -317,59 +310,35 @@ async function queryCommandLines(pids) {
317
310
  const map = new Map();
318
311
  if (valid.length === 0)
319
312
  return map;
320
- const noteFailure = (status) => {
321
- if (status === "ok")
322
- return;
313
+ const result = await queryProcessTable({
314
+ fields: ["pid", "command"],
315
+ filter: { column: "ProcessId", op: "eq", values: valid },
316
+ }, {
317
+ timeoutMs: BACKSTOP_SCAN_TIMEOUT_MS,
318
+ // #2527 review F2: same rationale as findPidsByMarkerWindows above —
319
+ // route through the reaper's own tree-kill-and-verify machinery with
320
+ // the backstop's kind rather than falling through to the unverified
321
+ // default kill with no ledger record.
322
+ onTimeout: (child) => terminateScannerChild(child, {
323
+ kind: "orphan-backstop-scanner-escalated",
324
+ timeoutMs: BACKSTOP_SCAN_TIMEOUT_MS,
325
+ }),
326
+ });
327
+ // POSIX `ps -p` exits nonzero when NONE of the requested pids exist, which
328
+ // is a legitimate clean result for this caller, so that one status is not
329
+ // recorded on that platform. The shared collector calls it an exit failure;
330
+ // this caller's command contract makes it the expected empty table. Windows
331
+ // CIM has no such convention, so an exit failure there is a real failure.
332
+ const psReportedNoSuchPid = !isWindows && result.status === "exit-error";
333
+ if (result.status !== "ok" && !psReportedNoSuchPid) {
323
334
  recordDegradationOnce({
324
335
  kind: "orphan-backstop-scan-failed",
325
336
  subject: "identity-query",
326
- reason: `command-line identity query ${status}; reap suppressed this sweep`,
337
+ reason: `command-line identity query ${result.status}; reap suppressed this sweep`,
327
338
  });
328
- };
329
- if (isWindows) {
330
- const filter = valid.map((p) => `ProcessId=${p}`).join(" OR ");
331
- const psScript = `Get-CimInstance Win32_Process -Filter "${filter}" ` +
332
- `| ForEach-Object { "$($_.ProcessId)\t$($_.CommandLine)" }`;
333
- const powershell = windowsExe("WindowsPowerShell\\v1.0\\powershell.exe");
334
- const result = await spawnCollectStdoutResult(powershell, ["-NoProfile", "-NonInteractive", "-Command", psScript], { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] }, { timeoutMs: BACKSTOP_SCAN_TIMEOUT_MS });
335
- noteFailure(result.status);
336
- for (const line of result.stdout.split(/\r?\n/)) {
337
- const tab = line.indexOf("\t");
338
- if (tab <= 0)
339
- continue;
340
- const pid = Number(line.slice(0, tab).trim());
341
- if (Number.isFinite(pid) && pid > 0)
342
- map.set(pid, line.slice(tab + 1));
343
- }
344
- return map;
345
- }
346
- const result = await spawnCollectStdoutResult(posixPsPath(), ["-p", valid.join(","), "-o", "pid=,args="], { shell: false, stdio: ["ignore", "pipe", "ignore"] }, { timeoutMs: BACKSTOP_SCAN_TIMEOUT_MS });
347
- // `ps -p` exits nonzero when NONE of the pids exist, which is a legitimate
348
- // clean result here, so only spawn/timeout failures are recorded. The
349
- // shared collector calls that an exit failure, but this caller's command
350
- // contract makes that status the expected empty-table result.
351
- if (result.status !== "exit-error")
352
- noteFailure(result.status);
353
- for (const line of result.stdout.split(/\r?\n/)) {
354
- // Linear parse (S8786/S6594: avoid regex backtracking on
355
- // attacker-lengthenable ps output) — trim leading whitespace,
356
- // then split on the first whitespace run: " 1234 args here".
357
- const trimmed = line.trimStart();
358
- if (!trimmed)
359
- continue;
360
- let i = 0;
361
- while (i < trimmed.length && trimmed[i] >= "0" && trimmed[i] <= "9")
362
- i++;
363
- if (i === 0)
364
- continue;
365
- const pidStr = trimmed.slice(0, i);
366
- let j = i;
367
- while (j < trimmed.length && (trimmed[j] === " " || trimmed[j] === "\t"))
368
- j++;
369
- const pid = Number(pidStr);
370
- if (Number.isFinite(pid) && pid > 0)
371
- map.set(pid, trimmed.slice(j));
372
339
  }
340
+ for (const row of result.rows)
341
+ map.set(row.pid, row.command);
373
342
  return map;
374
343
  }
375
344
  /**
@@ -605,104 +574,42 @@ export function partitionBackstopCandidates(processes, registry, isPidAlive, opt
605
574
  }
606
575
  /** Enumerate live OS processes whose command line contains one of
607
576
  * `MANAGED_BINARY_NAMES` (#658), independent of the instance registry.
608
- * Windows: one batched CIM/WQL query (mirrors `findPidsByMarkerWindows`'s
609
- * query pattern). POSIX: `ps -eo pid=,ppid=,args=`, filtered in JS. Returns
610
- * `{pid, parentPid, command}` rows. Best-effort: any failure ⇒ []. */
577
+ * One projected process-table query through the shared seam, narrowed to
578
+ * managed binaries in JS; returns `{pid, parentPid, command, ageMs}` rows.
579
+ * Best-effort: any failure ⇒ [], with the scan status carried alongside so
580
+ * an empty table is never mistaken for a scan that did not run. */
611
581
  async function enumerateManagedProcesses(options = {}) {
612
- const timeoutMs = options.timeoutMs ?? BACKSTOP_SCAN_TIMEOUT_MS;
613
- const collect = {
614
- timeoutMs,
615
- onTimeout: (child) => terminateScannerChild(child, options),
616
- };
617
- if (isWindows) {
618
- // #1857: `Name = '…'` equality replaces eight leading-wildcard
619
- // `CommandLine LIKE '%…%'` clauses, and the query now also projects
620
- // `CreationDate` so the spawn-grace guard has an age to work with. The
621
- // image-name set is DERIVED from MANAGED_BINARIES (MANAGED_IMAGE_NAMES),
622
- // so adding a managed binary cannot leave a stale parallel list behind.
623
- const clauses = MANAGED_IMAGE_NAMES.map((name) => `Name = '${escapeWqlStringValue(name)}'`).join(" OR ");
624
- // The age column is computed IN PowerShell as a millisecond delta, not
625
- // exported as `CreationDate.Ticks`. `Ticks` is the LOCAL-time
626
- // representation, so subtracting the Unix epoch in JS produced an age
627
- // wrong by the machine's UTC offset — measured on a UTC+3 host as
628
- // -8358s for a process started 40 minutes earlier. A delta computed on
629
- // one side of the boundary has no timezone to get wrong. The explicit
630
- // `[datetime]` test matters: `(Get-Date) - $null` yields a two-thousand
631
- // year TimeSpan, which would read as "ancient" and defeat the grace
632
- // guard exactly where the data is missing.
633
- const psScript = `Get-CimInstance -Query "SELECT ProcessId,ParentProcessId,CreationDate,CommandLine FROM Win32_Process WHERE ${clauses}" ` +
634
- `| ForEach-Object { $age = if ($_.CreationDate -is [datetime]) { [int64]((Get-Date) - $_.CreationDate).TotalMilliseconds } else { '' }; ` +
635
- `"$($_.ProcessId)\t$($_.ParentProcessId)\t$age\t$($_.CommandLine)" }`;
636
- const powershell = windowsExe("WindowsPowerShell\\v1.0\\powershell.exe");
637
- const result = await spawnCollectStdoutResult(powershell, ["-NoProfile", "-NonInteractive", "-Command", psScript], { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] }, collect);
638
- const processes = [];
639
- for (const line of result.stdout.split(/\r?\n/)) {
640
- const firstTab = line.indexOf("\t");
641
- if (firstTab <= 0)
642
- continue;
643
- const secondTab = line.indexOf("\t", firstTab + 1);
644
- if (secondTab <= 0)
645
- continue;
646
- const thirdTab = line.indexOf("\t", secondTab + 1);
647
- if (thirdTab <= 0)
648
- continue;
649
- const pid = Number(line.slice(0, firstTab).trim());
650
- const parentPid = Number(line.slice(firstTab + 1, secondTab).trim());
651
- const ageMs = parseNonNegativeMs(line.slice(secondTab + 1, thirdTab));
652
- const command = line.slice(thirdTab + 1);
653
- if (!Number.isFinite(pid) || pid <= 0)
654
- continue;
655
- processes.push({ pid, parentPid, command, ageMs });
656
- }
657
- return narrowToManagedBinaries(processes, result);
658
- }
659
- // POSIX: enumerate everything, filter in JS by managed-name substring —
660
- // there is no single-query WQL-style server-side filter available.
661
- // `etime` is the POSIX-standard elapsed-time column (Linux and macOS both
662
- // support it; `etimes` is Linux-only), and supplies the spawn-grace age.
663
- const result = await spawnCollectStdoutResult(posixPsPath(), [...POSIX_PS_ARGS], { shell: false, stdio: ["ignore", "pipe", "ignore"] }, collect);
664
- const processes = [];
665
- for (const line of result.stdout.split(/\r?\n/)) {
666
- // Linear parse (S8786/S6594): " pid ppid etime args here".
667
- const trimmed = line.trimStart();
668
- if (!trimmed)
669
- continue;
670
- let i = 0;
671
- while (i < trimmed.length && trimmed[i] >= "0" && trimmed[i] <= "9")
672
- i++;
673
- if (i === 0)
674
- continue;
675
- const pid = Number(trimmed.slice(0, i));
676
- let rest = trimmed.slice(i);
677
- let k = 0;
678
- while (k < rest.length && (rest[k] === " " || rest[k] === "\t"))
679
- k++;
680
- rest = rest.slice(k);
681
- let j = 0;
682
- while (j < rest.length && rest[j] >= "0" && rest[j] <= "9")
683
- j++;
684
- if (j === 0)
685
- continue;
686
- const parentPid = Number(rest.slice(0, j));
687
- rest = rest.slice(j);
688
- let m = 0;
689
- while (m < rest.length && (rest[m] === " " || rest[m] === "\t"))
690
- m++;
691
- rest = rest.slice(m);
692
- // etime token: a non-whitespace run of digits, ':' and '-'.
693
- let e = 0;
694
- while (e < rest.length && rest[e] !== " " && rest[e] !== "\t")
695
- e++;
696
- const ageMs = ageMsFromPosixEtime(rest.slice(0, e));
697
- let n = e;
698
- while (n < rest.length && (rest[n] === " " || rest[n] === "\t"))
699
- n++;
700
- const args = rest.slice(n);
701
- if (!Number.isFinite(pid) || pid <= 0)
702
- continue;
703
- processes.push({ pid, parentPid, command: args, ageMs });
704
- }
705
- return narrowToManagedBinaries(processes, result);
582
+ // #1857: `Name = '…'` equality replaces eight leading-wildcard
583
+ // `CommandLine LIKE '%…%'` clauses, and the projection carries an age so
584
+ // the spawn-grace guard has something to reason from. The image-name set is
585
+ // DERIVED from MANAGED_BINARIES (MANAGED_IMAGE_NAMES), so adding a managed
586
+ // binary cannot leave a stale parallel list behind.
587
+ //
588
+ // POSIX `ps` cannot express that filter — the seam reports it as
589
+ // `serverSideFiltered: false` and hands back the whole table — which is why
590
+ // `narrowToManagedBinaries` runs on BOTH platforms rather than only on the
591
+ // POSIX branch. Windows over-collects for its own reason anyway
592
+ // (`node.exe` is a superset image name covering every node process on the
593
+ // machine).
594
+ const scanTimeoutMs = options.timeoutMs ?? BACKSTOP_SCAN_TIMEOUT_MS;
595
+ const result = await queryProcessTable({
596
+ fields: ["pid", "ppid", "ageMs", "command"],
597
+ filter: { column: "Name", op: "eq", values: MANAGED_IMAGE_NAMES },
598
+ }, {
599
+ timeoutMs: scanTimeoutMs,
600
+ onTimeout: (child) => terminateScannerChild(child, {
601
+ verifyAttempts: options.verifyAttempts,
602
+ verifyIntervalMs: options.verifyIntervalMs,
603
+ kind: "orphan-backstop-scanner-escalated",
604
+ timeoutMs: scanTimeoutMs,
605
+ }),
606
+ });
607
+ return narrowToManagedBinaries(result.rows.map((row) => ({
608
+ pid: row.pid,
609
+ parentPid: row.ppid,
610
+ command: row.command,
611
+ ageMs: row.ageMs,
612
+ })), result);
706
613
  }
707
614
  /**
708
615
  * The ONE place a raw OS-process snapshot is narrowed to managed binaries.
@@ -730,6 +637,18 @@ function narrowToManagedBinaries(processes, result) {
730
637
  * exists to fix, so the scanner now gets exactly what an orphan gets:
731
638
  * `taskkill /F /T` or a POSIX group kill, then a verified liveness poll. The
732
639
  * escalation is recorded with the scanner's identity, never swallowed.
640
+ *
641
+ * The kill attempt itself is real regardless of what happens next: this
642
+ * handler is invoked the instant the caller's timer elapses, which can race
643
+ * a query child that is genuinely about to close successfully (its own
644
+ * "close" event may still win the overall settle — see
645
+ * `child-unref.ts#spawnCollectStdoutResult`, which resolves on whichever of
646
+ * "close" or the timeout handler's own settle lands first). This function has
647
+ * no way to observe that outcome — it runs strictly BEFORE the caller's
648
+ * promise resolves — so it always records the escalation it actually
649
+ * attempted; callers whose escalation kind can fire on that race classify it
650
+ * as informational rather than a warning (see
651
+ * `degradation-ledger.ts`'s `resource-sampler-scanner-escalated` doc comment).
733
652
  */
734
653
  export async function terminateScannerChild(child, options) {
735
654
  const pid = child.pid;
@@ -740,9 +659,9 @@ export async function terminateScannerChild(child, options) {
740
659
  })
741
660
  : "invalid";
742
661
  incrementDegradationCount({
743
- kind: "orphan-backstop-scanner-escalated",
662
+ kind: options.kind,
744
663
  subject: `${isWindows ? "win32-cim" : "posix-ps"}#${pid ?? "no-pid"}`,
745
- reason: `scan exceeded its timeout; tree kill reported ${outcome}`,
664
+ reason: `scan exceeded its ${options.timeoutMs}ms timeout; tree kill reported ${outcome}`,
746
665
  });
747
666
  return outcome;
748
667
  }
@@ -755,51 +674,6 @@ function matchesManagedBinary(command) {
755
674
  const lower = command.toLowerCase();
756
675
  return MANAGED_BINARY_NAMES.some((name) => lower.includes(name.toLowerCase()));
757
676
  }
758
- /**
759
- * Parse the Windows age column: a non-negative integer millisecond count, or
760
- * anything else. A negative or malformed value is `undefined`, not zero and
761
- * not a clamp — a nonsensical age means the age is UNKNOWN, and the grace
762
- * guard must spare an unknown-age process rather than reason from a number it
763
- * cannot trust. That rule is what surfaced the local-time tick bug during the
764
- * host probe instead of silently sparing everything.
765
- */
766
- function parseNonNegativeMs(raw) {
767
- const token = raw.trim();
768
- if (!/^\d+$/.test(token))
769
- return undefined;
770
- const value = Number(token);
771
- return Number.isFinite(value) ? value : undefined;
772
- }
773
- /**
774
- * POSIX enumeration columns. Exported so a real-`ps` test can assert this
775
- * exact argument vector is accepted by the host's `ps` — a rejected column
776
- * would make the whole backstop silently return zero rows, and this is the
777
- * one part of the fix that cannot be checked from a Windows dev machine.
778
- */
779
- export const POSIX_PS_ARGS = [
780
- "-eo",
781
- "pid=,ppid=,etime=,args=",
782
- ];
783
- /** Parse `ps -o etime` output: `[[dd-]hh:]mm:ss`. Returns undefined for any
784
- * shape the column did not produce (a `ps` without the column emits the
785
- * command line where the token was expected). */
786
- export function ageMsFromPosixEtime(raw) {
787
- const token = raw.trim();
788
- if (!/^(?:\d+-)?(?:\d+:)?\d+:\d+$/.test(token))
789
- return undefined;
790
- let days = 0;
791
- let rest = token;
792
- const dash = rest.indexOf("-");
793
- if (dash >= 0) {
794
- days = Number(rest.slice(0, dash));
795
- rest = rest.slice(dash + 1);
796
- }
797
- const parts = rest.split(":").map(Number);
798
- if (parts.some((part) => !Number.isFinite(part)))
799
- return undefined;
800
- const [hours, minutes, seconds] = parts.length === 3 ? parts : [0, parts[0], parts[1]];
801
- return ((days * 24 + hours) * 60 * 60 + minutes * 60 + seconds) * 1000;
802
- }
803
677
  /**
804
678
  * Fire-and-forget REGISTRY-INDEPENDENT backstop sweep (#658): finds
805
679
  * pi-lens-managed LSP/scanner processes the registry-driven `sweepOrphans`