@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
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { getProjectDataDir } from "../../file-utils.js";
4
4
  import { safeSpawnAsync } from "../../safe-spawn.js";
5
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
5
6
  import { PRIORITY } from "../priorities.js";
6
7
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
7
8
  // Per-cwd cached `--version` probes (#120). Before this, each dispatch
@@ -141,7 +142,7 @@ const elixirCheckRunner = {
141
142
  priority: PRIORITY.GENERAL_ANALYSIS,
142
143
  skipTestFiles: false,
143
144
  async run(ctx) {
144
- const cwd = ctx.cwd || process.cwd();
145
+ const cwd = resolveRunnerCwd(ctx, "elixir-check");
145
146
  const absPath = path.resolve(cwd, ctx.filePath);
146
147
  let command;
147
148
  let args = [];
@@ -7,6 +7,7 @@
7
7
  * Gate: skips when no ESLint config is detected (project uses Biome/OxLint instead).
8
8
  */
9
9
  import { safeSpawnAsync } from "../../safe-spawn.js";
10
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
10
11
  import { getAutofixCapability, hasEslintConfig } from "../../tool-policy.js";
11
12
  import { PRIORITY } from "../priorities.js";
12
13
  import { createCwdCachedProbe, resolveToolCommand, } from "./utils/runner-helpers.js";
@@ -79,7 +80,7 @@ const eslintRunner = {
79
80
  appliesTo: ["jsts"],
80
81
  priority: PRIORITY.LINT_SECONDARY,
81
82
  async run(ctx) {
82
- const cwd = ctx.cwd || process.cwd();
83
+ const cwd = resolveRunnerCwd(ctx, "eslint");
83
84
  const userHasConfig = hasEslintConfig(cwd);
84
85
  // Only run if project has an ESLint config.
85
86
  if (!userHasConfig) {
@@ -1,4 +1,5 @@
1
1
  import { safeSpawnAsync } from "../../safe-spawn.js";
2
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
2
3
  import { PRIORITY } from "../priorities.js";
3
4
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
4
5
  // fish_indent ships with fish — not separately installable, no managed fallback
@@ -9,7 +10,7 @@ const fishIndentRunner = {
9
10
  priority: PRIORITY.FORMAT_AND_LINT_PRIMARY,
10
11
  skipTestFiles: false,
11
12
  async run(ctx) {
12
- const cwd = ctx.cwd || process.cwd();
13
+ const cwd = resolveRunnerCwd(ctx, "fish-indent");
13
14
  const available = await fishIndent.isAvailableAsync(cwd);
14
15
  if (!available)
15
16
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -1,4 +1,5 @@
1
1
  import { safeSpawnAsync } from "../../safe-spawn.js";
2
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
2
3
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
3
4
  import { PRIORITY } from "../priorities.js";
4
5
  const gleam = createAvailabilityChecker("gleam", ".exe");
@@ -41,7 +42,7 @@ const gleamCheckRunner = {
41
42
  priority: PRIORITY.GENERAL_ANALYSIS,
42
43
  skipTestFiles: false,
43
44
  async run(ctx) {
44
- const cwd = ctx.cwd || process.cwd();
45
+ const cwd = resolveRunnerCwd(ctx, "gleam-check");
45
46
  if (!(await gleam.isAvailableAsync(cwd))) {
46
47
  return { status: "skipped", diagnostics: [], semantic: "none" };
47
48
  }
@@ -6,6 +6,7 @@
6
6
  import { relative, resolve, sep, posix } from "node:path";
7
7
  import { goClient } from "../../go-client.js";
8
8
  import { safeSpawnAsync } from "../../safe-spawn.js";
9
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
9
10
  import { stripAnsi } from "../../sanitize.js";
10
11
  import { skipUnlessToolRan } from "./utils/tool-failure.js";
11
12
  import { parseGoVetOutput } from "./utils/diagnostic-parsers.js";
@@ -29,7 +30,7 @@ const goVetRunner = {
29
30
  // `go.mod file not found` (#263). ctx.cwd is the go.mod module root
30
31
  // (resolveLanguageRootForFile, markers ["go.mod"]), so vet the file's
31
32
  // package from there.
32
- const cwd = ctx.cwd || process.cwd();
33
+ const cwd = resolveRunnerCwd(ctx, "go-vet");
33
34
  const fileRel = relative(cwd, ctx.filePath).split(sep).join(posix.sep);
34
35
  const pkgPath = fileRel.startsWith("../")
35
36
  ? // File isn't under ctx.cwd (unexpected — ctx.cwd is the go.mod root).
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import * as path from "node:path";
13
13
  import { safeSpawnAsync } from "../../safe-spawn.js";
14
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
14
15
  import { getLinterPolicyForCwd, hasGolangciConfig } from "../../tool-policy.js";
15
16
  import { PRIORITY } from "../priorities.js";
16
17
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
@@ -80,7 +81,7 @@ const golangciRunner = {
80
81
  priority: PRIORITY.GENERAL_ANALYSIS,
81
82
  timeoutMs: 90_000,
82
83
  async run(ctx) {
83
- const cwd = ctx.cwd || process.cwd();
84
+ const cwd = resolveRunnerCwd(ctx, "golangci-lint");
84
85
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
85
86
  if (policy && !policy.preferredRunners.includes("golangci-lint")) {
86
87
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -1,5 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
3
4
  import { getLinterPolicyForCwd } from "../../tool-policy.js";
4
5
  import { PRIORITY } from "../priorities.js";
5
6
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
@@ -37,7 +38,7 @@ const hadolintRunner = {
37
38
  priority: PRIORITY.GENERAL_ANALYSIS,
38
39
  skipTestFiles: false,
39
40
  async run(ctx) {
40
- const cwd = ctx.cwd || process.cwd();
41
+ const cwd = resolveRunnerCwd(ctx, "hadolint");
41
42
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
42
43
  if (policy && !policy.preferredRunners.includes("hadolint")) {
43
44
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -3,6 +3,7 @@ import { PathKeyedMap } from "../../path-keyed-map.js";
3
3
  import { normalizeMapKey } from "../../path-utils.js";
4
4
  import { safeSpawnAsync } from "../../safe-spawn.js";
5
5
  import { truncatedByOutputCap } from "../../spawn-output-cap.js";
6
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
6
7
  import { findNearestDirWithMarker } from "../../workspace-topology.js";
7
8
  import { PRIORITY } from "../priorities.js";
8
9
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
@@ -147,6 +148,7 @@ const helmLintRunner = {
147
148
  skipTestFiles: false,
148
149
  timeoutMs: 35_000,
149
150
  async run(ctx) {
151
+ const cwd = resolveRunnerCwd(ctx, "helm");
150
152
  const workspaceRoot = path.resolve(ctx.projectRoot ?? ctx.cwd);
151
153
  const startDir = path.dirname(path.resolve(ctx.filePath));
152
154
  const discovered = findNearestDirWithMarker(startDir, "chartYamlPath");
@@ -158,7 +160,7 @@ const helmLintRunner = {
158
160
  const existing = inFlightByChartRoot.get(chartRoot);
159
161
  if (existing)
160
162
  return existing;
161
- const promise = lintChart(chartRoot, ctx.cwd).finally(() => {
163
+ const promise = lintChart(chartRoot, cwd).finally(() => {
162
164
  if (inFlightByChartRoot.get(chartRoot) === promise)
163
165
  inFlightByChartRoot.delete(chartRoot);
164
166
  });
@@ -78,6 +78,7 @@ import { normalizeMapKey } from "../../path-utils.js";
78
78
  import { loadPiLensProjectConfig } from "../../project-lens-config.js";
79
79
  import { getProjectTrustState, projectTrustDenialReason, } from "../../project-trust.js";
80
80
  import { safeSpawnAsync } from "../../safe-spawn.js";
81
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
81
82
  import { killedForOutputCap, truncatedByOutputCap, } from "../../spawn-output-cap.js";
82
83
  import { isTrivyEnabled, resolveSeverityFloor } from "../../trivy-client.js";
83
84
  import { findNearestDirWithMarker } from "../../workspace-topology.js";
@@ -896,7 +897,7 @@ const helmRenderRunner = {
896
897
  skipTestFiles: false,
897
898
  timeoutMs: RENDER_TIMEOUT_MS + TRIVY_TIMEOUT_MS + 10_000,
898
899
  async run(ctx) {
899
- const cwd = ctx.cwd || process.cwd();
900
+ const cwd = resolveRunnerCwd(ctx, "helm-render");
900
901
  const workspaceRoot = path.resolve(ctx.projectRoot ?? cwd);
901
902
  // Gate 1 — consent, read from the project whose chart would run. Keying
902
903
  // this on `cwd` let an opt-in in one directory authorize a DIFFERENT
@@ -1,5 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
3
4
  import { getLinterPolicyForCwd } from "../../tool-policy.js";
4
5
  import { PRIORITY } from "../priorities.js";
5
6
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
@@ -49,7 +50,7 @@ const htmlhintRunner = {
49
50
  priority: PRIORITY.GENERAL_ANALYSIS,
50
51
  skipTestFiles: false,
51
52
  async run(ctx) {
52
- const cwd = ctx.cwd || process.cwd();
53
+ const cwd = resolveRunnerCwd(ctx, "htmlhint");
53
54
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
54
55
  if (policy && !policy.preferredRunners.includes("htmlhint")) {
55
56
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -1,5 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
3
4
  import { hasJavaBuildDescriptor } from "../../tool-policy.js";
4
5
  import { PRIORITY } from "../priorities.js";
5
6
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
@@ -41,7 +42,7 @@ const javacRunner = {
41
42
  priority: PRIORITY.GENERAL_ANALYSIS,
42
43
  skipTestFiles: false,
43
44
  async run(ctx) {
44
- const cwd = ctx.cwd || process.cwd();
45
+ const cwd = resolveRunnerCwd(ctx, "javac");
45
46
  const absPath = path.resolve(cwd, ctx.filePath);
46
47
  // Inside a Maven/Gradle project a classpath-less single-file compile
47
48
  // cannot produce a valid verdict: every non-JDK import becomes a
@@ -1,5 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
3
4
  import { getAutofixCapability, getLinterPolicyForCwd, } from "../../tool-policy.js";
4
5
  import { PRIORITY } from "../priorities.js";
5
6
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
@@ -58,7 +59,7 @@ const ktlintRunner = {
58
59
  priority: PRIORITY.FORMAT_AND_LINT_PRIMARY,
59
60
  skipTestFiles: false,
60
61
  async run(ctx) {
61
- const cwd = ctx.cwd || process.cwd();
62
+ const cwd = resolveRunnerCwd(ctx, "ktlint");
62
63
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
63
64
  if (policy && !policy.preferredRunners.includes("ktlint")) {
64
65
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -126,6 +126,7 @@ const lspRunner = {
126
126
  // so this is tracked separately: the only claim it invalidates is "0
127
127
  // diagnostics means clean".
128
128
  let unconfirmedServerIds = [];
129
+ let deferredServerIds = [];
129
130
  let usedWarmAttach = false;
130
131
  let failureReason = "";
131
132
  const content = readFileContent(ctx.filePath);
@@ -180,6 +181,7 @@ const lspRunner = {
180
181
  lspDiags = touched.diags;
181
182
  diagnosticsInconclusive = touched.inconclusive === true;
182
183
  unconfirmedServerIds = touchCoverageGap(touched);
184
+ deferredServerIds = touched.deferredServerIds ?? [];
183
185
  }
184
186
  }
185
187
  catch (err) {
@@ -237,6 +239,15 @@ const lspRunner = {
237
239
  return { status: "skipped", diagnostics: [], semantic: "none" };
238
240
  }
239
241
  if (lspDiags.length === 0) {
242
+ if (deferredServerIds.length > 0) {
243
+ return {
244
+ status: "deferred",
245
+ diagnostics: [],
246
+ semantic: "none",
247
+ unconfirmedServerIds: [...unconfirmedServerIds],
248
+ deferredServerIds: [...deferredServerIds],
249
+ };
250
+ }
240
251
  if (unconfirmedServerIds.length > 0) {
241
252
  // #1470/#1493: an auxiliary never reported — cut off by the aux grace
242
253
  // timer, or silent with nothing published for this content — so this
@@ -345,7 +356,11 @@ const lspRunner = {
345
356
  ? "warning"
346
357
  : "none";
347
358
  return {
348
- status: hasErrors ? "failed" : "succeeded",
359
+ status: hasErrors
360
+ ? "failed"
361
+ : deferredServerIds.length > 0
362
+ ? "deferred"
363
+ : "succeeded",
349
364
  // "failed" here means the file has blocking type errors — the check ran
350
365
  // fine. Tag it so the smell analyzer doesn't read it as a runner crash.
351
366
  failureKind: hasErrors ? "blocking_diagnostics" : undefined,
@@ -354,6 +369,9 @@ const lspRunner = {
354
369
  ...(unconfirmedServerIds.length > 0 && {
355
370
  unconfirmedServerIds: [...unconfirmedServerIds],
356
371
  }),
372
+ ...(deferredServerIds.length > 0 && {
373
+ deferredServerIds: [...deferredServerIds],
374
+ }),
357
375
  };
358
376
  },
359
377
  };
@@ -1,4 +1,5 @@
1
1
  import { safeSpawnAsync } from "../../safe-spawn.js";
2
+ import { logRunnerAdvisoryOnce, resolveRunnerCwd } from "../../tool-cwd.js";
2
3
  import { getLinterPolicyForCwd, markdownlintConfigArgs, } from "../../tool-policy.js";
3
4
  import { PRIORITY } from "../priorities.js";
4
5
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
@@ -96,7 +97,7 @@ const markdownlintRunner = {
96
97
  priority: PRIORITY.DOC_QUALITY,
97
98
  skipTestFiles: false,
98
99
  async run(ctx) {
99
- const cwd = ctx.cwd || process.cwd();
100
+ const cwd = resolveRunnerCwd(ctx, "markdownlint");
100
101
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
101
102
  if (policy && !policy.preferredRunners.includes("markdownlint")) {
102
103
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -114,7 +115,7 @@ const markdownlintRunner = {
114
115
  // its budget in another lane (availability verify, autofix --fix). Skip
115
116
  // without spawning — "not checked", never re-reported as clean.
116
117
  if (isInSpawnTimeoutCooldown(cmd)) {
117
- ctx.log(`markdownlint: ${cmd} is cooling down after a spawn timeout — skipping (one bounded failure budget per edit)`);
118
+ logRunnerAdvisoryOnce(ctx, "markdownlint", cwd, `markdownlint: ${cmd} is cooling down after a spawn timeout — skipping (one bounded failure budget per edit)`);
118
119
  return { status: "skipped", diagnostics: [], semantic: "none" };
119
120
  }
120
121
  // Shared config-args seam (#1247): the autofix path consumes the same
@@ -1,5 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
3
4
  import { hasMypyConfig } from "../../tool-policy.js";
4
5
  import { PRIORITY } from "../priorities.js";
5
6
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
@@ -61,7 +62,7 @@ const mypyRunner = {
61
62
  priority: PRIORITY.GENERAL_ANALYSIS,
62
63
  skipTestFiles: false,
63
64
  async run(ctx) {
64
- const cwd = ctx.cwd || process.cwd();
65
+ const cwd = resolveRunnerCwd(ctx, "mypy");
65
66
  // Only run if mypy config exists — avoids false positives in untyped projects
66
67
  if (!hasMypyConfig(cwd)) {
67
68
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { findLocalBinUpwards } from "../../package-manager.js";
10
10
  import { safeSpawnAsync } from "../../safe-spawn.js";
11
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
11
12
  import { truncatedByOutputCap } from "../../spawn-output-cap.js";
12
13
  import { getJstsLintPolicyForCwd, hasVitePlusConfig, } from "../../tool-policy.js";
13
14
  import { PRIORITY } from "../priorities.js";
@@ -58,7 +59,7 @@ const oxlintRunner = {
58
59
  priority: PRIORITY.LINT_SECONDARY,
59
60
  skipTestFiles: false,
60
61
  async run(ctx) {
61
- const cwd = ctx.cwd || process.cwd();
62
+ const cwd = resolveRunnerCwd(ctx, "oxlint");
62
63
  const policy = getJstsLintPolicyForCwd(cwd);
63
64
  if (!policy.preferredRunners.includes("oxlint")) {
64
65
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -1,5 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
3
4
  import { PRIORITY } from "../priorities.js";
4
5
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
5
6
  const php = createAvailabilityChecker("php", ".exe");
@@ -31,7 +32,7 @@ const phpLintRunner = {
31
32
  priority: PRIORITY.GENERAL_ANALYSIS,
32
33
  skipTestFiles: false,
33
34
  async run(ctx) {
34
- const cwd = ctx.cwd || process.cwd();
35
+ const cwd = resolveRunnerCwd(ctx, "php-lint");
35
36
  if (!(await php.isAvailableAsync(cwd))) {
36
37
  return { status: "skipped", diagnostics: [], semantic: "none" };
37
38
  }
@@ -1,5 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
3
4
  import { getLinterPolicyForCwd, hasPhpstanConfig } from "../../tool-policy.js";
4
5
  import { PRIORITY } from "../priorities.js";
5
6
  import { createAvailabilityChecker, resolveVendorToolCommand, } from "./utils/runner-helpers.js";
@@ -84,7 +85,7 @@ const phpstanRunner = {
84
85
  priority: PRIORITY.GENERAL_ANALYSIS,
85
86
  skipTestFiles: false,
86
87
  async run(ctx) {
87
- const cwd = ctx.cwd || process.cwd();
88
+ const cwd = resolveRunnerCwd(ctx, "phpstan");
88
89
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
89
90
  if (policy && !policy.preferredRunners.includes("phpstan")) {
90
91
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -1,5 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
3
4
  import { PRIORITY } from "../priorities.js";
4
5
  import { resolveLocalFirstAsync } from "./utils/runner-helpers.js";
5
6
  function parsePrismaValidateOutput(raw, filePath) {
@@ -37,7 +38,7 @@ const prismaValidateRunner = {
37
38
  priority: PRIORITY.GENERAL_ANALYSIS,
38
39
  skipTestFiles: false,
39
40
  async run(ctx) {
40
- const cwd = ctx.cwd || process.cwd();
41
+ const cwd = resolveRunnerCwd(ctx, "prisma-validate");
41
42
  const resolved = await resolveLocalFirstAsync("prisma", cwd);
42
43
  const absPath = path.resolve(cwd, ctx.filePath);
43
44
  const result = await safeSpawnAsync(resolved.cmd, [...resolved.args, "validate", "--schema", absPath], { timeout: 20000, cwd });
@@ -3,6 +3,8 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { incrementDegradationCount } from "../../degradation-ledger.js";
5
5
  import { safeSpawnAsync } from "../../safe-spawn.js";
6
+ import { probeToolAsync } from "../../tool-probe.js";
7
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
6
8
  import { PRIORITY } from "../priorities.js";
7
9
  import { classifyProbeFailure, createAvailabilityLatch, isLatchingOutcome, logAvailabilityDecision, startHostStallSampler, } from "./utils/availability-policy.js";
8
10
  // The PS script written to a temp file each run. `-File` with a temp script
@@ -25,8 +27,13 @@ const PS_TIMEOUT_MS = 30000;
25
27
  * available". `safeSpawnAsync` carries the standard taxonomy — `failure`,
26
28
  * `spawnFailure.kind` and the errno on `error` — which is what lets the
27
29
  * availability policy tell a stall from an absence.
30
+ *
31
+ * The two presence probes that used to share this wrapper now go through
32
+ * `probeToolAsync` (#2894), so the only spawn left here is the ANALYSIS pass
33
+ * — which always has a project to resolve `PSScriptAnalyzerSettings.psd1`
34
+ * against. `cwd` is required to keep it that way.
28
35
  */
29
- function spawnPs(cmd, args, options = {}) {
36
+ function spawnPs(cmd, args, options) {
30
37
  const { timeoutMs = PS_TIMEOUT_MS, cwd } = options;
31
38
  return safeSpawnAsync(cmd, args, {
32
39
  cwd,
@@ -126,13 +133,7 @@ async function resolvePowerShellCmd() {
126
133
  for (const candidate of ["pwsh", "powershell"]) {
127
134
  const sampler = startHostStallSampler();
128
135
  const startedAt = Date.now();
129
- // cwd-exempt: global interpreter-presence probe ("is pwsh/powershell on PATH at all"), not tied to any project
130
- const result = await spawnPs(candidate, [
131
- "-NoProfile",
132
- "-NonInteractive",
133
- "-Command",
134
- "exit 0",
135
- ]);
136
+ const result = await probeToolAsync(candidate, ["-NoProfile", "-NonInteractive", "-Command", "exit 0"], { timeout: PS_TIMEOUT_MS, resourceLabel: "psscriptanalyzer" });
136
137
  const hostStallMs = sampler.stop();
137
138
  const elapsedMs = Date.now() - startedAt;
138
139
  elapsedTotalMs += elapsedMs;
@@ -178,13 +179,12 @@ async function checkModuleAvailable(cmd) {
178
179
  return memo;
179
180
  const sampler = startHostStallSampler();
180
181
  const startedAt = Date.now();
181
- // cwd-exempt: global module-presence probe -- PSScriptAnalyzer resolves from PowerShell's module search path, not a per-project install
182
- const result = await spawnPs(cmd, [
182
+ const result = await probeToolAsync(cmd, [
183
183
  "-NoProfile",
184
184
  "-NonInteractive",
185
185
  "-Command",
186
186
  "if (Get-Module -ListAvailable PSScriptAnalyzer) { exit 0 } else { exit 1 }",
187
- ]);
187
+ ], { timeout: PS_TIMEOUT_MS, resourceLabel: "psscriptanalyzer" });
188
188
  const hostStallMs = sampler.stop();
189
189
  const elapsedMs = Date.now() - startedAt;
190
190
  if (!result.error && result.status === 0) {
@@ -292,7 +292,7 @@ const psScriptAnalyzerRunner = {
292
292
  if (execLatch.read() === false) {
293
293
  return { status: "skipped", diagnostics: [], semantic: "none" };
294
294
  }
295
- const cwd = ctx.cwd || process.cwd();
295
+ const cwd = resolveRunnerCwd(ctx, "psscriptanalyzer");
296
296
  const absPath = path.resolve(cwd, ctx.filePath);
297
297
  // Write script to temp file so we avoid cmd.exe quoting entirely
298
298
  const tmpScript = path.join(os.tmpdir(), `pi-lens-psa-${process.pid}.ps1`);
@@ -10,6 +10,7 @@ import { logExtension } from "../../extension-log.js";
10
10
  import { getLSPService } from "../../lsp/index.js";
11
11
  import { augmentPythonEnvironment, detectPythonEnvironment, } from "../../python-environment.js";
12
12
  import { safeSpawnAsync } from "../../safe-spawn.js";
13
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
13
14
  import { PRIORITY } from "../priorities.js";
14
15
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
15
16
  import { finishParsedRun } from "./utils/tool-failure.js";
@@ -28,7 +29,7 @@ const pyrightRunner = {
28
29
  const lspService = getLSPService();
29
30
  await lspService.getClientForFile(ctx.filePath);
30
31
  }
31
- const cwd = ctx.cwd || process.cwd();
32
+ const cwd = resolveRunnerCwd(ctx, "pyright");
32
33
  // Get pyright command - try multiple strategies
33
34
  let cmd = null;
34
35
  // Strategy 1: Check cached availability (fast path)
@@ -8,6 +8,7 @@
8
8
  * Supports bundle exec (preferred in Bundler projects).
9
9
  */
10
10
  import { safeSpawnAsync } from "../../safe-spawn.js";
11
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
11
12
  import { getAutofixCapability, getLinterPolicyForCwd, getRubocopCommand, } from "../../tool-policy.js";
12
13
  import { PRIORITY } from "../priorities.js";
13
14
  import { resolveCommandArgsWithInstallFallback } from "./utils/runner-helpers.js";
@@ -55,7 +56,7 @@ const rubocopRunner = {
55
56
  appliesTo: ["ruby"],
56
57
  priority: PRIORITY.FORMAT_AND_LINT_PRIMARY,
57
58
  async run(ctx) {
58
- const cwd = ctx.cwd || process.cwd();
59
+ const cwd = resolveRunnerCwd(ctx, "rubocop");
59
60
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
60
61
  if (policy && !policy.preferredRunners.includes("rubocop")) {
61
62
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -11,7 +11,7 @@ import { stripAnsi } from "../../sanitize.js";
11
11
  import { getAutofixCapability, getLinterPolicyForCwd, ruffConfigArgs, } from "../../tool-policy.js";
12
12
  import { PRIORITY } from "../priorities.js";
13
13
  import { parseRuffOutput } from "./utils/diagnostic-parsers.js";
14
- import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
14
+ import { createAvailabilityChecker, resolveAvailableOrInstall, resolveRunnerCwd, } from "./utils/runner-helpers.js";
15
15
  const ruff = createAvailabilityChecker("ruff", ".exe");
16
16
  function parseRuffJson(raw, filePath) {
17
17
  try {
@@ -50,7 +50,7 @@ const ruffRunner = {
50
50
  appliesTo: ["python"],
51
51
  priority: PRIORITY.FORMAT_AND_LINT_PRIMARY,
52
52
  async run(ctx) {
53
- const cwd = ctx.cwd || process.cwd();
53
+ const cwd = resolveRunnerCwd(ctx, "ruff");
54
54
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
55
55
  if (policy && !policy.preferredRunners.includes("ruff-lint")) {
56
56
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -3,14 +3,15 @@
3
3
  *
4
4
  * Runs `cargo clippy` for Rust files to catch common mistakes.
5
5
  */
6
- import { dirname, isAbsolute, join, resolve } from "node:path";
7
- import { findNearestContaining } from "../../path-utils.js";
6
+ import { existsSync } from "node:fs";
7
+ import { isAbsolute, join, resolve } from "node:path";
8
8
  import { rustClient } from "../../rust-client.js";
9
9
  import { safeSpawnAsync } from "../../safe-spawn.js";
10
10
  import { stripAnsi } from "../../sanitize.js";
11
11
  import { getLazyInstallAttempt, tryLazyInstall, } from "./utils/lazy-installer.js";
12
12
  import { describeInstallAttempt, logAvailabilityDecision, } from "./utils/availability-policy.js";
13
13
  import { PRIORITY } from "../priorities.js";
14
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
14
15
  import { createCwdCachedProbe } from "./utils/runner-helpers.js";
15
16
  // Cached per-cwd `cargo clippy --version` probe (#120). Before this, the
16
17
  // probe fired on every Rust file save in a project where clippy was already
@@ -89,15 +90,20 @@ const rustClippyRunner = {
89
90
  return { status: "skipped", diagnostics: [], semantic: "none" };
90
91
  }
91
92
  }
92
- // Find the package root (where Cargo.toml is)
93
- const cargoToml = findCargoToml(ctx.filePath);
94
- if (!cargoToml) {
93
+ // The package root (where Cargo.toml is), through the shared cwd seam
94
+ // (#2894): `RUNNER_MARKERS["rust-clippy"]` is `["Cargo.toml"]`, so this
95
+ // IS the walk this runner used to do with `findNearestContaining` — plus
96
+ // the seam's dispatch-root and `$HOME` ceilings, which the hand-rolled
97
+ // walk had neither of. The gate then asks about exactly the directory
98
+ // cargo will run in, rather than about a separately-derived one.
99
+ const cargoDir = resolveRunnerCwd(ctx, "rust-clippy");
100
+ if (!existsSync(join(cargoDir, "Cargo.toml"))) {
95
101
  return { status: "skipped", diagnostics: [], semantic: "none" };
96
102
  }
97
103
  // Run cargo clippy on the package
98
104
  const result = await safeSpawnAsync(cargoExe, ["clippy", "--message-format=json", "-q"], {
99
105
  timeout: 60000,
100
- cwd: cargoToml.replace("Cargo.toml", ""),
106
+ cwd: cargoDir,
101
107
  });
102
108
  const raw = stripAnsi(result.stdout + result.stderr);
103
109
  if (result.status === 0 && !raw.trim()) {
@@ -105,7 +111,6 @@ const rustClippyRunner = {
105
111
  }
106
112
  // Parse JSON output. span.file is relative to the package root, so pass
107
113
  // the cargo dir to resolve diagnostics to absolute paths for filtering.
108
- const cargoDir = cargoToml.replace("Cargo.toml", "");
109
114
  const allDiagnostics = parseClippyOutput(raw, ctx.filePath, cargoDir);
110
115
  if (allDiagnostics.length === 0) {
111
116
  // Non-parseable output
@@ -134,10 +139,6 @@ const rustClippyRunner = {
134
139
  };
135
140
  },
136
141
  };
137
- function findCargoToml(filePath) {
138
- const dir = findNearestContaining(dirname(filePath), ["Cargo.toml"]);
139
- return dir ? join(dir, "Cargo.toml") : undefined;
140
- }
141
142
  /**
142
143
  * #1802 fix round: the original review claimed rustc/clippy's top-level
143
144
  * `compiler-message.level` is genuinely two-valued (error/warning). A live
@@ -22,6 +22,7 @@
22
22
  import * as fs from "node:fs";
23
23
  import * as path from "node:path";
24
24
  import { safeSpawnAsync } from "../../safe-spawn.js";
25
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
25
26
  import { PRIORITY } from "../priorities.js";
26
27
  import { createAvailabilityChecker, lspPrimaryCoversFile, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
27
28
  import { finishParsedRun } from "./utils/tool-failure.js";
@@ -110,7 +111,7 @@ const shellcheckRunner = {
110
111
  priority: PRIORITY.GENERAL_ANALYSIS,
111
112
  skipTestFiles: false, // Shell scripts in test directories should still be checked
112
113
  async run(ctx) {
113
- const cwd = ctx.cwd || process.cwd();
114
+ const cwd = resolveRunnerCwd(ctx, "shellcheck");
114
115
  // #233: bash-language-server runs shellcheck internally. When the `bash` LSP
115
116
  // covers this file AND shellcheck is on PATH (so the LSP actually emits its
116
117
  // findings), the warm server already produces these diagnostics — skip the
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
4
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
4
5
  import { PRIORITY } from "../priorities.js";
5
6
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
6
7
  const shfmt = createAvailabilityChecker("shfmt", ".exe");
@@ -31,7 +32,7 @@ const shfmtRunner = {
31
32
  priority: PRIORITY.FORMAT_AND_LINT_PRIMARY,
32
33
  skipTestFiles: false,
33
34
  async run(ctx) {
34
- const cwd = ctx.cwd || process.cwd();
35
+ const cwd = resolveRunnerCwd(ctx, "shfmt");
35
36
  let cmd = null;
36
37
  if (await shfmt.isAvailableAsync(cwd)) {
37
38
  cmd = shfmt.getCommand(cwd);
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import { safeSpawnAsync } from "../../safe-spawn.js";
22
22
  import { PRIORITY } from "../priorities.js";
23
- import { createAvailabilityChecker } from "./utils/runner-helpers.js";
23
+ import { createAvailabilityChecker, resolveRunnerCwd, } from "./utils/runner-helpers.js";
24
24
  import { parseToolRun } from "./utils/tool-failure.js";
25
25
  const typos = createAvailabilityChecker("typos", ".exe");
26
26
  // typos-cli exit codes: 0 = no typos, 2 = typos found, 1 = an error that
@@ -82,7 +82,7 @@ const spellcheckRunner = {
82
82
  priority: PRIORITY.DOC_QUALITY,
83
83
  skipTestFiles: false, // Check docs in test files too
84
84
  async run(ctx) {
85
- const cwd = ctx.cwd || process.cwd();
85
+ const cwd = resolveRunnerCwd(ctx, "spellcheck/typos");
86
86
  // Skip if typos-cli is not installed
87
87
  if (!(await typos.isAvailableAsync(cwd))) {
88
88
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -3,6 +3,7 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { BoundedLruCache } from "../../bounded-cache.js";
5
5
  import { safeSpawnAsync } from "../../safe-spawn.js";
6
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
6
7
  import { findCompiledClassesDir } from "../../tool-policy.js";
7
8
  import { PRIORITY } from "../priorities.js";
8
9
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
@@ -191,7 +192,7 @@ const spotbugsRunner = {
191
192
  appliesTo: ["java", "kotlin"],
192
193
  priority: PRIORITY.DEEP_LANGUAGE_ANALYSIS,
193
194
  async run(ctx) {
194
- const cwd = ctx.cwd || process.cwd();
195
+ const cwd = resolveRunnerCwd(ctx, "spotbugs");
195
196
  const classesDir = findCompiledClassesDir(cwd);
196
197
  if (!classesDir) {
197
198
  ctx.log?.("spotbugs: Java project detected but no compiled .class files found — run `mvn compile` / `gradle build` first");