@herbertgao/pi-extensions 2026.8.5 → 2026.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (389) hide show
  1. package/README.md +5 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +19 -7
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +30 -9
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +272 -22
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +94 -77
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +48 -6
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +1 -1
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +14 -0
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -17
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +41 -2
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +75 -6
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +7 -3
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +64 -1
  17. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  18. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +16 -0
  19. package/node_modules/@herbertgao/pi-subagents/README.md +30 -22
  20. package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +5 -5
  21. package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
  22. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +7 -1
  23. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +48 -19
  24. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +22 -15
  25. package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +26 -2
  26. package/node_modules/@herbertgao/pi-subagents/src/index.ts +219 -40
  27. package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +101 -3
  28. package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +18 -4
  29. package/node_modules/@herbertgao/pi-subagents/src/settings.ts +99 -0
  30. package/node_modules/@herbertgao/pi-subagents/src/types.ts +22 -3
  31. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +51 -3
  32. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +20 -2
  33. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +34 -10
  34. package/node_modules/@herbertgao/pi-subagents/src/usage.ts +129 -1
  35. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +29 -5
  36. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +3 -0
  37. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +46 -10
  38. package/node_modules/@juicesharp/rpiv-ask-user-question/config.ts +25 -0
  39. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/configuration.md +13 -4
  40. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +8 -1
  41. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +3 -1
  42. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +2 -2
  43. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/zh.json +2 -2
  44. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  45. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +10 -0
  46. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +3 -1
  47. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +30 -7
  48. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
  49. package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +30 -4
  50. package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +26 -5
  51. package/node_modules/@narumitw/pi-btw/README.md +119 -103
  52. package/node_modules/@narumitw/pi-btw/dist/index.ts +2898 -0
  53. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +7 -0
  54. package/node_modules/@narumitw/pi-btw/package.json +13 -9
  55. package/node_modules/@narumitw/pi-btw/src/btw.ts +40 -42
  56. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +55 -10
  57. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +390 -0
  58. package/node_modules/@narumitw/pi-btw/src/menu.ts +78 -15
  59. package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
  60. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
  61. package/node_modules/pi-lens/CHANGELOG.md +1004 -0
  62. package/node_modules/pi-lens/README.md +11 -3
  63. package/node_modules/pi-lens/config/ruff/core.toml +42 -0
  64. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +130 -27
  65. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +427 -0
  66. package/node_modules/pi-lens/dist/clients/agent-nudge.js +79 -16
  67. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +9 -3
  68. package/node_modules/pi-lens/dist/clients/atomic-write.js +9 -3
  69. package/node_modules/pi-lens/dist/clients/bash-file-access.js +285 -8
  70. package/node_modules/pi-lens/dist/clients/biome-client.js +176 -37
  71. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
  72. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +119 -0
  73. package/node_modules/pi-lens/dist/clients/bootstrap.js +12 -1
  74. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +12 -3
  75. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +192 -0
  76. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
  77. package/node_modules/pi-lens/dist/clients/bus-publish.js +11 -3
  78. package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
  79. package/node_modules/pi-lens/dist/clients/cascade-format.js +76 -2
  80. package/node_modules/pi-lens/dist/clients/cascade-types.js +9 -1
  81. package/node_modules/pi-lens/dist/clients/child-unref.js +54 -6
  82. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +76 -24
  83. package/node_modules/pi-lens/dist/clients/console-guard-install.js +16 -4
  84. package/node_modules/pi-lens/dist/clients/dead-code-client.js +249 -52
  85. package/node_modules/pi-lens/dist/clients/deadline-utils.js +8 -1
  86. package/node_modules/pi-lens/dist/clients/debug-handles.js +5 -0
  87. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +62 -13
  88. package/node_modules/pi-lens/dist/clients/dependency-checker.js +43 -9
  89. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +153 -37
  90. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
  91. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  92. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +165 -11
  93. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +325 -72
  94. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +7 -4
  95. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +5 -0
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +234 -18
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +187 -19
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +6 -3
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +11 -9
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +398 -0
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +11 -3
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +12 -7
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +18 -0
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +895 -0
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +4 -0
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +16 -3
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +81 -41
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -0
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +23 -0
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +331 -55
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +38 -5
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +97 -19
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +14 -0
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +20 -0
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +22 -0
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +10 -0
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +18 -6
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +73 -12
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +488 -0
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +84 -0
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +260 -27
  122. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +943 -93
  123. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +86 -11
  124. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +72 -0
  125. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +106 -0
  126. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +9 -0
  127. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -1
  128. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
  129. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  130. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
  131. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
  132. package/node_modules/pi-lens/dist/clients/extension-log.js +317 -3
  133. package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
  134. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +294 -0
  135. package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
  136. package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
  137. package/node_modules/pi-lens/dist/clients/format-events-publish.js +39 -8
  138. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +3 -2
  139. package/node_modules/pi-lens/dist/clients/formatters.js +500 -115
  140. package/node_modules/pi-lens/dist/clients/generation-guard.js +216 -0
  141. package/node_modules/pi-lens/dist/clients/git-guard.js +49 -21
  142. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
  143. package/node_modules/pi-lens/dist/clients/go-client.js +21 -39
  144. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +219 -10
  145. package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
  146. package/node_modules/pi-lens/dist/clients/host-ports.js +9 -1
  147. package/node_modules/pi-lens/dist/clients/installer/index.js +1119 -101
  148. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +58 -0
  149. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +715 -0
  150. package/node_modules/pi-lens/dist/clients/instance-reaper.js +643 -97
  151. package/node_modules/pi-lens/dist/clients/instance-registry.js +87 -17
  152. package/node_modules/pi-lens/dist/clients/jscpd-client.js +86 -15
  153. package/node_modules/pi-lens/dist/clients/knip-client.js +361 -24
  154. package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
  155. package/node_modules/pi-lens/dist/clients/language-profile.js +2 -0
  156. package/node_modules/pi-lens/dist/clients/latency-logger.js +487 -9
  157. package/node_modules/pi-lens/dist/clients/lazy-import.js +21 -0
  158. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -0
  159. package/node_modules/pi-lens/dist/clients/lens-events.js +56 -25
  160. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +16 -0
  161. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +45 -2
  162. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
  163. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +160 -20
  164. package/node_modules/pi-lens/dist/clients/lsp/client.js +1666 -148
  165. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +111 -0
  166. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
  167. package/node_modules/pi-lens/dist/clients/lsp/index.js +2496 -171
  168. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +215 -0
  169. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +146 -14
  170. package/node_modules/pi-lens/dist/clients/lsp/language.js +48 -4
  171. package/node_modules/pi-lens/dist/clients/lsp/server.js +517 -69
  172. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +22 -0
  173. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +43 -0
  174. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +172 -0
  175. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
  176. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +61 -3
  177. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +422 -28
  178. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +30 -0
  179. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +175 -0
  180. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +3 -2
  181. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +4 -0
  182. package/node_modules/pi-lens/dist/clients/mcp/session.js +39 -18
  183. package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
  184. package/node_modules/pi-lens/dist/clients/module-report.js +8 -0
  185. package/node_modules/pi-lens/dist/clients/opengrep-client.js +15 -1
  186. package/node_modules/pi-lens/dist/clients/package-manager.js +170 -14
  187. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +27 -0
  188. package/node_modules/pi-lens/dist/clients/path-utils.js +160 -0
  189. package/node_modules/pi-lens/dist/clients/pipeline.js +178 -30
  190. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +37 -0
  191. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +17 -3
  192. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +89 -22
  193. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +85 -15
  194. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +15 -3
  195. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
  196. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +50 -2
  197. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +5 -0
  198. package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
  199. package/node_modules/pi-lens/dist/clients/project-snapshot.js +299 -0
  200. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +47 -0
  201. package/node_modules/pi-lens/dist/clients/quiet-window.js +11 -25
  202. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +1 -1
  203. package/node_modules/pi-lens/dist/clients/read-guard.js +184 -10
  204. package/node_modules/pi-lens/dist/clients/resource-sampler.js +49 -17
  205. package/node_modules/pi-lens/dist/clients/reverse-deps.js +50 -11
  206. package/node_modules/pi-lens/dist/clients/review-graph/query.js +76 -4
  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 +248 -13
  209. package/node_modules/pi-lens/dist/clients/runtime-context.js +112 -12
  210. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +472 -61
  211. package/node_modules/pi-lens/dist/clients/runtime-session.js +359 -13
  212. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
  213. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +269 -22
  214. package/node_modules/pi-lens/dist/clients/runtime-turn.js +948 -87
  215. package/node_modules/pi-lens/dist/clients/rust-client.js +21 -37
  216. package/node_modules/pi-lens/dist/clients/safe-spawn.js +220 -7
  217. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +161 -0
  218. package/node_modules/pi-lens/dist/clients/search-read-registration.js +34 -6
  219. package/node_modules/pi-lens/dist/clients/security-scan-client.js +224 -8
  220. package/node_modules/pi-lens/dist/clients/sg-runner.js +249 -37
  221. package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
  222. package/node_modules/pi-lens/dist/clients/smells-rollup.js +18 -11
  223. package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
  224. package/node_modules/pi-lens/dist/clients/startup-timing.js +26 -2
  225. package/node_modules/pi-lens/dist/clients/test-runner-client.js +574 -32
  226. package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
  227. package/node_modules/pi-lens/dist/clients/tool-policy.js +127 -20
  228. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
  229. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +75 -1
  230. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +769 -64
  231. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +11 -0
  232. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +90 -0
  233. package/node_modules/pi-lens/dist/clients/trivy-client.js +19 -1
  234. package/node_modules/pi-lens/dist/clients/warm-attach.js +17 -0
  235. package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
  236. package/node_modules/pi-lens/dist/clients/word-index.js +305 -33
  237. package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
  238. package/node_modules/pi-lens/dist/index.js +64677 -49484
  239. package/node_modules/pi-lens/dist/mcp/server.js +8 -2
  240. package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
  241. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
  242. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
  243. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +469 -56
  244. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +190 -46
  245. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
  246. package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
  247. package/node_modules/pi-lens/docs/agent-guide.md +44 -14
  248. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +518 -501
  249. package/node_modules/pi-lens/docs/dispositions.md +21 -0
  250. package/node_modules/pi-lens/docs/features.md +33 -6
  251. package/node_modules/pi-lens/docs/globalconfig.md +14 -0
  252. package/node_modules/pi-lens/docs/language-coverage.md +71 -5
  253. package/node_modules/pi-lens/docs/mcp.md +2 -2
  254. package/node_modules/pi-lens/docs/servercapabilities.md +4 -1
  255. package/node_modules/pi-lens/docs/settings.md +25 -1
  256. package/node_modules/pi-lens/docs/usage.md +24 -5
  257. package/node_modules/pi-lens/docs/word-index.md +35 -0
  258. package/node_modules/pi-lens/package.json +10 -6
  259. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-js-snapshot.yml +23 -0
  260. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-snapshot.yml +23 -0
  261. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/ts-void-zero-snapshot.yml +23 -0
  262. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-object-param-test.yml +16 -0
  263. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +22 -0
  264. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-compile-call-test.yml +29 -1
  265. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
  266. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
  267. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-dupe-class-members-test.yml +33 -0
  268. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-js-test.yml +7 -1
  269. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-test.yml +7 -1
  270. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-known-value-widening-test.yml +9 -0
  271. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-non-null-assertion-test.yml +216 -0
  272. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +9 -0
  273. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +9 -0
  274. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +9 -0
  275. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +9 -0
  276. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-return-value-in-generator-test.yml +12 -0
  277. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-runtime-typeof-test.yml +33 -0
  278. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-server-bind-wildcard-test.yml +9 -0
  279. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-shape-in-symbol-names-test.yml +13 -0
  280. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-js-test.yml +5 -1
  281. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-test.yml +5 -1
  282. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
  283. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-parameters-test.yml +12 -0
  284. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-returns-test.yml +16 -0
  285. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-any-test.yml +10 -0
  286. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-unknown-test.yml +8 -0
  287. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/redundant-unsafe-function-test.yml +35 -0
  288. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/require-safety-comment-for-as-unknown-as-test.yml +177 -0
  289. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ts-void-zero-test.yml +1 -0
  290. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member-js.yml +13 -2
  291. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member.yml +13 -2
  292. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +2 -0
  293. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-object-param.yml +60 -0
  294. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +66 -0
  295. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-compile-call.yml +63 -5
  296. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
  297. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
  298. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-dupe-class-members.yml +52 -8
  299. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html-js.yml +27 -2
  300. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html.yml +27 -2
  301. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-known-value-widening.yml +71 -0
  302. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-non-null-assertion.yml +316 -0
  303. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +7 -0
  304. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +29 -0
  305. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +30 -0
  306. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +41 -0
  307. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +42 -0
  308. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-return-value-in-generator.yml +22 -5
  309. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-runtime-typeof.yml +121 -0
  310. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-server-bind-wildcard.yml +23 -7
  311. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-shape-in-symbol-names.yml +68 -0
  312. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +41 -2
  313. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code.yml +41 -2
  314. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +32 -0
  315. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-parameters.yml +67 -0
  316. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-returns.yml +106 -0
  317. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-any.yml +50 -0
  318. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-unknown.yml +56 -0
  319. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +2 -0
  320. package/node_modules/pi-lens/rules/ast-grep-rules/rules/redundant-unsafe-function.yml +32 -0
  321. package/node_modules/pi-lens/rules/ast-grep-rules/rules/require-safety-comment-for-as-unknown-as.yml +184 -0
  322. package/node_modules/pi-lens/rules/ast-grep-rules/rules/ts-void-zero.yml +5 -0
  323. package/node_modules/pi-lens/rules/ast-grep-rules/self-scan-baseline.json +5 -0
  324. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +132 -1
  325. package/node_modules/pi-lens/scripts/download-grammars.js +25 -0
  326. package/node_modules/pi-lens/scripts/grammars.lock.json +9 -0
  327. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +12 -11
  328. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
  329. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +87 -116
  330. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +170 -0
  331. package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
  332. package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
  333. package/node_modules/pi-mcp-adapter/CHANGELOG.md +49 -0
  334. package/node_modules/pi-mcp-adapter/README.md +51 -6
  335. package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
  336. package/node_modules/pi-mcp-adapter/cli.js +188 -5
  337. package/node_modules/pi-mcp-adapter/commands.ts +128 -22
  338. package/node_modules/pi-mcp-adapter/config.ts +28 -10
  339. package/node_modules/pi-mcp-adapter/direct-tools.ts +23 -19
  340. package/node_modules/pi-mcp-adapter/index.ts +219 -32
  341. package/node_modules/pi-mcp-adapter/init.ts +36 -4
  342. package/node_modules/pi-mcp-adapter/lifecycle.ts +315 -24
  343. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +39 -6
  344. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +387 -0
  345. package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +27 -3
  346. package/node_modules/pi-mcp-adapter/mcp-code.ts +4 -0
  347. package/node_modules/pi-mcp-adapter/mcp-panel.ts +42 -13
  348. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
  349. package/node_modules/pi-mcp-adapter/metadata-cache.ts +46 -25
  350. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
  351. package/node_modules/pi-mcp-adapter/package.json +4 -1
  352. package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
  353. package/node_modules/pi-mcp-adapter/request-headers-command.ts +330 -0
  354. package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
  355. package/node_modules/pi-mcp-adapter/server-manager.ts +145 -2
  356. package/node_modules/pi-mcp-adapter/session-recovery.ts +7 -0
  357. package/node_modules/pi-mcp-adapter/tool-approval.ts +15 -2
  358. package/node_modules/pi-mcp-adapter/tool-metadata.ts +38 -18
  359. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
  360. package/node_modules/pi-mcp-adapter/types.ts +95 -10
  361. package/node_modules/pi-mcp-adapter/ui-server.ts +18 -16
  362. package/node_modules/pi-mcp-adapter/utils.ts +70 -0
  363. package/node_modules/pi-web-access/CHANGELOG.md +37 -0
  364. package/node_modules/pi-web-access/README.md +38 -14
  365. package/node_modules/pi-web-access/anysearch.ts +4 -2
  366. package/node_modules/pi-web-access/auth-fetch.ts +148 -0
  367. package/node_modules/pi-web-access/brave.ts +16 -4
  368. package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
  369. package/node_modules/pi-web-access/curator-page.ts +8 -3
  370. package/node_modules/pi-web-access/curator-server.ts +5 -1
  371. package/node_modules/pi-web-access/exa.ts +18 -7
  372. package/node_modules/pi-web-access/extract.ts +134 -37
  373. package/node_modules/pi-web-access/fetch-params.ts +17 -3
  374. package/node_modules/pi-web-access/firecrawl.ts +172 -12
  375. package/node_modules/pi-web-access/gemini-search.ts +33 -7
  376. package/node_modules/pi-web-access/index.ts +160 -104
  377. package/node_modules/pi-web-access/package.json +2 -2
  378. package/node_modules/pi-web-access/page-query.ts +4 -2
  379. package/node_modules/pi-web-access/parallel-mcp.ts +249 -0
  380. package/node_modules/pi-web-access/parallel.ts +2 -2
  381. package/node_modules/pi-web-access/query-rewrite.ts +51 -0
  382. package/node_modules/pi-web-access/serper.ts +211 -0
  383. package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
  384. package/node_modules/pi-web-access/summary-review.ts +51 -23
  385. package/node_modules/pi-web-access/tavily.ts +16 -4
  386. package/node_modules/pi-web-access/utils.ts +87 -0
  387. package/node_modules/pi-web-access/valyu.ts +199 -0
  388. package/node_modules/pi-web-access/youtube-extract.ts +2 -2
  389. package/package.json +13 -13
