@herbertgao/pi-extensions 2026.9.2 → 2026.9.3

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 (221) hide show
  1. package/README.md +3 -2
  2. package/THIRD_PARTY_NOTICES.md +24 -0
  3. package/node_modules/@herbertgao/sol-pi/LICENSE +19 -0
  4. package/node_modules/@herbertgao/sol-pi/README.md +159 -0
  5. package/node_modules/@herbertgao/sol-pi/SECURITY.md +26 -0
  6. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +19 -0
  7. package/node_modules/@herbertgao/sol-pi/agents-install.md +150 -0
  8. package/node_modules/@herbertgao/sol-pi/assets/sol-pi-hero.png +0 -0
  9. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +69 -0
  10. package/node_modules/@herbertgao/sol-pi/docs/configuration.md +75 -0
  11. package/node_modules/@herbertgao/sol-pi/package.json +76 -0
  12. package/node_modules/@herbertgao/sol-pi/scripts/check-pi-compat.mjs +32 -0
  13. package/node_modules/@herbertgao/sol-pi/scripts/check-sol-pi-config.mjs +120 -0
  14. package/node_modules/@herbertgao/sol-pi/sol-pi.example.json +10 -0
  15. package/node_modules/@herbertgao/sol-pi/src/sol-pi/config.ts +135 -0
  16. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/file-queue.ts +74 -0
  17. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +185 -0
  18. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/then-run.ts +128 -0
  19. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/archive.ts +53 -0
  20. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/candidate.ts +101 -0
  21. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/config.ts +71 -0
  22. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/index.ts +220 -0
  23. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/journal.ts +25 -0
  24. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/provider.ts +164 -0
  25. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/receipt.ts +177 -0
  26. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/index.ts +227 -0
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/ledger.ts +20 -0
  28. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/observation.ts +252 -0
  29. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +237 -0
  30. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +455 -0
  31. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/index.ts +49 -0
  32. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/plan.ts +79 -0
  33. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +208 -0
  34. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/tools.ts +100 -0
  35. package/node_modules/@herbertgao/sol-pi/src/sol-pi/index.ts +42 -0
  36. package/node_modules/@herbertgao/sol-pi/src/sol-pi/runtime-paths.ts +17 -0
  37. package/node_modules/@herbertgao/sol-pi/src/sol-pi/tui.ts +71 -0
  38. package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -2
  39. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
  40. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  41. package/node_modules/@narumitw/pi-btw/src/btw.ts +13 -2
  42. package/node_modules/pi-lens/CHANGELOG.md +176 -0
  43. package/node_modules/pi-lens/README.md +13 -8
  44. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -1
  45. package/node_modules/pi-lens/dist/clients/analysed-root.js +1 -0
  46. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +1 -1
  47. package/node_modules/pi-lens/dist/clients/biome-client.js +13 -2
  48. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +2 -0
  49. package/node_modules/pi-lens/dist/clients/cache-observability.js +122 -1
  50. package/node_modules/pi-lens/dist/clients/complexity-client.js +5 -0
  51. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
  52. package/node_modules/pi-lens/dist/clients/config-resolve.js +6 -3
  53. package/node_modules/pi-lens/dist/clients/config-schema.js +17 -0
  54. package/node_modules/pi-lens/dist/clients/config-warn.js +2 -2
  55. package/node_modules/pi-lens/dist/clients/dead-code-client.js +8 -1
  56. package/node_modules/pi-lens/dist/clients/dependency-checker.js +5 -1
  57. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +38 -15
  58. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +1 -1
  59. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +5 -1
  60. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +2 -1
  61. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +2 -3
  62. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +4 -3
  63. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +2 -1
  64. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +2 -1
  65. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +2 -1
  66. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +2 -1
  67. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +2 -1
  68. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +2 -1
  69. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +2 -1
  70. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +2 -1
  71. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +2 -1
  72. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +2 -1
  73. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +2 -1
  74. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  75. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +3 -1
  76. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +2 -1
  77. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +2 -1
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +2 -1
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +2 -1
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +19 -1
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +3 -2
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +2 -1
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +2 -1
  85. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +2 -1
  86. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +2 -1
  87. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +12 -12
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +2 -1
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +2 -1
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +2 -2
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +12 -11
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -1
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +2 -2
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +3 -2
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +3 -2
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +2 -1
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +2 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +2 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +2 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +2 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +4 -3
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +2 -1
  109. package/node_modules/pi-lens/dist/clients/effective-config.js +12 -2
  110. package/node_modules/pi-lens/dist/clients/extension-log.js +2 -0
  111. package/node_modules/pi-lens/dist/clients/file-role.js +18 -1
  112. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +4 -0
  113. package/node_modules/pi-lens/dist/clients/formatters.js +54 -188
  114. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -0
  115. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +42 -6
  116. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +48 -7
  117. package/node_modules/pi-lens/dist/clients/installer/index.js +196 -52
  118. package/node_modules/pi-lens/dist/clients/jscpd-client.js +9 -1
  119. package/node_modules/pi-lens/dist/clients/knip-client.js +9 -2
  120. package/node_modules/pi-lens/dist/clients/language-profile.js +12 -2
  121. package/node_modules/pi-lens/dist/clients/latency-logger.js +2 -0
  122. package/node_modules/pi-lens/dist/clients/lens-config.js +4 -0
  123. package/node_modules/pi-lens/dist/clients/lsp/config.js +36 -7
  124. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +24 -1
  125. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +78 -0
  126. package/node_modules/pi-lens/dist/clients/lsp/index.js +281 -45
  127. package/node_modules/pi-lens/dist/clients/lsp/server.js +62 -15
  128. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +37 -4
  129. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +3 -0
  130. package/node_modules/pi-lens/dist/clients/mcp/session.js +2 -0
  131. package/node_modules/pi-lens/dist/clients/opengrep-client.js +2 -0
  132. package/node_modules/pi-lens/dist/clients/package-manager.js +2 -1
  133. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +8 -3
  134. package/node_modules/pi-lens/dist/clients/pipeline.js +3 -2
  135. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +38 -10
  136. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
  137. package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
  138. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +2 -0
  139. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +2 -0
  140. package/node_modules/pi-lens/dist/clients/ruff-client.js +18 -9
  141. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +9 -0
  142. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +5 -0
  143. package/node_modules/pi-lens/dist/clients/runtime-session.js +13 -4
  144. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +4 -1
  145. package/node_modules/pi-lens/dist/clients/runtime-turn.js +48 -1
  146. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -2
  147. package/node_modules/pi-lens/dist/clients/session-event-guard.js +69 -1
  148. package/node_modules/pi-lens/dist/clients/sg-runner.js +2 -1
  149. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +108 -0
  150. package/node_modules/pi-lens/dist/clients/test-runner-client.js +291 -76
  151. package/node_modules/pi-lens/dist/clients/tool-config.js +199 -0
  152. package/node_modules/pi-lens/dist/clients/tool-cwd.js +299 -0
  153. package/node_modules/pi-lens/dist/clients/tool-probe.js +53 -0
  154. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +42 -0
  155. package/node_modules/pi-lens/dist/clients/trivy-client.js +9 -1
  156. package/node_modules/pi-lens/dist/clients/turn-context.js +52 -0
  157. package/node_modules/pi-lens/dist/clients/widget-state.js +4 -3
  158. package/node_modules/pi-lens/dist/index.js +21969 -20005
  159. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -2
  160. package/node_modules/pi-lens/dist/mcp/server.js +135 -159
  161. package/node_modules/pi-lens/dist/tools/activate-tools.js +10 -7
  162. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +2 -13
  163. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +8 -13
  164. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +83 -39
  165. package/node_modules/pi-lens/dist/tools/effective-config.js +4 -3
  166. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +1 -10
  167. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +353 -85
  168. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +21 -110
  169. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +20 -44
  170. package/node_modules/pi-lens/dist/tools/module-report.js +5 -9
  171. package/node_modules/pi-lens/dist/tools/project-report.js +9 -17
  172. package/node_modules/pi-lens/dist/tools/render-compact.js +280 -1
  173. package/node_modules/pi-lens/dist/tools/symbol-search.js +2 -2
  174. package/node_modules/pi-lens/docs/agent-guide.md +3 -4
  175. package/node_modules/pi-lens/docs/agent-tools.md +46 -22
  176. package/node_modules/pi-lens/docs/configuration.md +9 -0
  177. package/node_modules/pi-lens/docs/environment-variables.md +0 -5
  178. package/node_modules/pi-lens/docs/features.md +5 -5
  179. package/node_modules/pi-lens/docs/globalconfig.md +11 -1
  180. package/node_modules/pi-lens/docs/mcp.md +13 -1
  181. package/node_modules/pi-lens/docs/pi-lens-fixer.md +71 -0
  182. package/node_modules/pi-lens/docs/pi-lens-investigator.md +15 -0
  183. package/node_modules/pi-lens/docs/pi-lens-monitor.md +88 -0
  184. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +35 -0
  185. package/node_modules/pi-lens/docs/pi-lens-subagent.md +12 -4
  186. package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
  187. package/node_modules/pi-lens/docs/real-harness.md +46 -0
  188. package/node_modules/pi-lens/docs/release-qa-baseline.md +5 -1
  189. package/node_modules/pi-lens/docs/servercapabilities.md +1 -6
  190. package/node_modules/pi-lens/docs/settings.md +37 -2
  191. package/node_modules/pi-lens/docs/tools_improvement2.md +4 -4
  192. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  193. package/node_modules/pi-lens/docs/usage.md +13 -3
  194. package/node_modules/pi-lens/package.json +10 -1
  195. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +7 -3
  196. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +8 -6
  197. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +19 -9
  198. package/node_modules/pi-web-access/CHANGELOG.md +29 -0
  199. package/node_modules/pi-web-access/README.md +33 -22
  200. package/node_modules/pi-web-access/abortable.ts +17 -0
  201. package/node_modules/pi-web-access/crawl4ai.ts +204 -0
  202. package/node_modules/pi-web-access/credential-source.ts +1 -0
  203. package/node_modules/pi-web-access/curator-page.ts +20 -2
  204. package/node_modules/pi-web-access/curator-run.ts +44 -0
  205. package/node_modules/pi-web-access/curator-server.ts +3 -1
  206. package/node_modules/pi-web-access/duckduckgo.ts +1 -1
  207. package/node_modules/pi-web-access/extract.ts +57 -12
  208. package/node_modules/pi-web-access/fetch-params.ts +1 -1
  209. package/node_modules/pi-web-access/gemini-search.ts +9 -5
  210. package/node_modules/pi-web-access/index.ts +25 -18
  211. package/node_modules/pi-web-access/openai-search.ts +35 -13
  212. package/node_modules/pi-web-access/package.json +2 -2
  213. package/node_modules/pi-web-access/page-query.ts +5 -2
  214. package/node_modules/pi-web-access/query-rewrite.ts +5 -2
  215. package/node_modules/pi-web-access/serpapi.ts +220 -0
  216. package/node_modules/pi-web-access/ssrf-protection.ts +5 -1
  217. package/node_modules/pi-web-access/storage.ts +10 -0
  218. package/node_modules/pi-web-access/summary-review.ts +34 -21
  219. package/node_modules/pi-web-access/utils.ts +6 -4
  220. package/package.json +7 -4
  221. package/node_modules/pi-lens/dist/tools/ast-dump.js +0 -103
