@herbertgao/pi-extensions 2026.8.13 → 2026.8.15

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 (263) hide show
  1. package/README.md +19 -14
  2. package/THIRD_PARTY_NOTICES.md +0 -1
  3. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +19 -0
  4. package/node_modules/@czottmann/pi-automode/README.md +5 -1
  5. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +126 -13
  6. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +7 -3
  7. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +2 -0
  8. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +134 -23
  9. package/node_modules/@czottmann/pi-automode/package.json +1 -1
  10. package/node_modules/@herbertgao/{pi-stash → pi-bark}/LICENSE +4 -5
  11. package/node_modules/@herbertgao/pi-bark/README.md +44 -0
  12. package/node_modules/@herbertgao/pi-bark/assets/pi-icon.png +0 -0
  13. package/node_modules/@herbertgao/pi-bark/package.json +53 -0
  14. package/node_modules/@herbertgao/pi-bark/src/index.ts +178 -0
  15. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
  16. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
  17. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  18. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  19. package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
  20. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +8 -1
  21. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  22. package/node_modules/@narumitw/pi-btw/README.md +125 -88
  23. package/node_modules/@narumitw/pi-btw/dist/index.ts +140 -45
  24. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
  25. package/node_modules/@narumitw/pi-btw/package.json +5 -8
  26. package/node_modules/@narumitw/pi-btw/src/btw.ts +31 -26
  27. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +106 -17
  28. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
  29. package/node_modules/@narumitw/pi-btw/src/settings.ts +16 -0
  30. package/node_modules/@tifan/pi-handoff/README.md +36 -13
  31. package/node_modules/@tifan/pi-handoff/package.json +2 -2
  32. package/node_modules/@tifan/pi-handoff/src/index.ts +160 -4
  33. package/node_modules/@tifan/pi-preferred-thinking/README.md +1 -1
  34. package/node_modules/@tifan/pi-preferred-thinking/package.json +1 -1
  35. package/node_modules/@tifan/pi-preferred-thinking/src/index.ts +15 -2
  36. package/node_modules/@tifan/pi-recap/README.md +1 -1
  37. package/node_modules/@tifan/pi-recap/package.json +2 -2
  38. package/node_modules/@tifan/pi-recap/src/index.ts +15 -0
  39. package/node_modules/@tifan/pi-rename/README.md +42 -21
  40. package/node_modules/@tifan/pi-rename/package.json +1 -1
  41. package/node_modules/@tifan/pi-rename/src/index.ts +62 -11
  42. package/node_modules/@tifan/pi-rename/src/language.ts +18 -0
  43. package/node_modules/@tifan/pi-rename/src/models.ts +69 -13
  44. package/node_modules/@tifan/pi-rename/src/naming.ts +36 -7
  45. package/node_modules/@tifan/pi-rename/src/sanitize.ts +22 -14
  46. package/node_modules/pi-lens/CHANGELOG.md +320 -0
  47. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +79 -0
  48. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +16 -1
  49. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +13 -6
  50. package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +12 -0
  51. package/node_modules/pi-lens/dist/clients/biome-client.js +7 -0
  52. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +20 -0
  53. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +56 -19
  54. package/node_modules/pi-lens/dist/clients/build-identity.js +95 -0
  55. package/node_modules/pi-lens/dist/clients/bus-publish.js +3 -0
  56. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +2 -1
  57. package/node_modules/pi-lens/dist/clients/cache-observability.js +2 -1
  58. package/node_modules/pi-lens/dist/clients/cascade-logger.js +15 -1
  59. package/node_modules/pi-lens/dist/clients/dead-code-client.js +14 -11
  60. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +56 -4
  61. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +14 -0
  62. package/node_modules/pi-lens/dist/clients/dependency-checker.js +20 -3
  63. package/node_modules/pi-lens/dist/clients/deps/minimatch.js +1 -1
  64. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +39 -0
  65. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +150 -9
  66. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +231 -9
  67. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +205 -100
  68. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +87 -0
  69. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +21 -5
  70. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
  71. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +5 -1
  72. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +16 -3
  73. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +54 -9
  74. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +1 -3
  75. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -1
  76. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -1
  77. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +11 -4
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -0
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +8 -3
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +76 -25
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
  85. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +66 -1
  86. package/node_modules/pi-lens/dist/clients/file-utils.js +157 -7
  87. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +5 -0
  88. package/node_modules/pi-lens/dist/clients/formatters.js +28 -3
  89. package/node_modules/pi-lens/dist/clients/git-guard.js +197 -47
  90. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +32 -1
  91. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +5 -1
  92. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +7 -0
  93. package/node_modules/pi-lens/dist/clients/installer/index.js +131 -34
  94. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +3 -1
  95. package/node_modules/pi-lens/dist/clients/instance-reaper.js +4 -13
  96. package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +145 -0
  97. package/node_modules/pi-lens/dist/clients/instance-registry.js +432 -122
  98. package/node_modules/pi-lens/dist/clients/jscpd-client.js +6 -1
  99. package/node_modules/pi-lens/dist/clients/language-profile.js +4 -2
  100. package/node_modules/pi-lens/dist/clients/latency-logger.js +19 -1
  101. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  102. package/node_modules/pi-lens/dist/clients/lens-map.js +3 -1
  103. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +4 -0
  104. package/node_modules/pi-lens/dist/clients/lsp/client.js +516 -31
  105. package/node_modules/pi-lens/dist/clients/lsp/config.js +25 -2
  106. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +9 -4
  107. package/node_modules/pi-lens/dist/clients/lsp/index.js +509 -182
  108. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +5 -0
  109. package/node_modules/pi-lens/dist/clients/lsp/launch.js +36 -8
  110. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +116 -4
  111. package/node_modules/pi-lens/dist/clients/lsp/server.js +142 -13
  112. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +99 -0
  113. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +16 -6
  114. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +32 -23
  115. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +16 -2
  116. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +12 -6
  117. package/node_modules/pi-lens/dist/clients/mcp/session.js +14 -1
  118. package/node_modules/pi-lens/dist/clients/memory-sampler.js +23 -1
  119. package/node_modules/pi-lens/dist/clients/message-end-attribution.js +30 -0
  120. package/node_modules/pi-lens/dist/clients/metrics-history.js +21 -4
  121. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +232 -18
  122. package/node_modules/pi-lens/dist/clients/package-manager.js +6 -18
  123. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +14 -1
  124. package/node_modules/pi-lens/dist/clients/path-utils.js +32 -0
  125. package/node_modules/pi-lens/dist/clients/pipeline.js +0 -8
  126. package/node_modules/pi-lens/dist/clients/process-singletons.js +210 -0
  127. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
  128. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +10 -4
  129. package/node_modules/pi-lens/dist/clients/project-snapshot.js +33 -11
  130. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +24 -2
  131. package/node_modules/pi-lens/dist/clients/read-guard.js +74 -7
  132. package/node_modules/pi-lens/dist/clients/recent-touches.js +17 -5
  133. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +639 -62
  134. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +5 -1
  135. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +21 -1
  136. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +68 -0
  137. package/node_modules/pi-lens/dist/clients/runtime-session.js +30 -12
  138. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +15 -0
  139. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +106 -7
  140. package/node_modules/pi-lens/dist/clients/runtime-turn.js +242 -27
  141. package/node_modules/pi-lens/dist/clients/safe-spawn.js +158 -18
  142. package/node_modules/pi-lens/dist/clients/security-scan-client.js +48 -17
  143. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +238 -42
  144. package/node_modules/pi-lens/dist/clients/sg-runner.js +27 -15
  145. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +338 -0
  146. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +37 -0
  147. package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +15 -1
  148. package/node_modules/pi-lens/dist/clients/startup-scan.js +6 -7
  149. package/node_modules/pi-lens/dist/clients/startup-timing.js +69 -4
  150. package/node_modules/pi-lens/dist/clients/string-utils.js +20 -0
  151. package/node_modules/pi-lens/dist/clients/test-runner-client.js +419 -88
  152. package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
  153. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +11 -5
  154. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +17 -1
  155. package/node_modules/pi-lens/dist/clients/warm-attach.js +8 -10
  156. package/node_modules/pi-lens/dist/clients/word-index-logger.js +13 -1
  157. package/node_modules/pi-lens/dist/clients/word-index-store.js +448 -0
  158. package/node_modules/pi-lens/dist/clients/word-index.js +582 -104
  159. package/node_modules/pi-lens/dist/clients/workspace-topology.js +17 -0
  160. package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -6
  161. package/node_modules/pi-lens/dist/index.js +11243 -6651
  162. package/node_modules/pi-lens/dist/mcp/build-staleness.js +8 -7
  163. package/node_modules/pi-lens/dist/mcp/cli.js +3 -1
  164. package/node_modules/pi-lens/dist/mcp/server.js +6 -1
  165. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +4 -0
  166. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +14 -5
  167. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +3 -1
  168. package/node_modules/pi-lens/docs/agent-guide.md +4 -2
  169. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +41 -10
  170. package/node_modules/pi-lens/docs/astplayground.md +26 -17
  171. package/node_modules/pi-lens/package.json +5 -1
  172. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
  173. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
  174. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
  175. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
  176. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
  177. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
  178. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
  179. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
  180. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
  181. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
  182. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
  183. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
  184. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
  185. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
  186. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
  187. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
  188. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
  189. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
  190. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
  191. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
  192. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
  193. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
  194. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
  195. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
  196. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
  197. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
  198. package/node_modules/pi-lens/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
  199. package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
  200. package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
  201. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
  202. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
  203. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
  204. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
  205. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
  206. package/node_modules/pi-lens/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
  207. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
  208. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
  209. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
  210. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
  211. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
  212. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
  213. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
  214. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
  215. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-important.yml +21 -0
  216. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
  217. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
  218. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
  219. package/node_modules/pi-lens/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
  220. package/node_modules/pi-lens/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
  221. package/node_modules/pi-lens/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
  222. package/node_modules/pi-lens/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
  223. package/node_modules/pi-lens/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
  224. package/node_modules/pi-mcp-adapter/CHANGELOG.md +35 -0
  225. package/node_modules/pi-mcp-adapter/README.md +5 -2
  226. package/node_modules/pi-mcp-adapter/cli.js +4 -4
  227. package/node_modules/pi-mcp-adapter/commands.ts +1 -1
  228. package/node_modules/pi-mcp-adapter/config.ts +1 -0
  229. package/node_modules/pi-mcp-adapter/dist/config.js +1 -0
  230. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  231. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.d.ts +24 -0
  232. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +336 -0
  233. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -0
  234. package/node_modules/pi-mcp-adapter/dist/types.d.ts +3 -1
  235. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  236. package/node_modules/pi-mcp-adapter/index.ts +90 -6
  237. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +124 -32
  238. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +0 -2
  239. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +89 -0
  240. package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -1
  241. package/node_modules/pi-mcp-adapter/package.json +1 -1
  242. package/node_modules/pi-mcp-adapter/proxy-modes.ts +62 -22
  243. package/node_modules/pi-mcp-adapter/request-headers-command.ts +1 -1
  244. package/node_modules/pi-mcp-adapter/server-manager.ts +6 -0
  245. package/node_modules/pi-mcp-adapter/types.ts +3 -1
  246. package/node_modules/pi-web-access/CHANGELOG.md +42 -0
  247. package/node_modules/pi-web-access/README.md +26 -10
  248. package/node_modules/pi-web-access/curator-page.ts +12 -3
  249. package/node_modules/pi-web-access/curator-server.ts +3 -1
  250. package/node_modules/pi-web-access/extract.ts +88 -13
  251. package/node_modules/pi-web-access/gemini-search.ts +10 -5
  252. package/node_modules/pi-web-access/github-extract.ts +47 -3
  253. package/node_modules/pi-web-access/index.ts +40 -40
  254. package/node_modules/pi-web-access/package.json +1 -1
  255. package/node_modules/pi-web-access/page-query.ts +61 -8
  256. package/node_modules/pi-web-access/xcrawl.ts +264 -0
  257. package/package.json +19 -22
  258. package/node_modules/@herbertgao/pi-stash/README.md +0 -34
  259. package/node_modules/@herbertgao/pi-stash/package.json +0 -51
  260. package/node_modules/@herbertgao/pi-stash/src/index.ts +0 -118
  261. package/node_modules/@tifan/pi-titlebar-spinner/README.md +0 -24
  262. package/node_modules/@tifan/pi-titlebar-spinner/package.json +0 -39
  263. package/node_modules/@tifan/pi-titlebar-spinner/src/index.ts +0 -49