@@ -12,11 +12,73 @@
12
12
  *
13
13
  * Refs: #130, #131, #132
14
14
  */
15
+ import * as path from "node:path";
15
16
  import { createSubsystemLogger } from "./extension-log.js";
16
17
  import { safeSpawnAsync } from "./safe-spawn.js";
18
+ import { classifyProbeFailure, createAvailabilityLatch, describeInstallAttempt, describeProbeEvidence, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
17
19
  export class SecurityScanClient {
18
20
  toolName;
19
- available = null;
21
+ /**
22
+ * Availability memo, backed by the shared transient-aware latch (#1467).
23
+ *
24
+ * Assigning `false` still means "durable: this machine does not have the
25
+ * tool" — every existing subclass write keeps its meaning. A transient
26
+ * failure must expire instead: `probeVersion` latches its own transient
27
+ * verdicts (#1501), and any other transient failure (a scan or install
28
+ * timeout) goes through `markTransientlyUnavailable`, so the tool can come
29
+ * back without a host restart.
30
+ */
31
+ availabilityLatch = createAvailabilityLatch();
32
+ get available() {
33
+ return this.availabilityLatch.read();
34
+ }
35
+ set available(value) {
36
+ if (value === true) {
37
+ this.availabilityLatch.noteAvailable();
38
+ return;
39
+ }
40
+ if (value !== false) {
41
+ this.availabilityLatch.reset();
42
+ return;
43
+ }
44
+ this.availabilityLatch.noteUnavailable("missing", "not-found");
45
+ this.logDurableAbsence();
46
+ }
47
+ /**
48
+ * Latch a durable absence WITH the facts behind it. Prefer this over
49
+ * `available = false` wherever the call site knows what actually failed — a
50
+ * `missing` row carrying `install: "failed"` is a retry candidate, one
51
+ * carrying `install: "not-attempted"` is a policy decision, and a bare one is
52
+ * a plain absence. Only the record can tell them apart.
53
+ */
54
+ noteDurableAbsence(evidence, options = {}) {
55
+ this.availabilityLatch.noteUnavailable("missing", "not-found");
56
+ this.logDurableAbsence(evidence, options.elapsedMs);
57
+ }
58
+ /**
59
+ * One record per durable-absence ASSERTION, whether or not the call site had
60
+ * evidence. Before this, `available = false` after a failed install was a
61
+ * silent latch: the tool went quiet for the session with nothing in
62
+ * latency.log to audit (#1500). Bounded — the verdict latches, so at most one
63
+ * row per client per session.
64
+ *
65
+ * `elapsedMs` is whatever the caller measured (an install attempt's duration);
66
+ * there is no probe here, so it is 0 unless the caller has something real.
67
+ */
68
+ logDurableAbsence(evidence, elapsedMs = 0) {
69
+ logAvailabilityDecision({
70
+ tool: this.toolName,
71
+ verdict: "unavailable",
72
+ outcome: "missing",
73
+ cause: "not-found",
74
+ elapsedMs,
75
+ latched: true,
76
+ classifiedBy: "caller",
77
+ ...(evidence !== undefined && { evidence }),
78
+ });
79
+ }
80
+ /** Outcome of the most recent `probeVersion` call. */
81
+ lastProbeOutcome = null;
20
82
  ensureInFlight = null;
21
83
  inFlight = new Map();
22
84
  binaryPath = null;
@@ -51,18 +113,128 @@ export class SecurityScanClient {
51
113
  }
52
114
  /**
53
115
  * Spawn `toolName <versionArgs>` and report whether it answered cleanly.
54
- * Does NOT mutate `this.available` — callers decide what a hit/miss means.
116
+ * Classifies the failure (`lastProbeOutcome`) and emits one
117
+ * availability-decision record, so a probe that keeps timing out is visible
118
+ * in latency.log rather than inferred from silence (#1467).
119
+ *
120
+ * Latch ownership is split by outcome: success and durable failures leave
121
+ * `this.available` to the caller (a missing binary may still be
122
+ * auto-installed), while a TRANSIENT failure is latched by the seam itself
123
+ * with its cooldown — the retry schedule only exists once the latch owns
124
+ * the verdict, and the record is incomplete without it (#1501). Callers
125
+ * must not re-mark a probe transient (a second noteUnavailable would
126
+ * double-escalate the cooldown).
55
127
  */
56
128
  async probeVersion(versionArgs) {
57
- const probe = await safeSpawnAsync(this.toolName, versionArgs, {
58
- timeout: 5000,
59
- });
129
+ const sampler = startHostStallSampler();
130
+ const startedAt = Date.now();
131
+ let probe;
132
+ let hostStallMs;
133
+ try {
134
+ probe = await safeSpawnAsync(this.toolName, versionArgs, {
135
+ timeout: 5000,
136
+ });
137
+ }
138
+ finally {
139
+ hostStallMs = sampler.stop();
140
+ }
141
+ const elapsedMs = Date.now() - startedAt;
60
142
  if (!probe.error && probe.status === 0) {
143
+ this.lastProbeOutcome = "success";
61
144
  this.log(`${this.toolName} found: ${probe.stdout.trim().split("\n")[0]}`);
145
+ logAvailabilityDecision({
146
+ tool: this.toolName,
147
+ verdict: "available",
148
+ outcome: "success",
149
+ cause: "ok",
150
+ elapsedMs,
151
+ latched: true,
152
+ hostStallMs,
153
+ budgetMs: 5000,
154
+ classifiedBy: "probe",
155
+ evidence: describeProbeEvidence(probe),
156
+ });
62
157
  return true;
63
158
  }
159
+ const { outcome, cause, evidence } = classifyProbeFailure(probe, {
160
+ hostStallMs,
161
+ });
162
+ this.lastProbeOutcome = outcome;
163
+ // A transient verdict's record is incomplete without the retry schedule,
164
+ // and the schedule only exists once the latch owns the verdict — so the
165
+ // seam writes the expiring verdict itself, at log time (#1501). Durable
166
+ // outcomes stay caller-owned (a missing binary may still be installed).
167
+ const retryAfterMs = outcome === "transient"
168
+ ? this.availabilityLatch.noteUnavailable("transient", cause)
169
+ : 0;
170
+ logAvailabilityDecision({
171
+ tool: this.toolName,
172
+ verdict: "unavailable",
173
+ outcome,
174
+ cause,
175
+ elapsedMs,
176
+ latched: outcome !== "transient",
177
+ hostStallMs,
178
+ ...(retryAfterMs > 0 && { retryAfterMs }),
179
+ budgetMs: 5000,
180
+ classifiedBy: "probe",
181
+ evidence,
182
+ });
64
183
  return false;
65
184
  }
185
+ /** True when the last probe failed for a reason that is not the tool's fault. */
186
+ probeWasTransient() {
187
+ return this.lastProbeOutcome === "transient";
188
+ }
189
+ /**
190
+ * Record a non-durable unavailability: the verdict expires after a cooldown
191
+ * and the next `ensureAvailable` re-probes.
192
+ *
193
+ * Returns that cooldown in ms so the caller can put it in its decision
194
+ * record. A latch you can read in `latency.log` without the retry schedule
195
+ * beside it only tells you the tool is off, not when it comes back.
196
+ *
197
+ * For NON-probe transient failures only (scan/install timeouts):
198
+ * `probeVersion` latches and logs its own transient verdicts (#1501), so
199
+ * re-marking one here would double-escalate the cooldown.
200
+ *
201
+ * Pass `opts.operationClass: "install"` when the failed operation was a
202
+ * network install/compile rather than a cheap probe (#1497): the retry
203
+ * escalates on the install-class schedule, on its OWN cooldown slot (a
204
+ * cheap probe failure can never shorten it), and latches for the session at
205
+ * the attempt ceiling, in which case the return is 0 (latched).
206
+ */
207
+ markTransientlyUnavailable(cause = "probe-timeout", opts) {
208
+ return this.availabilityLatch.noteUnavailable("transient", cause, opts);
209
+ }
210
+ /**
211
+ * The cause the LATCH settled on, which is not always the cause the caller
212
+ * passed in: at the install-class ceiling the latch rewrites it to
213
+ * `install-retry-exhausted` (#1497). A decision record built from the
214
+ * caller's own cause would say `probe-timeout` for a verdict that is no
215
+ * longer being retried, so the record reads it back from here.
216
+ */
217
+ latchedCause() {
218
+ return this.availabilityLatch.getCause();
219
+ }
220
+ /**
221
+ * #1623: public window onto the availability verdict for callers OUTSIDE
222
+ * the dispatch graph (mode=full's fresh-fetch) that need to say WHY
223
+ * `ensureAvailable()` most recently returned false — using the SAME
224
+ * `AvailabilityOutcome`/`AvailabilityCause` taxonomy every dispatch-side
225
+ * message is built from, rather than a re-guessed "binary unavailable"
226
+ * that can't tell a transient retry-cooldown probe from a durable absence.
227
+ * `latchedCause()`/`lastProbeOutcome` above stay `protected` for
228
+ * dispatch-internal callers; this is the one public seam for everyone
229
+ * else.
230
+ */
231
+ getAvailabilityVerdict() {
232
+ return {
233
+ outcome: this.availabilityLatch.getOutcome(),
234
+ cause: this.availabilityLatch.getCause(),
235
+ retryAtMs: this.availabilityLatch.getRetryAtMs(),
236
+ };
237
+ }
66
238
  /**
67
239
  * Standard availability path for the GitHub-release tools (gitleaks, trivy):
68
240
  * PATH probe first, then fall back to the pi-lens installer's `ensureTool`.
@@ -73,17 +245,61 @@ export class SecurityScanClient {
73
245
  this.available = true;
74
246
  return true;
75
247
  }
248
+ if (this.probeWasTransient()) {
249
+ // A timed-out probe is not evidence the tool is absent, so it must not
250
+ // trigger an install NOR latch a permanent `false`. probeVersion has
251
+ // already recorded the expiring verdict and its retry schedule
252
+ // (#1501) — re-marking it here would double-escalate the cooldown.
253
+ this.log(`${this.toolName} availability probe timed out; not installing, will retry`);
254
+ return false;
255
+ }
76
256
  this.log(`${this.toolName} not found, attempting auto-install`);
77
- const { ensureTool } = await import("./installer/index.js");
257
+ const { ensureTool, getInstallAttempt } = await import("./installer/index.js");
258
+ const installStartedAt = Date.now();
78
259
  const installed = await ensureTool(this.toolName);
79
260
  if (!installed) {
80
- this.log(`${this.toolName} auto-install failed`);
81
- this.available = false;
261
+ // #1500: this classification is ASSERTED, not derived. It is justified —
262
+ // the probe already classified a genuine absence, and a failed repair
263
+ // leaves the tool absent — but the install failure has no taxonomy of
264
+ // its own, so a network blip and a permanently unavailable release look
265
+ // identical from here. Record what happened rather than only the verdict.
266
+ //
267
+ // `ensureTool` answers the same empty result whether it TRIED and failed
268
+ // or declined to try (auto-install off, project trust denied), so the
269
+ // attempt is read from the reason it records rather than assumed — the
270
+ // review found `install: "failed"` being written for attempts that never
271
+ // happened.
272
+ const attempt = getInstallAttempt(this.toolName);
273
+ this.log(attempt?.outcome === "failed"
274
+ ? `${this.toolName} auto-install failed: ${attempt.reason ?? "unknown reason"}`
275
+ : `${this.toolName} auto-install did not run: ${attempt?.reason ?? "no attempt recorded"}`);
276
+ this.noteDurableAbsence(describeInstallAttempt(attempt), {
277
+ elapsedMs: Date.now() - installStartedAt,
278
+ });
82
279
  return false;
83
280
  }
84
281
  this.binaryPath = installed;
85
282
  this.available = true;
86
283
  this.log(`${this.toolName} auto-installed at ${installed}`);
284
+ // The probe already wrote a latched `unavailable` row (#1500's durable
285
+ // assertion doesn't fire here — install SUCCEEDED — but probeVersion's
286
+ // verdict still did, before this call ever ran). Without a compensating
287
+ // row, the durable record says the lane is off when it just came back
288
+ // on: the exact defect #1606 was filed over.
289
+ logAvailabilityDecision({
290
+ tool: this.toolName,
291
+ verdict: "available",
292
+ outcome: "success",
293
+ cause: "ok",
294
+ elapsedMs: Date.now() - installStartedAt,
295
+ latched: true,
296
+ classifiedBy: "caller",
297
+ evidence: {
298
+ install: "succeeded",
299
+ binary: path.basename(installed),
300
+ source: "managed-dir",
301
+ },
302
+ });
87
303
  return true;
88
304
  }
89
305
  /**
@@ -12,6 +12,8 @@ import { getSgCommand, resolveManagedToolClient, } from "./dispatch/runners/util
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
+ import { createSingleFlight } from "./single-flight.js";
16
+ import { classifyProbeFailure, describeInstallAttempt, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
15
17
  /**
16
18
  * Build the `bash -c` argv that runs `cmd` with `allArgs` as POSITIONAL
17
19
  * parameters. The script is the constant `"$0" "$@"`, so bash re-emits the
@@ -33,6 +35,8 @@ function sgExcludeArgsForProject(rootDir) {
33
35
  ]);
34
36
  }
35
37
  const DEFAULT_EXEC_TIMEOUT_MS = 30_000;
38
+ /** Budget for a single `--version` candidate probe, ms. */
39
+ const PROBE_TIMEOUT_MS = 5_000;
36
40
  const MAX_SG_OUTPUT_BYTES = 8 * 1024 * 1024;
37
41
  /**
38
42
  * Format metavariable captures for display below a match line.
@@ -94,8 +98,58 @@ export class SgRunner {
94
98
  log;
95
99
  sgPath = null;
96
100
  sgArgsPrefix = [];
97
- available = null;
98
- ensureInFlight = null;
101
+ /**
102
+ * Availability memo, backed by the shared transient-aware latch (#1476).
103
+ *
104
+ * Before this, one failed sweep over every candidate — timeouts included —
105
+ * latched `false` for the life of the process AND ran a full auto-install
106
+ * first. A host stall during warm-up therefore both disabled ast-grep until
107
+ * restart and paid for an install nobody needed.
108
+ */
109
+ availabilityLatch = createAvailabilityLatch();
110
+ /**
111
+ * At-most-one sweep in flight, via the shared primitive (#1753). One
112
+ * instance owns one question, so the key is a constant.
113
+ */
114
+ ensureFlight = createSingleFlight();
115
+ /**
116
+ * Whether a DIRECT candidate — one that would have been ast-grep itself —
117
+ * failed for a transient reason in the current sweep. Only these block the
118
+ * install: a slow `npx` says nothing about whether ast-grep is on this
119
+ * machine, and letting it block turned "ast-grep is genuinely absent on a
120
+ * slow host" into "ast-grep is never installed and npx is re-spawned every
121
+ * sweep, forever".
122
+ */
123
+ sweepSawTransient = false;
124
+ sweepTransientCause = "probe-timeout";
125
+ /**
126
+ * The DIRECT candidates that were unreachable, in ask order (#1568).
127
+ *
128
+ * The sweep returns at the first candidate that answers, so at the moment of
129
+ * a win this is exactly the set of preferred tiers the winner did not beat on
130
+ * the merits. Fallbacks are excluded on purpose: `npx` is asked before the
131
+ * global-bin and platform-package tiers, and a slow `npx` says nothing about
132
+ * a winner that is a real binary.
133
+ *
134
+ * Basenames, because a candidate can be an absolute global-bin or
135
+ * platform-package path and this list is written to latency.log (#1568
136
+ * review F3).
137
+ */
138
+ sweepUnreachable = [];
139
+ /** A transient on the npx fallback: not evidence, but not nothing either. */
140
+ sweepFallbackTransient = false;
141
+ sweepFallbackCause = "probe-timeout";
142
+ /** Host stall summed over every probe of the current sweep, ms. */
143
+ sweepHostStallMs = 0;
144
+ /**
145
+ * Candidates — direct or fallback — that this sweep probed and found
146
+ * DURABLY missing (real ENOENT/non-installable, not a stall) (#1593). The
147
+ * retained-arm fallback below only sees whether SOME candidate stalled; this
148
+ * list lets it also check whether the memoized winner itself is one of the
149
+ * candidates this very sweep just disproved, rather than re-serving a
150
+ * command that ENOENTed a moment ago because an unrelated sibling stalled.
151
+ */
152
+ sweepDurablyMissing = [];
99
153
  constructor(verbose = false) {
100
154
  this.log = verbose
101
155
  ? createSubsystemLogger("sg-runner")
@@ -104,38 +158,44 @@ export class SgRunner {
104
158
  /**
105
159
  * Check if ast-grep CLI is available, auto-install if not.
106
160
  *
107
- * Re-entrancy safe: concurrent first-time callers share a single
108
- * `ensureInFlight` promise so probing/auto-install isn't duplicated
109
- * across session-start tasks. Mirrors the dedupe pattern in
110
- * `KnipClient.ensureAvailable` and `DependencyChecker.ensureAvailable`.
161
+ * Re-entrancy safe: concurrent first-time callers share one flight, so
162
+ * probing/auto-install isn't duplicated across session-start tasks. The
163
+ * share and the clear-in-finally belong to `singleFlight` (#1753); this
164
+ * method owns only the latch short-circuit above it. The sweep-local
165
+ * bookkeeping `doEnsureAvailable` resets on entry is untouched — it is
166
+ * per-sweep state, not concurrency state.
111
167
  */
112
168
  async ensureAvailable() {
113
- // Fast path: already checked.
114
- if (this.available !== null)
115
- return this.available;
116
- if (this.ensureInFlight)
117
- return this.ensureInFlight;
118
- this.ensureInFlight = this.doEnsureAvailable();
119
- try {
120
- return await this.ensureInFlight;
121
- }
122
- finally {
123
- this.ensureInFlight = null;
124
- }
169
+ // Fast path: already decided. `read()` returns null when the last verdict
170
+ // was transient and its cooldown expired, which re-enters the sweep.
171
+ const memo = this.availabilityLatch.read();
172
+ if (memo !== null)
173
+ return memo;
174
+ return this.ensureFlight.run("ast-grep", () => this.doEnsureAvailable());
125
175
  }
126
176
  async doEnsureAvailable() {
177
+ const startedAt = Date.now();
178
+ this.sweepSawTransient = false;
179
+ this.sweepTransientCause = "probe-timeout";
180
+ this.sweepUnreachable = [];
181
+ this.sweepFallbackTransient = false;
182
+ this.sweepFallbackCause = "probe-timeout";
183
+ this.sweepHostStallMs = 0;
184
+ this.sweepDurablyMissing = [];
127
185
  // Step 1: PATH — canonical binary names + npx fallback.
128
186
  // Prefer ast-grep over sg on Linux: /usr/bin/sg is util-linux, not ast-grep.
129
187
  const pathCommand = await this.probeCommandCandidates([
130
188
  { cmd: "ast-grep", argsPrefix: [] },
131
189
  { cmd: "sg", argsPrefix: [] },
132
- { cmd: "npx", argsPrefix: ["--no", "--", "ast-grep"] },
190
+ // `npx --no -- ast-grep` starts a Node process before it can answer, so
191
+ // it is the candidate most likely to blow a 5 s budget on a cold or busy
192
+ // box. Marked a fallback so its timeout cannot veto the install.
193
+ { cmd: "npx", argsPrefix: ["--no", "--", "ast-grep"], fallback: true },
133
194
  ]);
134
195
  if (pathCommand) {
135
196
  this.sgPath = pathCommand.cmd;
136
197
  this.sgArgsPrefix = pathCommand.argsPrefix;
137
- this.available = true;
138
- this.log(`ast-grep found on PATH: ${pathCommand.cmd}`);
198
+ this.noteAvailable(startedAt, `ast-grep found on PATH: ${pathCommand.cmd}`);
139
199
  return true;
140
200
  }
141
201
  // Step 1b: any package manager's global bin dir (npm/pnpm/yarn/bun).
@@ -147,8 +207,7 @@ export class SgRunner {
147
207
  if (globalBin && (await this.probeCommand(globalBin, []))) {
148
208
  this.sgPath = globalBin;
149
209
  this.sgArgsPrefix = [];
150
- this.available = true;
151
- this.log(`ast-grep found in global bin: ${globalBin}`);
210
+ this.noteAvailable(startedAt, `ast-grep found in global bin: ${globalBin}`);
152
211
  return true;
153
212
  }
154
213
  }
@@ -159,8 +218,7 @@ export class SgRunner {
159
218
  if (platformBinary) {
160
219
  this.sgPath = platformBinary;
161
220
  this.sgArgsPrefix = [];
162
- this.available = true;
163
- this.log(`ast-grep found via platform package: ${platformBinary}`);
221
+ this.noteAvailable(startedAt, `ast-grep found via platform package: ${platformBinary}`);
164
222
  return true;
165
223
  }
166
224
  // Step 3: Homebrew (macOS only).
@@ -169,10 +227,36 @@ export class SgRunner {
169
227
  if (brewBinary) {
170
228
  this.sgPath = brewBinary;
171
229
  this.sgArgsPrefix = [];
172
- this.available = true;
173
- this.log(`ast-grep found via Homebrew: ${brewBinary}`);
230
+ this.noteAvailable(startedAt, `ast-grep found via Homebrew: ${brewBinary}`);
231
+ return true;
232
+ }
233
+ }
234
+ // A timeout on a DIRECT candidate means the machine answered, not the tool
235
+ // (#1476). Installing ast-grep because the host event loop stalled is a
236
+ // heavyweight reaction to a hiccup, and latching the result disabled the
237
+ // tool until restart. Retry the sweep later instead.
238
+ //
239
+ // The npx fallback is deliberately NOT part of this test. Gating the
240
+ // install on it regressed the very host this change targets: a slow box
241
+ // with no ast-grep timed out `npx --no -- ast-grep` every sweep, so the
242
+ // install below was never reached and the slow npx was re-spawned on each
243
+ // escalating retry instead — worse than the latch it replaced.
244
+ if (this.sweepSawTransient) {
245
+ // Nothing answered, transiently, while we are still holding a command a
246
+ // previous provisional sweep proved working. Discarding it here would
247
+ // run #1476 backwards — a timeout erasing a positive result — and it
248
+ // would send the sweep on to Step 4 to install a tool that is already
249
+ // runnable. Keep the winner and re-arm the cooldown (#1568 review F1).
250
+ // #1593: a sibling tier stalling this sweep is not license to re-serve
251
+ // a winner that THIS SAME sweep just proved durably missing.
252
+ if (this.availabilityLatch.isProvisional() &&
253
+ this.sgPath &&
254
+ !this.sweepDurablyMissing.includes(this.sgPath)) {
255
+ this.noteAvailable(startedAt, `ast-grep re-probe stalled; keeping ${this.sgPath}`, { retained: true });
174
256
  return true;
175
257
  }
258
+ this.log("ast-grep availability probe timed out; will retry (not installing)");
259
+ return this.noteUnavailable(startedAt, "transient", this.sweepTransientCause);
176
260
  }
177
261
  // Step 4: install via the typed shared seam, then validate the returned
178
262
  // absolute binary before publishing it.
@@ -185,11 +269,101 @@ export class SgRunner {
185
269
  if (installed.outcome === "success") {
186
270
  this.sgPath = installed.value;
187
271
  this.sgArgsPrefix = [];
188
- this.available = true;
189
- this.log(`ast-grep auto-installed: ${installed.value}`);
272
+ this.noteAvailable(startedAt, `ast-grep auto-installed: ${installed.value}`);
190
273
  return true;
191
274
  }
192
- this.available = false;
275
+ // The install failed AND the npx fallback never got a fair hearing, so
276
+ // "ast-grep is not installed" is not a fact yet. Expire the verdict.
277
+ if (this.sweepFallbackTransient) {
278
+ return this.noteUnavailable(startedAt, "transient", this.sweepFallbackCause);
279
+ }
280
+ // #1500: ASSERTED, not derived — and justified, because every candidate
281
+ // probe answered "not found", so the absence is real. What the install did
282
+ // is recorded as evidence rather than folded into the verdict: it is the one
283
+ // fact separating "never installable here" from "the download failed", and
284
+ // from "no install was attempted at all" (auto-install off, trust denied,
285
+ // or an attempt already suppressed this session).
286
+ const { getInstallAttempt } = await import("./installer/index.js");
287
+ return this.noteUnavailable(startedAt, "missing", "not-found", describeInstallAttempt(getInstallAttempt("ast-grep"), {
288
+ installedButRejected: installed.outcome === "non-installable",
289
+ }), "caller");
290
+ }
291
+ /**
292
+ * Record a successful sweep, with one decision record.
293
+ *
294
+ * The win is latched for the session UNLESS a direct candidate ahead of it
295
+ * was unreachable (#1568). `sweepSawTransient` is set only by candidates the
296
+ * sweep already asked, and the sweep returns at the first one that answers,
297
+ * so at this point the flag means "a tier the winner is supposed to lose to
298
+ * never got a fair hearing". Latching that pinned the session to
299
+ * `npx --no -- ast-grep` — a Node start per invocation — over a healthy
300
+ * ast-grep on PATH, until the next restart. Provisional instead: served now,
301
+ * re-swept once the stalled tier's cooldown expires.
302
+ *
303
+ * The install arm cannot reach here provisionally: `doEnsureAvailable`
304
+ * returns before Step 4 whenever `sweepSawTransient` is set.
305
+ *
306
+ * `retained` marks the other provisional case (#1568 review F1): no candidate
307
+ * answered at all, and the winner being reported is the one the previous
308
+ * sweep found, kept rather than discarded on a timeout.
309
+ */
310
+ noteAvailable(startedAt, message, opts = {}) {
311
+ const provisional = this.sweepSawTransient;
312
+ let retryAfterMs = 0;
313
+ if (provisional) {
314
+ retryAfterMs = this.availabilityLatch.noteProvisionallyAvailable(this.sweepTransientCause);
315
+ }
316
+ else {
317
+ this.availabilityLatch.noteAvailable();
318
+ }
319
+ this.log(message);
320
+ logAvailabilityDecision({
321
+ tool: "ast-grep",
322
+ verdict: "available",
323
+ outcome: "success",
324
+ cause: provisional ? this.sweepTransientCause : "ok",
325
+ elapsedMs: Date.now() - startedAt,
326
+ latched: !provisional,
327
+ hostStallMs: this.sweepHostStallMs,
328
+ budgetMs: PROBE_TIMEOUT_MS,
329
+ ...(provisional && {
330
+ provisional: true,
331
+ unreachablePreferred: [...this.sweepUnreachable],
332
+ ...(opts.retained === true && { retained: true }),
333
+ ...(retryAfterMs > 0 && { retryAfterMs }),
334
+ }),
335
+ });
336
+ }
337
+ /**
338
+ * Record a failed sweep. A `transient` outcome expires after a bounded
339
+ * cooldown; anything else is remembered for the session, as before. The
340
+ * elapsed/stall numbers describe the WHOLE sweep, because the verdict is
341
+ * about the sweep rather than any single candidate — reporting zero here
342
+ * would erase the evidence that cracked #1467.
343
+ */
344
+ noteUnavailable(startedAt, outcome, cause, evidence,
345
+ /**
346
+ * How this arm reached its verdict. Per arm on purpose (#1500 review): the
347
+ * sweep's own transient/missing conclusions ARE derived from candidate
348
+ * probes, but the post-install assertion is not, and hardcoding `"probe"`
349
+ * here labelled an assertion as a derivation — the exact confusion the
350
+ * field was added to remove.
351
+ */
352
+ classifiedBy = "probe") {
353
+ const retryAfterMs = this.availabilityLatch.noteUnavailable(outcome, cause);
354
+ logAvailabilityDecision({
355
+ tool: "ast-grep",
356
+ verdict: "unavailable",
357
+ outcome,
358
+ cause,
359
+ elapsedMs: Date.now() - startedAt,
360
+ latched: outcome !== "transient",
361
+ hostStallMs: this.sweepHostStallMs,
362
+ ...(retryAfterMs > 0 && { retryAfterMs }),
363
+ budgetMs: PROBE_TIMEOUT_MS,
364
+ classifiedBy,
365
+ ...(evidence !== undefined && { evidence }),
366
+ });
193
367
  return false;
194
368
  }
195
369
  /**
@@ -270,17 +444,55 @@ export class SgRunner {
270
444
  isAstGrepVersionOutput(output) {
271
445
  return /\bast[- ]grep\b/i.test(output);
272
446
  }
273
- async probeCommand(cmd, argsPrefix) {
274
- const result = await safeSpawnAsync(cmd, [...argsPrefix, "--version"], {
275
- timeout: 5000,
276
- });
277
- return (!result.error &&
447
+ /**
448
+ * Probe one candidate. A failure is CLASSIFIED, not merely counted: a
449
+ * timeout/abort marks the whole sweep transient so the caller retries later
450
+ * instead of installing and latching. A command that answers but is not
451
+ * ast-grep (Linux `/usr/bin/sg` is util-linux) is a durable no.
452
+ */
453
+ async probeCommand(cmd, argsPrefix, fallback = false) {
454
+ // Host-side budget: measure the loop stall that overlapped the window so
455
+ // the classifier can tell "the tool is slow" from "the host was busy".
456
+ const sampler = startHostStallSampler();
457
+ let result;
458
+ let hostStallMs;
459
+ try {
460
+ result = await safeSpawnAsync(cmd, [...argsPrefix, "--version"], {
461
+ timeout: PROBE_TIMEOUT_MS,
462
+ });
463
+ }
464
+ finally {
465
+ hostStallMs = sampler.stop();
466
+ this.sweepHostStallMs += hostStallMs;
467
+ }
468
+ if (!result.error &&
278
469
  result.status === 0 &&
279
- this.isAstGrepVersionOutput(`${result.stdout}\n${result.stderr}`));
470
+ this.isAstGrepVersionOutput(`${result.stdout}\n${result.stderr}`)) {
471
+ return true;
472
+ }
473
+ const { outcome, cause } = classifyProbeFailure(result, { hostStallMs });
474
+ if (outcome === "transient") {
475
+ if (fallback) {
476
+ this.sweepFallbackTransient = true;
477
+ this.sweepFallbackCause = cause;
478
+ }
479
+ else {
480
+ this.sweepSawTransient = true;
481
+ this.sweepTransientCause = cause;
482
+ const name = path.basename(cmd);
483
+ if (!this.sweepUnreachable.includes(name)) {
484
+ this.sweepUnreachable.push(name);
485
+ }
486
+ }
487
+ }
488
+ else if (!this.sweepDurablyMissing.includes(cmd)) {
489
+ this.sweepDurablyMissing.push(cmd);
490
+ }
491
+ return false;
280
492
  }
281
493
  async probeCommandCandidates(candidates) {
282
494
  for (const candidate of candidates) {
283
- if (await this.probeCommand(candidate.cmd, candidate.argsPrefix)) {
495
+ if (await this.probeCommand(candidate.cmd, candidate.argsPrefix, candidate.fallback ?? false)) {
284
496
  return candidate;
285
497
  }
286
498
  }