@herbertgao/pi-extensions 2026.9.2 → 2026.9.4

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 (255) hide show
  1. package/README.md +4 -2
  2. package/THIRD_PARTY_NOTICES.md +49 -0
  3. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +3 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +16 -11
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +20 -6
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +12 -7
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +98 -0
  8. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  9. package/node_modules/@herbertgao/sol-pi/LICENSE +19 -0
  10. package/node_modules/@herbertgao/sol-pi/README.md +159 -0
  11. package/node_modules/@herbertgao/sol-pi/SECURITY.md +26 -0
  12. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +19 -0
  13. package/node_modules/@herbertgao/sol-pi/agents-install.md +150 -0
  14. package/node_modules/@herbertgao/sol-pi/assets/sol-pi-hero.png +0 -0
  15. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +69 -0
  16. package/node_modules/@herbertgao/sol-pi/docs/configuration.md +75 -0
  17. package/node_modules/@herbertgao/sol-pi/package.json +76 -0
  18. package/node_modules/@herbertgao/sol-pi/scripts/check-pi-compat.mjs +32 -0
  19. package/node_modules/@herbertgao/sol-pi/scripts/check-sol-pi-config.mjs +120 -0
  20. package/node_modules/@herbertgao/sol-pi/sol-pi.example.json +10 -0
  21. package/node_modules/@herbertgao/sol-pi/src/sol-pi/config.ts +135 -0
  22. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/file-queue.ts +74 -0
  23. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +185 -0
  24. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/then-run.ts +128 -0
  25. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/archive.ts +53 -0
  26. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/candidate.ts +101 -0
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/config.ts +71 -0
  28. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/index.ts +220 -0
  29. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/journal.ts +25 -0
  30. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/provider.ts +164 -0
  31. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/receipt.ts +177 -0
  32. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/index.ts +227 -0
  33. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/ledger.ts +20 -0
  34. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/observation.ts +252 -0
  35. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +237 -0
  36. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +455 -0
  37. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/index.ts +49 -0
  38. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/plan.ts +79 -0
  39. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +208 -0
  40. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/tools.ts +100 -0
  41. package/node_modules/@herbertgao/sol-pi/src/sol-pi/index.ts +42 -0
  42. package/node_modules/@herbertgao/sol-pi/src/sol-pi/runtime-paths.ts +17 -0
  43. package/node_modules/@herbertgao/sol-pi/src/sol-pi/tui.ts +71 -0
  44. package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -2
  45. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
  46. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  47. package/node_modules/@narumitw/pi-btw/src/btw.ts +13 -2
  48. package/node_modules/pi-antigravity/LICENSE +21 -0
  49. package/node_modules/pi-antigravity/README.md +194 -0
  50. package/node_modules/pi-antigravity/package.json +67 -0
  51. package/node_modules/pi-antigravity/src/auth/index.ts +14 -0
  52. package/node_modules/pi-antigravity/src/auth/oauth.ts +442 -0
  53. package/node_modules/pi-antigravity/src/client/client.ts +561 -0
  54. package/node_modules/pi-antigravity/src/client/index.ts +1 -0
  55. package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
  56. package/node_modules/pi-antigravity/src/diagnostics/index.ts +1 -0
  57. package/node_modules/pi-antigravity/src/image/image.ts +336 -0
  58. package/node_modules/pi-antigravity/src/image/index.ts +1 -0
  59. package/node_modules/pi-antigravity/src/index.ts +280 -0
  60. package/node_modules/pi-antigravity/src/models/discovery.ts +154 -0
  61. package/node_modules/pi-antigravity/src/models/grouping.ts +424 -0
  62. package/node_modules/pi-antigravity/src/models/index.ts +3 -0
  63. package/node_modules/pi-antigravity/src/models/models.ts +500 -0
  64. package/node_modules/pi-antigravity/src/stream/index.ts +1 -0
  65. package/node_modules/pi-antigravity/src/stream/stream.ts +1460 -0
  66. package/node_modules/pi-antigravity/src/types/enums.ts +42 -0
  67. package/node_modules/pi-antigravity/src/types/index.ts +2 -0
  68. package/node_modules/pi-antigravity/src/types/types.ts +292 -0
  69. package/node_modules/pi-antigravity/src/usage/index.ts +1 -0
  70. package/node_modules/pi-antigravity/src/usage/usage.ts +371 -0
  71. package/node_modules/pi-antigravity/src/utils/http.ts +91 -0
  72. package/node_modules/pi-antigravity/src/utils/index.ts +3 -0
  73. package/node_modules/pi-antigravity/src/utils/security.ts +73 -0
  74. package/node_modules/pi-antigravity/src/utils/util.ts +132 -0
  75. package/node_modules/pi-antigravity/tsconfig.json +21 -0
  76. package/node_modules/pi-lens/CHANGELOG.md +176 -0
  77. package/node_modules/pi-lens/README.md +13 -8
  78. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -1
  79. package/node_modules/pi-lens/dist/clients/analysed-root.js +1 -0
  80. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +1 -1
  81. package/node_modules/pi-lens/dist/clients/biome-client.js +13 -2
  82. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +2 -0
  83. package/node_modules/pi-lens/dist/clients/cache-observability.js +122 -1
  84. package/node_modules/pi-lens/dist/clients/complexity-client.js +5 -0
  85. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
  86. package/node_modules/pi-lens/dist/clients/config-resolve.js +6 -3
  87. package/node_modules/pi-lens/dist/clients/config-schema.js +17 -0
  88. package/node_modules/pi-lens/dist/clients/config-warn.js +2 -2
  89. package/node_modules/pi-lens/dist/clients/dead-code-client.js +8 -1
  90. package/node_modules/pi-lens/dist/clients/dependency-checker.js +5 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +38 -15
  92. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +1 -1
  93. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +5 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +2 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +2 -3
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +4 -3
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +2 -1
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +2 -1
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +2 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +2 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +2 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +2 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +2 -1
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +2 -1
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +2 -1
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +2 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +2 -1
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +3 -1
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +2 -1
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +2 -1
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +2 -1
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +2 -1
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +19 -1
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +3 -2
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +2 -1
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +2 -1
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +2 -1
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +2 -1
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +12 -12
  122. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +2 -1
  123. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +2 -1
  124. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +2 -2
  125. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +12 -11
  126. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -1
  127. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -1
  128. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +2 -2
  129. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  130. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +3 -2
  131. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +3 -2
  132. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +2 -1
  133. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  134. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +2 -1
  135. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +2 -1
  136. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +2 -1
  137. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
  138. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
  139. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
  140. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +2 -1
  141. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +4 -3
  142. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +2 -1
  143. package/node_modules/pi-lens/dist/clients/effective-config.js +12 -2
  144. package/node_modules/pi-lens/dist/clients/extension-log.js +2 -0
  145. package/node_modules/pi-lens/dist/clients/file-role.js +18 -1
  146. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +4 -0
  147. package/node_modules/pi-lens/dist/clients/formatters.js +54 -188
  148. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -0
  149. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +42 -6
  150. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +48 -7
  151. package/node_modules/pi-lens/dist/clients/installer/index.js +196 -52
  152. package/node_modules/pi-lens/dist/clients/jscpd-client.js +9 -1
  153. package/node_modules/pi-lens/dist/clients/knip-client.js +9 -2
  154. package/node_modules/pi-lens/dist/clients/language-profile.js +12 -2
  155. package/node_modules/pi-lens/dist/clients/latency-logger.js +2 -0
  156. package/node_modules/pi-lens/dist/clients/lens-config.js +4 -0
  157. package/node_modules/pi-lens/dist/clients/lsp/config.js +36 -7
  158. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +24 -1
  159. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +78 -0
  160. package/node_modules/pi-lens/dist/clients/lsp/index.js +281 -45
  161. package/node_modules/pi-lens/dist/clients/lsp/server.js +62 -15
  162. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +37 -4
  163. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +3 -0
  164. package/node_modules/pi-lens/dist/clients/mcp/session.js +2 -0
  165. package/node_modules/pi-lens/dist/clients/opengrep-client.js +2 -0
  166. package/node_modules/pi-lens/dist/clients/package-manager.js +2 -1
  167. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +8 -3
  168. package/node_modules/pi-lens/dist/clients/pipeline.js +3 -2
  169. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +38 -10
  170. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
  171. package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
  172. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +2 -0
  173. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +2 -0
  174. package/node_modules/pi-lens/dist/clients/ruff-client.js +18 -9
  175. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +9 -0
  176. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +5 -0
  177. package/node_modules/pi-lens/dist/clients/runtime-session.js +13 -4
  178. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +4 -1
  179. package/node_modules/pi-lens/dist/clients/runtime-turn.js +48 -1
  180. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -2
  181. package/node_modules/pi-lens/dist/clients/session-event-guard.js +69 -1
  182. package/node_modules/pi-lens/dist/clients/sg-runner.js +2 -1
  183. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +108 -0
  184. package/node_modules/pi-lens/dist/clients/test-runner-client.js +291 -76
  185. package/node_modules/pi-lens/dist/clients/tool-config.js +199 -0
  186. package/node_modules/pi-lens/dist/clients/tool-cwd.js +299 -0
  187. package/node_modules/pi-lens/dist/clients/tool-probe.js +53 -0
  188. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +42 -0
  189. package/node_modules/pi-lens/dist/clients/trivy-client.js +9 -1
  190. package/node_modules/pi-lens/dist/clients/turn-context.js +52 -0
  191. package/node_modules/pi-lens/dist/clients/widget-state.js +4 -3
  192. package/node_modules/pi-lens/dist/index.js +21969 -20005
  193. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -2
  194. package/node_modules/pi-lens/dist/mcp/server.js +135 -159
  195. package/node_modules/pi-lens/dist/tools/activate-tools.js +10 -7
  196. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +2 -13
  197. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +8 -13
  198. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +83 -39
  199. package/node_modules/pi-lens/dist/tools/effective-config.js +4 -3
  200. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +1 -10
  201. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +353 -85
  202. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +21 -110
  203. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +20 -44
  204. package/node_modules/pi-lens/dist/tools/module-report.js +5 -9
  205. package/node_modules/pi-lens/dist/tools/project-report.js +9 -17
  206. package/node_modules/pi-lens/dist/tools/render-compact.js +280 -1
  207. package/node_modules/pi-lens/dist/tools/symbol-search.js +2 -2
  208. package/node_modules/pi-lens/docs/agent-guide.md +3 -4
  209. package/node_modules/pi-lens/docs/agent-tools.md +46 -22
  210. package/node_modules/pi-lens/docs/configuration.md +9 -0
  211. package/node_modules/pi-lens/docs/environment-variables.md +0 -5
  212. package/node_modules/pi-lens/docs/features.md +5 -5
  213. package/node_modules/pi-lens/docs/globalconfig.md +11 -1
  214. package/node_modules/pi-lens/docs/mcp.md +13 -1
  215. package/node_modules/pi-lens/docs/pi-lens-fixer.md +71 -0
  216. package/node_modules/pi-lens/docs/pi-lens-investigator.md +15 -0
  217. package/node_modules/pi-lens/docs/pi-lens-monitor.md +88 -0
  218. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +35 -0
  219. package/node_modules/pi-lens/docs/pi-lens-subagent.md +12 -4
  220. package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
  221. package/node_modules/pi-lens/docs/real-harness.md +46 -0
  222. package/node_modules/pi-lens/docs/release-qa-baseline.md +5 -1
  223. package/node_modules/pi-lens/docs/servercapabilities.md +1 -6
  224. package/node_modules/pi-lens/docs/settings.md +37 -2
  225. package/node_modules/pi-lens/docs/tools_improvement2.md +4 -4
  226. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  227. package/node_modules/pi-lens/docs/usage.md +13 -3
  228. package/node_modules/pi-lens/package.json +10 -1
  229. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +7 -3
  230. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +8 -6
  231. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +19 -9
  232. package/node_modules/pi-web-access/CHANGELOG.md +29 -0
  233. package/node_modules/pi-web-access/README.md +33 -22
  234. package/node_modules/pi-web-access/abortable.ts +17 -0
  235. package/node_modules/pi-web-access/crawl4ai.ts +204 -0
  236. package/node_modules/pi-web-access/credential-source.ts +1 -0
  237. package/node_modules/pi-web-access/curator-page.ts +20 -2
  238. package/node_modules/pi-web-access/curator-run.ts +44 -0
  239. package/node_modules/pi-web-access/curator-server.ts +3 -1
  240. package/node_modules/pi-web-access/duckduckgo.ts +1 -1
  241. package/node_modules/pi-web-access/extract.ts +57 -12
  242. package/node_modules/pi-web-access/fetch-params.ts +1 -1
  243. package/node_modules/pi-web-access/gemini-search.ts +9 -5
  244. package/node_modules/pi-web-access/index.ts +25 -18
  245. package/node_modules/pi-web-access/openai-search.ts +35 -13
  246. package/node_modules/pi-web-access/package.json +2 -2
  247. package/node_modules/pi-web-access/page-query.ts +5 -2
  248. package/node_modules/pi-web-access/query-rewrite.ts +5 -2
  249. package/node_modules/pi-web-access/serpapi.ts +220 -0
  250. package/node_modules/pi-web-access/ssrf-protection.ts +5 -1
  251. package/node_modules/pi-web-access/storage.ts +10 -0
  252. package/node_modules/pi-web-access/summary-review.ts +34 -21
  253. package/node_modules/pi-web-access/utils.ts +6 -4
  254. package/package.json +11 -5
  255. package/node_modules/pi-lens/dist/tools/ast-dump.js +0 -103
