@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
@@ -45,6 +45,7 @@
45
45
  * - pip packages
46
46
  * - GitHub releases (platform-specific binaries → ~/.pi-lens/bin/)
47
47
  */
48
+ import { logExtension } from "../extension-log.js";
48
49
  import { spawn } from "node:child_process";
49
50
  import { existsSync, statSync, unlinkSync } from "node:fs";
50
51
  import fs from "node:fs/promises";
@@ -52,17 +53,29 @@ import https from "node:https";
52
53
  import { createRequire } from "node:module";
53
54
  import os from "node:os";
54
55
  import path from "node:path";
56
+ import { BoundedLruCache } from "../bounded-cache.js";
57
+ import { isFullyQualified } from "../path-utils.js";
58
+ import { assertInstallAllowed, projectTrustDenialReason, } from "../project-trust.js";
55
59
  const _installerRequire = createRequire(import.meta.url);
56
60
  import { createGunzip } from "node:zlib";
57
61
  import { logSessionStart } from "../sessionstart-logger.js";
62
+ import { commitDurableStoreAsync } from "../durable-store.js";
58
63
  import { getGlobalPiLensDir } from "../file-utils.js";
59
64
  import { allAvailableGlobalBinDirs, installArgs, pmBinary, resolveNodePackageManager, } from "../package-manager.js";
60
- import { safeSpawnAsync } from "../safe-spawn.js";
65
+ import { resetSafeSpawnWindowsCommandCache, safeSpawnAsync, } from "../safe-spawn.js";
61
66
  // Global installation directory for pi-lens tools
62
67
  const TOOLS_DIR = path.join(getGlobalPiLensDir(), "tools");
63
68
  const INSTALL_LOCK_PATH = path.join(TOOLS_DIR, ".install.lock");
64
69
  const activeInstallLocks = new Set();
65
70
  let installLockExitCleanupRegistered = false;
71
+ /**
72
+ * The managed tools tree, for callers that need to tell a path `getToolPath()`
73
+ * returned from the managed install apart from a global/PATH hit (the tool
74
+ * registry is this module's business — don't re-derive `<pi-lens home>/tools`).
75
+ */
76
+ export function getManagedToolsDir() {
77
+ return TOOLS_DIR;
78
+ }
66
79
  function isProcessAlive(pid) {
67
80
  try {
68
81
  process.kill(pid, 0);
@@ -164,12 +177,27 @@ async function acquireInstallLock() {
164
177
  const GITHUB_BIN_DIR = path.join(getGlobalPiLensDir(), "bin");
165
178
  // Debug flag - set via PI_LENS_DEBUG=1 or --debug
166
179
  const DEBUG = process.env.PI_LENS_DEBUG === "1" || process.argv.includes("--debug");
180
+ /** Test-only platform seam for Windows resource-layout coverage on Linux CI. */
181
+ function installerPlatform() {
182
+ const override = process.env.PI_LENS_TEST_PLATFORM;
183
+ if (override === "win32" || override === "linux") {
184
+ return override;
185
+ }
186
+ return process.platform;
187
+ }
167
188
  /**
168
189
  * Log debug messages only when DEBUG is enabled
169
190
  */
170
191
  function debugLog(...args) {
171
192
  if (DEBUG) {
172
- console.error("[auto-install:debug]", ...args);
193
+ // #1333: DEBUG gate preserved; sink is extension.log, never the TUI.
194
+ logExtension({
195
+ subsystem: "auto-install",
196
+ level: "debug",
197
+ message: args
198
+ .map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg)))
199
+ .join(" "),
200
+ });
173
201
  }
174
202
  }
175
203
  /**
@@ -177,16 +205,25 @@ function debugLog(...args) {
177
205
  * the copy-pasted `if (platform === "linux") return arch === "arm64" ? … : …`
178
206
  * ladder that several release entries repeat verbatim. Each platform maps to its
179
207
  * `x64` (default) and optional `arm64` asset substring; a missing platform or
180
- * arch ⇒ unsupported (`undefined`), exactly as the hand-written ladders behaved.
208
+ * arch ⇒ unsupported (`undefined`). Arches outside x64/arm64 (ia32, ppc64, …)
209
+ * are unsupported too — no release entry ships a 32-bit or exotic asset, and
210
+ * handing back the x64 substring there would install an unrunnable binary.
181
211
  */
182
212
  function archAssetMatch(table) {
183
213
  return (platform, arch) => {
184
- const entry = table[platform];
185
- if (!entry)
214
+ if (arch !== "x64" && arch !== "arm64")
186
215
  return undefined;
187
- return arch === "arm64" ? entry.arm64 : entry.x64;
216
+ return table[platform]?.[arch];
188
217
  };
189
218
  }
