@herbertgao/pi-extensions 2026.8.3 → 2026.8.5

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 (408) hide show
  1. package/README.md +45 -4
  2. package/examples/pi-footer.json +275 -0
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
  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 +5 -8
  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 +8 -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 +15 -2
  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/schedule-menu.ts +9 -8
  25. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  26. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  27. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  31. package/node_modules/@narumitw/pi-btw/README.md +8 -7
  32. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  33. package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
  34. package/node_modules/pi-footer/CHANGELOG.md +145 -0
  35. package/node_modules/pi-footer/LICENSE +21 -0
  36. package/node_modules/pi-footer/README.md +354 -0
  37. package/node_modules/pi-footer/package.json +68 -0
  38. package/node_modules/pi-footer/src/cache.ts +106 -0
  39. package/node_modules/pi-footer/src/colors.ts +237 -0
  40. package/node_modules/pi-footer/src/config.ts +202 -0
  41. package/node_modules/pi-footer/src/event-widgets.ts +41 -0
  42. package/node_modules/pi-footer/src/extension-statuses.ts +102 -0
  43. package/node_modules/pi-footer/src/git.ts +135 -0
  44. package/node_modules/pi-footer/src/index.ts +268 -0
  45. package/node_modules/pi-footer/src/metrics.ts +141 -0
  46. package/node_modules/pi-footer/src/presets.ts +383 -0
  47. package/node_modules/pi-footer/src/render.ts +114 -0
  48. package/node_modules/pi-footer/src/separators.ts +52 -0
  49. package/node_modules/pi-footer/src/types.ts +122 -0
  50. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +22 -0
  51. package/node_modules/pi-footer/src/ui/color-options.ts +195 -0
  52. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +61 -0
  53. package/node_modules/pi-footer/src/ui/edit-colors.ts +13 -0
  54. package/node_modules/pi-footer/src/ui/events.ts +35 -0
  55. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +60 -0
  56. package/node_modules/pi-footer/src/ui/extension-statuses.ts +73 -0
  57. package/node_modules/pi-footer/src/ui/fields.ts +252 -0
  58. package/node_modules/pi-footer/src/ui/global-menu.ts +126 -0
  59. package/node_modules/pi-footer/src/ui/helpers.ts +44 -0
  60. package/node_modules/pi-footer/src/ui/layout.ts +17 -0
  61. package/node_modules/pi-footer/src/ui/line-list.ts +19 -0
  62. package/node_modules/pi-footer/src/ui/model.ts +64 -0
  63. package/node_modules/pi-footer/src/ui/navigation.ts +29 -0
  64. package/node_modules/pi-footer/src/ui/option-edit.ts +93 -0
  65. package/node_modules/pi-footer/src/ui/overlay-render.ts +103 -0
  66. package/node_modules/pi-footer/src/ui/screen-context.ts +18 -0
  67. package/node_modules/pi-footer/src/ui/screen-controller.ts +27 -0
  68. package/node_modules/pi-footer/src/ui/screen-render.ts +32 -0
  69. package/node_modules/pi-footer/src/ui/screen-state.ts +20 -0
  70. package/node_modules/pi-footer/src/ui/screen.ts +243 -0
  71. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +92 -0
  72. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +52 -0
  73. package/node_modules/pi-footer/src/ui/screens/controller.ts +12 -0
  74. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +68 -0
  75. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +115 -0
  76. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +61 -0
  77. package/node_modules/pi-footer/src/ui/screens/global.ts +75 -0
  78. package/node_modules/pi-footer/src/ui/screens/line-list.ts +108 -0
  79. package/node_modules/pi-footer/src/ui/screens/main.ts +99 -0
  80. package/node_modules/pi-footer/src/ui/screens/terminal.ts +121 -0
  81. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +154 -0
  82. package/node_modules/pi-footer/src/ui/terminal-menu.ts +32 -0
  83. package/node_modules/pi-footer/src/ui/theme.ts +48 -0
  84. package/node_modules/pi-footer/src/ui/title-bar.ts +100 -0
  85. package/node_modules/pi-footer/src/ui/widget-actions.ts +76 -0
  86. package/node_modules/pi-footer/src/ui.ts +66 -0
  87. package/node_modules/pi-footer/src/widgets/context.ts +28 -0
  88. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +17 -0
  89. package/node_modules/pi-footer/src/widgets/core/context-window.ts +24 -0
  90. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +19 -0
  91. package/node_modules/pi-footer/src/widgets/core/cwd.ts +108 -0
  92. package/node_modules/pi-footer/src/widgets/core/event.ts +34 -0
  93. package/node_modules/pi-footer/src/widgets/core/external-status.ts +102 -0
  94. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +20 -0
  95. package/node_modules/pi-footer/src/widgets/core/model.ts +35 -0
  96. package/node_modules/pi-footer/src/widgets/core/provider.ts +17 -0
  97. package/node_modules/pi-footer/src/widgets/core/session-name.ts +17 -0
  98. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +17 -0
  99. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +17 -0
  100. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +17 -0
  101. package/node_modules/pi-footer/src/widgets/git/branch.ts +81 -0
  102. package/node_modules/pi-footer/src/widgets/git/clean.ts +19 -0
  103. package/node_modules/pi-footer/src/widgets/git/deletions.ts +17 -0
  104. package/node_modules/pi-footer/src/widgets/git/diff.ts +38 -0
  105. package/node_modules/pi-footer/src/widgets/git/insertions.ts +17 -0
  106. package/node_modules/pi-footer/src/widgets/git/remote.ts +17 -0
  107. package/node_modules/pi-footer/src/widgets/git/root.ts +17 -0
  108. package/node_modules/pi-footer/src/widgets/git/sha.ts +17 -0
  109. package/node_modules/pi-footer/src/widgets/git/staged.ts +17 -0
  110. package/node_modules/pi-footer/src/widgets/git/status.ts +19 -0
  111. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +17 -0
  112. package/node_modules/pi-footer/src/widgets/git/untracked.ts +17 -0
  113. package/node_modules/pi-footer/src/widgets/instance.ts +118 -0
  114. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +18 -0
  115. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +32 -0
  116. package/node_modules/pi-footer/src/widgets/layout/separator.ts +57 -0
  117. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +31 -0
  118. package/node_modules/pi-footer/src/widgets/options.ts +122 -0
  119. package/node_modules/pi-footer/src/widgets/project/runtime.ts +361 -0
  120. package/node_modules/pi-footer/src/widgets/registry.ts +223 -0
  121. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +17 -0
  122. package/node_modules/pi-footer/src/widgets/session/compactions.ts +17 -0
  123. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +18 -0
  124. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +18 -0
  125. package/node_modules/pi-footer/src/widgets/session/messages.ts +19 -0
  126. package/node_modules/pi-footer/src/widgets/session/session-id.ts +17 -0
  127. package/node_modules/pi-footer/src/widgets/session/session-start.ts +18 -0
  128. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +17 -0
  129. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +19 -0
  130. package/node_modules/pi-footer/src/widgets/session/total-time.ts +18 -0
  131. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +17 -0
  132. package/node_modules/pi-footer/src/widgets/store.ts +26 -0
  133. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +55 -0
  134. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +18 -0
  135. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +18 -0
  136. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +108 -0
  137. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +26 -0
  138. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +29 -0
  139. package/node_modules/pi-footer/src/widgets/tokens/context.ts +29 -0
  140. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +51 -0
  141. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +25 -0
  142. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +18 -0
  143. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +25 -0
  144. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +18 -0
  145. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +20 -0
  146. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +25 -0
  147. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +18 -0
  148. package/node_modules/pi-footer/src/widgets/types.ts +181 -0
  149. package/node_modules/pi-footer/src/widgets/utils/colors.ts +11 -0
  150. package/node_modules/pi-footer/src/widgets/utils/context.ts +112 -0
  151. package/node_modules/pi-footer/src/widgets/utils/session.ts +19 -0
  152. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +78 -0
  153. package/node_modules/pi-lens/CHANGELOG.md +2440 -954
  154. package/node_modules/pi-lens/README.md +13 -0
  155. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  156. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  157. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  158. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  159. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  160. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  161. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  162. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  163. package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
  164. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  165. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  166. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  167. package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
  168. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  169. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  170. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  171. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  172. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  173. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  174. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  175. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  176. package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
  177. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  178. package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
  179. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  180. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  181. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  182. package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
  183. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
  184. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  185. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
  186. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  187. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  188. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  189. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  190. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  191. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
  192. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  193. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  194. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  195. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  196. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  197. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  198. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  199. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  200. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  201. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  202. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  203. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
  204. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  205. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  206. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  207. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  208. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  209. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  210. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  211. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  212. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  213. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  214. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  215. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  216. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
  217. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  218. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  219. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  220. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  221. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  222. package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
  223. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  224. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  225. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  226. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  227. package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
  228. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  229. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  230. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  231. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  232. package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
  233. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  234. package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
  235. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
  236. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  237. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  238. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  239. package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
  240. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  241. package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
  242. package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
  243. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  244. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  245. package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
  246. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  247. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  248. package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
  249. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  250. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  251. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
  252. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  253. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
  254. package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
  255. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  256. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
  257. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  258. package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
  259. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  260. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  261. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  262. package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
  263. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  264. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  265. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  266. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
  267. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  268. package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
  269. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  270. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  271. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  272. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  273. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  274. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  275. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  276. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  277. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  278. package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
  279. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  280. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  281. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  282. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  283. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  284. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  285. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  286. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  287. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  288. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  289. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  290. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  291. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  292. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  293. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  294. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  295. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  296. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  297. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  298. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  299. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  300. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  301. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  302. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
  303. package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
  304. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
  305. package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
  306. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  307. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
  308. package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
  309. package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
  310. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  311. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
  312. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  313. package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
  314. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  315. package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
  316. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  317. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  318. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  319. package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
  320. package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
  321. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  322. package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
  323. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  324. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  325. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  326. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  327. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  328. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  329. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  330. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  331. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  332. package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
  333. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  334. package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
  335. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  336. package/node_modules/pi-lens/dist/index.js +49361 -36888
  337. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  338. package/node_modules/pi-lens/dist/mcp/server.js +211 -44
  339. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  340. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
  341. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
  342. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  343. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  344. package/node_modules/pi-lens/docs/agent-guide.md +2 -2
  345. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  346. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  347. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  348. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
  349. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  350. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  351. package/node_modules/pi-lens/docs/fable.md +27 -0
  352. package/node_modules/pi-lens/docs/features.md +31 -2
  353. package/node_modules/pi-lens/docs/globalconfig.md +45 -1
  354. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  355. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  356. package/node_modules/pi-lens/docs/mcp.md +37 -4
  357. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  358. package/node_modules/pi-lens/docs/settings.md +12 -1
  359. package/node_modules/pi-lens/docs/tools.md +4 -1
  360. package/node_modules/pi-lens/docs/usage.md +18 -0
  361. package/node_modules/pi-lens/package.json +10 -10
  362. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  363. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  364. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  365. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  366. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  367. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  368. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  369. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  370. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  371. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  372. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  373. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  374. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
  375. package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
  376. package/node_modules/pi-mcp-adapter/README.md +34 -3
  377. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  378. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  379. package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
  380. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  381. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  382. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  383. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  384. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  385. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  386. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  387. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  388. package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
  389. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  390. package/node_modules/pi-mcp-adapter/package.json +3 -3
  391. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  392. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  393. package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
  394. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  395. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  396. package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
  397. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  398. package/node_modules/pi-mcp-adapter/types.ts +93 -27
  399. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  400. package/package.json +17 -13
  401. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  402. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  403. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  404. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  405. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  406. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  407. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  408. 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
  {
@@ -418,6 +455,31 @@ export const TOOLS = [
418
455
  },
419
456
  },
