@herbertgao/pi-extensions 2026.8.4 → 2026.8.6

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/README.md +5 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +11 -3
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +11 -3
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +53 -14
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
  14. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  15. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +18 -0
  16. package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
  17. package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
  18. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +20 -4
  21. package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
  22. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
  23. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
  24. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
  25. package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
  26. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  27. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
  31. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  32. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  33. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  34. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  35. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  36. package/node_modules/@narumitw/pi-btw/README.md +32 -24
  37. package/node_modules/@narumitw/pi-btw/package.json +6 -6
  38. package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
  39. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -13
  40. package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
  41. package/node_modules/pi-lens/CHANGELOG.md +2535 -954
  42. package/node_modules/pi-lens/README.md +13 -0
  43. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  44. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
  45. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  46. package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
  47. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  48. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  49. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  50. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  51. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  52. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  53. package/node_modules/pi-lens/dist/clients/biome-client.js +136 -24
  54. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  55. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  56. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  57. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
  58. package/node_modules/pi-lens/dist/clients/bus-publish.js +19 -4
  59. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  60. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  61. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  62. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  63. package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
  64. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  65. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  66. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  67. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  68. package/node_modules/pi-lens/dist/clients/console-guard-install.js +26 -0
  69. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  70. package/node_modules/pi-lens/dist/clients/dead-code-client.js +137 -31
  71. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  72. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  73. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  74. package/node_modules/pi-lens/dist/clients/dependency-checker.js +361 -77
  75. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +65 -13
  76. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +18 -4
  77. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +176 -29
  78. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  79. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  80. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  81. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  82. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  83. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1172 -683
  84. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  85. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  86. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  87. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +47 -6
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +473 -42
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  114. package/node_modules/pi-lens/dist/clients/disposition-publish.js +18 -4
  115. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  116. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
  117. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  118. package/node_modules/pi-lens/dist/clients/extension-log.js +432 -0
  119. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  120. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  121. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  122. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  123. package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
  124. package/node_modules/pi-lens/dist/clients/format-events-publish.js +51 -7
  125. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  126. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  127. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  128. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  129. package/node_modules/pi-lens/dist/clients/git-guard.js +678 -11
  130. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  131. package/node_modules/pi-lens/dist/clients/go-client.js +23 -40
  132. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +124 -7
  133. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  134. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  135. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  136. package/node_modules/pi-lens/dist/clients/installer/index.js +395 -100
  137. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  138. package/node_modules/pi-lens/dist/clients/jscpd-client.js +27 -55
  139. package/node_modules/pi-lens/dist/clients/knip-client.js +58 -42
  140. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  141. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  142. package/node_modules/pi-lens/dist/clients/latency-logger.js +72 -1
  143. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  144. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  145. package/node_modules/pi-lens/dist/clients/lens-events.js +63 -9
  146. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +16 -0
  147. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  148. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +73 -0
  149. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  150. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
  151. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +59 -14
  152. package/node_modules/pi-lens/dist/clients/lsp/client.js +662 -56
  153. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  154. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +115 -0
  155. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  156. package/node_modules/pi-lens/dist/clients/lsp/index.js +2025 -800
  157. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  158. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  159. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  160. package/node_modules/pi-lens/dist/clients/lsp/server.js +437 -86
  161. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
  162. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
  163. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
  164. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  165. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  166. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  167. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +127 -32
  168. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  169. package/node_modules/pi-lens/dist/clients/mcp/session.js +235 -14
  170. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  171. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  172. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  173. package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
  174. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  175. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  176. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  177. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  178. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  179. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  180. package/node_modules/pi-lens/dist/clients/pipeline.js +76 -15
  181. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  182. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  183. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  184. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
  185. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  186. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
  187. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  188. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  189. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  190. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  191. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  192. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  193. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  194. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  195. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  196. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  197. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  198. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  199. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  200. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  201. package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
  202. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  203. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  204. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  205. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  206. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  207. package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
  208. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +257 -77
  209. package/node_modules/pi-lens/dist/clients/runtime-context.js +129 -12
  210. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +242 -26
  211. package/node_modules/pi-lens/dist/clients/runtime-session.js +428 -117
  212. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  213. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +342 -33
  214. package/node_modules/pi-lens/dist/clients/runtime-turn.js +619 -93
  215. package/node_modules/pi-lens/dist/clients/rust-client.js +23 -38
  216. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  217. package/node_modules/pi-lens/dist/clients/security-scan-client.js +90 -6
  218. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  219. package/node_modules/pi-lens/dist/clients/sg-runner.js +424 -196
  220. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  221. package/node_modules/pi-lens/dist/clients/smells-rollup.js +231 -0
  222. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  223. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  224. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  225. package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
  226. package/node_modules/pi-lens/dist/clients/test-runner-client.js +469 -32
  227. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  228. package/node_modules/pi-lens/dist/clients/tool-policy.js +390 -39
  229. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  230. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
  231. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  232. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  233. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  234. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  235. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  236. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  237. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  238. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  239. package/node_modules/pi-lens/dist/clients/warm-attach.js +50 -13
  240. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  241. package/node_modules/pi-lens/dist/clients/word-index.js +680 -81
  242. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  243. package/node_modules/pi-lens/dist/index.js +51785 -36118
  244. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  245. package/node_modules/pi-lens/dist/mcp/server.js +219 -46
  246. package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
  247. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
  248. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  249. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
  250. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +203 -38
  251. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +210 -42
  252. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  253. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  254. package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
  255. package/node_modules/pi-lens/docs/agent-guide.md +40 -15
  256. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  257. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  258. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  259. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +14 -3
  260. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  261. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  262. package/node_modules/pi-lens/docs/fable.md +27 -0
  263. package/node_modules/pi-lens/docs/features.md +45 -3
  264. package/node_modules/pi-lens/docs/globalconfig.md +56 -1
  265. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  266. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  267. package/node_modules/pi-lens/docs/mcp.md +37 -4
  268. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  269. package/node_modules/pi-lens/docs/settings.md +18 -1
  270. package/node_modules/pi-lens/docs/tools.md +4 -1
  271. package/node_modules/pi-lens/docs/usage.md +41 -5
  272. package/node_modules/pi-lens/docs/word-index.md +35 -0
  273. package/node_modules/pi-lens/package.json +10 -10
  274. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  275. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  276. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
  277. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
  278. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
  279. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  280. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  281. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
  282. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
  283. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
  284. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
  285. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
  286. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  287. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  288. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  289. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
  290. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
  291. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
  292. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  293. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  294. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
  295. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
  296. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
  297. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
  298. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
  299. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  300. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  301. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  302. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +134 -2
  303. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
  304. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
  305. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
  306. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
  307. package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
  308. package/node_modules/pi-mcp-adapter/CHANGELOG.md +38 -0
  309. package/node_modules/pi-mcp-adapter/README.md +38 -4
  310. package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
  311. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  312. package/node_modules/pi-mcp-adapter/cli.js +25 -4
  313. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  314. package/node_modules/pi-mcp-adapter/config.ts +4 -4
  315. package/node_modules/pi-mcp-adapter/direct-tools.ts +58 -8
  316. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  317. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  318. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  319. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  320. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  321. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  322. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  323. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  324. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
  325. package/node_modules/pi-mcp-adapter/metadata-cache.ts +37 -4
  326. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  327. package/node_modules/pi-mcp-adapter/package.json +4 -3
  328. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  329. package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
  330. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  331. package/node_modules/pi-mcp-adapter/server-manager.ts +41 -7
  332. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  333. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  334. package/node_modules/pi-mcp-adapter/tool-metadata.ts +62 -4
  335. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  336. package/node_modules/pi-mcp-adapter/types.ts +176 -27
  337. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  338. package/node_modules/pi-web-access/CHANGELOG.md +14 -0
  339. package/node_modules/pi-web-access/README.md +18 -12
  340. package/node_modules/pi-web-access/auth-fetch.ts +148 -0
  341. package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
  342. package/node_modules/pi-web-access/curator-page.ts +5 -3
  343. package/node_modules/pi-web-access/curator-server.ts +2 -1
  344. package/node_modules/pi-web-access/extract.ts +106 -34
  345. package/node_modules/pi-web-access/fetch-params.ts +17 -3
  346. package/node_modules/pi-web-access/firecrawl.ts +172 -12
  347. package/node_modules/pi-web-access/gemini-search.ts +18 -4
  348. package/node_modules/pi-web-access/index.ts +120 -48
  349. package/node_modules/pi-web-access/package.json +2 -2
  350. package/node_modules/pi-web-access/summary-review.ts +11 -5
  351. package/node_modules/pi-web-access/youtube-extract.ts +2 -2
  352. package/package.json +12 -13
  353. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  354. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  355. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  356. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  357. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  358. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  359. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  360. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -4,25 +4,15 @@