@@ -383,6 +383,12 @@ export const TOOLS = [
383
383
  name: "Bash Language Server",
384
384
  checkCommand: "bash-language-server",
385
385
  checkArgs: ["--version"],
386
+ // The #2188 sweep measured a 9,667ms cold `--version` start with closed
387
+ // stdin — close enough to the 10s installer default that modest host
388
+ // contention pushes it over and emits a false verification degradation.
389
+ // 20s gives the same kind of headroom Vue's 30s bound gives its own
390
+ // slower cold start (#2176), without inventing a shared literal (#2194).
391
+ verificationTimeoutMs: 20_000,
386
392
  installStrategy: "npm",
387
393
  packageName: "bash-language-server",
388
394
  binaryName: "bash-language-server",
@@ -419,6 +425,11 @@ export const TOOLS = [
419
425
  name: "VSCode JSON Language Server",
420
426
  checkCommand: "vscode-json-language-server",
421
427
  checkArgs: ["--version"],
428
+ // The #2188 sweep measured an 11,047ms cold `--version` start with closed
429
+ // stdin — over the 10s installer default, so a cold or contended host can
430
+ // see a false verification degradation before the binary ever answers.
431
+ // 20s mirrors the bash-language-server bound above (#2194).
432
+ verificationTimeoutMs: 20_000,
422
433
  installStrategy: "npm",
423
434
  packageName: "vscode-langservers-extracted",
424
435
  binaryName: "vscode-json-language-server",
@@ -465,7 +476,8 @@ export const TOOLS = [
465
476
  id: "helm",
466
477
  name: "Helm",
467
478
  checkCommand: "helm",
468
- checkArgs: ["version", "--short"],
479
+ // intended: version --short — unverified against real binary (shape 16), do not wire until probed
480
+ checkArgs: ["--version"],
469
481
  installStrategy: "github",
470
482
  binaryName: "helm",
471
483
  github: {
@@ -543,6 +555,11 @@ export const TOOLS = [
543
555
  name: "Prisma Language Server",
544
556
  checkCommand: "prisma-language-server",
545
557
  checkArgs: ["--version"],
558
+ // #2169: a real closed-stdin cold run measured 27,265ms, and a warm-cache
559
+ // rerun still took 9,860ms — well past the 10s installer default. 40s
560
+ // keeps the same margin-over-worst-observed ratio Vue's 30s bound uses
561
+ // (#2176) rather than trimming it for a slower binary.
562
+ verificationTimeoutMs: 40_000,
546
563
  installStrategy: "npm",
547
564
  packageName: "@prisma/language-server",
548
565
  binaryName: "prisma-language-server",
@@ -552,6 +569,10 @@ export const TOOLS = [
552
569
  name: "Vue Language Server",
553
570
  checkCommand: "vue-language-server",
554
571
  checkArgs: ["--version"],
572
+ // Vue's launcher loads the full language-service bundle before answering
573
+ // --version. Its cold start exceeds the dispatch probe budget on some hosts
574
+ // even though warm starts complete quickly (#2176).
575
+ verificationTimeoutMs: 30_000,
555
576
  installStrategy: "npm",
556
577
  packageName: "@vue/language-server",
557
578
  binaryName: "vue-language-server",
@@ -561,6 +582,10 @@ export const TOOLS = [
561
582
  name: "Svelte Language Server",
562
583
  checkCommand: "svelteserver",
563
584
  checkArgs: ["--version"],
585
+ // #2169: a real closed-stdin cold run measured 12,410ms — over the 10s
586
+ // installer default, matching the bash/JSON class of false verification
587
+ // degradation from a cold-cache host (#2194). 20s mirrors that bound.
588
+ verificationTimeoutMs: 20_000,
564
589
  installStrategy: "npm",
565
590
  packageName: "svelte-language-server",
566
591
  binaryName: "svelteserver",
@@ -569,7 +594,9 @@ export const TOOLS = [
569
594
  id: "markdownlint",
570
595
  name: "markdownlint-cli2",
571
596
  checkCommand: "markdownlint-cli2",
572
- checkArgs: ["--version"],
597
+ // `--version` is interpreted as a file glob by markdownlint-cli2. Use
598
+ // stdin with globs disabled so verification cannot scan the workspace.
599
+ checkArgs: ["--no-globs", "-"],
573
600
  installStrategy: "npm",
574
601
  packageName: "markdownlint-cli2",
575
602
  binaryName: "markdownlint-cli2",
@@ -784,7 +811,8 @@ export const TOOLS = [
784
811
  id: "spotbugs",
785
812
  name: "SpotBugs",
786
813
  checkCommand: "spotbugs",
787
- checkArgs: ["-version"],
814
+ // intended: -version — unverified against real binary (shape 16), do not wire until probed
815
+ checkArgs: ["--version"],
788
816
  installStrategy: "archive",
789
817
  binaryName: "spotbugs",
790
818
  archive: {
@@ -1030,7 +1058,8 @@ export const TOOLS = [
1030
1058
  id: "gitleaks",
1031
1059
  name: "gitleaks",
1032
1060
  checkCommand: "gitleaks",
1033
- checkArgs: ["version"],
1061
+ // intended: version — unverified against real binary (shape 16), do not wire until probed
1062
+ checkArgs: ["--version"],
1034
1063
  installStrategy: "github",
1035
1064
  binaryName: "gitleaks",
1036
1065
  github: {
@@ -1144,7 +1173,8 @@ export const TOOLS = [
1144
1173
  id: "terraform-ls",
1145
1174
  name: "terraform-ls",
1146
1175
  checkCommand: "terraform-ls",
1147
- checkArgs: ["version"],
1176
+ // intended: version — unverified against real binary (shape 16), do not wire until probed
1177
+ checkArgs: ["--version"],
1148
1178
  installStrategy: "github",
1149
1179
  binaryName: "terraform-ls",
1150
1180
  github: {
@@ -1216,6 +1246,7 @@ export const TOOLS = [
1216
1246
  id: "cue",
1217
1247
  name: "CUE",
1218
1248
  checkCommand: "cue",
1249
+ // Probed locally with CUE v0.17.1: `cue version` exits 0.
1219
1250
  checkArgs: ["version"],
1220
1251
  installStrategy: "github",
1221
1252
  binaryName: "cue",
@@ -1324,6 +1355,10 @@ export const TOOLS = [
1324
1355
  },
1325
1356
  },
1326
1357
  ];
1358
+ /** Return the bounded managed-binary verification budget for a registered tool. */
1359
+ export function getToolVerificationTimeout(tool) {
1360
+ return tool.verificationTimeoutMs ?? 10_000;
1361
+ }
1327
1362
  const ensureInFlight = new Map();
1328
1363
  const installFailureReasons = new Map();
1329
1364
  /** Last honest install refusal/failure reason for callers that need diagnostics. */
@@ -1621,8 +1656,13 @@ export async function checkProbeCache(toolId) {
1621
1656
  try {
1622
1657
  await fs.access(entry.path);
1623
1658
  const stat = await fs.stat(entry.path);
1624
- if (stat.mtimeMs !== entry.mtimeMs) {
1625
- logSessionStart(`auto-install probe-cache ${toolId}: miss (mtime changed)`);
1659
+ // #2300: size alongside mtime, from the same stat — a binary replaced
1660
+ // in-place within the same coarse-granularity mtime tick would otherwise
1661
+ // pass this check unnoticed. `entry.sizeBytes` absent (pre-#2300 entry)
1662
+ // fails open to the mtime-only check (see the field's doc comment).
1663
+ if (stat.mtimeMs !== entry.mtimeMs ||
1664
+ (entry.sizeBytes !== undefined && stat.size !== entry.sizeBytes)) {
1665
+ logSessionStart(`auto-install probe-cache ${toolId}: miss (mtime/size changed)`);
1626
1666
  delete cache[toolId];
1627
1667
  markProbeCacheChange(toolId, null);
1628
1668
  return undefined;
@@ -1650,6 +1690,7 @@ export async function updateProbeCache(toolId, resolvedPath, transient = false)
1650
1690
  const entry = {
1651
1691
  path: resolvedPath,
1652
1692
  mtimeMs: stat.mtimeMs,
1693
+ sizeBytes: stat.size,
1653
1694
  cachedAt: Date.now(),
1654
1695
  ...(transient && { transient: true }),
1655
1696
  };
@@ -1681,6 +1722,14 @@ export function resetProbeCacheStateForTesting() {
1681
1722
  _probeCacheFlushTimer = null;
1682
1723
  }
1683
1724
  }
1725
+ /**
1726
+ * Exported for testing only. Read the `ensureTool` in-flight map directly
1727
+ * (#1968's ABA regression: a second writer replacing an entry mid-flight,
1728
+ * then a late-settling first run evicting it with a bare delete-by-key).
1729
+ */
1730
+ export function _peekEnsureInFlightForTesting() {
1731
+ return ensureInFlight;
1732
+ }
1684
1733
  // --- Check Functions ---
1685
1734
  const pathWalkMemo = new Map();
1686
1735
  function hashSync(value) {
@@ -1765,6 +1814,7 @@ export async function isSpawnableCommand(command) {
1765
1814
  export function isLspTransportRequiredError(output) {
1766
1815
  return /Connection (?:input|output) stream is not set|Use arguments of createConnection/i.test(output);
1767
1816
  }
1817
+ const lspTransportRequiredMatcher = /Connection (?:input|output) stream is not set|Use arguments of createConnection/i;
1768
1818
  /**
1769
1819
  * Parse the exact version pinned in a TOOLS `packageName` spec, e.g.
1770
1820
  * `"jscpd@3.5.10"` -> `"3.5.10"`. Scoped packages (`"@ast-grep/cli"`) have no
@@ -1796,7 +1846,7 @@ function extractVersionToken(output) {
1796
1846
  */
1797
1847
  const lastManagedInstallVersion = new Map();
1798
1848
  /**
1799
- * Verify a tool binary actually works by running --version
1849
+ * Verify a tool binary actually works by running its configured checkArgs.
1800
1850
  * This catches broken symlinks, partial installs, and corrupted binaries.
1801
1851
  * `onVersionOutput`, when provided, receives the raw stdout on a successful
1802
1852
  * (exit 0) probe — used to piggyback version-pin drift detection onto this
@@ -1822,7 +1872,7 @@ onTransient,
1822
1872
  * sensitive caller on the dispatch hot path passes a shorter one and treats
1823
1873
  * the expiry as transient rather than as a verdict (#1657).
1824
1874
  */
1825
- timeoutMs = 10000) {
1875
+ timeoutMs = 10000, verificationArgs = ["--version"]) {
1826
1876
  // #2015: safeSpawnAsync instead of raw spawn. Raw spawn's timeout
1827
1877
  // SIGTERMed only cmd.exe on Windows (.cmd shims run shell:true), orphaning
1828
1878
  // the grandchild node process - which kept scanning, held handles against
@@ -1849,16 +1899,29 @@ timeoutMs = 10000) {
1849
1899
  // there), so no shell option exists or is needed here.
1850
1900
  void useShell;
1851
1901
  try {
1852
- const result = await safeSpawnAsync(execPath, ["--version"], {
1902
+ const result = await safeSpawnAsync(execPath, verificationArgs, {
1853
1903
  timeout: timeoutMs,
1904
+ input: "",
1905
+ // A broken or version-blind language server can emit a bundled
1906
+ // megabytes-long diagnostic on stderr. Keep verification bounded even
1907
+ // when the child reaches its normal timeout first.
1908
+ maxOutputBytes: 64 * 1024,
1909
+ matchWhileStreaming: lspTransportRequiredMatcher,
1854
1910
  });
1855
1911
  const output = `${result.stdout}\n${result.stderr}`;
1912
+ if (result.outputTruncated) {
1913
+ recordDegradationOnce({
1914
+ kind: "installer-verification-output-truncated",
1915
+ subject: binPath,
1916
+ reason: `verification output exceeded 65536 bytes (${verificationArgs.join(" ")})`,
1917
+ });
1918
+ }
1856
1919
  if (result.status === 0 && !result.error) {
1857
- debugLog(`Verified: ${binPath} (version: ${result.stdout.trim()})`);
1920
+ debugLog(`Verified: ${binPath} (${verificationArgs.join(" ")}: ${result.stdout.trim()})`);
1858
1921
  onVersionOutput?.(result.stdout);
1859
1922
  return true;
1860
1923
  }
1861
- if (isLspTransportRequiredError(output)) {
1924
+ if (result.streamingMatch || isLspTransportRequiredError(output)) {
1862
1925
  // Valid stdio LSP server that rejects `--version` (#208) — the
1863
1926
  // transport-required error proves the binary works.
1864
1927
  debugLog(`Verified (stdio LSP, transport-required): ${binPath}`);
@@ -1868,7 +1931,13 @@ timeoutMs = 10000) {
1868
1931
  // verdict from the binary (#1569 transient semantics).
1869
1932
  if (result.signal !== undefined || result.spawnFailure)
1870
1933
  onTransient?.();
1871
- logSessionStart(`auto-install verify: failed for ${binPath} (kind=${result.signal ? `killed-signal=${result.signal}` : result.error ? "spawn-error" : "exit-nonzero"}${result.status !== null ? `, exit=${result.status}` : ""})`);
1934
+ const kind = result.spawnFailure?.kind ??
1935
+ (result.signal
1936
+ ? `killed-signal=${result.signal}`
1937
+ : result.error
1938
+ ? "spawn-error"
1939
+ : "exit-nonzero");
1940
+ logSessionStart(`auto-install verify: failed for ${binPath} (check=${verificationArgs.join(" ")}, kind=${kind}${result.status !== null ? `, exit=${result.status}` : ""})`);
1872
1941
  return false;
1873
1942
  }
1874
1943
  catch (err) {
@@ -1938,7 +2007,7 @@ export async function getAllToolStatuses() {
1938
2007
  }
1939
2008
  // 2. Check npm global
1940
2009
  if (tool.installStrategy === "npm") {
1941
- const npmPath = await findNpmGlobalToolPath(tool.binaryName || tool.id);
2010
+ const npmPath = await findNpmGlobalToolPath(tool.binaryName || tool.id, undefined, tool.checkArgs, getToolVerificationTimeout(tool));
1942
2011
  if (npmPath) {
1943
2012
  status.installed = true;
1944
2013
  status.source = "npm-global";
@@ -1949,7 +2018,7 @@ export async function getAllToolStatuses() {
1949
2018
  }
1950
2019
  // 3. Check pip user install
1951
2020
  if (tool.installStrategy === "pip") {
1952
- const pipPath = await findPipUserToolPath(tool.binaryName || tool.id);
2021
+ const pipPath = await findPipUserToolPath(tool.binaryName || tool.id, undefined, tool.checkArgs, getToolVerificationTimeout(tool));
1953
2022
  if (pipPath) {
1954
2023
  status.installed = true;
1955
2024
  status.source = "pip-user";
@@ -1981,7 +2050,7 @@ export async function getAllToolStatuses() {
1981
2050
  const localPath = installerPlatform() === "win32" ? `${localBase}.cmd` : localBase;
1982
2051
  try {
1983
2052
  await fs.access(localPath);
1984
- if (await verifyToolBinary(localPath)) {
2053
+ if (await verifyToolBinary(localPath, undefined, undefined, getToolVerificationTimeout(tool), tool.checkArgs)) {
1985
2054
  status.installed = true;
1986
2055
  status.source = "pi-lens-auto";
1987
2056
  status.path = localPath;
@@ -2150,7 +2219,7 @@ async function getToolPathResolved(toolId, onTransient) {
2150
2219
  const cmdPath = `${localBase}.cmd`;
2151
2220
  try {
2152
2221
  await fs.access(cmdPath);
2153
- if (await verifyToolBinary(cmdPath, recordVersion, onTransient)) {
2222
+ if (await verifyToolBinary(cmdPath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
2154
2223
  return cmdPath;
2155
2224
  }
2156
2225
  logSessionStart(`auto-install verify: ${cmdPath} exists but is broken, will reinstall`);
@@ -2163,7 +2232,7 @@ async function getToolPathResolved(toolId, onTransient) {
2163
2232
  const exePath = `${localBase}.exe`;
2164
2233
  try {
2165
2234
  await fs.access(exePath);
2166
- if (await verifyToolBinary(exePath, recordVersion, onTransient)) {
2235
+ if (await verifyToolBinary(exePath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
2167
2236
  return exePath;
2168
2237
  }
2169
2238
  logSessionStart(`auto-install verify: ${exePath} exists but is broken, will reinstall`);
@@ -2175,7 +2244,7 @@ async function getToolPathResolved(toolId, onTransient) {
2175
2244
  if (installerPlatform() !== "win32") {
2176
2245
  try {
2177
2246
  await fs.access(localBase);
2178
- if (await verifyToolBinary(localBase, recordVersion, onTransient)) {
2247
+ if (await verifyToolBinary(localBase, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
2179
2248
  return localBase;
2180
2249
  }
2181
2250
  logSessionStart(`auto-install verify: ${localBase} exists but is broken, will reinstall`);
@@ -2191,7 +2260,7 @@ async function getToolPathResolved(toolId, onTransient) {
2191
2260
  if (tool.platformPackage) {
2192
2261
  const platformBin = resolvePlatformPackageBinary(tool);
2193
2262
  if (platformBin &&
2194
- (await verifyToolBinary(platformBin, undefined, onTransient))) {
2263
+ (await verifyToolBinary(platformBin, undefined, onTransient, getToolVerificationTimeout(tool), tool.checkArgs))) {
2195
2264
  logSessionStart(`auto-install ${toolId}: resolved platform-package binary at ${platformBin}`);
2196
2265
  return platformBin;
2197
2266
  }
@@ -2213,14 +2282,14 @@ async function getToolPathResolved(toolId, onTransient) {
2213
2282
  return tool.checkCommand;
2214
2283
  }
2215
2284
  if (tool.installStrategy === "npm") {
2216
- const npmPath = await findNpmGlobalToolPath(tool.binaryName || tool.id, onTransient);
2285
+ const npmPath = await findNpmGlobalToolPath(tool.binaryName || tool.id, onTransient, tool.checkArgs, getToolVerificationTimeout(tool));
2217
2286
  if (npmPath) {
2218
2287
  return npmPath;
2219
2288
  }
2220
2289
  }
2221
2290
  // For pip tools, also probe user-level script locations
2222
2291
  if (tool.installStrategy === "pip") {
2223
- const pipPath = await findPipUserToolPath(tool.binaryName || tool.id, onTransient);
2292
+ const pipPath = await findPipUserToolPath(tool.binaryName || tool.id, onTransient, tool.checkArgs, getToolVerificationTimeout(tool));
2224
2293
  if (pipPath) {
2225
2294
  return pipPath;
2226
2295
  }
@@ -2248,6 +2317,18 @@ async function findGitHubToolPath(binaryName) {
2248
2317
  }
2249
2318
  return undefined;
2250
2319
  }
2320
+ /** Resolve a release-managed binary without probing PATH first. */
2321
+ export async function findManagedToolBinary(toolId) {
2322
+ const tool = TOOLS.find((candidate) => candidate.id === toolId);
2323
+ if (!tool)
2324
+ return undefined;
2325
+ if (tool.installStrategy !== "github" &&
2326
+ tool.installStrategy !== "maven" &&
2327
+ tool.installStrategy !== "archive") {
2328
+ return undefined;
2329
+ }
2330
+ return findGitHubToolPath(tool.binaryName || tool.id);
2331
+ }
2251
2332
  function hasExecutableExtension(name) {
2252
2333
  return /\.(exe|bat|cmd|ps1)$/i.test(name);
2253
2334
  }
@@ -2278,7 +2359,7 @@ function getArchiveBinaryCandidates(binaryName, platform, assetName) {
2278
2359
  candidates.add(`${binaryName}.cmd`);
2279
2360
  return [...candidates];
2280
2361
  }
2281
- async function findNpmGlobalToolPath(binaryName, onTransient) {
2362
+ async function findNpmGlobalToolPath(binaryName, onTransient, verificationArgs = ["--version"], verificationTimeoutMs = 10_000) {
2282
2363
  const isWindows = process.platform === "win32";
2283
2364
  const binDirs = await getNpmGlobalBinCandidates(onTransient);
2284
2365
  for (const dir of binDirs) {
@@ -2291,7 +2372,7 @@ async function findNpmGlobalToolPath(binaryName, onTransient) {
2291
2372
  for (const candidate of candidates) {
2292
2373
  try {
2293
2374
  await fs.access(candidate);
2294
- if (await verifyToolBinary(candidate, undefined, onTransient)) {
2375
+ if (await verifyToolBinary(candidate, undefined, onTransient, verificationTimeoutMs, verificationArgs)) {
2295
2376
  return candidate;
2296
2377
  }
2297
2378
  }
@@ -2332,7 +2413,7 @@ async function getNpmGlobalBinCandidates(onTransient) {
2332
2413
  }
2333
2414
  return dirs;
2334
2415
  }
2335
- async function findPipUserToolPath(binaryName, onTransient) {
2416
+ async function findPipUserToolPath(binaryName, onTransient, verificationArgs = ["--version"], verificationTimeoutMs = 10_000) {
2336
2417
  const isWindows = process.platform === "win32";
2337
2418
  const userBaseCandidates = await getPythonUserBaseCandidates();
2338
2419
  for (const userBase of userBaseCandidates) {
@@ -2365,7 +2446,7 @@ async function findPipUserToolPath(binaryName, onTransient) {
2365
2446
  for (const candidate of candidates) {
2366
2447
  try {
2367
2448
  await fs.access(candidate);
2368
- if (await verifyToolBinary(candidate, undefined, onTransient)) {
2449
+ if (await verifyToolBinary(candidate, undefined, onTransient, verificationTimeoutMs, verificationArgs)) {
2369
2450
  return candidate;
2370
2451
  }
2371
2452
  }
@@ -2837,8 +2918,10 @@ export function getRefreshableManagedTools() {
2837
2918
  refreshable.push({
2838
2919
  toolId: tool.id,
2839
2920
  strategy: "npm",
2921
+ checkArgs: tool.checkArgs,
2840
2922
  packageName: tool.packageName,
2841
2923
  binaryName: tool.binaryName,
2924
+ verificationTimeoutMs: tool.verificationTimeoutMs,
2842
2925
  });
2843
2926
  break;
2844
2927
  }
@@ -2849,6 +2932,7 @@ export function getRefreshableManagedTools() {
2849
2932
  refreshable.push({
2850
2933
  toolId: tool.id,
2851
2934
  strategy: tool.installStrategy,
2935
+ checkArgs: tool.checkArgs,
2852
2936
  packageName: tool.packageName,
2853
2937
  });
2854
2938
  break;
@@ -2856,7 +2940,11 @@ export function getRefreshableManagedTools() {
2856
2940
  case "github": {
2857
2941
  if (!tool.github)
2858
2942
  continue;
2859
- refreshable.push({ toolId: tool.id, strategy: "github" });
2943
+ refreshable.push({
2944
+ toolId: tool.id,
2945
+ strategy: "github",
2946
+ checkArgs: tool.checkArgs,
2947
+ });
2860
2948
  break;
2861
2949
  }
2862
2950
  case "maven": {
@@ -2865,6 +2953,7 @@ export function getRefreshableManagedTools() {
2865
2953
  refreshable.push({
2866
2954
  toolId: tool.id,
2867
2955
  strategy: "maven",
2956
+ checkArgs: tool.checkArgs,
2868
2957
  pinnedCoordinate: mavenCoordinate(tool.maven),
2869
2958
  });
2870
2959
  break;
@@ -2880,6 +2969,7 @@ export function getRefreshableManagedTools() {
2880
2969
  refreshable.push({
2881
2970
  toolId: tool.id,
2882
2971
  strategy: "archive",
2972
+ checkArgs: tool.checkArgs,
2883
2973
  pinnedCoordinate: url,
2884
2974
  });
2885
2975
  break;
@@ -2951,7 +3041,8 @@ async function probeManagedToolVersion(tool) {
2951
3041
  return undefined;
2952
3042
  try {
2953
3043
  const result = await safeSpawnAsync(cached.path, tool.checkArgs, {
2954
- timeout: 10_000,
3044
+ timeout: getToolVerificationTimeout(tool),
3045
+ input: "",
2955
3046
  ignoreAmbientSignal: true,
2956
3047
  resourceLabel: `tool-refresh-version:${tool.id}`,
2957
3048
  });
@@ -3187,8 +3278,8 @@ async function verifyRefreshedArtifact(tool, installedPath) {
3187
3278
  await updateProbeCache(tool.id, installedPath);
3188
3279
  return true;
3189
3280
  }
3190
- if (!(await verifyToolBinary(installedPath))) {
3191
- logSessionStart(`managed-tool-refresh ${tool.id}: refreshed artifact at ${installedPath} failed its --version check`);
3281
+ if (!(await verifyToolBinary(installedPath, undefined, undefined, getToolVerificationTimeout(tool), tool.checkArgs))) {
3282
+ logSessionStart(`managed-tool-refresh ${tool.id}: refreshed artifact at ${installedPath} failed its verification check`);
3192
3283
  return false;
3193
3284
  }
3194
3285
  // Record the NEW mtime rather than leaving the persisted entry to miss its
@@ -3643,7 +3734,7 @@ async function installArchiveTool(tool) {
3643
3734
  return undefined;
3644
3735
  }
3645
3736
  }
3646
- async function installNpmTool(packageName, binaryName) {
3737
+ async function installNpmTool(packageName, binaryName, verificationArgs = ["--version"], verificationTimeoutMs = 10_000) {
3647
3738
  try {
3648
3739
  // Ensure tools directory exists
3649
3740
  await fs.mkdir(TOOLS_DIR, { recursive: true });
@@ -3727,7 +3818,7 @@ async function installNpmTool(packageName, binaryName) {
3727
3818
  lastAttemptTransient = false;
3728
3819
  isValid = await verifyToolBinary(binPath, undefined, () => {
3729
3820
  lastAttemptTransient = true;
3730
- });
3821
+ }, verificationTimeoutMs, verificationArgs);
3731
3822
  if (isValid)
3732
3823
  break;
3733
3824
  if (attempt < 3) {
@@ -4022,7 +4113,7 @@ export async function installTool(toolId) {
4022
4113
  case "npm": {
4023
4114
  if (!tool.packageName || !tool.binaryName)
4024
4115
  return false;
4025
- const npmPath = await installNpmTool(tool.packageName, tool.binaryName);
4116
+ const npmPath = await installNpmTool(tool.packageName, tool.binaryName, tool.checkArgs, getToolVerificationTimeout(tool));
4026
4117
  if (npmPath !== undefined) {
4027
4118
  // #1746 review F4: an install just resolved this package's range
4028
4119
  // against the registry, so record it as freshly checked. Otherwise a
@@ -4316,7 +4407,13 @@ async function ensureToolResolved(toolId, opts) {
4316
4407
  return await ensurePromise;
4317
4408
  }
4318
4409
  finally {
4319
- ensureInFlight.delete(inFlightKey);
4410
+ // Identity-guarded release (#1968's pattern): delete only if THIS run is
4411
+ // still the registered one. A bare delete-by-key lets a late-settling run
4412
+ // evict a live successor a second writer registered under the same key
4413
+ // mid-flight, after which the next caller starts a duplicate ensure/install.
4414
+ if (ensureInFlight.get(inFlightKey) === ensurePromise) {
4415
+ ensureInFlight.delete(inFlightKey);
4416
+ }
4320
4417
  }
4321
4418
  }
4322
4419
  // --- Integration Helpers ---
@@ -259,6 +259,8 @@ async function installedRefreshCandidates() {
259
259
  candidates.push({
260
260
  toolId: tool.toolId,
261
261
  strategy: tool.strategy,
262
+ checkArgs: tool.checkArgs,
263
+ verificationTimeoutMs: tool.verificationTimeoutMs ?? 10_000,
262
264
  ...(tool.packageName !== undefined && { packageName: tool.packageName }),
263
265
  ...(tool.binaryName !== undefined && { binaryName: tool.binaryName }),
264
266
  });
@@ -513,7 +515,7 @@ async function performNpmRefresh(candidate, packageName, now) {
513
515
  // drops npm entries that do not.
514
516
  const binaryName = candidate.binaryName;
515
517
  const binPath = resolveManagedNpmBinPath(binaryName);
516
- const verified = await verifyToolBinary(binPath);
518
+ const verified = await verifyToolBinary(binPath, undefined, undefined, candidate.verificationTimeoutMs, candidate.checkArgs);
517
519
  if (!verified) {
518
520
  recordDegradationOnce({
519
521
  kind: "managed-tool-refresh",
@@ -70,7 +70,7 @@ import { acquireQuarantinePidFileLock } from "./bounded-pid-file-lock.js";
70
70
  import { spawnCollectStdoutResult, unrefChildAndPipes, } from "./child-unref.js";
71
71
  import { incrementDegradationCount, recordDegradationOnce, } from "./degradation-ledger.js";
72
72
  import { getGlobalPiLensDir } from "./file-utils.js";
73
- import { isInstanceRegistryEnabled, readInstanceRegistry, } from "./instance-registry.js";
73
+ import { isInstanceRegistryEnabled, pruneDeadInstances as pruneDeadRegistryInstances, readInstanceRegistry, } from "./instance-registry.js";
74
74
  import { logLatency } from "./latency-logger.js";
75
75
  const isWindows = process.platform === "win32";
76
76
  /**
@@ -1226,8 +1226,8 @@ export async function sweepOrphans() {
1226
1226
  /** Drop dead-parent AND stale-heartbeat (#525, record-cleanup-only)
1227
1227
  * instances from the registry. Re-reads immediately before
1228
1228
  * writing (rather than reusing the earlier `readInstanceRegistry()` snapshot)
1229
- * to narrow — not eliminate — the last-writer-wins race already accepted for
1230
- * slice 1's read-modify-write model.
1229
+ * under the instance-registry lock so the reaper cannot lose a registration
1230
+ * or heartbeat while pruning.
1231
1231
  *
1232
1232
  * Exported for the #1217 concurrency regression test; `sweepOrphans` is the
1233
1233
  * only production caller.
@@ -1244,17 +1244,8 @@ export async function sweepOrphans() {
1244
1244
  * rename, which the hand-rolled copy did not, and puts this writer back on
1245
1245
  * the same scheme as the other writer of this same file. */
1246
1246
  export async function pruneDeadInstances(deadPids) {
1247
- const target = path.join(getGlobalPiLensDir(), "instances.json");
1248
1247
  try {
1249
- const raw = await fs.promises.readFile(target, "utf-8");
1250
- const parsed = JSON.parse(raw);
1251
- if (!parsed || !Array.isArray(parsed.instances))
1252
- return;
1253
- const remaining = parsed.instances.filter((entry) => !deadPids.has(entry.pid));
1254
- if (remaining.length === parsed.instances.length)
1255
- return;
1256
- await fs.promises.mkdir(getGlobalPiLensDir(), { recursive: true });
1257
- await writeFileAtomicAsync(target, JSON.stringify({ instances: remaining }));
1248
+ await pruneDeadRegistryInstances(deadPids);
1258
1249
  }
1259
1250
  catch {
1260
1251
  // best-effort
@@ -0,0 +1,145 @@
1
+ /** Cross-process mutual exclusion for the machine-global instance registry. */
2
+ import * as fs from "node:fs";
3
+ import { randomInt } from "node:crypto";
4
+ import * as path from "node:path";
5
+ import { incrementDegradationCount } from "./degradation-ledger.js";
6
+ const LOCK_STALE_MS = 5_000;
7
+ const LOCK_WAIT_MS = 500;
8
+ const LOCK_MIN_BACKOFF_MS = 5;
9
+ const LOCK_MAX_BACKOFF_MS = 25;
10
+ function lockPath(target) {
11
+ return `${target}.lock`;
12
+ }
13
+ function ownerPid(lock) {
14
+ try {
15
+ const pid = Number(fs.readFileSync(lock, "utf8").trim().split(/\s+/)[0]);
16
+ return Number.isInteger(pid) && pid > 0 ? pid : undefined;
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ function isPidAlive(pid) {
23
+ if (pid === undefined)
24
+ return false;
25
+ try {
26
+ process.kill(pid, 0);
27
+ return true;
28
+ }
29
+ catch (error) {
30
+ return error?.code !== "ESRCH";
31
+ }
32
+ }
33
+ function staleLock(lock) {
34
+ try {
35
+ const stat = fs.statSync(lock);
36
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_MS)
37
+ return true;
38
+ const pid = ownerPid(lock);
39
+ return pid !== undefined && !isPidAlive(pid);
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ function recordLockTimeout(target) {
46
+ incrementDegradationCount({
47
+ kind: "instance-registry-lock-timeout",
48
+ subject: path.resolve(target),
49
+ reason: `lock acquisition exhausted for ${path.basename(target)}`,
50
+ });
51
+ }
52
+ function backoffMs() {
53
+ return randomInt(LOCK_MIN_BACKOFF_MS, LOCK_MAX_BACKOFF_MS + 1);
54
+ }
55
+ function backoff() {
56
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, backoffMs());
57
+ }
58
+ function ownsLock(lock) {
59
+ return ownerPid(lock) === process.pid;
60
+ }
61
+ async function releaseLock(lock) {
62
+ // Residual TOCTOU: ownership and unlink are separate syscalls, so a stale
63
+ // takeover can still replace the lock between them.
64
+ if (ownsLock(lock))
65
+ await fs.promises.unlink(lock).catch(() => { });
66
+ }
67
+ function releaseLockSync(lock) {
68
+ if (!ownsLock(lock))
69
+ return;
70
+ try {
71
+ fs.unlinkSync(lock);
72
+ }
73
+ catch {
74
+ // A stale takeover may have displaced the lock after ownership was read.
75
+ }
76
+ }
77
+ function takeOverStale(lock) {
78
+ if (!staleLock(lock))
79
+ return;
80
+ const displaced = `${lock}.stale-${process.pid}-${Date.now()}`;
81
+ try {
82
+ fs.renameSync(lock, displaced);
83
+ fs.unlinkSync(displaced);
84
+ }
85
+ catch {
86
+ // Another owner may have released or taken over it first.
87
+ }
88
+ }
89
+ function isLockContention(error) {
90
+ const code = error?.code;
91
+ return code === "EEXIST" || code === "EPERM" || code === "EBUSY";
92
+ }
93
+ export async function withInstanceRegistryLock(target, op) {
94
+ const lock = lockPath(target);
95
+ const deadline = Date.now() + LOCK_WAIT_MS;
96
+ await fs.promises.mkdir(path.dirname(target), { recursive: true });
97
+ while (Date.now() <= deadline) {
98
+ try {
99
+ await fs.promises.writeFile(lock, `${process.pid} ${Date.now()}\n`, {
100
+ flag: "wx",
101
+ });
102
+ }
103
+ catch (error) {
104
+ if (!isLockContention(error))
105
+ throw error;
106
+ takeOverStale(lock);
107
+ if (Date.now() <= deadline)
108
+ await new Promise((resolve) => setTimeout(resolve, backoffMs()));
109
+ continue;
110
+ }
111
+ try {
112
+ return await op();
113
+ }
114
+ finally {
115
+ await releaseLock(lock);
116
+ }
117
+ }
118
+ recordLockTimeout(target);
119
+ return undefined;
120
+ }
121
+ export function withInstanceRegistryLockSync(target, op) {
122
+ const lock = lockPath(target);
123
+ const deadline = Date.now() + LOCK_WAIT_MS;
124
+ fs.mkdirSync(path.dirname(target), { recursive: true });
125
+ while (Date.now() <= deadline) {
126
+ try {
127
+ fs.writeFileSync(lock, `${process.pid} ${Date.now()}\n`, { flag: "wx" });
128
+ try {
129
+ return op();
130
+ }
131
+ finally {
132
+ releaseLockSync(lock);
133
+ }
134
+ }
135
+ catch (error) {
136
+ if (!isLockContention(error))
137
+ throw error;
138
+ takeOverStale(lock);
139
+ if (Date.now() <= deadline)
140
+ backoff();
141
+ }
142
+ }
143
+ recordLockTimeout(target);
144
+ return undefined;
145
+ }