@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
@@ -186,6 +186,9 @@ function probeJavaOnPath() {
186
186
  latched: false,
187
187
  retryAfterMs: Math.max(0, javaAvailabilityLatch.getRetryAtMs() - Date.now()),
188
188
  budgetMs: JAVA_PROBE_TIMEOUT_MS,
189
+ // No probe ran here: the latch's own remembered cause is replayed
190
+ // as-is, so the call site is the one asserting it (#2209).
191
+ classifiedBy: "caller",
189
192
  });
190
193
  return Promise.resolve(undefined);
191
194
  }
@@ -226,6 +229,7 @@ async function runJavaProbe() {
226
229
  latched: true,
227
230
  hostStallMs,
228
231
  budgetMs: JAVA_PROBE_TIMEOUT_MS,
232
+ classifiedBy: "probe",
229
233
  });
230
234
  return true;
231
235
  }
@@ -247,6 +251,7 @@ async function runJavaProbe() {
247
251
  hostStallMs,
248
252
  ...(retryAfterMs > 0 && { retryAfterMs }),
249
253
  budgetMs: JAVA_PROBE_TIMEOUT_MS,
254
+ classifiedBy: "probe",
250
255
  });
251
256
  return outcome === "transient" ? undefined : false;
252
257
  }
@@ -89,22 +89,42 @@ export function combinePathValuesForPlatform(values, platform = process.platform
89
89
  function resolvePathValue(env) {
90
90
  return combinePathValuesForPlatform([env.PATH, env.Path, env.path]);
91
91
  }
92
- /** Read live system+user PATH from Windows registry (bypasses stale process.env.PATH). */
93
- function readWindowsRegistryPath() {
92
+ /**
93
+ * Run an `execFileSync` probe with the #2095 stderr guard applied, returning
94
+ * `""` on any failure. Exported as a small test seam: production always calls
95
+ * it with the fixed `powershell.exe` command/args below, but a test can pass
96
+ * a command guaranteed to fail with real stderr output (no real Windows or
97
+ * powershell.exe required) to prove the guard is actually reached and not
98
+ * vacuous.
99
+ */
100
+ export function runStderrGuardedProbe(command, args) {
94
101
  try {
95
102
  const { execFileSync } = require("node:child_process");
96
- const out = execFileSync("powershell.exe", [
97
- "-NoProfile",
98
- "-NonInteractive",
99
- "-Command",
100
- "[System.Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path','User')",
101
- ], { timeout: 3000, encoding: "utf8" });
103
+ const out = execFileSync(command, args, {
104
+ timeout: 3000,
105
+ encoding: "utf8",
106
+ // #2095 sibling: execFileSync inherits stderr to the parent by
107
+ // default (only stdout is captured into `out`), so a failed
108
+ // powershell.exe launch would print raw diagnostics into the pi
109
+ // TUI even though the surrounding catch already returns "" (read as
110
+ // "unknown" PATH by every caller). Pipe stderr instead of inheriting it.
111
+ stdio: ["ignore", "pipe", "ignore"],
112
+ });
102
113
  return out.trim();
103
114
  }
104
115
  catch {
105
116
  return "";
106
117
  }
107
118
  }
119
+ /** Read live system+user PATH from Windows registry (bypasses stale process.env.PATH). */
120
+ function readWindowsRegistryPath() {
121
+ return runStderrGuardedProbe("powershell.exe", [
122
+ "-NoProfile",
123
+ "-NonInteractive",
124
+ "-Command",
125
+ "[System.Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path','User')",
126
+ ]);
127
+ }
108
128
  let _liveWindowsPath = null;
109
129
  function getLiveWindowsPath() {
110
130
  if (_liveWindowsPath === null) {
@@ -234,6 +254,14 @@ function findBinaryOnPath(command, env) {
234
254
  encoding: "utf-8",
235
255
  stdio: ["ignore", "pipe", "ignore"],
236
256
  env,
257
+ // #1980 population sweep: this was the one synchronous
258
+ // child-process site in the repo with no bound at all. It runs on
259
+ // the LSP server spawn path, so an unresponsive `where`/`which` — a
260
+ // stale network PATH entry is the classic cause — parked the event
261
+ // loop with no ceiling, and read as ordinary compute in
262
+ // `loop_block`. 5s matches `isCommandAvailable`/`findCommand` in
263
+ // clients/safe-spawn.ts, which run these same two binaries.
264
+ timeout: 5000,
237
265
  })
238
266
  .split(/\r?\n/)
239
267
  .map((line) => line.trim())
@@ -36,6 +36,21 @@
36
36
  * index whose keys are produced by this same process's touch path, so the
37
37
  * cheap ephemeral normalizer is the lifetime-appropriate choice per the
38
38
  * PathKeyedMap guidance — no realpath walk on the per-edit path.
39
+ *
40
+ * #2324 F3/R2-A: this module ALSO hands off in the other direction, for
41
+ * ast-grep specifically. Gate B's napi runner is a second producer of
42
+ * coverage for the same (file, "ast-grep") pair — when it runs as the
43
+ * fallback and actually evaluates rules, it calls
44
+ * {@link recordNapiFallbackCoverage}. The PRODUCER above reads that record
45
+ * (via {@link napiFallbackCoveredSince}) before deciding whether to mark a
46
+ * pair, because napi's Gate-B check settles synchronously while the aux-grace
47
+ * wait's own decision takes up to its full grace budget — by the time the
48
+ * wait is ready to mark, THIS touch's napi run (if any) has already finished,
49
+ * so the wait can reliably see it; a clear issued from napi's side cannot,
50
+ * because the mark it would be racing has not been written yet.
51
+ * {@link clearPendingAuxiliaryCoverage} is still used, but only to drop a
52
+ * LEFTOVER pair from an EARLIER touch once napi has fresh evaluated findings
53
+ * to supersede it.
39
54
  */
40
55
  import { normalizeEphemeralMapKey } from "../path-utils.js";
41
56
  /** Maximum pending (filePath, serverId) pairs; oldest evicted beyond this. */
@@ -47,6 +62,7 @@ export const MAX_PENDING_AUX_ENTRIES = 50;
47
62
  * forever. Overridable via `PI_LENS_LATE_AUX_REARM_TTL_MS`.
48
63
  */
49
64
  export const DEFAULT_LATE_AUX_REARM_TTL_MS = 5 * 60_000;
65
+ export const MAX_LATE_AUX_REARMS = 8;
50
66
  /**
51
67
  * Read the `PI_LENS_LATE_AUX_REARM_TTL_MS` env override at call time (not
52
68
  * module load time) so tests and users can tune without a rebuild — same
@@ -73,6 +89,14 @@ export function isPendingAuxiliaryPastRearmTtl(pair, nowMs = Date.now()) {
73
89
  return nowMs - anchor >= readLateAuxRearmTtlMs();
74
90
  }
75
91
  const pending = new Map();
92
+ /**
93
+ * #2168: pairs evicted at the cap have no other retirement record — the
94
+ * drain-time reconciliation in `runtime-turn.ts` never sees them because
95
+ * they are gone before a drain can observe them. Counted here so the
96
+ * consumer can fold them into its per-turn `pairCreated`/retirement sum
97
+ * instead of the pair vanishing uncounted.
98
+ */
99
+ let capEvictedCount = 0;
76
100
  function pairKey(filePath, serverId) {
77
101
  return `${normalizeEphemeralMapKey(filePath)}\u0000${serverId}`;
78
102
  }
@@ -89,7 +113,7 @@ function pairKey(filePath, serverId) {
89
113
  * `rearmedAtMs`) marks a NEWER revision, so it resets any prior re-arm stamp
90
114
  * along with the baseline — the new pair's TTL starts from its own mark.
91
115
  */
92
- export function markPendingAuxiliaryCoverage(filePath, serverIds, markedAtMs = Date.now(), rearmedAtMs) {
116
+ export function markPendingAuxiliaryCoverage(filePath, serverIds, markedAtMs = Date.now(), rearmedAtMs, rearmCount) {
93
117
  for (const serverId of serverIds) {
94
118
  const key = pairKey(filePath, serverId);
95
119
  const existing = pending.get(key);
@@ -107,20 +131,45 @@ export function markPendingAuxiliaryCoverage(filePath, serverIds, markedAtMs = D
107
131
  serverId,
108
132
  markedAtMs: existing.markedAtMs,
109
133
  lastRearmedAtMs: rearmedAtMs,
134
+ rearmCount: rearmCount ?? (existing.rearmCount ?? 0) + 1,
110
135
  });
111
136
  continue;
112
137
  }
113
138
  pending.set(key, rearmedAtMs === undefined
114
139
  ? { filePath, serverId, markedAtMs }
115
- : { filePath, serverId, markedAtMs, lastRearmedAtMs: rearmedAtMs });
140
+ : {
141
+ filePath,
142
+ serverId,
143
+ markedAtMs,
144
+ lastRearmedAtMs: rearmedAtMs,
145
+ rearmCount: rearmCount ?? 1,
146
+ });
116
147
  while (pending.size > MAX_PENDING_AUX_ENTRIES) {
117
148
  const oldest = pending.keys().next().value;
118
149
  if (oldest === undefined)
119
150
  break;
120
151
  pending.delete(oldest);
152
+ capEvictedCount += 1;
121
153
  }
122
154
  }
123
155
  }
156
+ /** Re-arm a drained pair while carrying its ceiling count across the drain. */
157
+ export function rearmPendingAuxiliaryCoverage(pair, rearmedAtMs = Date.now(), refreshBaseline = false) {
158
+ const nextCount = (pair.rearmCount ?? 0) + 1;
159
+ markPendingAuxiliaryCoverage(pair.filePath, [pair.serverId], refreshBaseline ? rearmedAtMs : pair.markedAtMs, rearmedAtMs, nextCount);
160
+ }
161
+ /**
162
+ * Read-only check: is this (file, server) pair currently pending late
163
+ * delivery? #2324 F2: Gate B's "has this server EVER published for this
164
+ * file" answer goes stale the moment a later touch's aux-grace wait finds
165
+ * the server silent for the CURRENT content — a pending pair is exactly
166
+ * that signal, so a caller deciding whether to skip a fallback runner can
167
+ * tell "published, still current" from "published once, silent now" without
168
+ * draining or mutating the store.
169
+ */
170
+ export function hasPendingAuxiliaryCoverage(filePath, serverId) {
171
+ return pending.has(pairKey(filePath, serverId));
172
+ }
124
173
  /**
125
174
  * Remove one pair after its findings were delivered, or whenever the caller
126
175
  * knows it is resolved. Unknown pairs are a no-op.
@@ -139,14 +188,77 @@ export function drainPendingAuxiliaryCoverage() {
139
188
  pending.clear();
140
189
  return drained;
141
190
  }
142
- /** Test-only: current pending pair count. */
143
- export function pendingAuxiliaryCoverageSizeForTests() {
191
+ /** Current pending pair count for bounded reconciliation telemetry. */
192
+ export function pendingAuxiliaryCoverageSize() {
144
193
  return pending.size;
145
194
  }
195
+ /** Test-only alias for the store-size assertion seam. */
196
+ export const pendingAuxiliaryCoverageSizeForTests = pendingAuxiliaryCoverageSize;
197
+ /**
198
+ * Drain (read and reset) the cap-eviction count accumulated since the last
199
+ * drain. Mirrors {@link drainPendingAuxiliaryCoverage}'s consume-once shape
200
+ * so the turn-end consumer can fold evictions that happened between two
201
+ * turn ends into its reconciliation sum exactly once, never accumulating
202
+ * across turns.
203
+ */
204
+ export function drainPendingAuxCapEvictedCount() {
205
+ const count = capEvictedCount;
206
+ capEvictedCount = 0;
207
+ return count;
208
+ }
209
+ /**
210
+ * #2324 R2-A: (file) -> the timestamp of the LAST time the ast-grep napi
211
+ * fallback actually evaluated rules for it, as Gate B's runner
212
+ * (`clients/dispatch/runners/ast-grep-napi.ts`). This is the cross-module
213
+ * hand-off the aux-grace wait (`clients/lsp/index.ts`) needs to avoid
214
+ * marking a pending late-auxiliary pair for a file napi ALREADY delivered
215
+ * for on THIS touch.
216
+ *
217
+ * Ordering is why a synchronous clear-on-napi-run cannot do this job: the
218
+ * wait that decides whether to mark a pair runs for up to ~1800ms (its own
219
+ * grace budget) AFTER napi's Gate-B check, which is a single map lookup.
220
+ * Clearing at napi's run start (or end) races the mark and reliably loses —
221
+ * the mark, if any, has not been written yet. The wait itself is the only
222
+ * place that knows the outcome, so it is the one that must consult this
223
+ * store and skip marking, not the runner that must guess at the wait's
224
+ * future decision.
225
+ *
226
+ * Bounded like the pending-pair store itself; oldest evicted first.
227
+ */
228
+ const napiFallbackCoverage = new Map();
229
+ export const MAX_NAPI_COVERAGE_ENTRIES = 50;
230
+ /** Record that napi actually evaluated rules for `filePath` at `atMs`. */
231
+ export function recordNapiFallbackCoverage(filePath, atMs = Date.now()) {
232
+ const key = normalizeEphemeralMapKey(filePath);
233
+ napiFallbackCoverage.delete(key);
234
+ napiFallbackCoverage.set(key, atMs);
235
+ while (napiFallbackCoverage.size > MAX_NAPI_COVERAGE_ENTRIES) {
236
+ const oldest = napiFallbackCoverage.keys().next().value;
237
+ if (oldest === undefined)
238
+ break;
239
+ napiFallbackCoverage.delete(oldest);
240
+ }
241
+ }
242
+ /**
243
+ * Did napi cover `filePath` at or after `sinceMs`? The aux-grace wait passes
244
+ * its own `waitStartedAt` baseline so a STALE coverage record from an
245
+ * earlier touch (napi ran for a PREVIOUS revision) cannot suppress marking a
246
+ * pair this touch's silent server genuinely needs delivered later.
247
+ */
248
+ export function napiFallbackCoveredSince(filePath, sinceMs) {
249
+ const ts = napiFallbackCoverage.get(normalizeEphemeralMapKey(filePath));
250
+ return ts !== undefined && ts >= sinceMs;
251
+ }
252
+ /** Test-only reset for the napi-coverage hand-off. */
253
+ export function resetNapiFallbackCoverageForTests() {
254
+ napiFallbackCoverage.clear();
255
+ }
146
256
  /**
147
257
  * Session-boundary clear (#1635): pending baselines are unreachable after
148
258
  * reset (the generation-stamped keys no longer match any active session).
149
259
  */
150
260
  export function resetPendingAuxiliaryCoverage() {
151
261
  pending.clear();
262
+ capEvictedCount = 0;
263
+ napiFallbackCoverage.clear();
152
264
  }
@@ -6,7 +6,7 @@
6
6
  * - Auto-installation strategies
7
7
  * - Platform-specific handling
8
8
  */
9
- import { existsSync, mkdirSync } from "node:fs";
9
+ import { existsSync, mkdirSync, statSync } from "node:fs";
10
10
  import { access, readFile, readdir, stat } from "node:fs/promises";
11
11
  import os from "node:os";
12
12
  import path from "node:path";
@@ -14,7 +14,7 @@ import { getGlobalPiLensDir, getProjectIgnoreGlobs, isPathIgnoredByProject, } fr
14
14
  import { STAGE_TMP_PATTERN } from "../atomic-write-staging.js";
15
15
  import { DOTNET_CSHARP_ROOT_MARKERS, DOTNET_FSHARP_ROOT_MARKERS, KIND_EXTENSIONS, } from "../file-kinds.js";
16
16
  import { direntsHaveMarkerGlobMatch, isAtOrAboveHomeDir, isFullyQualified, isWindowsPath, pathsEqual, } from "../path-utils.js";
17
- import { ensureTool, getToolEnvironment, getToolPath, } from "../installer/index.js";
17
+ import { ensureTool, findManagedToolBinary, getToolEnvironment, getToolPath, } from "../installer/index.js";
18
18
  import { resolveOpengrepConfig } from "../opengrep-config.js";
19
19
  import { isZizmorAuditTarget, resolveZizmorGitHubToken, } from "../zizmor-config.js";
20
20
  import { logLatency } from "../latency-logger.js";
@@ -29,6 +29,7 @@ import { createLombokJdtlsArgs } from "./lombok.js";
29
29
  import { resolveJavaRuntimeEnv } from "./jvm-runtime.js";
30
30
  import { normalizeMapKey } from "./path-utils.js";
31
31
  import { getRubyVersionDirNamesSync } from "./ruby-drive-dirs.js";
32
+ import { getProcessSingleton } from "../process-singletons.js";
32
33
  const FIXTURE_ROOT_SEGMENTS = new Set(["__fixtures__", "testdata"]);
33
34
  const FALLBACK_PROJECT_MARKERS = [
34
35
  ".git",
@@ -77,10 +78,69 @@ const PROJECT_BOUNDARY_MARKERS = [
77
78
  // process (not once per session). Keep the root-boundary marker below aligned
78
79
  // with FALLBACK_PROJECT_MARKERS, the shared fallback root-policy marker set.
79
80
  const loggedRootCeilingClamps = new Set();
80
- function isSameOrWithin(ancestor, candidate) {
81
+ const posixCaseInsensitiveByPath = new Map();
82
+ function posixFilesystemIsCaseInsensitive(root) {
83
+ const resolved = path.resolve(root);
84
+ const cached = posixCaseInsensitiveByPath.get(resolved);
85
+ if (cached !== undefined)
86
+ return cached;
87
+ if (!existsSync(resolved)) {
88
+ return false;
89
+ }
90
+ const name = path.basename(resolved);
91
+ const alternate = name.toLowerCase() === name ? name.toUpperCase() : name.toLowerCase();
92
+ let insensitive = false;
93
+ const alternatePath = path.join(path.dirname(resolved), alternate);
94
+ if (alternate !== name && existsSync(alternatePath)) {
95
+ try {
96
+ const actual = statSync(resolved);
97
+ const alternateStat = statSync(alternatePath);
98
+ insensitive =
99
+ actual.dev === alternateStat.dev && actual.ino === alternateStat.ino;
100
+ }
101
+ catch {
102
+ insensitive = false;
103
+ }
104
+ }
105
+ posixCaseInsensitiveByPath.set(resolved, insensitive);
106
+ return insensitive;
107
+ }
108
+ export function resetLSPCaseSensitivityState() {
109
+ posixCaseInsensitiveByPath.clear();
110
+ }
111
+ export function _getPosixCaseSensitivityCacheSizeForTests() {
112
+ return posixCaseInsensitiveByPath.size;
113
+ }
114
+ /**
115
+ * Path-shape-aware containment test: is `candidate` the same path as
116
+ * `ancestor`, or inside it?
117
+ *
118
+ * Exported so the session-cwd registry (`clients/lsp/config.ts`) and the
119
+ * foreign-root decline gate (`clients/lsp/index.ts`) test containment with the
120
+ * SAME comparator the root ceiling uses. A second hand-rolled `path.relative`
121
+ * helper alongside this one is how the two drift apart on Windows-shaped
122
+ * paths (shape 2 / #1150): `path.isAbsolute("C:\\repo")` is false on POSIX, so
123
+ * a host-default comparator silently mis-answers a win32 path on Linux CI.
124
+ */
125
+ export function isSameOrWithin(ancestor, candidate, deps = {}) {
81
126
  const windowsShaped = isWindowsPath(ancestor) || isWindowsPath(candidate);
82
127
  const pathApi = windowsShaped ? path.win32 : path;
83
- const relative = pathApi.relative(pathApi.resolve(ancestor), pathApi.resolve(candidate));
128
+ const resolvedAncestor = pathApi.resolve(ancestor);
129
+ const resolvedCandidate = pathApi.resolve(candidate);
130
+ let caseInsensitive = false;
131
+ if (!windowsShaped) {
132
+ if (deps.caseInsensitiveProbe) {
133
+ const cached = posixCaseInsensitiveByPath.get(resolvedAncestor);
134
+ caseInsensitive = cached ?? deps.caseInsensitiveProbe(resolvedAncestor);
135
+ if (cached === undefined) {
136
+ posixCaseInsensitiveByPath.set(resolvedAncestor, caseInsensitive);
137
+ }
138
+ }
139
+ else {
140
+ caseInsensitive = posixFilesystemIsCaseInsensitive(resolvedAncestor);
141
+ }
142
+ }
143
+ const relative = pathApi.relative(caseInsensitive ? resolvedAncestor.toLowerCase() : resolvedAncestor, caseInsensitive ? resolvedCandidate.toLowerCase() : resolvedCandidate);
84
144
  return (relative === "" ||
85
145
  (!relative.startsWith("..") && !pathApi.isAbsolute(relative)));
86
146
  }
@@ -251,6 +311,24 @@ export async function resolveAndLaunch(spec, allowInstall) {
251
311
  lastRuntimeFailure = err instanceof Error ? err : new Error(message);
252
312
  }
253
313
  };
314
+ // #2140: consult the release-managed binary directory (~/.pi-lens/bin)
315
+ // BEFORE the bare-PATH candidates below. A bare `candidates: ["opengrep"]`
316
+ // entry only ever resolves through the OS's own PATH lookup — never that
317
+ // directory — so a github/maven/archive-strategy tool installed there with
318
+ // no PATH entry ENOENTs every direct candidate first, only to have the
319
+ // managed-install step further down find the very same binary a few
320
+ // hundred ms later. `findManagedToolBinary` does a bare `fs.access`, no
321
+ // spawn, so this costs at most a few stat calls per server launch attempt
322
+ // (not per file/dispatch — launches are session-scoped). Mirrors
323
+ // `findManagedNodeToolBinary`'s npm-managed fast path (runner-helpers.ts)
324
+ // and `SecurityScanClient.probeVersion`'s equivalent fix for the CLI-scan
325
+ // half of this same issue (#2140, landed in PR #2148/#2137).
326
+ const managedCandidate = spec.managedToolId
327
+ ? await findManagedToolBinary(spec.managedToolId)
328
+ : undefined;
329
+ const candidates = managedCandidate && !spec.candidates.includes(managedCandidate)
330
+ ? [managedCandidate, ...spec.candidates]
331
+ : spec.candidates;
254
332
  // A candidate that fails while a LATER candidate (or managed install)
255
333
  // succeeds is just fallback, not a failure — logging each immediately floods
256
334
  // the logs with scary "candidate failed / npm shim failed / Run npm install"
@@ -258,7 +336,7 @@ export async function resolveAndLaunch(spec, allowInstall) {
258
336
  // surface only if ALL direct candidates fail.
259
337
  const candidateFailures = [];
260
338
  // Step 1 & 2 — try all explicit candidates (includes bare command = PATH lookup)
261
- for (const [index, command] of spec.candidates.entries()) {
339
+ for (const [index, command] of candidates.entries()) {
262
340
  logLatency({
263
341
  type: "phase",
264
342
  phase: "lsp_launch_candidate_attempt",
@@ -268,11 +346,11 @@ export async function resolveAndLaunch(spec, allowInstall) {
268
346
  tool: toolLabel,
269
347
  command,
270
348
  index,
271
- totalCandidates: spec.candidates.length,
349
+ totalCandidates: candidates.length,
272
350
  allowInstall: canInstall(allowInstall),
273
351
  },
274
352
  });
275
- logSessionStart(`lsp launch candidate attempt tool=${toolLabel} idx=${index}/${spec.candidates.length - 1} command=${command} cwd=${spec.cwd}`);
353
+ logSessionStart(`lsp launch candidate attempt tool=${toolLabel} idx=${index}/${candidates.length - 1} command=${command} cwd=${spec.cwd}`);
276
354
  try {
277
355
  const proc = await launchLSP(command, spec.args, {
278
356
  cwd: spec.cwd,
@@ -896,19 +974,23 @@ async function findAncestorFileAmong(startDir, candidateSegmentLists) {
896
974
  * Svelte). Without this guard an offline or partial install re-runs a 120 s
897
975
  * forced reinstall on every spawn.
898
976
  *
899
- * The guard is a plain module-level flag, so without an explicit re-arm it
977
+ * The guard is process-singleton state, so without an explicit re-arm it
900
978
  * would latch for the whole extension-host process — a repair that failed
901
979
  * once (transient registry hiccup) would stay unrepairable for every later
902
980
  * session in that process. `resetClassicTsRepairGuard` re-arms it; callers
903
981
  * wire that into `session_start` alongside the other per-session resets
904
982
  * (#1570).
905
983
  */
906
- let classicTsRepairAttempted = false;
984
+ const CLASSIC_TS_REPAIR_FAMILY = "lsp.classic-ts-repair-guard";
985
+ const CLASSIC_TS_REPAIR_VERSION = 1;
986
+ function classicTsRepairState() {
987
+ return getProcessSingleton(CLASSIC_TS_REPAIR_FAMILY, CLASSIC_TS_REPAIR_VERSION, () => ({ attempted: false }));
988
+ }
907
989
  /** Re-arm the classic-repair guard so a new session gets its own attempt. */
908
990
  export function resetClassicTsRepairGuard() {
909
- classicTsRepairAttempted = false;
991
+ classicTsRepairState().attempted = false;
910
992
  }
911
- /** Test hook — clears the per-process classic-repair guard. */
993
+ /** Test hook — clears the process-singleton classic-repair guard. */
912
994
  export function _resetClassicTsRepairForTests() {
913
995
  resetClassicTsRepairGuard();
914
996
  }
@@ -1008,7 +1090,7 @@ async function findTsserverPath(root, allowInstall) {
1008
1090
  if (discoveredTsserver ||
1009
1091
  !discoveredTsc ||
1010
1092
  !installAllowed ||
1011
- classicTsRepairAttempted) {
1093
+ classicTsRepairState().attempted) {
1012
1094
  return discoveredTsserver;
1013
1095
  }
1014
1096
  // Repair only a compiler we can prove is TypeScript 7+. TypeScript 7 dropped
@@ -1021,7 +1103,7 @@ async function findTsserverPath(root, allowInstall) {
1021
1103
  // An older managed tree took `latest` before the registry pinned the classic
1022
1104
  // compiler. Reinstall the pinned version once so that tree self-heals,
1023
1105
  // without deleting user or project-local TypeScript installations.
1024
- classicTsRepairAttempted = true;
1106
+ classicTsRepairState().attempted = true;
1025
1107
  logSessionStart(`lsp typescript: managed compiler resolved to TypeScript ${discoveredVersion.version}, which ships no tsserver.js; reinstalling pinned classic fallback`);
1026
1108
  const repairedTsc = await ensureTool("typescript", {
1027
1109
  allowInstall: true,
@@ -2316,6 +2398,19 @@ export const BashServer = {
2316
2398
  name: "Bash Language Server",
2317
2399
  extensions: [".bash", ".sh", ".zsh"],
2318
2400
  root: FileDirRoot,
2401
+ // #2194 bounded the installer's own verification at 20s; the dispatch
2402
+ // touch's client-wait floor stayed at the shared 5s default, so a cold
2403
+ // spawn could still race that budget and read as unavailable even after
2404
+ // installer verification cleared it. Match the installer bound (#2169).
2405
+ // `initializeTimeoutMs` matches it too: the measured cold start
2406
+ // (9,667ms, #2194) has headroom under the unraised 15s default, but
2407
+ // leaving the two fields mismatched is the same latent shape that made
2408
+ // the Prisma/Vue raise a no-op (fix-round F1, #2233) — a slower host
2409
+ // than the one measured would still hit the 15s inner kill despite the
2410
+ // caller being willing to wait 20s. Equalizing costs nothing on the
2411
+ // success path and removes the mismatch outright.
2412
+ clientWaitTimeoutMs: 20_000,
2413
+ initializeTimeoutMs: 20_000,
2319
2414
  spawn(root, options) {
2320
2415
  return resolveAndLaunch({
2321
2416
  candidates: nodeBinCandidates(root, "bash-language-server"),
@@ -2402,6 +2497,13 @@ export const JsonServer = {
2402
2497
  ["package.json", "tsconfig.json", "jsconfig.json"],
2403
2498
  [".git"],
2404
2499
  ])),
2500
+ // See BashServer above: the installer's 20s verification bound (#2194)
2501
+ // does not, on its own, raise the dispatch touch's cold-spawn wait floor.
2502
+ // Mirror it here so a cold spawn cannot lose that race either (#2169).
2503
+ // `initializeTimeoutMs` matches it for the same reason as BashServer
2504
+ // above (fix-round F1, #2233).
2505
+ clientWaitTimeoutMs: 20_000,
2506
+ initializeTimeoutMs: 20_000,
2405
2507
  spawn(root, options) {
2406
2508
  return resolveAndLaunch({
2407
2509
  candidates: ["vscode-json-language-server"],
@@ -2444,6 +2546,17 @@ export const PrismaServer = {
2444
2546
  name: "Prisma Language Server",
2445
2547
  extensions: KIND_EXTENSIONS["prisma"],
2446
2548
  root: RootWithFallback(createRootDetector(["prisma/schema.prisma", "schema.prisma"])),
2549
+ // Matches the installer's 40s verification bound above (#2169): the
2550
+ // dispatch touch's cold-spawn wait floor defaults to 5s and would
2551
+ // otherwise time the client out well before the binary answers.
2552
+ // `initializeTimeoutMs` MUST be set alongside it (RubyServer's own
2553
+ // precedent below): unset, it falls back to the 15s
2554
+ // `INITIALIZE_TIMEOUT_MS` default in `clients/lsp/client.ts`, which
2555
+ // hard-kills the child mid-handshake — a real cold Prisma run measured
2556
+ // up to 27.3s, so the spawn would die there before this wait ever gets
2557
+ // a chance to matter (fix-round F1, #2233).
2558
+ clientWaitTimeoutMs: 40_000,
2559
+ initializeTimeoutMs: 40_000,
2447
2560
  spawn(root, options) {
2448
2561
  return resolveAndLaunch({
2449
2562
  candidates: nodeBinCandidates(root, "prisma-language-server"),
@@ -2466,6 +2579,15 @@ export const VueServer = {
2466
2579
  "pnpm-lock.yaml",
2467
2580
  "yarn.lock",
2468
2581
  ]))),
2582
+ // Vue's launcher loads the full language-service bundle before answering,
2583
+ // matching the installer's 30s verification bound (#2176). The dispatch
2584
+ // touch's cold-spawn wait floor defaults to 5s and needs the same raise so
2585
+ // a cold spawn cannot time out there instead. `initializeTimeoutMs` MUST
2586
+ // match: unset, it falls back to the 15s default in `clients/lsp/client.ts`
2587
+ // and hard-kills a cold spawn (measured up to 22.6s in #2188, 16.2s here)
2588
+ // mid-handshake before this wait can matter (fix-round F1, #2233).
2589
+ clientWaitTimeoutMs: 30_000,
2590
+ initializeTimeoutMs: 30_000,
2469
2591
  async spawn(root, options) {
2470
2592
  const tsserverPath = await findTsserverPath(root, options?.allowInstall);
2471
2593
  // Vue Language Server needs Vue dependencies installed to resolve types.
@@ -2505,6 +2627,13 @@ export const SvelteServer = {
2505
2627
  "pnpm-lock.yaml",
2506
2628
  "yarn.lock",
2507
2629
  ]))),
2630
+ // Matches the installer's 20s verification bound above (#2169): the
2631
+ // dispatch touch's cold-spawn wait floor defaults to 5s and would
2632
+ // otherwise time the client out well before the binary answers.
2633
+ // `initializeTimeoutMs` matches it for the same reason as BashServer
2634
+ // above (fix-round F1, #2233).
2635
+ clientWaitTimeoutMs: 20_000,
2636
+ initializeTimeoutMs: 20_000,
2508
2637
  async spawn(root, options) {
2509
2638
  const tsserverPath = await findTsserverPath(root, options?.allowInstall);
2510
2639
  const proc = await resolveAndLaunch({
@@ -0,0 +1,99 @@
1
+ /**
2
+ * The session-root registry: which project roots does this process serve?
3
+ *
4
+ * #2052. A file is IN-SESSION when it lies within any initialized session cwd.
5
+ * `initLSPConfig` (`clients/lsp/config.ts`) is the single writer, and every
6
+ * entry point funnels through it — `ensureLSPConfigInitialized` (`index.ts`),
7
+ * `ensureReady` (`mcp/server.ts`), `clients/runtime-session.ts`, and
8
+ * `clients/lens-engine.ts` — so registration needs no second call site.
9
+ *
10
+ * WHY THIS IS ITS OWN MODULE, not a helper inside `config.ts`: `clients/lsp/
11
+ * index.ts`'s import surface from `config.js` is mirrored by an explicit
12
+ * `vi.mock` factory in ~58 test files. Importing a new symbol from `config.js`
13
+ * breaks every one of them with "No export is defined on the mock", which is a
14
+ * standing tax on any future export. The registry is also a different concern
15
+ * from `workspaceConfigs`: that map stores per-root CONFIG PAYLOAD and is
16
+ * LRU-capped at 32, whereas this set answers "is this root served" and must
17
+ * not silently evict a live root and start declining its files.
18
+ *
19
+ * WHY IT IS NOT A LATCH. The first attempt at #2052 used a single
20
+ * last-writer-wins session cwd that fell back to `process.cwd()`. Two bugs
21
+ * followed directly from that shape, and this module's contract excludes both:
22
+ *
23
+ * - MULTI-ROOT. A process that initializes projA and then projB serves BOTH.
24
+ * A latch silently made projA's files foreign the moment projB registered.
25
+ * - FAIL-OPEN ON EMPTY. An empty registry declines NOTHING. Declining is a
26
+ * hard refusal to answer, so it must rest on positive evidence that a
27
+ * session exists AND that the file sits outside it — never on the absence
28
+ * of evidence. Callers that never declared a session (isolated tests, API
29
+ * consumers, a request racing initialization) therefore keep the pre-#2052
30
+ * clamp behavior instead of having `process.cwd()` gate a refusal.
31
+ *
32
+ * This is also the seam #2053 needs: an explicit `analysisRoot` registers here.
33
+ */
34
+ import { isSameOrWithin } from "./server.js";
35
+ import path from "node:path";
36
+ /**
37
+ * Insertion-ordered, so eviction drops the OLDEST root. Matches
38
+ * `LSP_CONFIG_CWD_CAP` in `index.ts`, the cap on the set of cwds that
39
+ * `ensureLSPConfigInitialized` will initialize at all — a root this process
40
+ * cannot re-initialize must not still be considered served.
41
+ */
42
+ const sessionRoots = new Set();
43
+ const SESSION_ROOT_CAP = 128;
44
+ /** Record a session cwd as served. Idempotent; re-registering refreshes nothing. */
45
+ export function registerSessionRoot(cwd) {
46
+ sessionRoots.add(path.resolve(cwd));
47
+ while (sessionRoots.size > SESSION_ROOT_CAP) {
48
+ const oldest = sessionRoots.values().next().value;
49
+ if (oldest === undefined)
50
+ break;
51
+ sessionRoots.delete(oldest);
52
+ }
53
+ }
54
+ /** Return whether this exact root is still served after cap eviction. */
55
+ export function isSessionRootRegistered(cwd) {
56
+ return sessionRoots.has(path.resolve(cwd));
57
+ }
58
+ /** True when a readiness memo must run initialization for this root. */
59
+ export function shouldInitializeSessionRoot(cwd, readyRoots) {
60
+ const normalized = path.resolve(cwd);
61
+ return !readyRoots.has(normalized) || !isSessionRootRegistered(normalized);
62
+ }
63
+ /**
64
+ * Is `filePath` outside EVERY registered session root?
65
+ *
66
+ * Returns false (serve it) when the registry is empty — see the fail-open rule
67
+ * in this module's header. Containment uses `isSameOrWithin`, the same
68
+ * comparator the LSP root ceiling uses, which selects `path.win32` by path
69
+ * SHAPE rather than by `process.platform` (shape 2 / #1150).
70
+ */
71
+ export function isOutsideAllSessionRoots(filePath) {
72
+ if (sessionRoots.size === 0)
73
+ return false;
74
+ const resolved = path.resolve(filePath);
75
+ for (const root of sessionRoots) {
76
+ if (isSameOrWithin(root, resolved))
77
+ return false;
78
+ }
79
+ return true;
80
+ }
81
+ /**
82
+ * The registered roots, for the decline telemetry record. Sorted with a
83
+ * code-unit comparator so the emitted string is deterministic across locales
84
+ * (#1883), and capped because the record is BOUNDED — a process with many
85
+ * roots must not emit an unbounded path list.
86
+ */
87
+ export function getSessionRootsForTelemetry(limit = 4) {
88
+ return [...sessionRoots]
89
+ .sort((a, b) => Number(a > b) - Number(a < b))
90
+ .slice(0, limit);
91
+ }
92
+ /**
93
+ * Clear the registry. Called by `resetLSPConfigStateForTests` so the registry
94
+ * and the config store reset together and cannot disagree about which roots
95
+ * exist.
96
+ */
97
+ export function resetSessionRootsForTests() {
98
+ sessionRoots.clear();
99
+ }