@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
@@ -9,12 +9,17 @@
9
9
  import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
+ import { logSessionStart } from "../../../sessionstart-logger.js";
12
13
  import { getGlobalPiLensDir } from "../../../file-utils.js";
13
- import { ensureTool, isSpawnableCommand } from "../../../installer/index.js";
14
+ import { PathKeyedMap } from "../../../path-keyed-map.js";
15
+ import { normalizeEphemeralMapKey, normalizeMapKey, } from "../../../path-utils.js";
16
+ import { ensureTool, isSpawnableCommand, resetPathWalkMemo, } from "../../../installer/index.js";
14
17
  import { getServersForFileWithConfig, isServerDisabled, } from "../../../lsp/config.js";
15
18
  import { findGlobalBinary } from "../../../package-manager.js";
16
19
  import { safeSpawnAsync } from "../../../safe-spawn.js";
17
20
  import { getToolCommandSpec, shouldAutoInstallTool, } from "../../../tool-policy.js";
21
+ import { classifyProbeFailure, createAvailabilityLatch, isLatchingOutcome, logAvailabilityDecision, startHostStallSampler, transientRetryDelayMs, } from "./availability-policy.js";
22
+ export { createAvailabilityLatch, classifyProbeFailure, describeUnavailability, isTransientDecision, logAvailabilityDecision, startHostStallSampler, } from "./availability-policy.js";
18
23
  /**
19
24
  * True when the LSP runner will cover `ctx.filePath` via the given PRIMARY server
20
25
  * id. Used by CLI runners that duplicate a linter a warm LSP already wraps
@@ -61,6 +66,29 @@ if (typeof __dirname !== "undefined") {
61
66
  }
62
67
  // Managed tools directory (~/.pi-lens/tools) — where ensureTool() installs binaries
63
68
  const _managedToolsDir = path.join(getGlobalPiLensDir(), "tools");
69
+ /**
70
+ * The managed shim for a Node CLI tool (`~/.pi-lens/tools/node_modules/.bin/<tool>`),
71
+ * or null when it is not on disk.
72
+ *
73
+ * When the shim exists the tool IS installed, so availability needs no spawn at
74
+ * all — and a spawn that cannot happen cannot time out (#1467). knip and jscpd
75
+ * each carried a line-for-line copy of this resolver; #1476 folds them into one
76
+ * definition so the next managed tool inherits the fast path instead of a
77
+ * fourth copy.
78
+ *
79
+ * The pi-lens dir is read per call, never memoized at module load, so tests that
80
+ * point `getGlobalPiLensDir` at a temp home still see their own tree.
81
+ */
82
+ export function findManagedNodeToolBinary(tool) {
83
+ const base = path.join(getGlobalPiLensDir(), "tools", "node_modules", ".bin", tool);
84
+ const candidates = process.platform === "win32" ? [`${base}.cmd`, `${base}.exe`, base] : [base];
85
+ try {
86
+ return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
64
92
  /**
65
93
  * Find a command in venv first, then fall back to global.
66
94
  * Checks common venv locations (.venv, venv) before trying global.
@@ -83,6 +111,102 @@ export function createVenvFinder(command, windowsExt = "", quoteWindows = false)
83
111
  return command;
84
112
  };
85
113
  }
114
+ /** Typed client-facing install seam for ordered/custom candidate probes. */
115
+ export async function resolveManagedToolClient(options) {
116
+ const probed = await options.probe();
117
+ if (probed.outcome !== "missing")
118
+ return probed;
119
+ if (!shouldAutoInstallTool(options.toolId))
120
+ return probed;
121
+ const state = installStateFor(options.cwd, options.toolId);
122
+ if (state.suppressed)
123
+ return probed;
124
+ const installed = await ensureTool(options.toolId);
125
+ if (!installed) {
126
+ noteInstallFailure(options.toolId, options.cwd);
127
+ return probed;
128
+ }
129
+ const value = await options.acceptInstalled(installed);
130
+ if (value === null) {
131
+ noteInstallFailure(options.toolId, options.cwd);
132
+ return { outcome: "non-installable" };
133
+ }
134
+ noteInstallSuccess(options.toolId, options.cwd);
135
+ return { outcome: "success", value };
136
+ }
137
+ /**
138
+ * Child environment for managed-installable tools. Keeping this beside the
139
+ * probe/install seam makes managed npm shims visible to every standalone
140
+ * client, rather than only to Knip (#1289).
141
+ */
142
+ export async function getManagedToolEnvironment(_toolId, cwd) {
143
+ let env;
144
+ try {
145
+ const { getToolEnvironment } = await import("../../../installer/index.js");
146
+ env = await getToolEnvironment();
147
+ }
148
+ catch {
149
+ // Installer-isolated unit tests historically mock only ensureTool. The
150
+ // ambient fallback preserves that isolation; production always exports it.
151
+ env = { ...process.env };
152
+ }
153
+ if (!cwd)
154
+ return env;
155
+ const separator = process.platform === "win32" ? ";" : ":";
156
+ const currentPath = env.PATH || env.Path || process.env.PATH || "";
157
+ const localBin = path.join(cwd, "node_modules", ".bin");
158
+ const augmentedPath = `${localBin}${separator}${currentPath}`;
159
+ return {
160
+ ...env,
161
+ PATH: augmentedPath,
162
+ ...(process.platform === "win32" ? { Path: augmentedPath } : {}),
163
+ };
164
+ }
165
+ /** Read-only managed/PATH discovery for spawn-time resolution memos. */
166
+ export async function discoverManagedTool(toolId) {
167
+ return (await ensureTool(toolId, { allowInstall: false })) ?? null;
168
+ }
169
+ // This is session-scoped state, not a process-global tool/path cache. The cwd
170
+ // key is normalized by PathKeyedMap and is cleared at session_start. A failed
171
+ // install must not become an install attempt on every eligible file/turn.
172
+ const installAttemptsByCwd = new PathKeyedMap(normalizeMapKey);
173
+ const resolveInstallInFlightByCwd = new PathKeyedMap(normalizeEphemeralMapKey);
174
+ // Checkers are created by runner modules and may also be created dynamically.
175
+ // Keep the session reset as a generation rather than retaining every checker
176
+ // reset closure forever.
177
+ let availabilityStateGeneration = 0;
178
+ function installStateFor(cwd, toolId) {
179
+ let states = installAttemptsByCwd.get(cwd);
180
+ if (!states) {
181
+ states = new Map();
182
+ installAttemptsByCwd.set(cwd, states);
183
+ }
184
+ let state = states.get(toolId);
185
+ if (!state) {
186
+ state = { attempts: 0, suppressed: false };
187
+ states.set(toolId, state);
188
+ }
189
+ return state;
190
+ }
191
+ function noteInstallFailure(toolId, cwd) {
192
+ const state = installStateFor(cwd, toolId);
193
+ state.attempts += 1;
194
+ state.suppressed = true;
195
+ logSessionStart(`dispatch availability ${toolId}: install attempt ${state.attempts} failed; suppressing retries until the next session or a successful install`);
196
+ }
197
+ function noteInstallSuccess(toolId, cwd) {
198
+ const states = installAttemptsByCwd.get(cwd);
199
+ states?.delete(toolId);
200
+ if (states?.size === 0)
201
+ installAttemptsByCwd.delete(cwd);
202
+ }
203
+ /** Reset availability/install suppression at the session boundary. */
204
+ export function resetDispatchAvailabilityState() {
205
+ installAttemptsByCwd.clear();
206
+ resolveInstallInFlightByCwd.clear();
207
+ resetPathWalkMemo();
208
+ availabilityStateGeneration += 1;
209
+ }
86
210
  /**
87
211
  * Create a cached availability checker for a command.
88
212
  * The checker will look for the command in venv first, then global.
@@ -92,50 +216,231 @@ export function createVenvFinder(command, windowsExt = "", quoteWindows = false)
92
216
  * `zig --version`). Passing the wrong probe makes the runner silently skip on
93
217
  * every machine, so toolchains with a non-standard version command must override
94
218
  * this.
219
+ *
220
+ * ## Latch policy (#1467)
221
+ *
222
+ * A `missing` / `non-installable` verdict is durable and is cached for the
223
+ * session. A `transient` verdict — timeout, abort, EAGAIN — is NOT: it is
224
+ * cached only for a bounded cooldown, after which the next caller re-probes.
225
+ * An installed tool therefore recovers on its own, without a host restart.
95
226
  */
96
- export function createAvailabilityChecker(command, windowsExt = "", versionArgs = ["--version"]) {
97
- const cacheByCwd = new Map();
98
- const inFlightByCwd = new Map();
227
+ export function createAvailabilityChecker(command, windowsExt = "", versionArgs = ["--version"], options = {}) {
228
+ const cacheByCwd = new PathKeyedMap(normalizeEphemeralMapKey);
229
+ const inFlightByCwd = new PathKeyedMap(normalizeEphemeralMapKey);
230
+ let checkerGeneration = availabilityStateGeneration;
99
231
  const findCommand = createVenvFinder(command, windowsExt, true);
232
+ function ensureCurrentGeneration() {
233
+ if (checkerGeneration === availabilityStateGeneration)
234
+ return;
235
+ cacheByCwd.clear();
236
+ inFlightByCwd.clear();
237
+ checkerGeneration = availabilityStateGeneration;
238
+ }
239
+ const reset = () => {
240
+ cacheByCwd.clear();
241
+ inFlightByCwd.clear();
242
+ checkerGeneration = availabilityStateGeneration;
243
+ };
100
244
  function getCache(cwd) {
245
+ ensureCurrentGeneration();
101
246
  const key = path.resolve(cwd || process.cwd());
102
247
  const existing = cacheByCwd.get(key);
103
248
  if (existing)
104
249
  return existing;
105
- const created = { available: null, command: null };
250
+ const created = {
251
+ available: null,
252
+ command: null,
253
+ outcome: null,
254
+ cause: null,
255
+ elapsedMs: 0,
256
+ retryAtMs: 0,
257
+ transientAttempts: 0,
258
+ };
106
259
  cacheByCwd.set(key, created);
107
260
  return created;
108
261
  }
262
+ /** Record a verdict on the cache and emit exactly one decision record. */
263
+ function noteDecision(cache, resolvedCwd, verdict) {
264
+ cache.available = verdict.available;
265
+ cache.outcome = verdict.outcome;
266
+ cache.cause = verdict.cause;
267
+ cache.elapsedMs = verdict.elapsedMs;
268
+ let retryAfterMs;
269
+ if (verdict.available) {
270
+ cache.retryAtMs = 0;
271
+ cache.transientAttempts = 0;
272
+ }
273
+ else if (isLatchingOutcome(verdict.outcome)) {
274
+ cache.retryAtMs = 0;
275
+ cache.transientAttempts = 0;
276
+ }
277
+ else {
278
+ cache.transientAttempts += 1;
279
+ retryAfterMs = transientRetryDelayMs(cache.transientAttempts, verdict.cause);
280
+ cache.retryAtMs = Date.now() + retryAfterMs;
281
+ }
282
+ logAvailabilityDecision({
283
+ tool: command,
284
+ verdict: verdict.available ? "available" : "unavailable",
285
+ outcome: verdict.outcome,
286
+ cause: verdict.cause,
287
+ elapsedMs: verdict.elapsedMs,
288
+ latched: verdict.available || isLatchingOutcome(verdict.outcome),
289
+ ...(verdict.hostStallMs !== undefined && {
290
+ hostStallMs: verdict.hostStallMs,
291
+ }),
292
+ ...(retryAfterMs !== undefined && { retryAfterMs }),
293
+ budgetMs: options.probeTimeout ?? 5000,
294
+ }, resolvedCwd);
295
+ }
109
296
  async function isAvailableAsync(cwd) {
297
+ ensureCurrentGeneration();
110
298
  const resolvedCwd = cwd || process.cwd();
111
299
  const cache = getCache(resolvedCwd);
112
- if (cache.available !== null)
113
- return cache.available;
300
+ if (cache.available === false) {
301
+ // A durable "this machine does not have the tool" stays cached; a
302
+ // transient probe failure only holds until its cooldown expires, so an
303
+ // installed tool cannot be disabled for the life of the process by one
304
+ // slow second at warm-up (#1467).
305
+ if (cache.outcome !== "transient")
306
+ return false;
307
+ if (Date.now() < cache.retryAtMs)
308
+ return false;
309
+ cache.available = null;
310
+ }
311
+ if (cache.available === true && cache.command) {
312
+ if (await isSpawnableCommand(cache.command))
313
+ return true;
314
+ // Cached-positive spawn feedback: a removed absolute path or vanished
315
+ // PATH command must fall through to a fresh probe immediately.
316
+ cache.available = null;
317
+ cache.command = null;
318
+ cache.outcome = null;
319
+ cache.cause = null;
320
+ }
114
321
  const key = path.resolve(resolvedCwd);
115
322
  const existing = inFlightByCwd.get(key);
116
323
  if (existing)
117
324
  return existing;
118
- const promise = (async () => {
325
+ const promiseGeneration = checkerGeneration;
326
+ let promise;
327
+ promise = (async () => {
328
+ const fastPath = options.fastPath?.();
329
+ if (fastPath) {
330
+ cache.command = fastPath;
331
+ noteDecision(cache, resolvedCwd, {
332
+ available: true,
333
+ outcome: "success",
334
+ cause: "fast-path",
335
+ elapsedMs: 0,
336
+ });
337
+ return true;
338
+ }
339
+ // A bad/removed workspace must not be mistaken for a missing tool and
340
+ // trigger an install. This async probe stays off the synchronous dispatch
341
+ // burst and makes the failure taxonomy explicit at the seam.
342
+ try {
343
+ const cwdStat = await fs.promises.stat(resolvedCwd);
344
+ if (!cwdStat.isDirectory()) {
345
+ noteDecision(cache, resolvedCwd, {
346
+ available: false,
347
+ outcome: "non-installable",
348
+ cause: "bad-cwd",
349
+ elapsedMs: 0,
350
+ });
351
+ return false;
352
+ }
353
+ }
354
+ catch {
355
+ noteDecision(cache, resolvedCwd, {
356
+ available: false,
357
+ outcome: "non-installable",
358
+ cause: "bad-cwd",
359
+ elapsedMs: 0,
360
+ });
361
+ return false;
362
+ }
119
363
  const cmd = findCommand(resolvedCwd);
120
- const result = await safeSpawnAsync(cmd, versionArgs, {
121
- timeout: 5000,
122
- });
123
- cache.available = !result.error && result.status === 0;
124
- if (cache.available) {
364
+ const env = await options.environment?.(resolvedCwd);
365
+ // The probe budget is enforced by a HOST-side timer, so host event-loop
366
+ // stalls are charged to the child. Measure the stall that overlapped the
367
+ // window and hand it to the classifier (#1467).
368
+ const stallSampler = startHostStallSampler();
369
+ const startedAt = Date.now();
370
+ let result;
371
+ let hostStallMs;
372
+ try {
373
+ result = await safeSpawnAsync(cmd, versionArgs, {
374
+ timeout: options.probeTimeout ?? 5000,
375
+ cwd: resolvedCwd,
376
+ env,
377
+ });
378
+ }
379
+ finally {
380
+ hostStallMs = stallSampler.stop();
381
+ }
382
+ const elapsedMs = Date.now() - startedAt;
383
+ if (!result.error && result.status === 0) {
125
384
  cache.command = cmd;
385
+ noteDecision(cache, resolvedCwd, {
386
+ available: true,
387
+ outcome: "success",
388
+ cause: "ok",
389
+ elapsedMs,
390
+ hostStallMs,
391
+ });
392
+ return true;
126
393
  }
127
- return cache.available;
394
+ const { outcome, cause } = classifyProbeFailure(result, {
395
+ hostStallMs,
396
+ unclassifiedFailureOutcome: options.unclassifiedFailureOutcome,
397
+ });
398
+ // Only a TYPED tool-not-found invalidates the PATH walk memo; an
399
+ // `unclassifiedFailureOutcome: "missing"` compatibility verdict is a
400
+ // guess, not evidence that PATH changed.
401
+ if (result.spawnFailure?.kind === "tool-not-found")
402
+ resetPathWalkMemo();
403
+ noteDecision(cache, resolvedCwd, {
404
+ available: false,
405
+ outcome,
406
+ cause,
407
+ elapsedMs,
408
+ hostStallMs,
409
+ });
410
+ return false;
128
411
  })().finally(() => {
129
- inFlightByCwd.delete(key);
412
+ // A session reset clears this map and a caller may immediately start a
413
+ // replacement probe for the same cwd. The old promise must not delete
414
+ // that newer-generation entry when it settles.
415
+ if (checkerGeneration === promiseGeneration &&
416
+ inFlightByCwd.get(key) === promise) {
417
+ inFlightByCwd.delete(key);
418
+ }
130
419
  });
131
420
  inFlightByCwd.set(key, promise);
132
421
  return promise;
133
422
  }
134
423
  function getCommand(cwd) {
424
+ ensureCurrentGeneration();
135
425
  const cache = getCache(cwd || process.cwd());
136
426
  return cache.command;
137
427
  }
138
- return { isAvailableAsync, getCommand };
428
+ function getOutcome(cwd) {
429
+ ensureCurrentGeneration();
430
+ return getCache(cwd || process.cwd()).outcome;
431
+ }
432
+ function getVerdict(cwd) {
433
+ ensureCurrentGeneration();
434
+ const cache = getCache(cwd || process.cwd());
435
+ return {
436
+ outcome: cache.outcome,
437
+ cause: cache.cause,
438
+ elapsedMs: cache.elapsedMs,
439
+ latched: cache.available !== false || isLatchingOutcome(cache.outcome ?? "missing"),
440
+ retryAtMs: cache.retryAtMs,
441
+ };
442
+ }
443
+ return { isAvailableAsync, getCommand, getOutcome, getVerdict, reset };
139
444
  }
140
445
  /**
141
446
  * Per-cwd cached availability probe for spawn signatures that don't fit
@@ -150,8 +455,13 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
150
455
  * fresh probe for retry-after-install flows.
151
456
  */
152
457
  export function createCwdCachedProbe(probe) {
153
- const cacheByCwd = new Map();
458
+ const cacheByCwd = new PathKeyedMap(normalizeEphemeralMapKey);
459
+ let probeGeneration = availabilityStateGeneration;
154
460
  return (cwd) => {
461
+ if (probeGeneration !== availabilityStateGeneration) {
462
+ cacheByCwd.clear();
463
+ probeGeneration = availabilityStateGeneration;
464
+ }
155
465
  const key = path.resolve(cwd || process.cwd());
156
466
  const existing = cacheByCwd.get(key);
157
467
  if (existing)
@@ -209,7 +519,8 @@ export async function resolveToolCommandWithInstallFallback(cwd, toolId, timeout
209
519
  async function verifyOrInstallCommand(command, toolId, cwd, versionArgs = ["--version"], timeout = 5000) {
210
520
  // Skip the --version spawn when the command isn't even on disk — the ~μs
211
521
  // stat/PATH walk beats a guaranteed-to-fail spawn round-trip.
212
- if (await isSpawnableCommand(command)) {
522
+ const spawnable = await isSpawnableCommand(command);
523
+ if (spawnable) {
213
524
  const versionCheck = await safeSpawnAsync(command, versionArgs, {
214
525
  timeout,
215
526
  cwd,
@@ -217,16 +528,22 @@ async function verifyOrInstallCommand(command, toolId, cwd, versionArgs = ["--ve
217
528
  if (!versionCheck.error && versionCheck.status === 0) {
218
529
  return command;
219
530
  }
531
+ // A command that was found but rejected its probe is not fixed by a
532
+ // reinstall. This also covers permissions and malformed shims.
533
+ return null;
220
534
  }
221
- if (!shouldAutoInstallTool(toolId)) {
535
+ if (!shouldAutoInstallTool(toolId))
536
+ return null;
537
+ const state = installStateFor(cwd, toolId);
538
+ if (state.suppressed)
222
539
  return null;
540
+ const installed = await ensureTool(toolId);
541
+ if (installed) {
542
+ noteInstallSuccess(toolId, cwd);
543
+ return installed;
223
544
  }
224
- // ensureTool's result is already trusted: a probe-cache hit validated
225
- // path+mtime, and a fresh install verified the binary. Re-probing
226
- // --version here booted the tool's node shim once per invocation for
227
- // zero new information; a broken install now surfaces as the runner's
228
- // own spawn error instead of a silent null.
229
- return (await ensureTool(toolId)) ?? null;
545
+ noteInstallFailure(toolId, cwd);
546
+ return null;
230
547
  }
231
548
  export async function resolveCommandArgsWithInstallFallback(command, toolId, cwd, versionArgs = ["--version"], timeout = 5000) {
232
549
  const versionCheck = await safeSpawnAsync(command.cmd, [...command.args, ...versionArgs], { timeout, cwd });
@@ -245,34 +562,99 @@ export async function resolveCommandArgsWithInstallFallback(command, toolId, cwd
245
562
  export async function resolveCommandWithInstallFallback(command, toolId, cwd, versionArgs = ["--version"], timeout = 5000) {
246
563
  return verifyOrInstallCommand(command, toolId, cwd, versionArgs, timeout);
247
564
  }
248
- export async function resolveAvailableOrInstall(checker, toolId, cwd) {
565
+ async function resolveAvailableOrInstallUnshared(checker, toolId, cwd) {
249
566
  const available = await checker.isAvailableAsync(cwd);
250
567
  if (available) {
251
568
  return checker.getCommand(cwd);
252
569
  }
253
- if (!shouldAutoInstallTool(toolId)) {
570
+ // Only a typed ENOENT/missing-command result is repairable by installing.
571
+ // Probe failures caused by bad cwd, permissions, rejected flags, aborts, and
572
+ // timeouts are unavailable/non-installable and must not enter an install loop.
573
+ if (checker.getOutcome?.(cwd) !== "missing")
574
+ return null;
575
+ if (!shouldAutoInstallTool(toolId))
576
+ return null;
577
+ const state = installStateFor(cwd, toolId);
578
+ if (state.suppressed) {
254
579
  return null;
255
580
  }
256
581
  const installed = await ensureTool(toolId);
257
- return installed ?? null;
582
+ if (installed) {
583
+ noteInstallSuccess(toolId, cwd);
584
+ checker.reset?.();
585
+ return installed;
586
+ }
587
+ noteInstallFailure(toolId, cwd);
588
+ return null;
589
+ }
590
+ /** Share the complete probe/install transaction for each cwd/tool pair. */
591
+ export function resolveAvailableOrInstall(checker, toolId, cwd) {
592
+ const key = normalizeEphemeralMapKey(cwd);
593
+ let byTool = resolveInstallInFlightByCwd.get(key);
594
+ if (!byTool) {
595
+ byTool = new Map();
596
+ resolveInstallInFlightByCwd.set(key, byTool);
597
+ }
598
+ const existing = byTool.get(toolId);
599
+ if (existing)
600
+ return existing;
601
+ const generation = availabilityStateGeneration;
602
+ const promise = resolveAvailableOrInstallUnshared(checker, toolId, cwd).finally(() => {
603
+ if (generation !== availabilityStateGeneration)
604
+ return;
605
+ const current = resolveInstallInFlightByCwd.get(key);
606
+ if (current?.get(toolId) === promise) {
607
+ current.delete(toolId);
608
+ if (current.size === 0)
609
+ resolveInstallInFlightByCwd.delete(key);
610
+ }
611
+ });
612
+ byTool.set(toolId, promise);
613
+ return promise;
258
614
  }
259
615
  // =============================================================================
260
616
  // SHARED AST-GREP AVAILABILITY
261
617
  // =============================================================================
262
- // Shared ast-grep availability cache across all slop runners
263
- let sgAvailable = null;
618
+ /**
619
+ * Shared ast-grep availability across all slop runners, behind the transient-
620
+ * aware latch (#1476). This module-level memo carried the same shape `SgRunner`
621
+ * did — one failed sweep, including a timeout, disabled ast-grep for every slop
622
+ * runner for the life of the process.
623
+ */
624
+ const sgLatch = createAvailabilityLatch();
264
625
  let sgCmd = null;
265
626
  let sgCmdArgs = [];
627
+ /** Classification of the current sweep, accumulated across candidates. */
628
+ let sgSweepSawTransient = false;
629
+ let sgSweepTransientCause = "probe-timeout";
630
+ let sgSweepHostStallMs = 0;
266
631
  function isAstGrepVersionOutput(output) {
267
632
  return /\bast[- ]grep\b/i.test(output);
268
633
  }
269
634
  async function probeAstGrepCommandAsync(cmd, argsPrefix = []) {
270
- const check = await safeSpawnAsync(cmd, [...argsPrefix, "--version"], {
271
- timeout: 5000,
272
- });
273
- return (!check.error &&
635
+ const sampler = startHostStallSampler();
636
+ let check;
637
+ let hostStallMs;
638
+ try {
639
+ check = await safeSpawnAsync(cmd, [...argsPrefix, "--version"], {
640
+ timeout: 5000,
641
+ });
642
+ }
643
+ finally {
644
+ hostStallMs = sampler.stop();
645
+ sgSweepHostStallMs += hostStallMs;
646
+ }
647
+ if (!check.error &&
274
648
  check.status === 0 &&
275
- isAstGrepVersionOutput(`${check.stdout}\n${check.stderr}`));
649
+ isAstGrepVersionOutput(`${check.stdout}\n${check.stderr}`)) {
650
+ return true;
651
+ }
652
+ const { outcome, cause } = classifyProbeFailure(check, { hostStallMs });
653
+ if (outcome === "transient") {
654
+ sgSweepSawTransient = true;
655
+ sgSweepTransientCause = cause;
656
+ }
657
+ return false;
276
658
  }
277
659
  /** Pre-filter local node_modules/.bin candidates that actually exist on disk. */
278
660
  function buildSgLocalBins() {
@@ -302,18 +684,36 @@ function buildSgLocalBins() {
302
684
  return bins;
303
685
  }
304
686
  let sgAvailableInFlight = null;
687
+ let sgAvailabilityGeneration = availabilityStateGeneration;
688
+ function ensureCurrentSgGeneration() {
689
+ if (sgAvailabilityGeneration === availabilityStateGeneration)
690
+ return;
691
+ sgLatch.reset();
692
+ sgCmd = null;
693
+ sgCmdArgs = [];
694
+ sgAvailableInFlight = null;
695
+ sgAvailabilityGeneration = availabilityStateGeneration;
696
+ }
305
697
  export async function isSgAvailableAsync() {
306
- if (sgAvailable !== null)
307
- return sgAvailable;
698
+ ensureCurrentSgGeneration();
699
+ // `read()` returns null when the last verdict was transient and its cooldown
700
+ // expired: re-probe rather than stay dead for the session (#1476).
701
+ const memo = sgLatch.read();
702
+ if (memo !== null)
703
+ return memo;
308
704
  if (sgAvailableInFlight)
309
705
  return sgAvailableInFlight;
310
706
  sgAvailableInFlight = (async () => {
707
+ const startedAt = Date.now();
708
+ sgSweepSawTransient = false;
709
+ sgSweepTransientCause = "probe-timeout";
710
+ sgSweepHostStallMs = 0;
311
711
  // 1. Local node_modules/.bin
312
712
  for (const localBin of buildSgLocalBins()) {
313
713
  if (await probeAstGrepCommandAsync(localBin)) {
314
714
  sgCmd = localBin;
315
715
  sgCmdArgs = [];
316
- sgAvailable = true;
716
+ noteSgAvailable(startedAt);
317
717
  return true;
318
718
  }
319
719
  }
@@ -322,7 +722,7 @@ export async function isSgAvailableAsync() {
322
722
  if (await probeAstGrepCommandAsync(cmd)) {
323
723
  sgCmd = cmd;
324
724
  sgCmdArgs = [];
325
- sgAvailable = true;
725
+ noteSgAvailable(startedAt);
326
726
  return true;
327
727
  }
328
728
  }
@@ -333,7 +733,7 @@ export async function isSgAvailableAsync() {
333
733
  if (globalBin && (await probeAstGrepCommandAsync(globalBin))) {
334
734
  sgCmd = globalBin;
335
735
  sgCmdArgs = [];
336
- sgAvailable = true;
736
+ noteSgAvailable(startedAt);
337
737
  return true;
338
738
  }
339
739
  }
@@ -341,17 +741,48 @@ export async function isSgAvailableAsync() {
341
741
  if (await probeAstGrepCommandAsync("npx", ["--no", "--", "ast-grep"])) {
342
742
  sgCmd = "npx";
343
743
  sgCmdArgs = ["--no", "--", "ast-grep"];
344
- sgAvailable = true;
744
+ noteSgAvailable(startedAt);
345
745
  return true;
346
746
  }
347
- sgAvailable = false;
747
+ // A timeout on ANY candidate is evidence about the host, not the tool.
748
+ noteSgUnavailable(startedAt, sgSweepSawTransient ? "transient" : "missing", sgSweepSawTransient ? sgSweepTransientCause : "not-found");
348
749
  return false;
349
750
  })().finally(() => {
350
751
  sgAvailableInFlight = null;
351
752
  });
352
753
  return sgAvailableInFlight;
353
754
  }
755
+ /** Record a successful shared-ast-grep sweep, with one decision record. */
756
+ function noteSgAvailable(startedAt) {
757
+ sgLatch.noteAvailable();
758
+ logAvailabilityDecision({
759
+ tool: "ast-grep",
760
+ verdict: "available",
761
+ outcome: "success",
762
+ cause: "ok",
763
+ elapsedMs: Date.now() - startedAt,
764
+ latched: true,
765
+ hostStallMs: sgSweepHostStallMs,
766
+ budgetMs: 5000,
767
+ });
768
+ }
769
+ /** Record a failed shared-ast-grep sweep; a transient verdict expires. */
770
+ function noteSgUnavailable(startedAt, outcome, cause) {
771
+ const retryAfterMs = sgLatch.noteUnavailable(outcome, cause);
772
+ logAvailabilityDecision({
773
+ tool: "ast-grep",
774
+ verdict: "unavailable",
775
+ outcome,
776
+ cause,
777
+ elapsedMs: Date.now() - startedAt,
778
+ latched: outcome !== "transient",
779
+ hostStallMs: sgSweepHostStallMs,
780
+ ...(retryAfterMs > 0 && { retryAfterMs }),
781
+ budgetMs: 5000,
782
+ });
783
+ }
354
784
  export function getSgCommand() {
785
+ ensureCurrentSgGeneration();
355
786
  return {
356
787
  cmd: sgCmd ?? "npx",
357
788
  args: sgCmdArgs.length ? sgCmdArgs : ["--no", "--", "ast-grep"],