420
457
  },
458
+ {
459
+ id: "helm",
460
+ name: "Helm",
461
+ checkCommand: "helm",
462
+ checkArgs: ["version", "--short"],
463
+ installStrategy: "github",
464
+ binaryName: "helm",
465
+ github: {
466
+ repo: "helm/helm",
467
+ assetMatch: (platform, arch) => {
468
+ // helm publishes per-OS archives: tar.gz for POSIX, zip for Windows.
469
+ const cpu = arch === "arm64" ? "arm64" : "amd64";
470
+ if (platform === "linux")
471
+ return `linux-${cpu}.tar.gz`;
472
+ if (platform === "darwin")
473
+ return `darwin-${cpu}.tar.gz`;
474
+ if (platform === "win32")
475
+ return `windows-${cpu}.zip`;
476
+ return undefined;
477
+ },
478
+ // Release archives nest the executable under an OS/arch directory;
479
+ // the installer searches recursively and adds the Windows suffix.
480
+ binaryInArchive: "helm",
481
+ },
482
+ },
421
483
  {
422
484
  // Opengrep: a single standalone binary per platform on GitHub releases —
423
485
  // no login, no telemetry (the reason for switching off Semgrep, #111).
@@ -919,18 +981,39 @@ export const TOOLS = [
919
981
  binaryName: "tflint",
920
982
  github: {
921
983
  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
- },
984
+ assetMatch: archAssetMatch(OS_ARCH_ZIP_ASSETS),
931
985
  binaryInArchive: "tflint",
932
986
  },
933
987
  },