4
4
  * Extracted from AstGrepClient to simplify the main client.
5
5
  * Handles: spawn, spawnSync, temp dir management, JSON parsing.
6
6
  */
7
- import { spawn } from "node:child_process";
7
+ import { createSubsystemLogger } from "./extension-log.js";
8
8
  import * as fs from "node:fs";
9
9
  import * as os from "node:os";
10
10
  import * as path from "node:path";
11
- import { getSgCommand } from "./dispatch/runners/utils/runner-helpers.js";
11
+ import { getSgCommand, resolveManagedToolClient, } from "./dispatch/runners/utils/runner-helpers.js";
12
12
  import { getProjectIgnoreGlobs } from "./file-utils.js";
13
13
  import { findGlobalBinary } from "./package-manager.js";
14
14
  import { safeSpawnAsync } from "./safe-spawn.js";
15
- /**
16
- * Escape an argument for Windows cmd.exe shell execution.
17
- * Handles spaces, quotes, and special characters.
18
- */
19
- function escapeWindowsArg(arg) {
20
- // If no special characters, return as-is
21
- if (!/[\s"]/.test(arg))
22
- return arg;
23
- // Escape quotes by doubling them
24
- return `"${arg.replace(/"/g, '""')}"`;
25
- }
15
+ import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
26
16
  /**
27
17
  * Build the `bash -c` argv that runs `cmd` with `allArgs` as POSITIONAL
28
18
  * parameters. The script is the constant `"$0" "$@"`, so bash re-emits the
@@ -43,6 +33,10 @@ function sgExcludeArgsForProject(rootDir) {
43
33
  `!${glob}`,
44
34
  ]);
45
35
  }
36
+ const DEFAULT_EXEC_TIMEOUT_MS = 30_000;
37
+ /** Budget for a single `--version` candidate probe, ms. */
38
+ const PROBE_TIMEOUT_MS = 5_000;
39
+ const MAX_SG_OUTPUT_BYTES = 8 * 1024 * 1024;
46
40
  /**
47
41
  * Format metavariable captures for display below a match line.
48
42
  * Single captures: $VAR=x $NAME=foo
@@ -72,15 +66,65 @@ function formatMetaVarCaptures(mv) {
72
66
  return undefined;
73
67
  return ` ${parts.join(" ")}`;
74
68
  }
69
+ /**
70
+ * Parse ast-grep `--json` stdout into a match array, or return `null` when the
71
+ * text is not a match payload. ast-grep emits either a JSON array or (rarely) a
72
+ * single object; both are normalized to an array. A JSON scalar/`null` is
73
+ * rejected (returns `null`) so a hypothetical error-report scalar on stdout can
74
+ * never be misread as a phantom match.
75
+ */
76
+ function tryParseSgMatches(stdout) {
77
+ try {
78
+ const parsed = JSON.parse(stdout);
79
+ if (Array.isArray(parsed))
80
+ return parsed;
81
+ if (parsed !== null && typeof parsed === "object")
82
+ return [parsed];
83
+ return null;
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ function tryParseNonZeroSgMatches(result) {
90
+ if (result.outputTruncated || !result.stdout.trim())
91
+ return null;
92
+ // Trimmed on purpose: a leading BOM survives JSON.parse's whitespace
93
+ // tolerance but String.trim() strips it (matches the pre-refactor guard).
94
+ return tryParseSgMatches(result.stdout.trim());
95
+ }
75
96
  export class SgRunner {
76
97
  log;
77
98
  sgPath = null;
78
99
  sgArgsPrefix = [];
79
- available = null;
100
+ /**
101
+ * Availability memo, backed by the shared transient-aware latch (#1476).
102
+ *
103
+ * Before this, one failed sweep over every candidate — timeouts included —
104
+ * latched `false` for the life of the process AND ran a full auto-install
105
+ * first. A host stall during warm-up therefore both disabled ast-grep until
106
+ * restart and paid for an install nobody needed.
107
+ */
108
+ availabilityLatch = createAvailabilityLatch();
80
109
  ensureInFlight = null;
110
+ /**
111
+ * Whether a DIRECT candidate — one that would have been ast-grep itself —
112
+ * failed for a transient reason in the current sweep. Only these block the
113
+ * install: a slow `npx` says nothing about whether ast-grep is on this
114
+ * machine, and letting it block turned "ast-grep is genuinely absent on a
115
+ * slow host" into "ast-grep is never installed and npx is re-spawned every
116
+ * sweep, forever".
117
+ */
118
+ sweepSawTransient = false;
119
+ sweepTransientCause = "probe-timeout";
120
+ /** A transient on the npx fallback: not evidence, but not nothing either. */
121
+ sweepFallbackTransient = false;
122
+ sweepFallbackCause = "probe-timeout";
123
+ /** Host stall summed over every probe of the current sweep, ms. */
124
+ sweepHostStallMs = 0;
81
125
  constructor(verbose = false) {
82
126
  this.log = verbose
83
- ? (msg) => console.error(`[sg-runner] ${msg}`)
127
+ ? createSubsystemLogger("sg-runner")
84
128
  : () => { };
85
129
  }
86
130
  /**
@@ -92,9 +136,11 @@ export class SgRunner {
92
136
  * `KnipClient.ensureAvailable` and `DependencyChecker.ensureAvailable`.
93
137
  */
94
138
  async ensureAvailable() {
95
- // Fast path: already checked.
96
- if (this.available !== null)
97
- return this.available;
139
+ // Fast path: already decided. `read()` returns null when the last verdict
140
+ // was transient and its cooldown expired, which re-enters the sweep.
141
+ const memo = this.availabilityLatch.read();
142
+ if (memo !== null)
143
+ return memo;
98
144
  if (this.ensureInFlight)
99
145
  return this.ensureInFlight;
100
146
  this.ensureInFlight = this.doEnsureAvailable();
@@ -106,18 +152,26 @@ export class SgRunner {
106
152
  }
107
153
  }
108
154
  async doEnsureAvailable() {
155
+ const startedAt = Date.now();
156
+ this.sweepSawTransient = false;
157
+ this.sweepTransientCause = "probe-timeout";
158
+ this.sweepFallbackTransient = false;
159
+ this.sweepFallbackCause = "probe-timeout";
160
+ this.sweepHostStallMs = 0;
109
161
  // Step 1: PATH — canonical binary names + npx fallback.
110
162
  // Prefer ast-grep over sg on Linux: /usr/bin/sg is util-linux, not ast-grep.
111
163
  const pathCommand = await this.probeCommandCandidates([
112
164
  { cmd: "ast-grep", argsPrefix: [] },
113
165
  { cmd: "sg", argsPrefix: [] },
114
- { cmd: "npx", argsPrefix: ["--no", "--", "ast-grep"] },
166
+ // `npx --no -- ast-grep` starts a Node process before it can answer, so
167
+ // it is the candidate most likely to blow a 5 s budget on a cold or busy
168
+ // box. Marked a fallback so its timeout cannot veto the install.
169
+ { cmd: "npx", argsPrefix: ["--no", "--", "ast-grep"], fallback: true },
115
170
  ]);
116
171
  if (pathCommand) {
117
172
  this.sgPath = pathCommand.cmd;
118
173
  this.sgArgsPrefix = pathCommand.argsPrefix;
119
- this.available = true;
120
- this.log(`ast-grep found on PATH: ${pathCommand.cmd}`);
174
+ this.noteAvailable(startedAt, `ast-grep found on PATH: ${pathCommand.cmd}`);
121
175
  return true;
122
176
  }
123
177
  // Step 1b: any package manager's global bin dir (npm/pnpm/yarn/bun).
@@ -129,8 +183,7 @@ export class SgRunner {
129
183
  if (globalBin && (await this.probeCommand(globalBin, []))) {
130
184
  this.sgPath = globalBin;
131
185
  this.sgArgsPrefix = [];
132
- this.available = true;
133
- this.log(`ast-grep found in global bin: ${globalBin}`);
186
+ this.noteAvailable(startedAt, `ast-grep found in global bin: ${globalBin}`);
134
187
  return true;
135
188
  }
136
189
  }
@@ -141,8 +194,7 @@ export class SgRunner {
141
194
  if (platformBinary) {
142
195
  this.sgPath = platformBinary;
143
196
  this.sgArgsPrefix = [];
144
- this.available = true;
145
- this.log(`ast-grep found via platform package: ${platformBinary}`);
197
+ this.noteAvailable(startedAt, `ast-grep found via platform package: ${platformBinary}`);
146
198
  return true;
147
199
  }
148
200
  // Step 3: Homebrew (macOS only).
@@ -151,23 +203,80 @@ export class SgRunner {
151
203
  if (brewBinary) {
152
204
  this.sgPath = brewBinary;
153
205
  this.sgArgsPrefix = [];
154
- this.available = true;
155
- this.log(`ast-grep found via Homebrew: ${brewBinary}`);
206
+ this.noteAvailable(startedAt, `ast-grep found via Homebrew: ${brewBinary}`);
156
207
  return true;
157
208
  }
158
209
  }
159
- // Step 4: auto-install via pi-lens installer.
160
- this.log("ast-grep not found, attempting auto-install...");
161
- const { ensureTool } = await import("./installer/index.js");
162
- const installedPath = await ensureTool("ast-grep");
163
- if (installedPath && (await this.probeCommand(installedPath, []))) {
164
- this.sgPath = installedPath;
210
+ // A timeout on a DIRECT candidate means the machine answered, not the tool
211
+ // (#1476). Installing ast-grep because the host event loop stalled is a
212
+ // heavyweight reaction to a hiccup, and latching the result disabled the
213
+ // tool until restart. Retry the sweep later instead.
214
+ //
215
+ // The npx fallback is deliberately NOT part of this test. Gating the
216
+ // install on it regressed the very host this change targets: a slow box
217
+ // with no ast-grep timed out `npx --no -- ast-grep` every sweep, so the
218
+ // install below was never reached and the slow npx was re-spawned on each
219
+ // escalating retry instead — worse than the latch it replaced.
220
+ if (this.sweepSawTransient) {
221
+ this.log("ast-grep availability probe timed out; will retry (not installing)");
222
+ return this.noteUnavailable(startedAt, "transient", this.sweepTransientCause);
223
+ }
224
+ // Step 4: install via the typed shared seam, then validate the returned
225
+ // absolute binary before publishing it.
226
+ const installed = await resolveManagedToolClient({
227
+ toolId: "ast-grep",
228
+ cwd: process.cwd(),
229
+ probe: async () => ({ outcome: "missing" }),
230
+ acceptInstalled: async (installedPath) => (await this.probeCommand(installedPath, [])) ? installedPath : null,
231
+ });
232
+ if (installed.outcome === "success") {
233
+ this.sgPath = installed.value;
165
234
  this.sgArgsPrefix = [];
166
- this.available = true;
167
- this.log(`ast-grep auto-installed: ${installedPath}`);
235
+ this.noteAvailable(startedAt, `ast-grep auto-installed: ${installed.value}`);
168
236
  return true;
169
237
  }
170
- this.available = false;
238
+ // The install failed AND the npx fallback never got a fair hearing, so
239
+ // "ast-grep is not installed" is not a fact yet. Expire the verdict.
240
+ if (this.sweepFallbackTransient) {
241
+ return this.noteUnavailable(startedAt, "transient", this.sweepFallbackCause);
242
+ }
243
+ return this.noteUnavailable(startedAt, "missing", "not-found");
244
+ }
245
+ /** Record a successful sweep: available, latched, one decision record. */
246
+ noteAvailable(startedAt, message) {
247
+ this.availabilityLatch.noteAvailable();
248
+ this.log(message);
249
+ logAvailabilityDecision({
250
+ tool: "ast-grep",
251
+ verdict: "available",
252
+ outcome: "success",
253
+ cause: "ok",
254
+ elapsedMs: Date.now() - startedAt,
255
+ latched: true,
256
+ hostStallMs: this.sweepHostStallMs,
257
+ budgetMs: PROBE_TIMEOUT_MS,
258
+ });
259
+ }
260
+ /**
261
+ * Record a failed sweep. A `transient` outcome expires after a bounded
262
+ * cooldown; anything else is remembered for the session, as before. The
263
+ * elapsed/stall numbers describe the WHOLE sweep, because the verdict is
264
+ * about the sweep rather than any single candidate — reporting zero here
265
+ * would erase the evidence that cracked #1467.
266
+ */
267
+ noteUnavailable(startedAt, outcome, cause) {
268
+ const retryAfterMs = this.availabilityLatch.noteUnavailable(outcome, cause);
269
+ logAvailabilityDecision({
270
+ tool: "ast-grep",
271
+ verdict: "unavailable",
272
+ outcome,
273
+ cause,
274
+ elapsedMs: Date.now() - startedAt,
275
+ latched: outcome !== "transient",
276
+ hostStallMs: this.sweepHostStallMs,
277
+ ...(retryAfterMs > 0 && { retryAfterMs }),
278
+ budgetMs: PROBE_TIMEOUT_MS,
279
+ });
171
280
  return false;
172
281
  }
173
282
  /**
@@ -206,7 +315,8 @@ export class SgRunner {
206
315
  for (const root of searchRoots) {
207
316
  const candidate = path.join(root, pkgName, exeName);
208
317
  try {
209
- if (fs.existsSync(candidate) && (await this.probeCommand(candidate, []))) {
318
+ if (fs.existsSync(candidate) &&
319
+ (await this.probeCommand(candidate, []))) {
210
320
  return candidate;
211
321
  }
212
322
  }
@@ -233,7 +343,8 @@ export class SgRunner {
233
343
  return undefined;
234
344
  for (const name of ["ast-grep", "sg"]) {
235
345
  const candidate = path.join(prefix, "bin", name);
236
- if (fs.existsSync(candidate) && (await this.probeCommand(candidate, []))) {
346
+ if (fs.existsSync(candidate) &&
347
+ (await this.probeCommand(candidate, []))) {
237
348
  return candidate;
238
349
  }
239
350
  }
@@ -246,17 +357,48 @@ export class SgRunner {
246
357
  isAstGrepVersionOutput(output) {
247
358
  return /\bast[- ]grep\b/i.test(output);
248
359
  }
249
- async probeCommand(cmd, argsPrefix) {
250
- const result = await safeSpawnAsync(cmd, [...argsPrefix, "--version"], {
251
- timeout: 5000,
252
- });
253
- return (!result.error &&
360
+ /**
361
+ * Probe one candidate. A failure is CLASSIFIED, not merely counted: a
362
+ * timeout/abort marks the whole sweep transient so the caller retries later
363
+ * instead of installing and latching. A command that answers but is not
364
+ * ast-grep (Linux `/usr/bin/sg` is util-linux) is a durable no.
365
+ */
366
+ async probeCommand(cmd, argsPrefix, fallback = false) {
367
+ // Host-side budget: measure the loop stall that overlapped the window so
368
+ // the classifier can tell "the tool is slow" from "the host was busy".
369
+ const sampler = startHostStallSampler();
370
+ let result;
371
+ let hostStallMs;
372
+ try {
373
+ result = await safeSpawnAsync(cmd, [...argsPrefix, "--version"], {
374
+ timeout: PROBE_TIMEOUT_MS,
375
+ });
376
+ }
377
+ finally {
378
+ hostStallMs = sampler.stop();
379
+ this.sweepHostStallMs += hostStallMs;
380
+ }
381
+ if (!result.error &&
254
382
  result.status === 0 &&
255
- this.isAstGrepVersionOutput(`${result.stdout}\n${result.stderr}`));
383
+ this.isAstGrepVersionOutput(`${result.stdout}\n${result.stderr}`)) {
384
+ return true;
385
+ }
386
+ const { outcome, cause } = classifyProbeFailure(result, { hostStallMs });
387
+ if (outcome === "transient") {
388
+ if (fallback) {
389
+ this.sweepFallbackTransient = true;
390
+ this.sweepFallbackCause = cause;
391
+ }
392
+ else {
393
+ this.sweepSawTransient = true;
394
+ this.sweepTransientCause = cause;
395
+ }
396
+ }
397
+ return false;
256
398
  }
257
399
  async probeCommandCandidates(candidates) {
258
400
  for (const candidate of candidates) {
259
- if (await this.probeCommand(candidate.cmd, candidate.argsPrefix)) {
401
+ if (await this.probeCommand(candidate.cmd, candidate.argsPrefix, candidate.fallback ?? false)) {
260
402
  return candidate;
261
403
  }
262
404
  }
@@ -271,147 +413,140 @@ export class SgRunner {
271
413
  argsPrefix: this.sgArgsPrefix,
272
414
  };
273
415
  }
274
- async execRaw(args, timeout = 30000) {
416
+ failureForSpawnResult(result) {
417
+ if (result.failure === "aborted")
418
+ return "aborted";
419
+ switch (result.spawnFailure?.kind) {
420
+ case "tool-not-found":
421
+ return "unavailable";
422
+ case "timeout":
423
+ return "timeout";
424
+ case "killed":
425
+ return result.failure === "aborted" ? "aborted" : "cli-failure";
426
+ case "cwd-unresolvable":
427
+ case "permission-denied":
428
+ case "spawn-failed":
429
+ case undefined:
430
+ return result.error ? "cli-failure" : undefined;
431
+ }
432
+ }
433
+ formatPatternError(stderr, args) {
434
+ if (stderr.includes("Multiple AST nodes are detected")) {
435
+ return (`Invalid AST pattern: The pattern appears to contain multiple AST nodes or is malformed.\n` +
436
+ `Common causes:\n` +
437
+ ` 1. Missing parentheses: use it($TEST) not it"test"\n` +
438
+ ` 2. Raw text without structure: use console.log($MSG) not just "console.log"\n` +
439
+ ` 3. Unclosed quotes or brackets\n\n` +
440
+ `Original error: ${stderr}`);
441
+ }
442
+ if (stderr.includes("Cannot parse query")) {
443
+ return (`Pattern syntax error: The pattern could not be parsed as valid code.\n` +
444
+ `Tips:\n` +
445
+ ` - Patterns must be valid ${args.includes("--lang") ? args[args.indexOf("--lang") + 1] : "language"} syntax\n` +
446
+ ` - Use metavariables like $NAME, $ARGS for variable parts\n` +
447
+ ` - Example: 'function $NAME($$$PARAMS) { $$$BODY }'\n\n` +
448
+ `Original error: ${stderr}`);
449
+ }
450
+ return stderr;
451
+ }
452
+ async execRaw(args, timeout = DEFAULT_EXEC_TIMEOUT_MS, options = {}) {
275
453
  const command = this.getSgCommand();
276
- const result = await safeSpawnAsync(command.cmd, [...command.argsPrefix, ...args], { timeout });
454
+ const result = await safeSpawnAsync(command.cmd, [...command.argsPrefix, ...args], {
455
+ timeout,
456
+ deadlineAt: options.deadlineAt,
457
+ signal: options.signal,
458
+ maxOutputBytes: MAX_SG_OUTPUT_BYTES,
459
+ });
460
+ const failure = this.failureForSpawnResult(result);
277
461
  return {
278
462
  stdout: result.stdout,
279
463
  stderr: result.stderr,
280
464
  status: result.status,
281
465
  error: result.error?.message,
466
+ failure,
467
+ ...(result.outputTruncated ? { outputTruncated: true } : {}),
282
468
  };
283
469
  }
284
470
  /**
285
- * Run ast-grep asynchronously, return parsed matches
471
+ * Run ast-grep asynchronously, return parsed matches. The Windows Git
472
+ * Bash/MSYS path deliberately remains positional-argument based, but the
473
+ * child itself now goes through safeSpawnAsync so cancellation, deadlines,
474
+ * tree-kill, and output caps are shared with every other CLI runner.
286
475
  */
287
- async exec(args) {
288
- return new Promise((resolve) => {
289
- const command = this.getSgCommand();
290
- const allArgs = [...command.argsPrefix, ...args];
291
- // On Windows with Git Bash/MSYS2, we need to use bash to properly
292
- // handle $variables in patterns (prevent shell expansion)
293
- const isWindows = process.platform === "win32";
294
- const hasBash = process.env.MSYSTEM || process.env.GIT_SHELL;
295
- const empty = () => ({
296
- matches: [],
297
- totalMatches: 0,
298
- truncated: false,
299
- });
300
- let proc;
301
- try {
302
- if (isWindows && hasBash) {
303
- // Run via bash (Git Bash/MSYS2) so $-metavariables in ast-grep
304
- // patterns aren't shell-expanded. Pass the command + args as
305
- // POSITIONAL parameters (`"$0"`/`"$@"`) instead of interpolating
306
- // them into the -c string: bash re-emits `"$@"` verbatim — no
307
- // parameter expansion, no word-splitting — so patterns stay literal
308
- // AND an environment-derived command path cannot inject shell
309
- // (fixes CodeQL js/shell-command-injection-from-environment). This
310
- // also removes the brittle hand-rolled quoting it replaced.
311
- proc = spawn("bash", buildBashRunArgs(command.cmd, allArgs), {
312
- stdio: ["ignore", "pipe", "pipe"],
313
- windowsHide: true,
314
- });
315
- }
316
- else if (isWindows) {
317
- // Fallback: shell:true needed for npm-installed .cmd wrappers on Windows.
318
- // Pass cmd and args separately — do not concatenate into one string.
319
- proc = spawn(command.cmd, allArgs.map(escapeWindowsArg), {
320
- stdio: ["ignore", "pipe", "pipe"],
321
- shell: true,
322
- windowsHide: true,
323
- });
324
- }
325
- else {
326
- // Unix: normal spawn without shell
327
- proc = spawn(command.cmd, allArgs, {
328
- stdio: ["ignore", "pipe", "pipe"],
329
- });
330
- }
331
- }
332
- catch (err) {
333
- // A SYNCHRONOUS spawn throw (Windows `spawn UNKNOWN`/EINVAL — the
334
- // pidusage bug class, #533) would reject this Promise; `exec` is
335
- // contracted to always resolve (callers read `error`, never catch),
336
- // so resolve gracefully instead of letting it escape as an
337
- // unhandledRejection. Async spawn `'error'` events are already
338
- // handled by the `proc.on("error", ...)` listener below.
339
- resolve({
340
- ...empty(),
341
- error: err instanceof Error ? err.message : String(err),
342
- });
343
- return;
344
- }
345
- let stdout = "";
346
- let stderr = "";
347
- proc.stdout?.on("data", (data) => (stdout += data.toString()));
348
- proc.stderr?.on("data", (data) => (stderr += data.toString()));
349
- proc.on("error", (err) => {
350
- if (err.message.includes("ENOENT")) {
351
- resolve({
352
- ...empty(),
353
- error: "ast-grep CLI not found. Install: npm i -D @ast-grep/cli",
354
- });
355
- }
356
- else {
357
- resolve({ ...empty(), error: err.message });
358
- }
359
- });
360
- proc.on("close", (code) => {
361
- if (code !== 0 && !stdout.trim()) {
362
- const stderrMsg = stderr.trim();
363
- if (code === 1 && !stderrMsg) {
364
- resolve(empty());
365
- return;
366
- }
367
- if (stderrMsg.includes("Multiple AST nodes are detected")) {
368
- resolve({
369
- ...empty(),
370
- error: `Invalid AST pattern: The pattern appears to contain multiple AST nodes or is malformed.\n` +
371
- `Common causes:\n` +
372
- ` 1. Missing parentheses: use it($TEST) not it"test"\n` +
373
- ` 2. Raw text without structure: use console.log($MSG) not just "console.log"\n` +
374
- ` 3. Unclosed quotes or brackets\n\n` +
375
- `Original error: ${stderrMsg}`,
376
- });
377
- return;
378
- }
379
- if (stderrMsg.includes("Cannot parse query")) {
380
- resolve({
381
- ...empty(),
382
- error: `Pattern syntax error: The pattern could not be parsed as valid code.\n` +
383
- `Tips:\n` +
384
- ` - Patterns must be valid ${args.includes("--lang") ? args[args.indexOf("--lang") + 1] : "language"} syntax\n` +
385
- ` - Use metavariables like $NAME, $ARGS for variable parts\n` +
386
- ` - Example: 'function $NAME($$$PARAMS) { $$$BODY }'\n\n` +
387
- `Original error: ${stderrMsg}`,
388
- });
389
- return;
390
- }
391
- resolve({
392
- ...empty(),
393
- error: stderrMsg || `Command failed with exit code ${code}`,
394
- });
395
- return;
396
- }
397
- if (!stdout.trim()) {
398
- resolve(empty());
399
- return;
400
- }
401
- try {
402
- const parsed = JSON.parse(stdout);
403
- const matches = Array.isArray(parsed) ? parsed : [parsed];
404
- resolve({
476
+ async exec(args, options = {}) {
477
+ const command = this.getSgCommand();
478
+ const allArgs = [...command.argsPrefix, ...args];
479
+ const isWindows = process.platform === "win32";
480
+ const hasBash = Boolean(process.env.MSYSTEM || process.env.GIT_SHELL);
481
+ const useBash = isWindows && hasBash;
482
+ const result = await safeSpawnAsync(useBash ? "bash" : command.cmd, useBash ? buildBashRunArgs(command.cmd, allArgs) : allArgs, {
483
+ timeout: DEFAULT_EXEC_TIMEOUT_MS,
484
+ deadlineAt: options.deadlineAt,
485
+ signal: options.signal,
486
+ maxOutputBytes: MAX_SG_OUTPUT_BYTES,
487
+ });
488
+ const empty = () => ({
489
+ matches: [],
490
+ totalMatches: 0,
491
+ truncated: false,
492
+ });
493
+ const spawnFailure = this.failureForSpawnResult(result);
494
+ if (spawnFailure) {
495
+ return {
496
+ ...empty(),
497
+ error: spawnFailure === "unavailable"
498
+ ? "ast-grep CLI not found. Install: npm i -D @ast-grep/cli"
499
+ : result.error?.message || "ast-grep CLI failed to start",
500
+ };
501
+ }
502
+ if (result.status !== 0) {
503
+ const stdout = result.stdout.trim();
504
+ const stderr = result.stderr.trim();
505
+ // ast-grep's linter-style contract: a rule with `severity: error`
506
+ // that MATCHES exits 1 with valid JSON matches on stdout (stderr
507
+ // carries "Scan succeeded and found error level diagnostics"). An
508
+ // exit code that means "scan succeeded with findings" must never be
509
+ // classified as a CLI failure — parse the matches. Only fall through
510
+ // to failure when the JSON isn't parseable (a real diagnostic).
511
+ if (result.status === 1 && stdout) {
512
+ const matches = tryParseNonZeroSgMatches(result);
513
+ if (matches) {
514
+ return {
405
515
  matches,
406
516
  totalMatches: matches.length,
407
517
  truncated: false,
408
- });
409
- }
410
- catch {
411
- resolve({ ...empty(), error: "Failed to parse output" });
518
+ };
412
519
  }
413
- });
414
- });
520
+ }
521
+ // ast-grep uses status 1 with no output for a genuine no-match in
522
+ // some CLI versions. Preserve that historical empty-result behavior;
523
+ // any stderr (including an invalid kind/YAML diagnostic) is a failure.
524
+ if (result.status === 1 && !stdout && !stderr)
525
+ return empty();
526
+ return {
527
+ ...empty(),
528
+ error: this.formatPatternError(stderr || `Command failed with exit code ${result.status}`, args),
529
+ };
530
+ }
531
+ if (!result.stdout.trim())
532
+ return empty();
533
+ if (result.outputTruncated) {
534
+ return {
535
+ ...empty(),
536
+ error: "Failed to parse output: output was truncated",
537
+ };
538
+ }
539
+ {
540
+ const matches = tryParseSgMatches(result.stdout);
541
+ if (matches) {
542
+ return {
543
+ matches,
544
+ totalMatches: matches.length,
545
+ truncated: false,
546
+ };
547
+ }
548
+ return { ...empty(), error: "Failed to parse output" };
549
+ }
415
550
  }
416
551
  // --- Shared helpers for temp-dir rule scans ---
417
552
  prepareTempScan(ruleId, ruleYaml) {
@@ -423,12 +558,6 @@ export class SgRunner {
423
558
  fs.writeFileSync(path.join(rulesSubdir, `${ruleId}.yml`), ruleYaml);
424
559
  return { sessionDir, configFile };
425
560
  }
426
- parseScanOutput(output) {
427
- if (!output.trim())
428
- return [];
429
- const items = JSON.parse(output);
430
- return Array.isArray(items) ? items : [items];
431
- }
432
561
  cleanupTempScan(sessionDir) {
433
562
  try {
434
563
  fs.rmSync(sessionDir, { recursive: true, force: true });
@@ -437,7 +566,67 @@ export class SgRunner {
437
566
  this.log(`Cleanup failed: ${err.message}`);
438
567
  }
439
568
  }
440
- async tempScanAsync(dir, ruleId, ruleYaml, timeout = 30000) {
569
+ interpretScanResult(result, args) {
570
+ const failure = this.failureForSpawnResult(result);
571
+ if (failure) {
572
+ return {
573
+ matches: [],
574
+ status: result.status,
575
+ error: result.error?.message || "ast-grep CLI failed to start",
576
+ failure,
577
+ };
578
+ }
579
+ if (result.status !== 0) {
580
+ const stdout = result.stdout.trim();
581
+ const stderr = result.stderr.trim();
582
+ // ast-grep's linter-style contract: a rule with `severity: error`
583
+ // that MATCHES exits 1 with valid JSON matches on stdout (stderr
584
+ // carries "Scan succeeded and found error level diagnostics"). An
585
+ // exit code that means "scan succeeded with findings" must never be
586
+ // classified as a CLI failure — parse the matches. Only fall through
587
+ // to failure when the JSON isn't parseable (a real diagnostic).
588
+ if (result.status === 1 && stdout) {
589
+ const matches = tryParseNonZeroSgMatches(result);
590
+ if (matches) {
591
+ return { matches, status: result.status };
592
+ }
593
+ }
594
+ // Preserve ast-grep's status-1/no-output no-match convention. A
595
+ // diagnostic on stderr is never treated as a no-match.
596
+ if (result.status === 1 && !stdout && !stderr) {
597
+ return { matches: [], status: result.status };
598
+ }
599
+ return {
600
+ matches: [],
601
+ status: result.status,
602
+ error: this.formatPatternError(stderr || `ast-grep scan failed with exit code ${result.status}`, args),
603
+ failure: "cli-failure",
604
+ };
605
+ }
606
+ if (!result.stdout.trim())
607
+ return { matches: [], status: result.status };
608
+ if (result.outputTruncated) {
609
+ return {
610
+ matches: [],
611
+ status: result.status,
612
+ error: "Failed to parse ast-grep scan output: output was truncated",
613
+ failure: "parse-failure",
614
+ };
615
+ }
616
+ {
617
+ const matches = tryParseSgMatches(result.stdout);
618
+ if (matches) {
619
+ return { matches, status: result.status };
620
+ }
621
+ return {
622
+ matches: [],
623
+ status: result.status,
624
+ error: "Failed to parse ast-grep scan output: invalid JSON",
625
+ failure: "parse-failure",
626
+ };
627
+ }
628
+ }
629
+ async tempScanDetailedAsync(dir, ruleId, ruleYaml, timeout = DEFAULT_EXEC_TIMEOUT_MS, options = {}) {
441
630
  const { sessionDir, configFile } = this.prepareTempScan(ruleId, ruleYaml);
442
631
  try {
443
632
  const { cmd: sgCmd, args: sgPre } = getSgCommand();
@@ -449,42 +638,81 @@ export class SgRunner {
449
638
  "--json",
450
639
  ...sgExcludeArgsForProject(dir),
451
640
  dir,
452
- ], { timeout });
453
- return this.parseScanOutput(result.stdout || result.stderr || "");
454
- }
455
- catch {
456
- return [];
641
+ ], {
642
+ timeout,
643
+ deadlineAt: options.deadlineAt,
644
+ signal: options.signal,
645
+ maxOutputBytes: MAX_SG_OUTPUT_BYTES,
646
+ });
647
+ return this.interpretScanResult(result, ["scan"]);
457
648
  }
458
649
  finally {
459
650
  this.cleanupTempScan(sessionDir);
460
651
  }
461
652
  }
653
+ /** Backwards-compatible match-only wrapper for existing session scanners. */
654
+ async tempScanAsync(dir, ruleId, ruleYaml, timeout = DEFAULT_EXEC_TIMEOUT_MS, options = {}) {
655
+ const result = await this.tempScanDetailedAsync(dir, ruleId, ruleYaml, timeout, options);
656
+ return result.matches;
657
+ }
462
658
  /**
463
659
  * Run a rule scan with optional fix application.
464
660
  * Dry-run: --json (returns matches for preview).
465
661
  * Apply: --update-all (writes fixes defined in the YAML `fix:` field).
466
662
  */
467
- async tempScanWithFixAsync(dir, ruleId, ruleYaml, applyFixes, timeout = 30000) {
663
+ async tempScanWithFixAsync(dir, ruleId, ruleYaml, applyFixes, timeout = DEFAULT_EXEC_TIMEOUT_MS, options = {}) {
468
664
  const { sessionDir, configFile } = this.prepareTempScan(ruleId, ruleYaml);
469
665
  try {
470
666
  const { cmd: sgCmd, args: sgPre } = getSgCommand();
667
+ const scanArgs = [
668
+ ...sgPre,
669
+ "scan",
670
+ "--config",
671
+ configFile,
672
+ "--json",
673
+ ...sgExcludeArgsForProject(dir),
674
+ dir,
675
+ ];
676
+ const spawnOptions = {
677
+ timeout,
678
+ deadlineAt: options.deadlineAt,
679
+ signal: options.signal,
680
+ maxOutputBytes: MAX_SG_OUTPUT_BYTES,
681
+ };
471
682
  if (!applyFixes) {
472
- const result = await safeSpawnAsync(sgCmd, [...sgPre, "scan", "--config", configFile, "--json",
473
- ...sgExcludeArgsForProject(dir), dir], { timeout });
474
- return { matches: this.parseScanOutput(result.stdout || result.stderr || "") };
683
+ const result = await safeSpawnAsync(sgCmd, scanArgs, spawnOptions);
684
+ const scan = this.interpretScanResult(result, ["scan"]);
685
+ return scan.failure || scan.error
686
+ ? { matches: [], error: scan.error }
687
+ : { matches: scan.matches };
475
688
  }
476
689
  // Apply: capture matches BEFORE writing — once --update-all applies
477
690
  // the fix the rule no longer matches, so a post-apply json pass would
478
691
  // report zero even on a successful apply. Count first, then write.
479
- const jsonResult = await safeSpawnAsync(sgCmd, [...sgPre, "scan", "--config", configFile, "--json",
480
- ...sgExcludeArgsForProject(dir), dir], { timeout });
481
- const matches = this.parseScanOutput(jsonResult.stdout || jsonResult.stderr || "");
482
- const applyResult = await safeSpawnAsync(sgCmd, [...sgPre, "scan", "--config", configFile, "--update-all",
483
- ...sgExcludeArgsForProject(dir), dir], { timeout });
484
- if (applyResult.error) {
485
- return { matches: [], error: applyResult.error.message };
692
+ const jsonResult = await safeSpawnAsync(sgCmd, scanArgs, spawnOptions);
693
+ const scan = this.interpretScanResult(jsonResult, ["scan"]);
694
+ if (scan.failure || scan.error) {
695
+ return { matches: [], error: scan.error };
696
+ }
697
+ const applyResult = await safeSpawnAsync(sgCmd, [
698
+ ...sgPre,
699
+ "scan",
700
+ "--config",
701
+ configFile,
702
+ "--update-all",
703
+ ...sgExcludeArgsForProject(dir),
704
+ dir,
705
+ ], spawnOptions);
706
+ if (applyResult.error ||
707
+ applyResult.failure ||
708
+ applyResult.status !== 0) {
709
+ return {
710
+ matches: [],
711
+ error: applyResult.error?.message ||
712
+ `ast-grep apply failed with exit code ${applyResult.status}`,
713
+ };
486
714
  }
487
- return { matches };
715
+ return { matches: scan.matches };
488
716
  }
489
717
  catch (err) {
490
718
  return { matches: [], error: String(err) };