@@ -66,6 +66,7 @@ import { commitDurableStoreAsync } from "../durable-store.js";
66
66
  import { getGlobalPiLensDir } from "../file-utils.js";
67
67
  import { allAvailableGlobalBinDirs, installArgs, pmBinary, resolveNodePackageManager, } from "../package-manager.js";
68
68
  import { resetSafeSpawnWindowsCommandCache, safeSpawnAsync, } from "../safe-spawn.js";
69
+ import { probeToolAsync } from "../tool-probe.js";
69
70
  import { logSessionStart } from "../sessionstart-logger.js";
70
71
  // Global installation directory for pi-lens tools
71
72
  const TOOLS_DIR = path.join(getGlobalPiLensDir(), "tools");
@@ -227,6 +228,114 @@ const OS_ARCH_ZIP_ASSETS = {
227
228
  darwin: { x64: "darwin_amd64.zip", arm64: "darwin_arm64.zip" },
228
229
  win32: { x64: "windows_amd64.zip", arm64: "windows_arm64.zip" },
229
230
  };
231
+ function managedPackageFormatterTool(spec) {
232
+ return {
233
+ id: spec.id,
234
+ name: spec.name,
235
+ checkCommand: spec.id,
236
+ checkArgs: ["--version"],
237
+ installStrategy: spec.installStrategy,
238
+ packageName: spec.packageName,
239
+ binaryName: spec.id,
240
+ };
241
+ }
242
+ const MANAGED_PACKAGE_FORMATTERS = [
243
+ { id: "black", name: "Black", installStrategy: "pip", packageName: "black" },
244
+ {
245
+ id: "cmake-format",
246
+ name: "cmake-format",
247
+ installStrategy: "pip",
248
+ packageName: "cmakelang",
249
+ },
250
+ { id: "oxfmt", name: "oxfmt", installStrategy: "npm", packageName: "oxfmt" },
251
+ ];
252
+ function managedGitHubFormatterTool(spec) {
253
+ return {
254
+ id: spec.id,
255
+ name: spec.name,
256
+ checkCommand: spec.id,
257
+ checkArgs: ["--version"],
258
+ installStrategy: "github",
259
+ binaryName: spec.id,
260
+ github: {
261
+ repo: `${spec.owner}/${spec.repo}`,
262
+ assetMatch: spec.assetPattern,
263
+ ...(spec.binaryInArchive && { binaryInArchive: spec.binaryInArchive }),
264
+ ...(spec.kind === "phar" && { launcher: "php" }),
265
+ ...(spec.kind === "jar" && { launcher: "java" }),
266
+ },
267
+ };
268
+ }
269
+ const MANAGED_GITHUB_FORMATTERS = [
270
+ {
271
+ id: "stylua",
272
+ name: "StyLua",
273
+ owner: "JohnnyMorganz",
274
+ repo: "StyLua",
275
+ assetPattern: archAssetMatch({
276
+ linux: { x64: "linux-x86_64.zip", arm64: "linux-aarch64.zip" },
277
+ darwin: { x64: "macos-x86_64.zip", arm64: "macos-aarch64.zip" },
278
+ win32: { x64: "windows-x86_64.zip" },
279
+ }),
280
+ kind: "binary",
281
+ binaryInArchive: "stylua",
282
+ },
283
+ {
284
+ id: "php-cs-fixer",
285
+ name: "PHP CS Fixer",
286
+ owner: "PHP-CS-Fixer",
287
+ repo: "PHP-CS-Fixer",
288
+ assetPattern: (platform) => platform === "linux" || platform === "darwin" || platform === "win32"
289
+ ? "php-cs-fixer.phar"
290
+ : undefined,
291
+ kind: "phar",
292
+ },
293
+ {
294
+ id: "cljfmt",
295
+ name: "cljfmt",
296
+ owner: "weavejester",
297
+ repo: "cljfmt",
298
+ assetPattern: (platform, arch) => {
299
+ if (platform === "linux")
300
+ return arch === "arm64"
301
+ ? "standalone.jar"
302
+ : "linux-amd64-static.tar.gz";
303
+ if (platform === "darwin")
304
+ return "standalone.jar";
305
+ if (platform === "win32")
306
+ return "win-amd64.zip";
307
+ return undefined;
308
+ },
309
+ kind: "jar",
310
+ binaryInArchive: "cljfmt",
311
+ },
312
+ ];
313
+ const MANAGED_MAVEN_FORMATTERS = [
314
+ {
315
+ id: "google-java-format",
316
+ name: "google-java-format",
317
+ groupId: "com.google.googlejavaformat",
318
+ artifactId: "google-java-format",
319
+ version: "1.27.0",
320
+ classifier: "all-deps",
321
+ },
322
+ ];
323
+ function managedMavenFormatterTool(spec) {
324
+ return {
325
+ id: spec.id,
326
+ name: spec.name,
327
+ checkCommand: spec.id,
328
+ checkArgs: ["--version"],
329
+ installStrategy: "maven",
330
+ binaryName: spec.id,
331
+ maven: {
332
+ groupId: spec.groupId,
333
+ artifactId: spec.artifactId,
334
+ version: spec.version,
335
+ classifier: spec.classifier,
336
+ },
337
+ };
338
+ }
230
339
  export const TOOLS = [
231
340
  // Core LSP servers
232
341
  {
@@ -275,6 +384,7 @@ export const TOOLS = [
275
384
  packageName: "prettier",
276
385
  binaryName: "prettier",
277
386
  },
387
+ ...MANAGED_PACKAGE_FORMATTERS.map(managedPackageFormatterTool),
278
388
  {
279
389
  id: "ruff",
280
390
  name: "Ruff",
@@ -707,6 +817,8 @@ export const TOOLS = [
707
817
  // bare binary, no archive
708
818
  },
709
819
  },
820
+ ...MANAGED_GITHUB_FORMATTERS.map(managedGitHubFormatterTool),
821
+ ...MANAGED_MAVEN_FORMATTERS.map(managedMavenFormatterTool),
710
822
  {
711
823
  id: "rust-analyzer",
712
824
  name: "rust-analyzer",
@@ -1633,29 +1745,16 @@ function isAstGrepVersionOutput(output) {
1633
1745
  return /\bast[- ]grep\b/i.test(output);
1634
1746
  }
1635
1747
  async function verifyAstGrepProbePath(binPath) {
1636
- return new Promise((resolve) => {
1637
- let proc;
1638
- try {
1639
- proc = spawn(binPath, ["--version"], {
1640
- stdio: ["ignore", "pipe", "pipe"],
1641
- shell: process.platform === "win32" && /\.(cmd|bat)$/i.test(binPath),
1642
- timeout: 5000,
1643
- });
1644
- }
1645
- catch {
1646
- // SYNCHRONOUS spawn throw (Windows `spawn UNKNOWN`/EINVAL, the pidusage
1647
- // bug class, #533) — best-effort probe, resolve rather than reject.
1648
- resolve(false);
1649
- return;
1650
- }
1651
- let output = "";
1652
- proc.stdout?.on("data", (data) => (output += data));
1653
- proc.stderr?.on("data", (data) => (output += data));
1654
- proc.on("exit", (code) => {
1655
- resolve(code === 0 && isAstGrepVersionOutput(output));
1656
- });
1657
- proc.on("error", () => resolve(false));
1748
+ // #2894: through the probe seam rather than a hand-rolled `spawn` promise.
1749
+ // `probeToolAsync` already owns the synchronous-throw case (#533), the
1750
+ // Windows `.cmd`/`.bat` resolution this used `shell: true` for, and the
1751
+ // timeout tree-kill a bare `spawn({ timeout })` does not do.
1752
+ const result = await probeToolAsync(binPath, ["--version"], {
1753
+ timeout: 5000,
1658
1754
  });
1755
+ return (!result.error &&
1756
+ result.status === 0 &&
1757
+ isAstGrepVersionOutput(`${result.stdout}${result.stderr}`));
1659
1758
  }
1660
1759
  // Exported for testing only.
1661
1760
  export async function checkProbeCache(toolId) {
@@ -2151,29 +2250,15 @@ export async function getAllToolStatuses() {
2151
2250
  status.installed = true;
2152
2251
  status.source = "global-path";
2153
2252
  status.path = tool.checkCommand;
2154
- // Try to get version
2155
- const versionResult = await new Promise((resolve) => {
2156
- let proc;
2157
- try {
2158
- proc = spawn(tool.checkCommand, ["--version"], {
2159
- stdio: ["ignore", "pipe", "pipe"],
2160
- shell: process.platform === "win32",
2161
- timeout: 5000,
2162
- });
2163
- }
2164
- catch {
2165
- // SYNCHRONOUS spawn throw (Windows `spawn UNKNOWN`/EINVAL, the
2166
- // pidusage bug class, #533) — best-effort, resolve empty version.
2167
- resolve("");
2168
- return;
2169
- }
2170
- let out = "";
2171
- proc.stdout?.on("data", (d) => (out += d));
2172
- proc.stderr?.on("data", (d) => (out += d));
2173
- proc.on("exit", () => resolve(out.trim().split("\n")[0]?.slice(0, 30) || ""));
2174
- proc.on("error", () => resolve(""));
2253
+ // Try to get version — through the probe seam (#2894), which owns the
2254
+ // synchronous-throw case (#533) and the timeout tree-kill that a bare
2255
+ // `spawn({ timeout })` promise did not do.
2256
+ const probe = await probeToolAsync(tool.checkCommand, ["--version"], {
2257
+ timeout: 5000,
2175
2258
  });
2176
- status.version = versionResult || undefined;
2259
+ status.version =
2260
+ `${probe.stdout}${probe.stderr}`.trim().split("\n")[0]?.slice(0, 30) ||
2261
+ undefined;
2177
2262
  statuses.push(status);
2178
2263
  continue;
2179
2264
  }
@@ -2515,6 +2600,13 @@ function getGitHubInstalledBinaryName(binaryName, platform, assetName) {
2515
2600
  return `${binaryName}.cmd`;
2516
2601
  return `${binaryName}.exe`;
2517
2602
  }
2603
+ function launcherForGitHubAsset(spec, assetName) {
2604
+ if (spec.launcher === "php" && assetName.endsWith(".phar"))
2605
+ return "php";
2606
+ if (spec.launcher === "java" && assetName.endsWith(".jar"))
2607
+ return "java";
2608
+ return undefined;
2609
+ }
2518
2610
  function getArchiveBinaryCandidates(binaryName, platform, assetName) {
2519
2611
  if (platform !== "win32")
2520
2612
  return [binaryName];
@@ -2867,10 +2959,35 @@ prefetchedRelease) {
2867
2959
  const binaryName = tool.binaryName ?? tool.id;
2868
2960
  const isWindows = platform === "win32";
2869
2961
  const finalBinaryName = getGitHubInstalledBinaryName(binaryName, platform, asset.name);
2870
- const destPath = path.join(GITHUB_BIN_DIR, finalBinaryName);
2962
+ let destPath = path.join(GITHUB_BIN_DIR, finalBinaryName);
2871
2963
  const assetName = asset.name;
2964
+ const launcherRuntime = launcherForGitHubAsset(spec, assetName);
2965
+ if (launcherRuntime &&
2966
+ !(await isCommandAvailable(launcherRuntime, ["--version"]))) {
2967
+ logSessionStart(`github-install ${tool.id}: ${launcherRuntime} not found — asset requires a runtime launcher`);
2968
+ return undefined;
2969
+ }
2872
2970
  try {
2873
- if (assetName.endsWith(".gz") && !assetName.endsWith(".tar.gz")) {
2971
+ if (launcherRuntime) {
2972
+ const assetPath = path.join(GITHUB_BIN_DIR, `${tool.id}${assetName.endsWith(".phar") ? ".phar" : ".jar"}`);
2973
+ await writeFileAtomicAsync(assetPath, assetBuffer, {
2974
+ bestEffort: false,
2975
+ mode: 0o750,
2976
+ });
2977
+ const launcherName = isWindows ? `${binaryName}.bat` : binaryName;
2978
+ destPath = path.join(GITHUB_BIN_DIR, launcherName);
2979
+ const runtimeTarget = assetName.endsWith(".phar")
2980
+ ? `${tool.id}.phar`
2981
+ : `${tool.id}.jar`;
2982
+ const command = isWindows
2983
+ ? `@echo off\r\n${launcherRuntime} "%~dp0${runtimeTarget}" %*\r\n`
2984
+ : `#!/bin/sh\nexec ${launcherRuntime} "$(dirname "$0")/${runtimeTarget}" "$@"\n`;
2985
+ await writeFileAtomicAsync(destPath, command, {
2986
+ bestEffort: false,
2987
+ mode: isWindows ? undefined : 0o750,
2988
+ });
2989
+ }
2990
+ else if (assetName.endsWith(".gz") && !assetName.endsWith(".tar.gz")) {
2874
2991
  // Bare gzip (e.g. rust-analyzer-x86_64-unknown-linux-gnu.gz) — decompress directly
2875
2992
  const decompressed = await new Promise((resolve, reject) => {
2876
2993
  const gunzip = createGunzip();
@@ -3220,7 +3337,7 @@ async function probeManagedToolVersion(tool) {
3220
3337
  if (!cached?.path || !existsSync(cached.path))
3221
3338
  return undefined;
3222
3339
  try {
3223
- const result = await safeSpawnAsync(cached.path, tool.checkArgs, {
3340
+ const result = await probeToolAsync(cached.path, tool.checkArgs, {
3224
3341
  timeout: getToolVerificationTimeout(tool),
3225
3342
  input: "",
3226
3343
  ignoreAmbientSignal: true,
@@ -3926,11 +4043,17 @@ async function installArchiveTool(tool) {
3926
4043
  * generic fallback `finishInstallAttempt` uses when nothing set it.
3927
4044
  */
3928
4045
  function recordPackageManagerInstallException(toolId, strategyLabel, packageName, err) {
3929
- const message = err.message;
4046
+ const message = boundInstallError(err.message);
3930
4047
  logSessionStart(`auto-install ${strategyLabel} ${packageName}: exception: ${message}`);
3931
4048
  installFailureReasons.set(toolId, message);
3932
4049
  return undefined;
3933
4050
  }
4051
+ const INSTALL_ERROR_LINE_LIMIT = 1000;
4052
+ const INSTALL_CANDIDATE_ERROR_LIMIT = 200;
4053
+ function boundInstallError(value, limit = INSTALL_ERROR_LINE_LIMIT) {
4054
+ const line = value.replace(/[\r\n]+/g, " ").trim();
4055
+ return line.length > limit ? `${line.slice(0, limit - 3)}...` : line;
4056
+ }
3934
4057
  async function installNpmTool(toolId, packageName, binaryName, verificationArgs = ["--version"], verificationTimeoutMs = 10_000,
3935
4058
  /** See {@link packageEntryVerification} — spawn-free verification (#2722). */
3936
4059
  packageEntryOf) {
@@ -4136,7 +4259,7 @@ options = {}) {
4136
4259
  ? [...verb, packageName]
4137
4260
  : ["-m", "pip", ...verb, packageName],
4138
4261
  }));
4139
- let lastError = "";
4262
+ const errors = [];
4140
4263
  for (const candidate of pipCandidates) {
4141
4264
  const pipResult = await safeSpawnAsync(candidate.command, candidate.args, {
4142
4265
  timeout: 120_000,
@@ -4226,10 +4349,11 @@ options = {}) {
4226
4349
  }
4227
4350
  return packageName;
4228
4351
  }
4229
- lastError = `${candidate.command} ${candidate.args.join(" ")}: ${outcome.error}`;
4230
- debugLog(`[pip-fallback] ${lastError}`);
4352
+ const candidateError = `${candidate.command} ${candidate.args.join(" ")}: ${boundInstallError(outcome.error, INSTALL_CANDIDATE_ERROR_LIMIT)}`;
4353
+ errors.push(candidateError);
4354
+ debugLog(`[pip-fallback] ${candidateError}`);
4231
4355
  }
4232
- throw new Error(`Failed to install ${packageName}: no usable pip command found (${lastError || "unknown error"})`);
4356
+ throw new Error(`Failed to install ${packageName}: no usable pip command found (${errors.join(" | ") || "unknown error"})`);
4233
4357
  }
4234
4358
  catch (err) {
4235
4359
  return recordPackageManagerInstallException(toolId, "pip", packageName, err);
@@ -4381,6 +4505,12 @@ export async function installTool(toolId) {
4381
4505
  case "github": {
4382
4506
  if (!tool.github)
4383
4507
  return false;
4508
+ if (!tool.github.assetMatch(process.platform, process.arch)) {
4509
+ const reason = `unsupported platform=${process.platform} arch=${process.arch}`;
4510
+ noteInstallAttempt(tool.id, "unavailable", reason);
4511
+ logSessionStart(`auto-install ${tool.id}: ${reason}`);
4512
+ return false;
4513
+ }
4384
4514
  const ghPath = await installGitHubTool(tool);
4385
4515
  return finishInstallAttempt(tool.id, ghPath !== undefined, startedAt);
4386
4516
  }
@@ -4393,6 +4523,12 @@ export async function installTool(toolId) {
4393
4523
  case "archive": {
4394
4524
  if (!tool.archive)
4395
4525
  return false;
4526
+ if (!resolveArchiveUrl(tool.archive)) {
4527
+ const reason = `unsupported platform=${process.platform} arch=${process.arch}`;
4528
+ noteInstallAttempt(tool.id, "unavailable", reason);
4529
+ logSessionStart(`auto-install ${tool.id}: ${reason}`);
4530
+ return false;
4531
+ }
4396
4532
  const archivePath = await installArchiveTool(tool);
4397
4533
  return finishInstallAttempt(tool.id, archivePath !== undefined, startedAt);
4398
4534
  }
@@ -4711,6 +4847,8 @@ export function getToolInstallStrategy(toolId) {
4711
4847
  * "at least one platform" guard instead.
4712
4848
  */
4713
4849
  export const GITHUB_TOOLS = [
4850
+ "cljfmt",
4851
+ "php-cs-fixer",
4714
4852
  "shellcheck",
4715
4853
  "shfmt",
4716
4854
  "rust-analyzer",
@@ -4745,6 +4883,12 @@ export function resolveGitHubAsset(toolId, platform, arch) {
4745
4883
  const tool = TOOLS.find((t) => t.id === toolId);
4746
4884
  return tool?.github?.assetMatch(platform, arch);
4747
4885
  }
4886
+ export function resolveGitHubAssetLauncher(toolId, _platform, assetName) {
4887
+ const tool = TOOLS.find((t) => t.id === toolId);
4888
+ return tool?.github
4889
+ ? launcherForGitHubAsset(tool.github, assetName)
4890
+ : undefined;
4891
+ }
4748
4892
  export function resolveGitHubInstalledBinaryName(toolId, platform, assetName) {
4749
4893
  const tool = TOOLS.find((t) => t.id === toolId);
4750
4894
  if (!tool)
@@ -346,7 +346,15 @@ export class JscpdClient {
346
346
  lines: c.lines ?? 0,
347
347
  tokens: c.tokens ?? 0,
348
348
  }));
349
- return { success: true, clones, duplicatedLines, totalLines, percentage };
349
+ // #2154: the one jscpd site that parsed a scan of this root.
350
+ return {
351
+ success: true,
352
+ analyzed: true,
353
+ clones,
354
+ duplicatedLines,
355
+ totalLines,
356
+ percentage,
357
+ };
350
358
  }
351
359
  catch (err) {
352
360
  void err;
@@ -323,7 +323,10 @@ export class KnipClient {
323
323
  completed?.projectSeq === options.projectSeq &&
324
324
  this.matchesMemoSignal(targetDir, completed.signal)) {
325
325
  this.log(`Analysis cache hit for ${key} at projectSeq ${options.projectSeq}`);
326
- return { ...completed.result, execution: "cache" };
326
+ // #2154: a memo hit did not scan the root during THIS call, so it
327
+ // carries no authority to retire a retained finding — the flag is
328
+ // cleared even though the memoised result set it when it ran.
329
+ return { ...completed.result, execution: "cache", analyzed: false };
327
330
  }
328
331
  // A project that ships its own knip needs no managed install and no
329
332
  // managed probe — the shim is already on disk, and gating on
@@ -510,7 +513,9 @@ export class KnipClient {
510
513
  // status === 0 with empty stdout has never been observed against a
511
514
  // real knip binary (a genuine clean run still prints
512
515
  // `{"issues":[]}`), but a defensive fallback is cheap insurance
513
- // against a future knip release that changes this.
516
+ // against a future knip release that changes this. Deliberately NOT
517
+ // `analyzed` (#2154): nothing was parsed, so this shape must not
518
+ // retire a retained knip finding — it is reported cold instead.
514
519
  return {
515
520
  ...EMPTY_RESULT,
516
521
  success: true,
@@ -711,6 +716,8 @@ export class KnipClient {
711
716
  }
712
717
  return {
713
718
  success: true,
719
+ // #2154: the one knip site that parsed a scan of this root.
720
+ analyzed: true,
714
721
  issues,
715
722
  unusedExports,
716
723
  unusedFiles,
@@ -30,7 +30,13 @@ const PROJECT_MARKERS_BY_KIND = {
30
30
  sql: [".sqlfluff", "pyproject.toml"],
31
31
  php: ["composer.json", "composer.lock"],
32
32
  prisma: ["schema.prisma", "prisma/schema.prisma"],
33
- java: ["pom.xml", "build.gradle", ".classpath"],
33
+ // #2870: `build.gradle.kts` is the Kotlin-DSL spelling of `build.gradle`
34
+ // and marks a Java module just as well — its absence here (and in
35
+ // ROOT_MARKERS_BY_KIND below) is what left a `.java` file in a
36
+ // Kotlin-DSL Gradle module resolving to the workspace root, where a
37
+ // polyglot repo's go.mod then claimed it. `kotlin` already lists both
38
+ // spellings; java listed only the Groovy one.
39
+ java: ["pom.xml", "build.gradle", "build.gradle.kts", ".classpath"],
34
40
  kotlin: ["build.gradle.kts", "build.gradle", "pom.xml"],
35
41
  swift: ["Package.swift"],
36
42
  dart: ["pubspec.yaml"],
@@ -76,7 +82,11 @@ const ROOT_MARKERS_BY_KIND = {
76
82
  sql: [".sqlfluff", "pyproject.toml", "setup.cfg", "tox.ini"],
77
83
  php: ["composer.json", "composer.lock"],
78
84
  prisma: ["prisma/schema.prisma", "schema.prisma"],
79
- java: ["pom.xml", "build.gradle", ".classpath"],
85
+ // #2870: see PROJECT_MARKERS_BY_KIND.java above — this is the table
86
+ // `resolveLanguageRootForFile` walks, so the missing Kotlin-DSL spelling
87
+ // is what anchored a nested Gradle module's `.java` file at the
88
+ // workspace root.
89
+ java: ["pom.xml", "build.gradle", "build.gradle.kts", ".classpath"],
80
90
  kotlin: ["build.gradle.kts", "build.gradle", "pom.xml"],
81
91
  swift: ["Package.swift"],
82
92
  dart: ["pubspec.yaml"],
@@ -4,6 +4,7 @@ import { getGlobalPiLensLogDir } from "./probe-home-state.js";
4
4
  import { createNdjsonLogger } from "./ndjson-logger.js";
5
5
  import { getMaxLogSizeMB } from "./log-cleanup.js";
6
6
  import { normalizeLoggedPath } from "./path-utils.js";
7
+ import { getTurnId } from "./turn-context.js";
7
8
  const LATENCY_LOG_DIR = getGlobalPiLensLogDir();
8
9
  const LATENCY_LOG_FILE = path.join(LATENCY_LOG_DIR, "latency.log");
9
10
  const writer = createNdjsonLogger({
@@ -697,6 +698,7 @@ export function logLatency(entry) {
697
698
  ...entry,
698
699
  ts,
699
700
  pid: process.pid,
701
+ turnId: entry.turnId ?? getTurnId(),
700
702
  filePath: normalizeLoggedPath(entry.filePath),
701
703
  });
702
704
  }
@@ -9,6 +9,7 @@ import { CANONICAL_GLOBAL_CONFIG_FILE, getPiLensGlobalConfigPath, GLOBAL_CONFIG_
9
9
  export { getPiLensGlobalConfigPath } from "./config-locations.js";
10
10
  import { ignoredRecordCollector, readConfigDocument, reportConfigReadFailure, reportPiLensConfigRecords, resolveOnePiLensConfigDocument, } from "./config-resolve.js";
11
11
  import { findNestedProjectMutationValue, } from "./project-lens-config.js";
12
+ import { readToolConfig } from "./tool-config.js";
12
13
  /**
13
14
  * The canonical global location, looked up rather than constructed, so a change
14
15
  * to the shared table cannot leave this loader describing a file that is no
@@ -123,6 +124,9 @@ export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath())
123
124
  const raw = resolved.value;
124
125
  const warnInvalid = note;
125
126
  const config = {};
127
+ const toolConfig = readToolConfig(raw, warnInvalid);
128
+ if (toolConfig)
129
+ config.tools = toolConfig;
126
130
  for (const spec of LENS_FLAGS) {
127
131
  if (spec.readGlobal)
128
132
  continue;
@@ -63,10 +63,11 @@ import { getGlobalPiLensDir } from "../file-utils.js";
63
63
  import { claimPhaseOncePerSession, currentSessionRecordId, logLatency, releaseOncePerSessionPhase, releasePhaseClaim, } from "../latency-logger.js";
64
64
  import { getPiLensGlobalConfigPath } from "../lens-config.js";
65
65
  import { normalizeFilePath } from "../path-utils.js";
66
+ import { resolveToolCwd } from "../tool-cwd.js";
66
67
  import { logSessionStart } from "../sessionstart-logger.js";
67
68
  import { launchLSP } from "./launch.js";
68
69
  import { registerSessionRoot, resetSessionRootsForTests, sessionRootConfigEntries, setSessionRootConfig, } from "./session-roots.js";
69
- import { createRootDetector, LSP_SERVERS, resetLSPCaseSensitivityState, } from "./server.js";
70
+ import { LSP_SERVERS, resetLSPCaseSensitivityState, resolveLspServerCwd, } from "./server.js";
70
71
  // --- Config Loading ---
71
72
  /**
72
73
  * For tests that need to force the warn-once cache to reset between cases —
@@ -338,9 +339,13 @@ export function createCustomServer(config, id) {
338
339
  name: config.name,
339
340
  custom: true,
340
341
  extensions: config.extensions,
342
+ ...(config.rootMarkers ? { rootMarkers: config.rootMarkers } : {}),
341
343
  root: config.rootMarkers
342
- ? createRootDetector(config.rootMarkers)
343
- : async () => process.cwd(),
344
+ ? async (file) => resolveToolCwd("lsp", id, file, {
345
+ cwd: process.cwd(),
346
+ ...(config.rootMarkers ? { rootMarkers: config.rootMarkers } : {}),
347
+ })
348
+ : async (file) => resolveToolCwd("lsp", id, file, { cwd: process.cwd() }),
344
349
  async spawn(root) {
345
350
  const proc = await launchLSP(config.command, config.args ?? ["--stdio"], {
346
351
  cwd: root,
@@ -600,12 +605,21 @@ export function getServersForFileWithConfig(filePath) {
600
605
  const base = path.basename(filePath).toLowerCase();
601
606
  return registeredServers(config).filter((server) => selectionReason(server, config, filePath, ext, base) === "selected");
602
607
  }
608
+ /**
609
+ * The primary language server ENTRY for a file — the one "first non-auxiliary
610
+ * server" predicate, shared by {@link primaryServerId} and
611
+ * {@link resolveLspCwdForFile} so the id-level and entry-level consumers
612
+ * cannot drift. `role` is only ever set to "auxiliary" on cross-cutting
613
+ * scanner entries (ast-grep, opengrep, zizmor, typos, marksman, ...) — see
614
+ * clients/lsp/server.ts; undefined here means a real language server.
615
+ */
616
+ function primaryServerEntry(filePath) {
617
+ return getServersForFileWithConfig(filePath).find((s) => s.role !== "auxiliary");
618
+ }
603
619
  /**
604
620
  * The primary language server for a file (e.g. "typescript"), as opposed to a
605
621
  * cross-cutting auxiliary scanner attached via clientScope "all"/
606
- * "with-auxiliary" (ast-grep, opengrep, zizmor, typos, marksman, ...). `role`
607
- * is only ever set to "auxiliary" on those auxiliary entries (see
608
- * clients/lsp/server.ts) — undefined means a real language server. Used to
622
+ * "with-auxiliary" (ast-grep, opengrep, zizmor, typos, marksman, ...). Used to
609
623
  * split a file's diagnostics into "primary confirmation" vs "auxiliary
610
624
  * findings" so a page of ast-grep/opengrep/marksman noise never buries
611
625
  * whether the actual type checker/compiler confirmed the file clean.
@@ -616,7 +630,22 @@ export function getServersForFileWithConfig(filePath) {
616
630
  * now report the same primary-vs-auxiliary split for the same file.
617
631
  */
618
632
  export function primaryServerId(filePath) {
619
- return getServersForFileWithConfig(filePath).find((s) => s.role !== "auxiliary")?.id;
633
+ return primaryServerEntry(filePath)?.id;
634
+ }
635
+ /**
636
+ * #2777 O1: the one seam a tool uses to answer "which cwd did this file's
637
+ * primary LSP server resolve to". Folds the three-step lookup callers used to
638
+ * hand-roll (`primaryServerId` + `getServersForFileWithConfig` +
639
+ * `resolveLspServerCwd`) into one call that returns `undefined` for a file
640
+ * with no primary LSP server, so the caller must handle the absent case
641
+ * instead of rendering it (the N1 `cwd=undefined` row becomes structurally
642
+ * impossible).
643
+ */
644
+ export async function resolveLspCwdForFile(filePath, sessionCwd) {
645
+ const primary = primaryServerEntry(filePath);
646
+ if (!primary)
647
+ return undefined;
648
+ return resolveLspServerCwd(primary, filePath, sessionCwd);
620
649
  }
621
650
  /**
622
651
  * Look up an initializationOptions override for a built-in server.
@@ -73,6 +73,27 @@ export function touchCoverageGap(result) {
73
73
  export function touchCompletedConfirmationPolicy(result) {
74
74
  return result?.confirmation !== undefined;
75
75
  }
76
+ /**
77
+ * #2878: one publication-evidence predicate for auxiliary coverage. A
78
+ * content-hash binding proves that the stored publication describes these
79
+ * bytes. For a version-less publisher, an advanced per-path publication stamp
80
+ * proves that the auxiliary published after this touch's pre-notify baseline;
81
+ * the stamp alone cannot prove that the bytes matched. The pre-notify boolean
82
+ * passed as `bindingMatchesContent` preserves a binding that the notify
83
+ * cleared, while the live stamp covers a publication that landed after the
84
+ * wait began. This predicate is the union. Four callers use it; only the
85
+ * demoted row's `publishedThisContent` takes the union — the sibling and
86
+ * aggregate rows compute that field from `auxCoversThisContent` directly
87
+ * (#2914), while their `outcome` field still reads the stamp through this
88
+ * predicate. Absent evidence fails closed.
89
+ */
90
+ export function auxiliaryPublicationEvidence({ bindingMatchesContent, baseline, currentPathVersion, raced = true, }) {
91
+ return (bindingMatchesContent ||
92
+ (raced &&
93
+ Number.isFinite(baseline) &&
94
+ currentPathVersion !== undefined &&
95
+ currentPathVersion > baseline));
96
+ }
76
97
  /**
77
98
  * #1493: the single policy deciding which auxiliaries a touch carries NO
78
99
  * evidence from. Both no-answer shapes belong here, because they are the same
@@ -106,7 +127,9 @@ export function touchCompletedConfirmationPolicy(result) {
106
127
  * irrelevant once a verified publication for them exists. Exempting `silent` but
107
128
  * not `cut_off` on identical evidence would report the same coverage two ways
108
129
  * depending on which timer happened to win. This stays fail-closed — it
109
- * un-narrows only against a content-hash match, never against a timer.
130
+ * un-narrows only against publication evidence (content-hash match, or an
131
+ * advanced per-path publication stamp for version-less publishers), never
132
+ * against a timer.
110
133
  */
111
134
  export function auxiliaryCoverageGap(evidence) {
112
135
  return evidence