988
+ {
989
+ // Terragrunt ships a bare native binary per platform on GitHub releases.
990
+ // Windows arm64 uses the x64 binary through Windows' built-in emulation —
991
+ // there is no terragrunt_windows_arm64.exe upstream.
992
+ id: "terragrunt",
993
+ name: "terragrunt",
994
+ checkCommand: "terragrunt",
995
+ checkArgs: ["--version"],
996
+ installStrategy: "github",
997
+ binaryName: "terragrunt",
998
+ github: {
999
+ repo: "gruntwork-io/terragrunt",
1000
+ assetMatch: archAssetMatch({
1001
+ linux: {
1002
+ x64: "terragrunt_linux_amd64",
1003
+ arm64: "terragrunt_linux_arm64",
1004
+ },
1005
+ darwin: {
1006
+ x64: "terragrunt_darwin_amd64",
1007
+ arm64: "terragrunt_darwin_arm64",
1008
+ },
1009
+ win32: {
1010
+ x64: "terragrunt_windows_amd64.exe",
1011
+ arm64: "terragrunt_windows_amd64.exe",
1012
+ },
1013
+ }),
1014
+ // bare binary — no binaryInArchive
1015
+ },
1016
+ },
934
1017
  {
935
1018
  id: "gitleaks",
936
1019
  name: "gitleaks",
@@ -1055,15 +1138,7 @@ export const TOOLS = [
1055
1138
  github: {
1056
1139
  repo: "hashicorp/terraform-ls",
1057
1140
  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
- },
1141
+ assetMatch: archAssetMatch(OS_ARCH_ZIP_ASSETS),
1067
1142
  binaryInArchive: "terraform-ls",
1068
1143
  },
1069
1144
  },
@@ -1221,33 +1296,164 @@ export function getInstallFailureReason(toolId) {
1221
1296
  return installFailureReasons.get(toolId);
1222
1297
  }
1223
1298
  // Session-lifetime cache: once a tool path is resolved, skip the process-spawn check on subsequent calls.
1224
- const resolvedPathCache = new Map();
1299
+ const resolvedPathCache = new BoundedLruCache(256);
1225
1300
  const PROBE_CACHE_PATH = path.join(getGlobalPiLensDir(), "probe-cache.json");
1301
+ const PROBE_CACHE_LOCK_STALE_MS = 180_000;
1226
1302
  const PROBE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
1303
+ const PROBE_CACHE_FLUSH_LOCK_WAIT_MS = 250;
1304
+ const PROBE_CACHE_FLUSH_RETRY_DELAY_MS = 300;
1305
+ const PROBE_CACHE_FLUSH_RETRY_MAX_DELAY_MS = 30_000;
1227
1306
  let _probeCache = null;
1228
1307
  let _probeCacheDirty = false;
1229
1308
  let _probeCacheFlushTimer = null;
1309
+ let _probeCacheWriteInFlight = null;
1310
+ let _probeCacheRetryAttempt = 0;
1311
+ let _probeCacheChangeGeneration = 0;
1312
+ // Read-modify-write deltas let a long-lived process merge with entries another
1313
+ // process discovered after this process's initial read. `null` is a deliberate
1314
+ // deletion (TTL/stale-path eviction), not an absent map value.
1315
+ const _probeCacheChanges = new Map();
1316
+ const _probeCacheChangeVersions = new Map();
1230
1317
  async function readProbeCache() {
1231
1318
  if (_probeCache !== null)
1232
1319
  return _probeCache;
1233
1320
  try {
1234
1321
  const raw = await fs.readFile(PROBE_CACHE_PATH, "utf-8");
1235
- _probeCache = JSON.parse(raw);
1322
+ const parsed = JSON.parse(raw);
1323
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1324
+ throw new Error("probe-cache root is not an object");
1325
+ }
1326
+ _probeCache = parsed;
1236
1327
  }
1237
- catch {
1328
+ catch (err) {
1329
+ const code = err?.code;
1330
+ if (code !== "ENOENT") {
1331
+ logSessionStart(`auto-install probe-cache: read failed (${code ?? "invalid"}); treating cache as unavailable`);
1332
+ }
1238
1333
  _probeCache = {};
1239
1334
  }
1240
1335
  return _probeCache;
1241
1336
  }