@@ -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");
@@ -1,4 +1,5 @@
1
1
  import { safeSpawnAsync } from "../../safe-spawn.js";
2
+ import { logRunnerAdvisoryOnce, resolveRunnerCwd } from "../../tool-cwd.js";
2
3
  import { getLinterPolicyForCwd, hasSqlfluffConfig } from "../../tool-policy.js";
3
4
  import { PRIORITY } from "../priorities.js";
4
5
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
@@ -117,14 +118,14 @@ const sqlfluffRunner = {
117
118
  priority: PRIORITY.SQL_LINT,
118
119
  skipTestFiles: false,
119
120
  async run(ctx) {
120
- const cwd = ctx.cwd || process.cwd();
121
+ const cwd = resolveRunnerCwd(ctx, "sqlfluff");
121
122
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
122
123
  if (policy && !policy.preferredRunners.includes("sqlfluff")) {
123
124
  return { status: "skipped", diagnostics: [], semantic: "none" };
124
125
  }
125
126
  const hasConfig = hasSqlfluffConfig(cwd);
126
127
  if (!hasConfig) {
127
- ctx.log("sqlfluff: no config detected, using ANSI dialect defaults");
128
+ logRunnerAdvisoryOnce(ctx, "sqlfluff", cwd, "sqlfluff: no config detected, using ANSI dialect defaults");
128
129
  }
129
130
  let cmd = null;
130
131
  if (await sqlfluff.isAvailableAsync(cwd)) {
@@ -1,5 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
+ import { logRunnerAdvisoryOnce, resolveRunnerCwd } from "../../tool-cwd.js";
3
4
  import { getLinterPolicyForCwd, hasStylelintConfig, } from "../../tool-policy.js";
4
5
  import { PRIORITY } from "../priorities.js";
5
6
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
@@ -116,7 +117,7 @@ const stylelintRunner = {
116
117
  priority: PRIORITY.FORMAT_AND_LINT_PRIMARY,
117
118
  skipTestFiles: false,
118
119
  async run(ctx) {
119
- const cwd = ctx.cwd || process.cwd();
120
+ const cwd = resolveRunnerCwd(ctx, "stylelint");
120
121
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
121
122
  if (policy && !policy.preferredRunners.includes("stylelint")) {
122
123
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -124,7 +125,7 @@ const stylelintRunner = {
124
125
  const fileDir = path.dirname(path.resolve(cwd, ctx.filePath));
125
126
  const hasConfig = hasStylelintConfig(fileDir) || hasStylelintConfig(cwd);
126
127
  if (!hasConfig) {
127
- ctx.log("stylelint: no config detected, running with default rules");
128
+ logRunnerAdvisoryOnce(ctx, "stylelint", cwd, "stylelint: no config detected, running with default rules");
128
129
  }
129
130
  let cmd = null;
130
131
  if (await stylelint.isAvailableAsync(cwd)) {
@@ -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, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
5
6
  import { parseToolRun } from "./utils/tool-failure.js";
@@ -115,7 +116,7 @@ const swiftlintRunner = {
115
116
  priority: PRIORITY.GENERAL_ANALYSIS,
116
117
  skipTestFiles: false,
117
118
  async run(ctx) {
118
- const cwd = ctx.cwd || process.cwd();
119
+ const cwd = resolveRunnerCwd(ctx, "swiftlint");
119
120
  let cmd = null;
120
121
  if (await swiftlint.isAvailableAsync(cwd)) {
121
122
  cmd = swiftlint.getCommand(cwd);
@@ -1,6 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import { findLocalBinUpwards } from "../../package-manager.js";
3
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
4
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
4
5
  import { getLinterPolicyForCwd } from "../../tool-policy.js";
5
6
  import { PRIORITY } from "../priorities.js";
6
7
  import { createAvailabilityChecker, lspPrimaryCoversFile, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
@@ -106,7 +107,7 @@ const taploRunner = {
106
107
  priority: PRIORITY.FORMAT_AND_LINT_PRIMARY,
107
108
  skipTestFiles: false,
108
109
  async run(ctx) {
109
- const cwd = ctx.cwd || process.cwd();
110
+ const cwd = resolveRunnerCwd(ctx, "taplo");
110
111
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
111
112
  if (policy && !policy.preferredRunners.includes("taplo")) {
112
113
  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 { pathsEqual } from "../../path-utils.js";
4
5
  import { getLinterPolicyForCwd } from "../../tool-policy.js";
5
6
  import { PRIORITY } from "../priorities.js";
@@ -128,7 +129,7 @@ const terragruntRunner = {
128
129
  priority: PRIORITY.GENERAL_ANALYSIS,
129
130
  skipTestFiles: false,
130
131
  async run(ctx) {
131
- const cwd = ctx.cwd || process.cwd();
132
+ const cwd = resolveRunnerCwd(ctx, "terragrunt");
132
133
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
133
134
  if (policy && !policy.preferredRunners.includes("terragrunt"))
134
135
  return SKIPPED;
@@ -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 { findNearestDirWithAnyBasename } from "../../workspace-topology.js";
5
6
  import { PRIORITY } from "../priorities.js";
@@ -52,7 +53,7 @@ const tflintRunner = {
52
53
  priority: PRIORITY.GENERAL_ANALYSIS,
53
54
  skipTestFiles: false,
54
55
  async run(ctx) {
55
- const cwd = ctx.cwd || process.cwd();
56
+ const cwd = resolveRunnerCwd(ctx, "tflint");
56
57
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
57
58
  if (policy && !policy.preferredRunners.includes("tflint")) {
58
59
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -40,6 +40,7 @@ import * as fs from "node:fs";
40
40
  import * as path from "node:path";
41
41
  import { incrementDegradationCount } from "../../degradation-ledger.js";
42
42
  import { safeSpawnAsync } from "../../safe-spawn.js";
43
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
43
44
  import { isTrivyEnabled, resolveSeverityFloor, } from "../../trivy-client.js";
44
45
  import { PRIORITY } from "../priorities.js";
45
46
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
@@ -148,7 +149,7 @@ const trivyConfigRunner = {
148
149
  priority: PRIORITY.GENERAL_ANALYSIS,
149
150
  skipTestFiles: false,
150
151
  async run(ctx) {
151
- const cwd = ctx.cwd || process.cwd();
152
+ const cwd = resolveRunnerCwd(ctx, "trivy-config");
152
153
  // Single opt-in switch — trivy is opt-in across all modes.
153
154
  if (!isTrivyEnabled(cwd)) {
154
155
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -75,6 +75,11 @@ export function describeInstallAttempt(attempt, options = {}) {
75
75
  return { install: "succeeded", ...(reason && { installReason: reason }) };
76
76
  case "failed":
77
77
  return { install: "failed", ...(reason && { installReason: reason }) };
78
+ case "unavailable":
79
+ return {
80
+ install: "unavailable",
81
+ ...(reason && { installReason: reason }),
82
+ };
78
83
  default:
79
84
  return {
80
85
  install: "not-attempted",
@@ -11,7 +11,7 @@
11
11
  * Complexity limit. This is that sweep, written once.
12
12
  */
13
13
  import * as fs from "node:fs";
14
- import { safeSpawnAsync } from "../../../safe-spawn.js";
14
+ import { probeToolAsync } from "../../../tool-probe.js";
15
15
  import { classifyProbeFailure, startHostStallSampler, } from "./availability-policy.js";
16
16
  /**
17
17
  * Walk `candidates` in order. A candidate containing a path separator is
@@ -45,7 +45,7 @@ export async function probeAvailabilityCandidates(candidates, probeArgs, timeout
45
45
  let result;
46
46
  let stallMs;
47
47
  try {
48
- result = await safeSpawnAsync(candidate, [...probeArgs], {
48
+ result = await probeToolAsync(candidate, probeArgs, {
49
49
  timeout: timeoutMs,
50
50
  });
51
51
  }
@@ -20,9 +20,11 @@ import { getServersForFileWithConfig, isServerDisabled, } from "../../../lsp/con
20
20
  import { findGlobalBinary, findLocalBinAt, findLocalBinsUpwards, findLocalBinUpwards, VENDOR_BIN_DIRS, VENV_BIN_DIRS, } from "../../../package-manager.js";
21
21
  import { logLatency } from "../../../latency-logger.js";
22
22
  import { safeSpawnAsync } from "../../../safe-spawn.js";
23
+ import { probeToolAsync } from "../../../tool-probe.js";
23
24
  import { compareOrdinal } from "../../../string-utils.js";
24
25
  import { getToolCommandSpec, shouldAutoInstallTool, } from "../../../tool-policy.js";
25
26
  import { isInSpawnTimeoutCooldown } from "../../../spawn-timeout-cooldown.js";
27
+ import { resolveToolCwd } from "../../../tool-cwd.js";
26
28
  import { createAvailabilityProbeFlight } from "../../../availability-probe-flight.js";
27
29
  import { classifyProbeFailure, createAvailabilityLatch, describeInstallAttempt, describeProbeEvidence, isLatchingOutcome, logAvailabilityDecision, startHostStallSampler, transientRetryDelayMs, } from "./availability-policy.js";
28
30
  export { createAvailabilityLatch, classifyProbeFailure, describeProbeEvidence, describeUnavailability, isTransientDecision, logAvailabilityDecision, startHostStallSampler, } from "./availability-policy.js";
@@ -1390,7 +1392,7 @@ async function probeAstGrepCommandAsync(cmd, argsPrefix = []) {
1390
1392
  let check;
1391
1393
  let hostStallMs;
1392
1394
  try {
1393
- check = await safeSpawnAsync(cmd, [...argsPrefix, "--version"], {
1395
+ check = await probeToolAsync(cmd, [...argsPrefix, "--version"], {
1394
1396
  timeout: 5000,
1395
1397
  });
1396
1398
  }
@@ -1674,7 +1676,7 @@ export async function resolveLocalFirstAsync(toolName, cwd, windowsExt = ".cmd")
1674
1676
  if (globalBin)
1675
1677
  return { cmd: globalBin, args: [] };
1676
1678
  // 3. Global PATH (already installed system-wide, on PATH)
1677
- const globalCheck = await safeSpawnAsync(toolName, ["--version"], {
1679
+ const globalCheck = await probeToolAsync(toolName, ["--version"], {
1678
1680
  timeout: 3000,
1679
1681
  });
1680
1682
  if (!globalCheck.error && globalCheck.status === 0) {
@@ -1693,3 +1695,7 @@ export const sg = {
1693
1695
  isAvailableAsync: isSgAvailableAsync,
1694
1696
  getCommand: getSgCommand,
1695
1697
  };
1698
+ /** Shared cwd seam for runner probes and analysis children (#2777). */
1699
+ export function resolveRunnerCwd(ctx, tool) {
1700
+ return resolveToolCwd("runner", tool, ctx.filePath, ctx);
1701
+ }