219
+ // Go-style `<os>_<arch>.zip` release assets, shared verbatim by tflint and
220
+ // terraform-ls — both entries carried byte-identical ladders, down to the asset
221
+ // strings themselves.
222
+ const OS_ARCH_ZIP_ASSETS = {
223
+ linux: { x64: "linux_amd64.zip", arm64: "linux_arm64.zip" },
224
+ darwin: { x64: "darwin_amd64.zip", arm64: "darwin_arm64.zip" },
225
+ win32: { x64: "windows_amd64.zip", arm64: "windows_arm64.zip" },
226
+ };
190
227
  export const TOOLS = [
191
228
  // Core LSP servers
192
229
  {
@@ -204,7 +241,11 @@ export const TOOLS = [
204
241
  checkCommand: "tsc",
205
242
  checkArgs: ["--version"],
206
243
  installStrategy: "npm",
207
- packageName: "typescript",
244
+ // The managed compiler serves the classic typescript-language-server
245
+ // fallback. TypeScript 7 removed lib/tsserver.js and is selected only from
246
+ // project-local installs through the native `tsc --lsp --stdio` path.
247
+ // Revisit when typescript-language-server supports TS 7 — refs #1436.
248
+ packageName: "typescript@5.9.3",
208
249
  binaryName: "tsc",
209
250
  },
210
251
  {
@@ -418,6 +459,31 @@ export const TOOLS = [
418
459
  },
419
460
  },
420
461
  },
462
+ {
463
+ id: "helm",
464
+ name: "Helm",
465
+ checkCommand: "helm",
466
+ checkArgs: ["version", "--short"],
467
+ installStrategy: "github",
468
+ binaryName: "helm",
469
+ github: {
470
+ repo: "helm/helm",
471
+ assetMatch: (platform, arch) => {
472
+ // helm publishes per-OS archives: tar.gz for POSIX, zip for Windows.
473
+ const cpu = arch === "arm64" ? "arm64" : "amd64";
474
+ if (platform === "linux")
475
+ return `linux-${cpu}.tar.gz`;
476
+ if (platform === "darwin")
477
+ return `darwin-${cpu}.tar.gz`;
478
+ if (platform === "win32")
479
+ return `windows-${cpu}.zip`;
480
+ return undefined;
481
+ },
482
+ // Release archives nest the executable under an OS/arch directory;
483
+ // the installer searches recursively and adds the Windows suffix.
484
+ binaryInArchive: "helm",
485
+ },
486
+ },
421
487
  {
422
488
  // Opengrep: a single standalone binary per platform on GitHub releases —
423
489
  // no login, no telemetry (the reason for switching off Semgrep, #111).
@@ -919,18 +985,39 @@ export const TOOLS = [
919
985
  binaryName: "tflint",
920
986
  github: {
921
987
  repo: "terraform-linters/tflint",
922
- assetMatch: (platform, arch) => {
923
- if (platform === "linux")
924
- return arch === "arm64" ? "linux_arm64.zip" : "linux_amd64.zip";
925
- if (platform === "darwin")
926
- return arch === "arm64" ? "darwin_arm64.zip" : "darwin_amd64.zip";
927
- if (platform === "win32")
928
- return arch === "arm64" ? "windows_arm64.zip" : "windows_amd64.zip";
929
- return undefined;
930
- },
988
+ assetMatch: archAssetMatch(OS_ARCH_ZIP_ASSETS),
931
989
  binaryInArchive: "tflint",
932
990
  },
933
991
  },
992
+ {
993
+ // Terragrunt ships a bare native binary per platform on GitHub releases.
994
+ // Windows arm64 uses the x64 binary through Windows' built-in emulation —
995
+ // there is no terragrunt_windows_arm64.exe upstream.
996
+ id: "terragrunt",
997
+ name: "terragrunt",
998
+ checkCommand: "terragrunt",
999
+ checkArgs: ["--version"],
1000
+ installStrategy: "github",
1001
+ binaryName: "terragrunt",
1002
+ github: {
1003
+ repo: "gruntwork-io/terragrunt",
1004
+ assetMatch: archAssetMatch({
1005
+ linux: {
1006
+ x64: "terragrunt_linux_amd64",
1007
+ arm64: "terragrunt_linux_arm64",
1008
+ },
1009
+ darwin: {
1010
+ x64: "terragrunt_darwin_amd64",
1011
+ arm64: "terragrunt_darwin_arm64",
1012
+ },
1013
+ win32: {
1014
+ x64: "terragrunt_windows_amd64.exe",
1015
+ arm64: "terragrunt_windows_amd64.exe",
1016
+ },
1017
+ }),
1018
+ // bare binary — no binaryInArchive
1019
+ },
1020
+ },
934
1021
  {
935
1022
  id: "gitleaks",
936
1023
  name: "gitleaks",
@@ -1055,15 +1142,7 @@ export const TOOLS = [
1055
1142
  github: {
1056
1143
  repo: "hashicorp/terraform-ls",
1057
1144
  hashiCorpReleaseProduct: "terraform-ls",
1058
- assetMatch: (platform, arch) => {
1059
- if (platform === "linux")
1060
- return arch === "arm64" ? "linux_arm64.zip" : "linux_amd64.zip";
1061
- if (platform === "darwin")
1062
- return arch === "arm64" ? "darwin_arm64.zip" : "darwin_amd64.zip";
1063
- if (platform === "win32")
1064
- return arch === "arm64" ? "windows_arm64.zip" : "windows_amd64.zip";
1065
- return undefined;
1066
- },
1145
+ assetMatch: archAssetMatch(OS_ARCH_ZIP_ASSETS),
1067
1146
  binaryInArchive: "terraform-ls",
1068
1147
  },
1069
1148
  },
@@ -1221,33 +1300,164 @@ export function getInstallFailureReason(toolId) {
1221
1300
  return installFailureReasons.get(toolId);
1222
1301
  }
1223
1302
  // Session-lifetime cache: once a tool path is resolved, skip the process-spawn check on subsequent calls.
1224
- const resolvedPathCache = new Map();
1303
+ const resolvedPathCache = new BoundedLruCache(256);
1225
1304
  const PROBE_CACHE_PATH = path.join(getGlobalPiLensDir(), "probe-cache.json");
1305
+ const PROBE_CACHE_LOCK_STALE_MS = 180_000;
1226
1306
  const PROBE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
1307
+ const PROBE_CACHE_FLUSH_LOCK_WAIT_MS = 250;
1308
+ const PROBE_CACHE_FLUSH_RETRY_DELAY_MS = 300;
1309
+ const PROBE_CACHE_FLUSH_RETRY_MAX_DELAY_MS = 30_000;
1227
1310
  let _probeCache = null;
1228
1311
  let _probeCacheDirty = false;
1229
1312
  let _probeCacheFlushTimer = null;
1313
+ let _probeCacheWriteInFlight = null;
1314
+ let _probeCacheRetryAttempt = 0;
1315
+ let _probeCacheChangeGeneration = 0;
1316
+ // Read-modify-write deltas let a long-lived process merge with entries another
1317
+ // process discovered after this process's initial read. `null` is a deliberate
1318
+ // deletion (TTL/stale-path eviction), not an absent map value.
1319
+ const _probeCacheChanges = new Map();
1320
+ const _probeCacheChangeVersions = new Map();
1230
1321
  async function readProbeCache() {
1231
1322
  if (_probeCache !== null)
1232
1323
  return _probeCache;
1233
1324
  try {
1234
1325
  const raw = await fs.readFile(PROBE_CACHE_PATH, "utf-8");
1235
- _probeCache = JSON.parse(raw);
1326
+ const parsed = JSON.parse(raw);
1327
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1328
+ throw new Error("probe-cache root is not an object");
1329
+ }
1330
+ _probeCache = parsed;
1236
1331
  }
1237
- catch {
1332
+ catch (err) {
1333
+ const code = err?.code;
1334
+ if (code !== "ENOENT") {
1335
+ logSessionStart(`auto-install probe-cache: read failed (${code ?? "invalid"}); treating cache as unavailable`);
1336
+ }
1238
1337
  _probeCache = {};
1239
1338
  }
1240
1339
  return _probeCache;
1241
1340
  }
1242
- function scheduleProbeFlush() {
1341
+ function markProbeCacheChange(toolId, entry) {
1342
+ _probeCacheChanges.set(toolId, entry);
1343
+ _probeCacheChangeVersions.set(toolId, ++_probeCacheChangeGeneration);
1344
+ _probeCacheDirty = true;
1345
+ scheduleProbeFlush();
1346
+ }
1347
+ function scheduleProbeFlush(delayMs = PROBE_CACHE_FLUSH_RETRY_DELAY_MS) {
1243
1348
  if (_probeCacheFlushTimer !== null)
1244
1349
  return;
1245
1350
  _probeCacheFlushTimer = setTimeout(() => {
1246
1351
  void flushProbeCache();
1247
- }, 300);
1352
+ }, delayMs);
1248
1353
  _probeCacheFlushTimer.unref?.();
1249
1354
  }
1250
- let _probeCacheWriteInFlight = null;
1355
+ function scheduleProbeFlushRetry() {
1356
+ const delay = Math.min(PROBE_CACHE_FLUSH_RETRY_MAX_DELAY_MS, PROBE_CACHE_FLUSH_RETRY_DELAY_MS * 2 ** Math.min(_probeCacheRetryAttempt, 6));
1357
+ _probeCacheRetryAttempt += 1;
1358
+ scheduleProbeFlush(delay);
1359
+ }
1360
+ function snapshotProbeCacheChanges() {
1361
+ return {
1362
+ changes: new Map(_probeCacheChanges),
1363
+ versions: new Map(_probeCacheChangeVersions),
1364
+ };
1365
+ }
1366
+ function deserializeProbeCache(contents) {
1367
+ if (contents === undefined)
1368
+ return {};
1369
+ const parsed = JSON.parse(contents);
1370
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1371
+ throw new Error("probe-cache root is not an object");
1372
+ }
1373
+ return parsed;
1374
+ }
1375
+ function applyProbeCacheChanges(disk, changes) {
1376
+ for (const [toolId, entry] of changes) {
1377
+ if (entry === null)
1378
+ delete disk[toolId];
1379
+ else
1380
+ disk[toolId] = entry;
1381
+ }
1382
+ }
1383
+ /**
1384
+ * Install-lifetime ageing belongs to the authoritative merge, not only to the
1385
+ * lookup path: a sibling process may have left expired entries on disk since
1386
+ * this process loaded its in-memory snapshot. The async durable-store lock
1387
+ * supplies the probe cache's former quarantine/stale-owner recovery.
1388
+ */
1389
+ function ageProbeCache(disk) {
1390
+ const cutoff = Date.now() - PROBE_CACHE_TTL_MS;
1391
+ for (const [toolId, entry] of Object.entries(disk)) {
1392
+ if (!Number.isFinite(entry.cachedAt) || entry.cachedAt < cutoff) {
1393
+ delete disk[toolId];
1394
+ }
1395
+ }
1396
+ }
1397
+ function publishProbeCacheWrite(disk, snapshotVersions) {
1398
+ // Publish the sibling-process merge plus any updates that arrived during
1399
+ // the write. Only an unchanged snapshot entry is retired.
1400
+ const pendingAfterWrite = new Map(_probeCacheChanges);
1401
+ _probeCache = disk;
1402
+ for (const [toolId, entry] of pendingAfterWrite) {
1403
+ if (entry === null)
1404
+ delete _probeCache[toolId];
1405
+ else
1406
+ _probeCache[toolId] = entry;
1407
+ }
1408
+ for (const [toolId, version] of snapshotVersions) {
1409
+ if (_probeCacheChangeVersions.get(toolId) === version) {
1410
+ _probeCacheChanges.delete(toolId);
1411
+ _probeCacheChangeVersions.delete(toolId);
1412
+ }
1413
+ }
1414
+ _probeCacheDirty = _probeCacheChanges.size > 0;
1415
+ _probeCacheRetryAttempt = 0;
1416
+ return _probeCacheDirty ? "written-with-pending" : "written";
1417
+ }
1418
+ async function writeProbeCache() {
1419
+ try {
1420
+ // Snapshot versions before the awaited disk read/write. A new update for
1421
+ // the same tool may arrive while the atomic write is in flight; its newer
1422
+ // version must remain pending for the next flush.
1423
+ const { changes, versions } = snapshotProbeCacheChanges();
1424
+ let result = "written";
1425
+ const committed = await commitDurableStoreAsync({
1426
+ path: PROBE_CACHE_PATH,
1427
+ deserialize: deserializeProbeCache,
1428
+ merge: (disk) => {
1429
+ ageProbeCache(disk);
1430
+ applyProbeCacheChanges(disk, changes);
1431
+ return disk;
1432
+ },
1433
+ serialize: (disk) => JSON.stringify(disk, null, 2),
1434
+ waitMs: PROBE_CACHE_FLUSH_LOCK_WAIT_MS,
1435
+ retryMs: 25,
1436
+ staleMs: PROBE_CACHE_LOCK_STALE_MS,
1437
+ timeoutMessage: "Timed out waiting for probe-cache lock",
1438
+ onContention: "skip-log",
1439
+ logContention: () => {
1440
+ logSessionStart("auto-install probe-cache: flush deferred because another process owns the lock");
1441
+ },
1442
+ afterWriteLocked: (disk) => {
1443
+ result = publishProbeCacheWrite(disk, versions);
1444
+ },
1445
+ });
1446
+ if (committed === undefined) {
1447
+ scheduleProbeFlushRetry();
1448
+ return "deferred";
1449
+ }
1450
+ return result;
1451
+ }
1452
+ catch (err) {
1453
+ // Keep dirty state so a later timer/explicit flush can retry. The error
1454
+ // is logged without paths, source, or command text; an unavailable cache
1455
+ // must never look like a clean empty cache to operators.
1456
+ logSessionStart(`auto-install probe-cache: flush failed (${err?.code ?? "write error"}); pending update retained`);
1457
+ scheduleProbeFlushRetry();
1458
+ return "failed";
1459
+ }
1460
+ }
1251
1461
  /** Await pending probe-cache persistence before a one-shot process exits. */
1252
1462
  export async function flushProbeCache() {
1253
1463
  if (_probeCacheFlushTimer !== null) {
@@ -1257,22 +1467,17 @@ export async function flushProbeCache() {
1257
1467
  // #946 review F4: if the 300ms timer's write already started, the dirty
1258
1468
  // flag is false but the write may still be in flight — an immediate
1259
1469
  // process.exit would truncate it. Always await the in-flight write.
1260
- if (!_probeCacheDirty || _probeCache === null) {
1470
+ if (_probeCacheWriteInFlight) {
1261
1471
  await _probeCacheWriteInFlight;
1262
- return;
1472
+ if (!_probeCacheDirty)
1473
+ return "written";
1263
1474
  }
1264
- _probeCacheDirty = false;
1265
- const write = (async () => {
1266
- try {
1267
- await fs.writeFile(PROBE_CACHE_PATH, JSON.stringify(_probeCache, null, 2));
1268
- }
1269
- catch {
1270
- // Cache persistence is best effort; discovery remains authoritative.
1271
- }
1272
- })();
1475
+ if (!_probeCacheDirty || _probeCache === null)
1476
+ return "idle";
1477
+ const write = writeProbeCache();
1273
1478
  _probeCacheWriteInFlight = write;
1274
1479
  try {
1275
- await write;
1480
+ return await write;
1276
1481
  }
1277
1482
  finally {
1278
1483
  if (_probeCacheWriteInFlight === write)
@@ -1316,8 +1521,7 @@ export async function checkProbeCache(toolId) {
1316
1521
  if (Date.now() - entry.cachedAt > PROBE_CACHE_TTL_MS) {
1317
1522
  logSessionStart(`auto-install probe-cache ${toolId}: miss (ttl expired)`);
1318
1523
  delete cache[toolId];
1319
- _probeCacheDirty = true;
1320
- scheduleProbeFlush();
1524
+ markProbeCacheChange(toolId, null);
1321
1525
  return undefined;
1322
1526
  }
1323
1527
  try {
@@ -1326,15 +1530,13 @@ export async function checkProbeCache(toolId) {
1326
1530
  if (stat.mtimeMs !== entry.mtimeMs) {
1327
1531
  logSessionStart(`auto-install probe-cache ${toolId}: miss (mtime changed)`);
1328
1532
  delete cache[toolId];
1329
- _probeCacheDirty = true;
1330
- scheduleProbeFlush();
1533
+ markProbeCacheChange(toolId, null);
1331
1534
  return undefined;
1332
1535
  }
1333
1536
  if (toolId === "ast-grep" && !(await verifyAstGrepProbePath(entry.path))) {
1334
1537
  logSessionStart(`auto-install probe-cache ${toolId}: miss (not ast-grep: ${entry.path})`);
1335
1538
  delete cache[toolId];
1336
- _probeCacheDirty = true;
1337
- scheduleProbeFlush();
1539
+ markProbeCacheChange(toolId, null);
1338
1540
  return undefined;
1339
1541
  }
1340
1542
  return entry.path;
@@ -1342,8 +1544,7 @@ export async function checkProbeCache(toolId) {
1342
1544
  catch {
1343
1545
  logSessionStart(`auto-install probe-cache ${toolId}: miss (gone: ${entry.path})`);
1344
1546
  delete cache[toolId];
1345
- _probeCacheDirty = true;
1346
- scheduleProbeFlush();
1547
+ markProbeCacheChange(toolId, null);
1347
1548
  return undefined;
1348
1549
  }
1349
1550
  }
@@ -1352,13 +1553,13 @@ export async function updateProbeCache(toolId, resolvedPath) {
1352
1553
  try {
1353
1554
  const stat = await fs.stat(resolvedPath);
1354
1555
  const cache = await readProbeCache();
1355
- cache[toolId] = {
1556
+ const entry = {
1356
1557
  path: resolvedPath,
1357
1558
  mtimeMs: stat.mtimeMs,
1358
1559
  cachedAt: Date.now(),
1359
1560
  };
1360
- _probeCacheDirty = true;
1361
- scheduleProbeFlush();
1561
+ cache[toolId] = entry;
1562
+ markProbeCacheChange(toolId, entry);
1362
1563
  }
1363
1564
  catch {
1364
1565
  // best-effort
@@ -1368,16 +1569,33 @@ export async function updateProbeCache(toolId, resolvedPath) {
1368
1569
  export function resetProbeCacheStateForTesting() {
1369
1570
  _probeCache = null;
1370
1571
  _probeCacheDirty = false;
1572
+ _probeCacheChanges.clear();
1573
+ _probeCacheChangeVersions.clear();
1574
+ _probeCacheChangeGeneration = 0;
1575
+ _probeCacheRetryAttempt = 0;
1371
1576
  resolvedPathCache.clear();
1372
1577
  ensureInFlight.clear();
1373
1578
  installFailureReasons.clear();
1374
1579
  lastManagedInstallVersion.clear();
1580
+ resetPathWalkMemo();
1375
1581
  if (_probeCacheFlushTimer !== null) {
1376
1582
  clearTimeout(_probeCacheFlushTimer);
1377
1583
  _probeCacheFlushTimer = null;
1378
1584
  }
1379
1585
  }
1380
1586
  // --- Check Functions ---
1587
+ const pathWalkMemo = new Map();
1588
+ function hashSync(value) {
1589
+ let hash = 0x811c9dc5;
1590
+ for (let i = 0; i < value.length; i += 1) {
1591
+ hash ^= value.charCodeAt(i);
1592
+ hash = Math.imul(hash, 0x01000193);
1593
+ }
1594
+ return (hash >>> 0).toString(16);
1595
+ }
1596
+ export function resetPathWalkMemo() {
1597
+ pathWalkMemo.clear();
1598
+ }
1381
1599
  /**
1382
1600
  * Check if a command is available in PATH by walking PATH entries and
1383
1601
  * verifying each candidate is a real file with non-zero size.
@@ -1385,7 +1603,7 @@ export function resetProbeCacheStateForTesting() {
1385
1603
  * spawning a process — ~μs per candidate vs ~50ms for which/where.
1386
1604
  */
1387
1605
  export async function isCommandAvailable(command, _args) {
1388
- const isWindows = process.platform === "win32";
1606
+ const isWindows = installerPlatform() === "win32";
1389
1607
  const pathEnv = process.env.PATH || process.env.Path || process.env.path || "";
1390
1608
  const dirs = pathEnv.split(path.delimiter);
1391
1609
  // On Windows, probe .exe, .cmd, and .bat extensions in addition to bare name.
@@ -1427,7 +1645,12 @@ export async function isSpawnableCommand(command) {
1427
1645
  return false;
1428
1646
  }
1429
1647
  }
1430
- return isCommandAvailable(command);
1648
+ const memoKey = `${command}:${hashSync(process.env.PATH || "")}`;
1649
+ if (pathWalkMemo.has(memoKey))
1650
+ return pathWalkMemo.get(memoKey) ?? false;
1651
+ const isSpawnable = await isCommandAvailable(command);
1652
+ pathWalkMemo.set(memoKey, isSpawnable);
1653
+ return isSpawnable;
1431
1654
  }
1432
1655
  // --- Verification Functions
1433
1656
  /**
@@ -1483,7 +1706,7 @@ const lastManagedInstallVersion = new Map();
1483
1706
  */
1484
1707
  async function verifyToolBinary(binPath, onVersionOutput) {
1485
1708
  return new Promise((resolve) => {
1486
- const isWindows = process.platform === "win32";
1709
+ const isWindows = installerPlatform() === "win32";
1487
1710
  const hasKnownWindowsExt = /\.(cmd|exe|ps1)$/i.test(binPath);
1488
1711
  // On Windows, resolve the best executable path:
1489
1712
  // - extensionless → prefer .cmd (cmd.exe-safe)
@@ -1644,7 +1867,7 @@ export async function getAllToolStatuses() {
1644
1867
  }
1645
1868
  // 5. Check pi-lens auto-install (~/.pi-lens/tools/)
1646
1869
  const localBase = path.join(TOOLS_DIR, "node_modules", ".bin", tool.binaryName || tool.id);
1647
- const localPath = process.platform === "win32" ? `${localBase}.cmd` : localBase;
1870
+ const localPath = installerPlatform() === "win32" ? `${localBase}.cmd` : localBase;
1648
1871
  try {
1649
1872
  await fs.access(localPath);
1650
1873
  if (await verifyToolBinary(localPath)) {
@@ -1709,7 +1932,7 @@ export function resolvePlatformPackageBinary(tool) {
1709
1932
  const spec = tool.platformPackage;
1710
1933
  if (!spec || !tool.packageName)
1711
1934
  return undefined;
1712
- const suffix = spec.suffixes[`${process.platform}-${process.arch}`];
1935
+ const suffix = spec.suffixes[`${installerPlatform()}-${process.arch}`];
1713
1936
  if (!suffix)
1714
1937
  return undefined;
1715
1938
  const platformPkg = `${spec.base ?? tool.packageName}-${suffix}`;
@@ -1725,7 +1948,7 @@ export function resolvePlatformPackageBinary(tool) {
1725
1948
  catch {
1726
1949
  pkgDir = path.dirname(_installerRequire.resolve(`${platformPkg}/package.json`));
1727
1950
  }
1728
- const isWin = process.platform === "win32";
1951
+ const isWin = installerPlatform() === "win32";
1729
1952
  for (const bin of spec.binaries) {
1730
1953
  for (const name of isWin ? [`${bin}.exe`, bin] : [bin]) {
1731
1954
  const candidate = path.join(pkgDir, name);
@@ -1771,7 +1994,7 @@ export async function getToolPath(toolId) {
1771
1994
  // This avoids the ~2-5s overhead of spawning npm global probes and PATH
1772
1995
  // searches for tools we already manage locally.
1773
1996
  const localBase = path.join(TOOLS_DIR, "node_modules", ".bin", tool.binaryName || tool.id);
1774
- if (process.platform === "win32") {
1997
+ if (installerPlatform() === "win32") {
1775
1998
  // Prefer .cmd over extensionless — Node.js can't execute POSIX shell scripts on Windows
1776
1999
  const cmdPath = `${localBase}.cmd`;
1777
2000
  try {
@@ -1798,15 +2021,17 @@ export async function getToolPath(toolId) {
1798
2021
  // fall through to extensionless
1799
2022
  }
1800
2023
  }
1801
- try {
1802
- await fs.access(localBase);
1803
- if (await verifyToolBinary(localBase, recordVersion)) {
1804
- return localBase;
2024
+ if (installerPlatform() !== "win32") {
2025
+ try {
2026
+ await fs.access(localBase);
2027
+ if (await verifyToolBinary(localBase, recordVersion)) {
2028
+ return localBase;
2029
+ }
2030
+ logSessionStart(`auto-install verify: ${localBase} exists but is broken, will reinstall`);
2031
+ }
2032
+ catch {
2033
+ // fall through to global checks
1805
2034
  }
1806
- logSessionStart(`auto-install verify: ${localBase} exists but is broken, will reinstall`);
1807
- }
1808
- catch {
1809
- // fall through to global checks
1810
2035
  }
1811
2036
  // npm/pnpm/bun: prefer the native per-platform binary directly. The main
1812
2037
  // package's launcher often can't find it under a symlink store / after a
@@ -1909,7 +2134,6 @@ async function findNpmGlobalToolPath(binaryName) {
1909
2134
  ? [
1910
2135
  path.join(dir, `${binaryName}.cmd`),
1911
2136
  path.join(dir, `${binaryName}.exe`),
1912
- path.join(dir, binaryName),
1913
2137
  ]
1914
2138
  : [path.join(dir, binaryName)];
1915
2139
  for (const candidate of candidates) {
@@ -2160,7 +2384,7 @@ async function installGitHubTool(tool) {
2160
2384
  logSessionStart(`github-install ${tool.id}: release fetch failed: ${err.message}`);
2161
2385
  return undefined;
2162
2386
  }
2163
- const asset = releaseJson.assets.find((a) => a.name.includes(assetSubstring)) ??
2387
+ const asset = pickReleaseAsset(releaseJson.assets, assetSubstring) ??
2164
2388
  deriveHashiCorpReleaseAsset(tool, releaseJson.tag_name, assetSubstring);
2165
2389
  if (!asset) {
2166
2390
  logSessionStart(`github-install ${tool.id}: no asset matched "${assetSubstring}"`);
@@ -2537,7 +2761,7 @@ async function installNpmTool(packageName, binaryName) {
2537
2761
  await fs.writeFile(packageJsonPath, JSON.stringify({ name: "pi-lens-tools", version: "1.0.0" }, null, 2));
2538
2762
  }
2539
2763
  // Resolve the package manager for the tools dir and build install args.
2540
- const isWindows = process.platform === "win32";
2764
+ const isWindows = installerPlatform() === "win32";
2541
2765
  const pm = await resolveNodePackageManager(TOOLS_DIR);
2542
2766
  const testNpmScript = process.env.PI_LENS_TEST_MODE === "1"
2543
2767
  ? process.env.PI_LENS_TEST_NPM_SCRIPT
@@ -2583,9 +2807,12 @@ async function installNpmTool(packageName, binaryName) {
2583
2807
  if (!outcome.ok) {
2584
2808
  throw new Error(`Failed to install ${packageName}: ${outcome.stderr}`);
2585
2809
  }
2586
- const binPath = path.join(TOOLS_DIR, "node_modules", ".bin", binaryName);
2810
+ // npm creates a command shim on Windows; retain that actual executable path
2811
+ // rather than probing/storing the extensionless POSIX sibling.
2812
+ const binBase = path.join(TOOLS_DIR, "node_modules", ".bin", binaryName);
2813
+ const binPath = installerPlatform() === "win32" ? `${binBase}.cmd` : binBase;
2587
2814
  // Make executable on Unix
2588
- if (process.platform !== "win32") {
2815
+ if (installerPlatform() !== "win32") {
2589
2816
  try {
2590
2817
  await fs.chmod(binPath, 0o750);
2591
2818
  }
@@ -2615,10 +2842,10 @@ async function installNpmTool(packageName, binaryName) {
2615
2842
  try {
2616
2843
  const packagePath = path.join(TOOLS_DIR, "node_modules", packageName);
2617
2844
  await fs.rm(packagePath, { recursive: true, force: true });
2618
- await fs.rm(binPath, { force: true });
2845
+ await fs.rm(binBase, { force: true });
2619
2846
  if (isWindows) {
2620
- await fs.rm(`${binPath}.cmd`, { force: true });
2621
- await fs.rm(`${binPath}.ps1`, { force: true });
2847
+ await fs.rm(`${binBase}.cmd`, { force: true });
2848
+ await fs.rm(`${binBase}.ps1`, { force: true });
2622
2849
  }
2623
2850
  }
2624
2851
  catch {
@@ -2784,6 +3011,33 @@ async function installGemTool(packageName) {
2784
3011
  return undefined;
2785
3012
  }
2786
3013
  }
3014
+ async function finishInstallAttempt(toolId, ok, startedAt) {
3015
+ logSessionStart(`auto-install ${toolId}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
3016
+ if (ok) {
3017
+ // A prior availability probe may have cached ENOENT for this exact child
3018
+ // PATH. Make a successful mutation visible immediately rather than waiting
3019
+ // for the bounded negative-cache TTL or the next session reset (#1199).
3020
+ resetSafeSpawnWindowsCommandCache();
3021
+ // #1276: the madge managed-path memo is keyed only by projectRoot, but
3022
+ // reads PATH/discovery/install state that a completed install can just
3023
+ // have changed — drop it here too, right alongside the safe-spawn reset.
3024
+ // AWAITED (not fire-and-forget): a caller that starts the next madge
3025
+ // resolution the instant `installTool`/`ensureTool` resolves must observe
3026
+ // the reset memo, not a stale pre-install one, and a rejected dynamic
3027
+ // import must not become a silent unhandled rejection. Dynamic import
3028
+ // avoids a static dependency-checker.js <-> installer/index.js cycle
3029
+ // (dependency-checker.js already imports this module dynamically for the
3030
+ // same reason).
3031
+ try {
3032
+ const { resetMadgeManagedPathMemo } = await import("../dependency-checker.js");
3033
+ resetMadgeManagedPathMemo();
3034
+ }
3035
+ catch (err) {
3036
+ logSessionStart(`auto-install ${toolId}: madge memo reset failed: ${err.message}`);
3037
+ }
3038
+ }
3039
+ return ok;
3040
+ }
2787
3041
  /**
2788
3042
  * Install a tool by ID
2789
3043
  */
@@ -2806,49 +3060,37 @@ export async function installTool(toolId) {
2806
3060
  if (!tool.packageName || !tool.binaryName)
2807
3061
  return false;
2808
3062
  const npmPath = await installNpmTool(tool.packageName, tool.binaryName);
2809
- const ok = npmPath !== undefined;
2810
- logSessionStart(`auto-install ${tool.id}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
2811
- return ok;
3063
+ return finishInstallAttempt(tool.id, npmPath !== undefined, startedAt);
2812
3064
  }
2813
3065
  case "pip": {
2814
3066
  if (!tool.packageName)
2815
3067
  return false;
2816
3068
  const pipPath = await installPipTool(tool.packageName);
2817
- const ok = pipPath !== undefined;
2818
- logSessionStart(`auto-install ${tool.id}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
2819
- return ok;
3069
+ return finishInstallAttempt(tool.id, pipPath !== undefined, startedAt);
2820
3070
  }
2821
3071
  case "gem": {
2822
3072
  if (!tool.packageName)
2823
3073
  return false;
2824
3074
  const gemPath = await installGemTool(tool.packageName);
2825
- const ok = gemPath !== undefined;
2826
- logSessionStart(`auto-install ${tool.id}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
2827
- return ok;
3075
+ return finishInstallAttempt(tool.id, gemPath !== undefined, startedAt);
2828
3076
  }
2829
3077
  case "github": {
2830
3078
  if (!tool.github)
2831
3079
  return false;
2832
3080
  const ghPath = await installGitHubTool(tool);
2833
- const ok = ghPath !== undefined;
2834
- logSessionStart(`auto-install ${tool.id}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
2835
- return ok;
3081
+ return finishInstallAttempt(tool.id, ghPath !== undefined, startedAt);
2836
3082
  }
2837
3083
  case "maven": {
2838
3084
  if (!tool.maven)
2839
3085
  return false;
2840
3086
  const mavenPath = await installMavenTool(tool);
2841
- const ok = mavenPath !== undefined;
2842
- logSessionStart(`auto-install ${tool.id}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
2843
- return ok;
3087
+ return finishInstallAttempt(tool.id, mavenPath !== undefined, startedAt);
2844
3088
  }
2845
3089
  case "archive": {
2846
3090
  if (!tool.archive)
2847
3091
  return false;
2848
3092
  const archivePath = await installArchiveTool(tool);
2849
- const ok = archivePath !== undefined;
2850
- logSessionStart(`auto-install ${tool.id}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
2851
- return ok;
3093
+ return finishInstallAttempt(tool.id, archivePath !== undefined, startedAt);
2852
3094
  }
2853
3095
  default:
2854
3096
  logSessionStart(`auto-install ${tool.id}: unsupported strategy`);
@@ -2861,9 +3103,24 @@ export async function installTool(toolId) {
2861
3103
  }
2862
3104
  }
2863
3105
  /**
2864
- * Ensure a tool is installed (check first, install if missing)
3106
+ * Ensure a tool is installed (check first, install if missing).
3107
+ *
3108
+ * #1334 S5: when the pi host has actively denied project trust, the INSTALL
3109
+ * half is switched off here — the request degrades to the existing
3110
+ * `allowInstall:false` discovery-only path rather than failing outright, so an
3111
+ * already-present binary keeps working while nothing is downloaded or executed
3112
+ * on behalf of an untrusted project. A host with no trust surface at all
3113
+ * (`"unknown"`) is unaffected.
2865
3114
  */
2866
3115
  export async function ensureTool(toolId, opts) {
3116
+ if (opts?.allowInstall !== false &&
3117
+ !assertInstallAllowed(`managed tool ensure: ${toolId}`)) {
3118
+ logSessionStart(`auto-install ensure ${toolId}: install gated — ${projectTrustDenialReason()}; discovery only`);
3119
+ return ensureToolResolved(toolId, { ...opts, allowInstall: false });
3120
+ }
3121
+ return ensureToolResolved(toolId, opts);
3122
+ }
3123
+ async function ensureToolResolved(toolId, opts) {
2867
3124
  installFailureReasons.delete(toolId);
2868
3125
  const cacheResolvedPath = (result) => {
2869
3126
  if (result) {
@@ -2885,8 +3142,7 @@ export async function ensureTool(toolId, opts) {
2885
3142
  try {
2886
3143
  const probeCache = await readProbeCache();
2887
3144
  delete probeCache[toolId];
2888
- _probeCacheDirty = true;
2889
- scheduleProbeFlush();
3145
+ markProbeCacheChange(toolId, null);
2890
3146
  }
2891
3147
  catch {
2892
3148
  // best-effort
@@ -2925,8 +3181,23 @@ export async function ensureTool(toolId, opts) {
2925
3181
  }
2926
3182
  // Fast path 1: in-memory session cache — no I/O.
2927
3183
  const cached = resolvedPathCache.get(toolId);
2928
- if (cached)
2929
- return cached;
3184
+ if (cached) {
3185
+ if (!isFullyQualified(cached))
3186
+ return cached;
3187
+ try {
3188
+ await fs.access(cached);
3189
+ return cached;
3190
+ }
3191
+ catch {
3192
+ // The executor would report ENOENT for this cached positive. Evict it
3193
+ // before discovery so the failure heals on this call, not after restart.
3194
+ }
3195
+ resolvedPathCache.delete(toolId);
3196
+ const probeCache = await readProbeCache();
3197
+ delete probeCache[toolId];
3198
+ markProbeCacheChange(toolId, null);
3199
+ logSessionStart(`auto-install ensure ${toolId}: cached path disappeared; re-probing`);
3200
+ }
2930
3201
  // Fast path 2: persistent probe cache — fs.access + stat, no process spawn.
2931
3202
  const diskCached = await checkProbeCache(toolId);
2932
3203
  if (diskCached) {
@@ -3100,9 +3371,11 @@ export const GITHUB_TOOLS = [
3100
3371
  "zizmor",
3101
3372
  "typos-lsp",
3102
3373
  "tflint",
3374
+ "terragrunt",
3103
3375
  "terraform-ls",
3104
3376
  "zls",
3105
3377
  "hadolint",
3378
+ "helm",
3106
3379
  "gitleaks",
3107
3380
  "taplo",
3108
3381
  "vale",
@@ -3135,6 +3408,28 @@ export function resolveGitHubArchiveBinaryCandidates(toolId, platform, assetName
3135
3408
  const binaryName = tool.github?.binaryInArchive ?? tool.binaryName ?? tool.id;
3136
3409
  return getArchiveBinaryCandidates(binaryName, platform, assetName);
3137
3410
  }
3411
+ // Signature/checksum siblings a release publishes next to the real asset. A
3412
+ // bare-binary tool's substring IS the whole asset name, so `includes` alone
3413
+ // matches `<asset>.asc` too and would install a signature file as the binary.
3414
+ const ASSET_SIDECAR_SUFFIXES = [
3415
+ ".asc",
3416
+ ".sig",
3417
+ ".minisig",
3418
+ ".pem",
3419
+ ".cert",
3420
+ ".sbom",
3421
+ ".sha256",
3422
+ ".sha256sum",
3423
+ ".md5",
3424
+ ];
3425
+ function isAssetSidecar(name) {
3426
+ const lower = name.toLowerCase();
3427
+ return ASSET_SIDECAR_SUFFIXES.some((suffix) => lower.endsWith(suffix));
3428
+ }
3429
+ export function pickReleaseAsset(assets, assetSubstring) {
3430
+ return (assets.find((a) => a.name === assetSubstring) ??
3431
+ assets.find((a) => a.name.includes(assetSubstring) && !isAssetSidecar(a.name)));
3432
+ }
3138
3433
  function deriveHashiCorpReleaseAsset(tool, tagName, assetSubstring) {
3139
3434
  const product = tool.github?.hashiCorpReleaseProduct;
3140
3435
  if (!product || !tagName)