1242
- function scheduleProbeFlush() {
1337
+ function markProbeCacheChange(toolId, entry) {
1338
+ _probeCacheChanges.set(toolId, entry);
1339
+ _probeCacheChangeVersions.set(toolId, ++_probeCacheChangeGeneration);
1340
+ _probeCacheDirty = true;
1341
+ scheduleProbeFlush();
1342
+ }
1343
+ function scheduleProbeFlush(delayMs = PROBE_CACHE_FLUSH_RETRY_DELAY_MS) {
1243
1344
  if (_probeCacheFlushTimer !== null)
1244
1345
  return;
1245
1346
  _probeCacheFlushTimer = setTimeout(() => {
1246
1347
  void flushProbeCache();
1247
- }, 300);
1348
+ }, delayMs);
1248
1349
  _probeCacheFlushTimer.unref?.();
1249
1350
  }
1250
- let _probeCacheWriteInFlight = null;
1351
+ function scheduleProbeFlushRetry() {
1352
+ const delay = Math.min(PROBE_CACHE_FLUSH_RETRY_MAX_DELAY_MS, PROBE_CACHE_FLUSH_RETRY_DELAY_MS * 2 ** Math.min(_probeCacheRetryAttempt, 6));
1353
+ _probeCacheRetryAttempt += 1;
1354
+ scheduleProbeFlush(delay);
1355
+ }
1356
+ function snapshotProbeCacheChanges() {
1357
+ return {
1358
+ changes: new Map(_probeCacheChanges),
1359
+ versions: new Map(_probeCacheChangeVersions),
1360
+ };
1361
+ }
1362
+ function deserializeProbeCache(contents) {
1363
+ if (contents === undefined)
1364
+ return {};
1365
+ const parsed = JSON.parse(contents);
1366
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1367
+ throw new Error("probe-cache root is not an object");
1368
+ }
1369
+ return parsed;
1370
+ }
1371
+ function applyProbeCacheChanges(disk, changes) {
1372
+ for (const [toolId, entry] of changes) {
1373
+ if (entry === null)
1374
+ delete disk[toolId];
1375
+ else
1376
+ disk[toolId] = entry;
1377
+ }
1378
+ }
1379
+ /**
1380
+ * Install-lifetime ageing belongs to the authoritative merge, not only to the
1381
+ * lookup path: a sibling process may have left expired entries on disk since
1382
+ * this process loaded its in-memory snapshot. The async durable-store lock
1383
+ * supplies the probe cache's former quarantine/stale-owner recovery.
1384
+ */
1385
+ function ageProbeCache(disk) {
1386
+ const cutoff = Date.now() - PROBE_CACHE_TTL_MS;
1387
+ for (const [toolId, entry] of Object.entries(disk)) {
1388
+ if (!Number.isFinite(entry.cachedAt) || entry.cachedAt < cutoff) {
1389
+ delete disk[toolId];
1390
+ }
1391
+ }
1392
+ }
1393
+ function publishProbeCacheWrite(disk, snapshotVersions) {
1394
+ // Publish the sibling-process merge plus any updates that arrived during
1395
+ // the write. Only an unchanged snapshot entry is retired.
1396
+ const pendingAfterWrite = new Map(_probeCacheChanges);
1397
+ _probeCache = disk;
1398
+ for (const [toolId, entry] of pendingAfterWrite) {
1399
+ if (entry === null)
1400
+ delete _probeCache[toolId];
1401
+ else
1402
+ _probeCache[toolId] = entry;
1403
+ }
1404
+ for (const [toolId, version] of snapshotVersions) {
1405
+ if (_probeCacheChangeVersions.get(toolId) === version) {
1406
+ _probeCacheChanges.delete(toolId);
1407
+ _probeCacheChangeVersions.delete(toolId);
1408
+ }
1409
+ }
1410
+ _probeCacheDirty = _probeCacheChanges.size > 0;
1411
+ _probeCacheRetryAttempt = 0;
1412
+ return _probeCacheDirty ? "written-with-pending" : "written";
1413
+ }
1414
+ async function writeProbeCache() {
1415
+ try {
1416
+ // Snapshot versions before the awaited disk read/write. A new update for
1417
+ // the same tool may arrive while the atomic write is in flight; its newer
1418
+ // version must remain pending for the next flush.
1419
+ const { changes, versions } = snapshotProbeCacheChanges();
1420
+ let result = "written";
1421
+ const committed = await commitDurableStoreAsync({
1422
+ path: PROBE_CACHE_PATH,
1423
+ deserialize: deserializeProbeCache,
1424
+ merge: (disk) => {
1425
+ ageProbeCache(disk);
1426
+ applyProbeCacheChanges(disk, changes);
1427
+ return disk;
1428
+ },
1429
+ serialize: (disk) => JSON.stringify(disk, null, 2),
1430
+ waitMs: PROBE_CACHE_FLUSH_LOCK_WAIT_MS,
1431
+ retryMs: 25,
1432
+ staleMs: PROBE_CACHE_LOCK_STALE_MS,
1433
+ timeoutMessage: "Timed out waiting for probe-cache lock",
1434
+ onContention: "skip-log",
1435
+ logContention: () => {
1436
+ logSessionStart("auto-install probe-cache: flush deferred because another process owns the lock");
1437
+ },
1438
+ afterWriteLocked: (disk) => {
1439
+ result = publishProbeCacheWrite(disk, versions);
1440
+ },
1441
+ });
1442
+ if (committed === undefined) {
1443
+ scheduleProbeFlushRetry();
1444
+ return "deferred";
1445
+ }
1446
+ return result;
1447
+ }
1448
+ catch (err) {
1449
+ // Keep dirty state so a later timer/explicit flush can retry. The error
1450
+ // is logged without paths, source, or command text; an unavailable cache
1451
+ // must never look like a clean empty cache to operators.
1452
+ logSessionStart(`auto-install probe-cache: flush failed (${err?.code ?? "write error"}); pending update retained`);
1453
+ scheduleProbeFlushRetry();
1454
+ return "failed";
1455
+ }
1456
+ }
1251
1457
  /** Await pending probe-cache persistence before a one-shot process exits. */
1252
1458
  export async function flushProbeCache() {
1253
1459
  if (_probeCacheFlushTimer !== null) {
@@ -1257,22 +1463,17 @@ export async function flushProbeCache() {
1257
1463
  // #946 review F4: if the 300ms timer's write already started, the dirty
1258
1464
  // flag is false but the write may still be in flight — an immediate
1259
1465
  // process.exit would truncate it. Always await the in-flight write.
1260
- if (!_probeCacheDirty || _probeCache === null) {
1466
+ if (_probeCacheWriteInFlight) {
1261
1467
  await _probeCacheWriteInFlight;
1262
- return;
1468
+ if (!_probeCacheDirty)
1469
+ return "written";
1263
1470
  }
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
- })();
1471
+ if (!_probeCacheDirty || _probeCache === null)
1472
+ return "idle";
1473
+ const write = writeProbeCache();
1273
1474
  _probeCacheWriteInFlight = write;
1274
1475
  try {
1275
- await write;
1476
+ return await write;
1276
1477
  }
1277
1478
  finally {
1278
1479
  if (_probeCacheWriteInFlight === write)
@@ -1316,8 +1517,7 @@ export async function checkProbeCache(toolId) {
1316
1517
  if (Date.now() - entry.cachedAt > PROBE_CACHE_TTL_MS) {
1317
1518
  logSessionStart(`auto-install probe-cache ${toolId}: miss (ttl expired)`);
1318
1519
  delete cache[toolId];
1319
- _probeCacheDirty = true;
1320
- scheduleProbeFlush();
1520
+ markProbeCacheChange(toolId, null);
1321
1521
  return undefined;
1322
1522
  }
1323
1523
  try {
@@ -1326,15 +1526,13 @@ export async function checkProbeCache(toolId) {
1326
1526
  if (stat.mtimeMs !== entry.mtimeMs) {
1327
1527
  logSessionStart(`auto-install probe-cache ${toolId}: miss (mtime changed)`);
1328
1528
  delete cache[toolId];
1329
- _probeCacheDirty = true;
1330
- scheduleProbeFlush();
1529
+ markProbeCacheChange(toolId, null);
1331
1530
  return undefined;
1332
1531
  }
1333
1532
  if (toolId === "ast-grep" && !(await verifyAstGrepProbePath(entry.path))) {
1334
1533
  logSessionStart(`auto-install probe-cache ${toolId}: miss (not ast-grep: ${entry.path})`);
1335
1534
  delete cache[toolId];
1336
- _probeCacheDirty = true;
1337
- scheduleProbeFlush();
1535
+ markProbeCacheChange(toolId, null);
1338
1536
  return undefined;
1339
1537
  }
1340
1538
  return entry.path;
@@ -1342,8 +1540,7 @@ export async function checkProbeCache(toolId) {
1342
1540
  catch {
1343
1541
  logSessionStart(`auto-install probe-cache ${toolId}: miss (gone: ${entry.path})`);
1344
1542
  delete cache[toolId];
1345
- _probeCacheDirty = true;
1346
- scheduleProbeFlush();
1543
+ markProbeCacheChange(toolId, null);
1347
1544
  return undefined;
1348
1545
  }
1349
1546
  }
@@ -1352,13 +1549,13 @@ export async function updateProbeCache(toolId, resolvedPath) {
1352
1549
  try {
1353
1550
  const stat = await fs.stat(resolvedPath);
1354
1551
  const cache = await readProbeCache();
1355
- cache[toolId] = {
1552
+ const entry = {
1356
1553
  path: resolvedPath,
1357
1554
  mtimeMs: stat.mtimeMs,
1358
1555
  cachedAt: Date.now(),
1359
1556
  };
1360
- _probeCacheDirty = true;
1361
- scheduleProbeFlush();
1557
+ cache[toolId] = entry;
1558
+ markProbeCacheChange(toolId, entry);
1362
1559
  }
1363
1560
  catch {
1364
1561
  // best-effort
@@ -1368,16 +1565,33 @@ export async function updateProbeCache(toolId, resolvedPath) {
1368
1565
  export function resetProbeCacheStateForTesting() {
1369
1566
  _probeCache = null;
1370
1567
  _probeCacheDirty = false;
1568
+ _probeCacheChanges.clear();
1569
+ _probeCacheChangeVersions.clear();
1570
+ _probeCacheChangeGeneration = 0;
1571
+ _probeCacheRetryAttempt = 0;
1371
1572
  resolvedPathCache.clear();
1372
1573
  ensureInFlight.clear();
1373
1574
  installFailureReasons.clear();
1374
1575
  lastManagedInstallVersion.clear();
1576
+ resetPathWalkMemo();
1375
1577
  if (_probeCacheFlushTimer !== null) {
1376
1578
  clearTimeout(_probeCacheFlushTimer);
1377
1579
  _probeCacheFlushTimer = null;
1378
1580
  }
1379
1581
  }
1380
1582
  // --- Check Functions ---
1583
+ const pathWalkMemo = new Map();
1584
+ function hashSync(value) {
1585
+ let hash = 0x811c9dc5;
1586
+ for (let i = 0; i < value.length; i += 1) {
1587
+ hash ^= value.charCodeAt(i);
1588
+ hash = Math.imul(hash, 0x01000193);
1589
+ }
1590
+ return (hash >>> 0).toString(16);
1591
+ }
1592
+ export function resetPathWalkMemo() {
1593
+ pathWalkMemo.clear();
1594
+ }
1381
1595
  /**
1382
1596
  * Check if a command is available in PATH by walking PATH entries and
1383
1597
  * verifying each candidate is a real file with non-zero size.
@@ -1385,7 +1599,7 @@ export function resetProbeCacheStateForTesting() {
1385
1599
  * spawning a process — ~μs per candidate vs ~50ms for which/where.
1386
1600
  */
1387
1601
  export async function isCommandAvailable(command, _args) {
1388
- const isWindows = process.platform === "win32";
1602
+ const isWindows = installerPlatform() === "win32";
1389
1603
  const pathEnv = process.env.PATH || process.env.Path || process.env.path || "";
1390
1604
  const dirs = pathEnv.split(path.delimiter);
1391
1605
  // On Windows, probe .exe, .cmd, and .bat extensions in addition to bare name.
@@ -1427,7 +1641,12 @@ export async function isSpawnableCommand(command) {
1427
1641
  return false;
1428
1642
  }
1429
1643
  }
1430
- return isCommandAvailable(command);
1644
+ const memoKey = `${command}:${hashSync(process.env.PATH || "")}`;
1645
+ if (pathWalkMemo.has(memoKey))
1646
+ return pathWalkMemo.get(memoKey) ?? false;
1647
+ const isSpawnable = await isCommandAvailable(command);
1648
+ pathWalkMemo.set(memoKey, isSpawnable);
1649
+ return isSpawnable;
1431
1650
  }
1432
1651
  // --- Verification Functions
1433
1652
  /**
@@ -1483,7 +1702,7 @@ const lastManagedInstallVersion = new Map();
1483
1702
  */
1484
1703
  async function verifyToolBinary(binPath, onVersionOutput) {
1485
1704
  return new Promise((resolve) => {
1486
- const isWindows = process.platform === "win32";
1705
+ const isWindows = installerPlatform() === "win32";
1487
1706
  const hasKnownWindowsExt = /\.(cmd|exe|ps1)$/i.test(binPath);
1488
1707
  // On Windows, resolve the best executable path:
1489
1708
  // - extensionless → prefer .cmd (cmd.exe-safe)
@@ -1644,7 +1863,7 @@ export async function getAllToolStatuses() {
1644
1863
  }
1645
1864
  // 5. Check pi-lens auto-install (~/.pi-lens/tools/)
1646
1865
  const localBase = path.join(TOOLS_DIR, "node_modules", ".bin", tool.binaryName || tool.id);
1647
- const localPath = process.platform === "win32" ? `${localBase}.cmd` : localBase;
1866
+ const localPath = installerPlatform() === "win32" ? `${localBase}.cmd` : localBase;
1648
1867
  try {
1649
1868
  await fs.access(localPath);
1650
1869
  if (await verifyToolBinary(localPath)) {
@@ -1709,7 +1928,7 @@ export function resolvePlatformPackageBinary(tool) {
1709
1928
  const spec = tool.platformPackage;
1710
1929
  if (!spec || !tool.packageName)
1711
1930
  return undefined;
1712
- const suffix = spec.suffixes[`${process.platform}-${process.arch}`];
1931
+ const suffix = spec.suffixes[`${installerPlatform()}-${process.arch}`];
1713
1932
  if (!suffix)
1714
1933
  return undefined;
1715
1934
  const platformPkg = `${spec.base ?? tool.packageName}-${suffix}`;
@@ -1725,7 +1944,7 @@ export function resolvePlatformPackageBinary(tool) {
1725
1944
  catch {
1726
1945
  pkgDir = path.dirname(_installerRequire.resolve(`${platformPkg}/package.json`));
1727
1946
  }
1728
- const isWin = process.platform === "win32";
1947
+ const isWin = installerPlatform() === "win32";
1729
1948
  for (const bin of spec.binaries) {
1730
1949
  for (const name of isWin ? [`${bin}.exe`, bin] : [bin]) {
1731
1950
  const candidate = path.join(pkgDir, name);
@@ -1771,7 +1990,7 @@ export async function getToolPath(toolId) {
1771
1990
  // This avoids the ~2-5s overhead of spawning npm global probes and PATH
1772
1991
  // searches for tools we already manage locally.
1773
1992
  const localBase = path.join(TOOLS_DIR, "node_modules", ".bin", tool.binaryName || tool.id);
1774
- if (process.platform === "win32") {
1993
+ if (installerPlatform() === "win32") {
1775
1994
  // Prefer .cmd over extensionless — Node.js can't execute POSIX shell scripts on Windows
1776
1995
  const cmdPath = `${localBase}.cmd`;
1777
1996
  try {
@@ -1798,15 +2017,17 @@ export async function getToolPath(toolId) {
1798
2017
  // fall through to extensionless
1799
2018
  }
1800
2019
  }
1801
- try {
1802
- await fs.access(localBase);
1803
- if (await verifyToolBinary(localBase, recordVersion)) {
1804
- return localBase;
2020
+ if (installerPlatform() !== "win32") {
2021
+ try {
2022
+ await fs.access(localBase);
2023
+ if (await verifyToolBinary(localBase, recordVersion)) {
2024
+ return localBase;
2025
+ }
2026
+ logSessionStart(`auto-install verify: ${localBase} exists but is broken, will reinstall`);
2027
+ }
2028
+ catch {
2029
+ // fall through to global checks
1805
2030
  }
1806
- logSessionStart(`auto-install verify: ${localBase} exists but is broken, will reinstall`);
1807
- }
1808
- catch {
1809
- // fall through to global checks
1810
2031
  }
1811
2032
  // npm/pnpm/bun: prefer the native per-platform binary directly. The main
1812
2033
  // package's launcher often can't find it under a symlink store / after a
@@ -1909,7 +2130,6 @@ async function findNpmGlobalToolPath(binaryName) {
1909
2130
  ? [
1910
2131
  path.join(dir, `${binaryName}.cmd`),
1911
2132
  path.join(dir, `${binaryName}.exe`),
1912
- path.join(dir, binaryName),
1913
2133
  ]
1914
2134
  : [path.join(dir, binaryName)];
1915
2135
  for (const candidate of candidates) {
@@ -2160,7 +2380,7 @@ async function installGitHubTool(tool) {
2160
2380
  logSessionStart(`github-install ${tool.id}: release fetch failed: ${err.message}`);
2161
2381
  return undefined;
2162
2382
  }
2163
- const asset = releaseJson.assets.find((a) => a.name.includes(assetSubstring)) ??
2383
+ const asset = pickReleaseAsset(releaseJson.assets, assetSubstring) ??
2164
2384
  deriveHashiCorpReleaseAsset(tool, releaseJson.tag_name, assetSubstring);
2165
2385
  if (!asset) {
2166
2386
  logSessionStart(`github-install ${tool.id}: no asset matched "${assetSubstring}"`);
@@ -2537,7 +2757,7 @@ async function installNpmTool(packageName, binaryName) {
2537
2757
  await fs.writeFile(packageJsonPath, JSON.stringify({ name: "pi-lens-tools", version: "1.0.0" }, null, 2));
2538
2758
  }
2539
2759
  // Resolve the package manager for the tools dir and build install args.
2540
- const isWindows = process.platform === "win32";
2760
+ const isWindows = installerPlatform() === "win32";
2541
2761
  const pm = await resolveNodePackageManager(TOOLS_DIR);
2542
2762
  const testNpmScript = process.env.PI_LENS_TEST_MODE === "1"
2543
2763
  ? process.env.PI_LENS_TEST_NPM_SCRIPT
@@ -2583,9 +2803,12 @@ async function installNpmTool(packageName, binaryName) {
2583
2803
  if (!outcome.ok) {
2584
2804
  throw new Error(`Failed to install ${packageName}: ${outcome.stderr}`);
2585
2805
  }
2586
- const binPath = path.join(TOOLS_DIR, "node_modules", ".bin", binaryName);
2806
+ // npm creates a command shim on Windows; retain that actual executable path
2807
+ // rather than probing/storing the extensionless POSIX sibling.
2808
+ const binBase = path.join(TOOLS_DIR, "node_modules", ".bin", binaryName);
2809
+ const binPath = installerPlatform() === "win32" ? `${binBase}.cmd` : binBase;
2587
2810
  // Make executable on Unix
2588
- if (process.platform !== "win32") {
2811
+ if (installerPlatform() !== "win32") {
2589
2812
  try {
2590
2813
  await fs.chmod(binPath, 0o750);
2591
2814
  }
@@ -2615,10 +2838,10 @@ async function installNpmTool(packageName, binaryName) {
2615
2838
  try {
2616
2839
  const packagePath = path.join(TOOLS_DIR, "node_modules", packageName);
2617
2840
  await fs.rm(packagePath, { recursive: true, force: true });
2618
- await fs.rm(binPath, { force: true });
2841
+ await fs.rm(binBase, { force: true });
2619
2842
  if (isWindows) {
2620
- await fs.rm(`${binPath}.cmd`, { force: true });
2621
- await fs.rm(`${binPath}.ps1`, { force: true });
2843
+ await fs.rm(`${binBase}.cmd`, { force: true });
2844
+ await fs.rm(`${binBase}.ps1`, { force: true });
2622
2845
  }
2623
2846
  }
2624
2847
  catch {
@@ -2784,6 +3007,33 @@ async function installGemTool(packageName) {
2784
3007
  return undefined;
2785
3008
  }
2786
3009
  }
3010
+ async function finishInstallAttempt(toolId, ok, startedAt) {
3011
+ logSessionStart(`auto-install ${toolId}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
3012
+ if (ok) {
3013
+ // A prior availability probe may have cached ENOENT for this exact child
3014
+ // PATH. Make a successful mutation visible immediately rather than waiting
3015
+ // for the bounded negative-cache TTL or the next session reset (#1199).
3016
+ resetSafeSpawnWindowsCommandCache();
3017
+ // #1276: the madge managed-path memo is keyed only by projectRoot, but
3018
+ // reads PATH/discovery/install state that a completed install can just
3019
+ // have changed — drop it here too, right alongside the safe-spawn reset.
3020
+ // AWAITED (not fire-and-forget): a caller that starts the next madge
3021
+ // resolution the instant `installTool`/`ensureTool` resolves must observe
3022
+ // the reset memo, not a stale pre-install one, and a rejected dynamic
3023
+ // import must not become a silent unhandled rejection. Dynamic import
3024
+ // avoids a static dependency-checker.js <-> installer/index.js cycle
3025
+ // (dependency-checker.js already imports this module dynamically for the
3026
+ // same reason).
3027
+ try {
3028
+ const { resetMadgeManagedPathMemo } = await import("../dependency-checker.js");
3029
+ resetMadgeManagedPathMemo();
3030
+ }
3031
+ catch (err) {
3032
+ logSessionStart(`auto-install ${toolId}: madge memo reset failed: ${err.message}`);
3033
+ }
3034
+ }
3035
+ return ok;
3036
+ }
2787
3037
  /**
2788
3038
  * Install a tool by ID
2789
3039
  */
@@ -2806,49 +3056,37 @@ export async function installTool(toolId) {
2806
3056
  if (!tool.packageName || !tool.binaryName)
2807
3057
  return false;
2808
3058
  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;
3059
+ return finishInstallAttempt(tool.id, npmPath !== undefined, startedAt);
2812
3060
  }
2813
3061
  case "pip": {
2814
3062
  if (!tool.packageName)
2815
3063
  return false;
2816
3064
  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;
3065
+ return finishInstallAttempt(tool.id, pipPath !== undefined, startedAt);
2820
3066
  }
2821
3067
  case "gem": {
2822
3068
  if (!tool.packageName)
2823
3069
  return false;
2824
3070
  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;
3071
+ return finishInstallAttempt(tool.id, gemPath !== undefined, startedAt);
2828
3072
  }
2829
3073
  case "github": {
2830
3074
  if (!tool.github)
2831
3075
  return false;
2832
3076
  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;
3077
+ return finishInstallAttempt(tool.id, ghPath !== undefined, startedAt);
2836
3078
  }
2837
3079
  case "maven": {
2838
3080
  if (!tool.maven)
2839
3081
  return false;
2840
3082
  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;
3083
+ return finishInstallAttempt(tool.id, mavenPath !== undefined, startedAt);
2844
3084
  }
2845
3085
  case "archive": {
2846
3086
  if (!tool.archive)
2847
3087
  return false;
2848
3088
  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;
3089
+ return finishInstallAttempt(tool.id, archivePath !== undefined, startedAt);
2852
3090
  }
2853
3091
  default:
2854
3092
  logSessionStart(`auto-install ${tool.id}: unsupported strategy`);
@@ -2861,9 +3099,24 @@ export async function installTool(toolId) {
2861
3099
  }
2862
3100
  }
2863
3101
  /**
2864
- * Ensure a tool is installed (check first, install if missing)
3102
+ * Ensure a tool is installed (check first, install if missing).
3103
+ *
3104
+ * #1334 S5: when the pi host has actively denied project trust, the INSTALL
3105
+ * half is switched off here — the request degrades to the existing
3106
+ * `allowInstall:false` discovery-only path rather than failing outright, so an
3107
+ * already-present binary keeps working while nothing is downloaded or executed
3108
+ * on behalf of an untrusted project. A host with no trust surface at all
3109
+ * (`"unknown"`) is unaffected.
2865
3110
  */
2866
3111
  export async function ensureTool(toolId, opts) {
3112
+ if (opts?.allowInstall !== false &&
3113
+ !assertInstallAllowed(`managed tool ensure: ${toolId}`)) {
3114
+ logSessionStart(`auto-install ensure ${toolId}: install gated — ${projectTrustDenialReason()}; discovery only`);
3115
+ return ensureToolResolved(toolId, { ...opts, allowInstall: false });
3116
+ }
3117
+ return ensureToolResolved(toolId, opts);
3118
+ }
3119
+ async function ensureToolResolved(toolId, opts) {
2867
3120
  installFailureReasons.delete(toolId);
2868
3121
  const cacheResolvedPath = (result) => {
2869
3122
  if (result) {
@@ -2885,8 +3138,7 @@ export async function ensureTool(toolId, opts) {
2885
3138
  try {
2886
3139
  const probeCache = await readProbeCache();
2887
3140
  delete probeCache[toolId];
2888
- _probeCacheDirty = true;
2889
- scheduleProbeFlush();
3141
+ markProbeCacheChange(toolId, null);
2890
3142
  }
2891
3143
  catch {
2892
3144
  // best-effort
@@ -2925,8 +3177,23 @@ export async function ensureTool(toolId, opts) {
2925
3177
  }
2926
3178
  // Fast path 1: in-memory session cache — no I/O.
2927
3179
  const cached = resolvedPathCache.get(toolId);
2928
- if (cached)
2929
- return cached;
3180
+ if (cached) {
3181
+ if (!isFullyQualified(cached))
3182
+ return cached;
3183
+ try {
3184
+ await fs.access(cached);
3185
+ return cached;
3186
+ }
3187
+ catch {
3188
+ // The executor would report ENOENT for this cached positive. Evict it
3189
+ // before discovery so the failure heals on this call, not after restart.
3190
+ }
3191
+ resolvedPathCache.delete(toolId);
3192
+ const probeCache = await readProbeCache();
3193
+ delete probeCache[toolId];
3194
+ markProbeCacheChange(toolId, null);
3195
+ logSessionStart(`auto-install ensure ${toolId}: cached path disappeared; re-probing`);
3196
+ }
2930
3197
  // Fast path 2: persistent probe cache — fs.access + stat, no process spawn.
2931
3198
  const diskCached = await checkProbeCache(toolId);
2932
3199
  if (diskCached) {
@@ -3100,9 +3367,11 @@ export const GITHUB_TOOLS = [
3100
3367
  "zizmor",
3101
3368
  "typos-lsp",
3102
3369
  "tflint",
3370
+ "terragrunt",
3103
3371
  "terraform-ls",
3104
3372
  "zls",
3105
3373
  "hadolint",
3374
+ "helm",
3106
3375
  "gitleaks",
3107
3376
  "taplo",
3108
3377
  "vale",
@@ -3135,6 +3404,28 @@ export function resolveGitHubArchiveBinaryCandidates(toolId, platform, assetName
3135
3404
  const binaryName = tool.github?.binaryInArchive ?? tool.binaryName ?? tool.id;
3136
3405
  return getArchiveBinaryCandidates(binaryName, platform, assetName);
3137
3406
  }
3407
+ // Signature/checksum siblings a release publishes next to the real asset. A
3408
+ // bare-binary tool's substring IS the whole asset name, so `includes` alone
3409
+ // matches `<asset>.asc` too and would install a signature file as the binary.
3410
+ const ASSET_SIDECAR_SUFFIXES = [
3411
+ ".asc",
3412
+ ".sig",
3413
+ ".minisig",
3414
+ ".pem",
3415
+ ".cert",
3416
+ ".sbom",
3417
+ ".sha256",
3418
+ ".sha256sum",
3419
+ ".md5",
3420
+ ];
3421
+ function isAssetSidecar(name) {
3422
+ const lower = name.toLowerCase();
3423
+ return ASSET_SIDECAR_SUFFIXES.some((suffix) => lower.endsWith(suffix));
3424
+ }
3425
+ export function pickReleaseAsset(assets, assetSubstring) {
3426
+ return (assets.find((a) => a.name === assetSubstring) ??
3427
+ assets.find((a) => a.name.includes(assetSubstring) && !isAssetSidecar(a.name)));
3428
+ }
3138
3429
  function deriveHashiCorpReleaseAsset(tool, tagName, assetSubstring) {
3139
3430
  const product = tool.github?.hashiCorpReleaseProduct;
3140
3431
  if (!product || !tagName)