@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
@@ -15,7 +15,8 @@ import * as os from "node:os";
15
15
  import * as path from "node:path";
16
16
  import { BoundedLruCache } from "./bounded-cache.js";
17
17
  import { createGenerationSource } from "./generation-guard.js";
18
- import { findNearestMarkerRoot, isRealGitMarker, normalizeMapKey, } from "./path-utils.js";
18
+ import { normalizeMapKey } from "./path-utils.js";
19
+ import { resolveToolCwd } from "./tool-cwd.js";
19
20
  import { resolveCargoPackageEdition } from "./cargo-manifest.js";
20
21
  import { resolveKtfmtGradleStyle } from "./gradle-ktfmt-style.js";
21
22
  import { resolvePhpCsFixerConfig } from "./php-cs-fixer-config.js";
@@ -27,8 +28,10 @@ import { compareOrdinal } from "./string-utils.js";
27
28
  import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
28
29
  import { findGlobalBinary, findLocalBinUpwards, VENDOR_BIN_DIRS, VENV_BIN_DIRS, } from "./package-manager.js";
29
30
  import { safeSpawnAsync } from "./safe-spawn.js";
31
+ import { probeToolAsync } from "./tool-probe.js";
30
32
  import { assertInstallAllowed } from "./project-trust.js";
31
33
  import { tryLazyInstallForFormatter } from "./dispatch/runners/utils/lazy-installer.js";
34
+ import { getToolPath } from "./installer/index.js";
32
35
  import { findPSScriptAnalyzerConfigPath, getAutoInstallToolIdForFormatter, getFormatterPolicyForFile, getSmartDefaultFormatterName, hasBiomeConfig, hasBlackConfig, hasClangFormatConfig, hasCljfmtConfig, hasCmakeFormatConfig, hasCsharpierConfig, hasFantomasConfig, hasGoogleJavaFormatConfig, hasKtfmtConfig, hasKtlintConfig, hasMixFormatConfig, hasNearestPackageJsonDependency, hasNearestPackageJsonField, hasOcamlformatConfig, hasOrmoluConfig, hasOxfmtConfig, hasOxfmtSvelteConfig, hasPhpCsFixerConfig, hasPrettierConfig, hasPSScriptAnalyzerConfig, hasRubocopConfig, hasRuffConfig, hasSqlfluffConfig, hasStandardrbConfig, hasStyluaConfig, hasSwiftformatConfig, hasTaploConfig, hasTerraformConfig, hasVitePlusConfig, OXFMT_SUPPORTED_EXTENSIONS, } from "./tool-policy.js";
33
36
  /**
34
37
  * Lazy-install a formatter's tool, through the shared seam (#1537).
@@ -380,7 +383,7 @@ async function resolveGoFmtBinary() {
380
383
  const inPath = await which("gofmt");
381
384
  if (inPath)
382
385
  return inPath;
383
- const goCheck = await safeSpawnAsync("go", ["env", "GOROOT"], {
386
+ const goCheck = await probeToolAsync("go", ["env", "GOROOT"], {
384
387
  timeout: 5000,
385
388
  });
386
389
  if (goCheck.error || goCheck.status !== 0)
@@ -468,6 +471,22 @@ async function resolveManagedSmartDefaultCommand(formatterName, filePath, args)
468
471
  return null;
469
472
  return [installed, ...args, filePath];
470
473
  }
474
+ /** Resolve an explicitly selected formatter from the shared managed-tool seam. */
475
+ async function resolveManagedFormatterCommand(toolId, filePath, args, findLocal, cwd) {
476
+ const local = findLocal && cwd ? await findLocal(toolId, cwd) : null;
477
+ const installed = local ?? (await which(toolId)) ?? (await getToolPath(toolId));
478
+ return installed ? [installed, ...args, filePath] : FORMATTER_UNAVAILABLE;
479
+ }
480
+ function managedFormatterResolver(toolId, args, findLocal) {
481
+ return (filePath, cwd) => resolveManagedFormatterCommand(toolId, filePath, args, findLocal, cwd);
482
+ }
483
+ function managedToolDetect(toolId, configDetect, nativeDetect) {
484
+ return async (cwd) => {
485
+ const available = (nativeDetect && (await nativeDetect(cwd))) ||
486
+ Boolean(await getToolPath(toolId));
487
+ return available && (configDetect ? await configDetect(cwd) : true);
488
+ };
489
+ }
471
490
  /**
472
491
  * One entry per formatter that can be selected via explicit project config
473
492
  * (the `formatterPolicy` "explicit-config" branch of `getFormattersForFile`).
@@ -603,109 +622,19 @@ async function indentationArgs(filePath, tool, cwd) {
603
622
  String(indentation.width),
604
623
  ];
605
624
  }
606
- const FORMATTER_MARKERS_BY_NAME = new Map([
607
- ["biome", ["biome.json", "biome.jsonc", "package.json"]],
608
- [
609
- "prettier",
610
- [
611
- ".prettierrc",
612
- ".prettierrc.json",
613
- ".prettierrc.yaml",
614
- ".prettierrc.yml",
615
- ".prettierrc.js",
616
- ".prettierrc.cjs",
617
- ".prettierrc.mjs",
618
- "prettier.config.js",
619
- "prettier.config.cjs",
620
- "prettier.config.mjs",
621
- ".prettierignore",
622
- "package.json",
623
- ],
624
- ],
625
- [
626
- "oxfmt",
627
- ["oxfmt.toml", ".oxfmtrc.json", "vite-plus.json", "package.json"],
628
- ],
629
- ["ruff", ["pyproject.toml", "ruff.toml", ".ruff.toml"]],
630
- ["black", ["pyproject.toml", "black.toml", ".black"]],
631
- ["sqlfluff", [".sqlfluff", "pyproject.toml", "setup.cfg"]],
632
- ["rustfmt", ["rustfmt.toml", ".rustfmt.toml", "Cargo.toml"]],
633
- ["rubocop", [".rubocop.yml", ".rubocop.yaml"]],
634
- ["standardrb", [".standard.yml", ".standard.yaml"]],
635
- ["clang-format", [".clang-format", "_clang-format"]],
636
- ["php-cs-fixer", [".php-cs-fixer.php", ".php-cs-fixer.dist.php"]],
637
- ["stylua", ["stylua.toml", ".stylua.toml"]],
638
- ["ocamlformat", [".ocamlformat"]],
639
- ["google-java-format", [".google-java-format", ".editorconfig"]],
640
- ["cljfmt", [".cljfmt.edn", "cljfmt.edn", ".cljfmt"]],
641
- [
642
- "cmake-format",
643
- [
644
- ".cmake-format",
645
- ".cmake-format.yaml",
646
- ".cmake-format.yml",
647
- ".cmake-format.json",
648
- ".cmake-format.py",
649
- "cmake-format.yaml",
650
- "cmake-format.yml",
651
- ".editorconfig",
652
- ],
653
- ],
654
- [
655
- "psscriptanalyzer-format",
656
- ["PSScriptAnalyzerSettings.psd1", "ScriptAnalyzerSettings.psd1"],
657
- ],
658
- [
659
- "csharpier",
660
- [
661
- ".csharpierrc",
662
- ".csharpierrc.json",
663
- ".csharpierrc.yaml",
664
- ".csharpierrc.yml",
665
- ],
666
- ],
667
- ["ormolu", [".ormolu"]],
668
- ["taplo", ["taplo.toml", ".taplo.toml"]],
669
- ["terraform", [".terraform.lock.hcl"]],
670
- ["swiftformat", [".swiftformat"]],
671
- ["fantomas", [".fantomasignore", ".editorconfig"]],
672
- ["mix", [".formatter.exs"]],
673
- ["shfmt", [".editorconfig"]],
674
- ["ktlint", [".editorconfig"]],
675
- [
676
- "ktfmt",
677
- [
678
- ".editorconfig",
679
- ".ktfmt",
680
- ".ktfmt.kts",
681
- "build.gradle",
682
- "build.gradle.kts",
683
- "settings.gradle",
684
- "settings.gradle.kts",
685
- ],
686
- ],
687
- ]);
688
625
  /**
689
626
  * Resolve the cwd for the formatter child, capped at the user's home
690
627
  * directory. Files outside a project retain the historical file-directory
691
628
  * cwd. The optional homeDir is only for the ceiling regression test.
692
629
  */
693
630
  export function resolveFormatterCwd(absolutePath, formatterName, homeDir = os.homedir()) {
694
- const fileDir = path.dirname(path.resolve(absolutePath));
695
- const effectiveHome = homeDir || undefined;
696
- const markers = formatterName
697
- ? [...(FORMATTER_MARKERS_BY_NAME.get(formatterName) ?? []), ".gitignore"]
698
- : [".gitignore"];
699
- const root = findNearestMarkerRoot(fileDir, markers, {
700
- homeDir: effectiveHome,
701
- });
702
- if (root)
703
- return root;
704
- const gitRoot = findNearestMarkerRoot(fileDir, [".git"], {
705
- homeDir: effectiveHome,
706
- markerPredicate: isRealGitMarker,
631
+ return resolveToolCwd("formatter", formatterName ?? "unknown", absolutePath, {
632
+ // Formatter discovery historically walks above the file directory. The
633
+ // filesystem root is the neutral dispatch boundary; the seam's `$HOME`
634
+ // ceiling prevents it from escaping the user's workspace.
635
+ cwd: path.parse(path.resolve(absolutePath)).root,
636
+ homeDir,
707
637
  });
708
- return gitRoot ?? fileDir;
709
638
  }
710
639
  /**
711
640
  * Every biome invocation must carry this. Biome exits 1 with "No files were
@@ -888,11 +817,7 @@ export const oxfmtFormatter = {
888
817
  const found = await which("oxfmt");
889
818
  if (found)
890
819
  return [found, OXFMT_NO_ERROR_ON_UNMATCHED, filePath];
891
- // #2413: neither node_modules/.bin nor PATH has oxfmt, and `detect()` is
892
- // config-only (it never probes the binary), so selection can reach here
893
- // with nothing installed. The static command is bare `oxfmt` — spawning it
894
- // only reproduces the reported `spawn oxfmt ENOENT`. Prove it unavailable.
895
- return FORMATTER_UNAVAILABLE;
820
+ return ((await resolveManagedFormatterCommand("oxfmt", filePath, [OXFMT_NO_ERROR_ON_UNMATCHED], findInNodeModules, cwd)) ?? FORMATTER_UNAVAILABLE);
896
821
  },
897
822
  // Single source of truth: OXFMT_SUPPORTED_EXTENSIONS in tool-policy.ts.
898
823
  // Do not hand-maintain a second copy of this list (#1134 — previously two
@@ -941,21 +866,14 @@ export const ruffFormatter = {
941
866
  return true;
942
867
  // No-config fallback: if Ruff is already available, allow formatter usage.
943
868
  // This keeps Python default behavior consistent with startup defaults.
944
- const { getToolPath } = await import("./installer/index.js");
945
- const installed = await getToolPath("ruff");
946
- return Boolean(installed);
869
+ return Boolean(await getToolPath("ruff"));
947
870
  },
948
871
  };
949
872
  export const blackFormatter = {
950
873
  name: "black",
951
874
  command: ["black", "$FILE"],
952
875
  extensions: [".py", ".pyi"],
953
- async resolveCommand(filePath, cwd) {
954
- const venv = await findInVenv("black", cwd);
955
- if (venv)
956
- return [venv, filePath];
957
- return null;
958
- },
876
+ resolveCommand: managedFormatterResolver("black", [], findInVenv),
959
877
  async detect(cwd) {
960
878
  return hasBlackConfig(cwd);
961
879
  },
@@ -1054,12 +972,7 @@ export const shfmtFormatter = {
1054
972
  ...styleArgs,
1055
973
  ]);
1056
974
  },
1057
- async detect(_cwd) {
1058
- if ((await which("shfmt")) !== null)
1059
- return true;
1060
- const { getToolPath } = await import("./installer/index.js");
1061
- return Boolean(await getToolPath("shfmt"));
1062
- },
975
+ detect: managedToolDetect("shfmt", undefined, async () => (await which("shfmt")) !== null),
1063
976
  };
1064
977
  export const nixfmtFormatter = {
1065
978
  name: "nixfmt",
@@ -1121,12 +1034,7 @@ export const ktlintFormatter = {
1121
1034
  return [inPath, "-F", filePath];
1122
1035
  return resolveManagedSmartDefaultCommand("ktlint", filePath, ["-F"]);
1123
1036
  },
1124
- async detect(_cwd) {
1125
- if ((await which("ktlint")) !== null)
1126
- return true;
1127
- const { getToolPath } = await import("./installer/index.js");
1128
- return Boolean(await getToolPath("ktlint"));
1129
- },
1037
+ detect: managedToolDetect("ktlint", undefined, async () => (await which("ktlint")) !== null),
1130
1038
  };
1131
1039
  export const ktfmtFormatter = {
1132
1040
  name: "ktfmt",
@@ -1256,36 +1164,25 @@ export const phpCsFixerFormatter = {
1256
1164
  const configPath = resolvePhpCsFixerConfig(filePath);
1257
1165
  const binary = (await findInVendorBin("php-cs-fixer", cwd)) ??
1258
1166
  (await which("php-cs-fixer"));
1167
+ const managed = await resolveManagedFormatterCommand("php-cs-fixer", filePath, [], findInVendorBin, cwd);
1168
+ if (managed === FORMATTER_UNAVAILABLE)
1169
+ return FORMATTER_UNAVAILABLE;
1170
+ const resolved = binary ?? managed[0];
1259
1171
  // #2413/#2472 review F4: both probes (vendor/bin, then PATH) have
1260
1172
  // PROVEN the binary is absent — returning `null` here would fall back
1261
1173
  // to the static `command` above, which is the SAME bare
1262
1174
  // `php-cs-fixer` this just failed to find, spawning it only to
1263
1175
  // re-observe the ENOENT already known. Report the proven-missing
1264
1176
  // state instead so `formatFile` skips the wasted spawn.
1265
- if (!binary)
1177
+ if (!resolved)
1266
1178
  return FORMATTER_UNAVAILABLE;
1267
1179
  return configPath
1268
- ? [binary, "fix", "--config", configPath, filePath]
1269
- : [binary, "fix", filePath];
1270
- },
1271
- async detect(cwd) {
1272
- const vendorBin = await findInVendorBin("php-cs-fixer", cwd);
1273
- const globalBin = await which("php-cs-fixer");
1274
- if (!vendorBin && !globalBin)
1275
- return false;
1276
- // Only run if project has explicit config. This is a presence-only
1277
- // climb from the project `cwd` (not necessarily the formatted file's
1278
- // own directory) via this file's own `findUp` — deliberately NOT
1279
- // merged with `resolvePhpCsFixerConfig` above (#2472 AC4): that
1280
- // resolver climbs from the FILE's directory and needs the exact
1281
- // winning path for `--config`, while this only needs a yes/no answer
1282
- // from whatever `cwd` the caller passed. `rustfmtFormatter.detect`
1283
- // keeps the same non-merged shape against `resolveCargoPackageEdition`
1284
- // for the identical reason.
1285
- const configs = [".php-cs-fixer.php", ".php-cs-fixer.dist.php"];
1286
- const found = await findUp(configs, cwd);
1287
- return found.length > 0;
1180
+ ? [resolved, "fix", "--config", configPath, filePath]
1181
+ : [resolved, "fix", filePath];
1288
1182
  },
1183
+ detect: managedToolDetect("php-cs-fixer", async (cwd) => (await findUp([".php-cs-fixer.php", ".php-cs-fixer.dist.php"], cwd))
1184
+ .length > 0, async (cwd) => Boolean((await findInVendorBin("php-cs-fixer", cwd)) ||
1185
+ (await which("php-cs-fixer")))),
1289
1186
  };
1290
1187
  export const csharpierFormatter = {
1291
1188
  name: "csharpier",
@@ -1301,9 +1198,7 @@ export const csharpierFormatter = {
1301
1198
  }
1302
1199
  // CSharpier 0.x: invoked through the dotnet driver.
1303
1200
  if ((await which("dotnet")) !== null) {
1304
- const legacy = await safeSpawnAsync("dotnet", ["csharpier", "--version"], {
1305
- timeout: 5000,
1306
- });
1201
+ const legacy = await probeToolAsync("dotnet", ["csharpier", "--version"], { timeout: 5000 });
1307
1202
  if (!legacy.error && legacy.status === 0) {
1308
1203
  return ["dotnet", "csharpier", filePath];
1309
1204
  }
@@ -1317,7 +1212,7 @@ export const csharpierFormatter = {
1317
1212
  // … or the legacy dotnet-driver form (CSharpier 0.x).
1318
1213
  if ((await which("dotnet")) === null)
1319
1214
  return false;
1320
- const result = await safeSpawnAsync("dotnet", ["csharpier", "--version"], {
1215
+ const result = await probeToolAsync("dotnet", ["csharpier", "--version"], {
1321
1216
  timeout: 5000,
1322
1217
  });
1323
1218
  return !result.error && result.status === 0;
@@ -1343,23 +1238,8 @@ export const styluaFormatter = {
1343
1238
  name: "stylua",
1344
1239
  command: ["stylua", "$FILE"],
1345
1240
  extensions: [".lua"],
1346
- async resolveCommand(filePath, cwd) {
1347
- // Project binary first (#1731, discipline B): stylua has no pi-lens
1348
- // managed install, so before this the ONLY resolution was a bare
1349
- // `stylua` PATH lookup — a project-local install via npm
1350
- // `@johnnymorganz/stylua` (`node_modules/.bin/stylua`) was invisible.
1351
- const local = findLocalBinUpwards("stylua", cwd);
1352
- return local ? [local, filePath] : null;
1353
- },
1354
- async detect(cwd) {
1355
- const local = findLocalBinUpwards("stylua", cwd);
1356
- if (!local && (await which("stylua")) === null)
1357
- return false;
1358
- // Prefer explicit config but also run if binary is present in a Lua project
1359
- const configs = ["stylua.toml", ".stylua.toml"];
1360
- const found = await findUp(configs, cwd);
1361
- return found.length > 0;
1362
- },
1241
+ resolveCommand: managedFormatterResolver("stylua", [], findLocalBinUpwards),
1242
+ detect: managedToolDetect("stylua", async (cwd) => (await findUp(["stylua.toml", ".stylua.toml"], cwd)).length > 0, async (cwd) => Boolean(findLocalBinUpwards("stylua", cwd) || (await which("stylua")))),
1363
1243
  };
1364
1244
  export const ormoluFormatter = {
1365
1245
  name: "ormolu",
@@ -1379,42 +1259,28 @@ export const taploFormatter = {
1379
1259
  return [inPath, "fmt", filePath];
1380
1260
  return resolveManagedSmartDefaultCommand("taplo", filePath, ["fmt"]);
1381
1261
  },
1382
- async detect(_cwd) {
1383
- if ((await which("taplo")) !== null)
1384
- return true;
1385
- const { getToolPath } = await import("./installer/index.js");
1386
- return Boolean(await getToolPath("taplo"));
1387
- },
1262
+ detect: managedToolDetect("taplo", undefined, async () => (await which("taplo")) !== null),
1388
1263
  };
1389
1264
  export const googleJavaFormatFormatter = {
1390
1265
  name: "google-java-format",
1391
1266
  command: ["google-java-format", "--replace", "$FILE"],
1392
1267
  extensions: [".java"],
1393
- async detect(cwd) {
1394
- if ((await which("google-java-format")) === null)
1395
- return false;
1396
- return hasGoogleJavaFormatConfig(cwd);
1397
- },
1268
+ resolveCommand: managedFormatterResolver("google-java-format", ["--replace"]),
1269
+ detect: managedToolDetect("google-java-format", hasGoogleJavaFormatConfig, async () => (await which("google-java-format")) !== null),
1398
1270
  };
1399
1271
  export const cljfmtFormatter = {
1400
1272
  name: "cljfmt",
1401
1273
  command: ["cljfmt", "fix", "$FILE"],
1402
1274
  extensions: [".clj", ".cljc", ".cljs"],
1403
- async detect(cwd) {
1404
- if ((await which("cljfmt")) === null)
1405
- return false;
1406
- return hasCljfmtConfig(cwd);
1407
- },
1275
+ resolveCommand: managedFormatterResolver("cljfmt", ["fix"]),
1276
+ detect: managedToolDetect("cljfmt", hasCljfmtConfig, async () => (await which("cljfmt")) !== null),
1408
1277
  };
1409
1278
  export const cmakeFormatFormatter = {
1410
1279
  name: "cmake-format",
1411
1280
  command: ["cmake-format", "-i", "$FILE"],
1412
1281
  extensions: [".cmake"],
1413
- async detect(cwd) {
1414
- if ((await which("cmake-format")) === null)
1415
- return false;
1416
- return hasCmakeFormatConfig(cwd);
1417
- },
1282
+ resolveCommand: managedFormatterResolver("cmake-format", ["-i"]),
1283
+ detect: managedToolDetect("cmake-format", hasCmakeFormatConfig, async () => (await which("cmake-format")) !== null),
1418
1284
  };
1419
1285
  export const cueFormatter = {
1420
1286
  name: "cue",
@@ -1487,7 +1353,7 @@ export const psscriptanalyzerFormatFormatter = {
1487
1353
  if (!pwsh)
1488
1354
  return false;
1489
1355
  // Check PSScriptAnalyzer module is available
1490
- const result = await safeSpawnAsync(pwsh, [
1356
+ const result = await probeToolAsync(pwsh, [
1491
1357
  "-NoProfile",
1492
1358
  "-Command",
1493
1359
  "Get-Module -ListAvailable PSScriptAnalyzer | Select-Object -First 1 -ExpandProperty Name",
@@ -186,6 +186,10 @@ export function createGenerationMap(name, options = {}) {
186
186
  invalidations += 1;
187
187
  stamps.delete(normalize(key));
188
188
  },
189
+ clear() {
190
+ invalidations += 1;
191
+ stamps.clear();
192
+ },
189
193
  size() {
190
194
  return stamps.size;
191
195
  },
@@ -45,11 +45,12 @@
45
45
  * module doc for why blanket gitignore-respect is wrong for a secrets scanner
46
46
  * (an untracked `.env` with a real credential is exactly the case gitleaks
47
47
  * exists to catch, and `.env` is commonly gitignored). `classifyAndFilterFindings`
48
- * backstops the config with a TS-side re-classification: a finding under the
49
- * secrets-lane scratch tier is DEMOTED (not deleted) to `severity: "info"`,
50
- * `semantic: "none"`, `pathStatus: "scratch"` visible for an audit read,
51
- * but it can never surface as a blocking "leaked secret" alarm. Every other
52
- * finding gets `pathStatus` from git's tracked/ignored view.
48
+ * backstops the config with a TS-side re-classification. Findings under the
49
+ * secrets-lane scratch tier, in gitignored files, or inside nested repositories
50
+ * are DEMOTED (not deleted) to `severity: "info"` / `semantic: "none"` by the
51
+ * project-diagnostics adapter. They remain visible for an explicit audit but
52
+ * can never surface as blocking "leaked secret" alarms or automatic turn
53
+ * context. Tracked and ordinary untracked files remain blocking.
53
54
  *
54
55
  * Refs: #130, #1562
55
56
  */
@@ -333,8 +334,10 @@ export class GitleaksClient extends SecurityScanClient {
333
334
  }
334
335
  const rawFindings = parseGitleaksReport(fs.readFileSync(reportPath, "utf-8"));
335
336
  const findings = await classifyAndFilterFindings(rawFindings, cwd);
337
+ // #2154: the one gitleaks site that parsed a scan of this root.
336
338
  return {
337
339
  success: true,
340
+ analyzed: true,
338
341
  findings,
339
342
  scannedAt,
340
343
  };
@@ -375,7 +378,10 @@ export class GitleaksClient extends SecurityScanClient {
375
378
  * assert the config we hand it, so this backstop guarantees the demotion
376
379
  * even if the generated regex ever mismatches gitleaks's own path
377
380
  * normalization.
378
- * 2. Every OTHER finding gets `pathStatus` set from git's tracked/ignored
381
+ * 2. A finding below a nested git boundary is labelled
382
+ * `nested-repository`. Parent scans must not claim ownership of a
383
+ * submodule's or nested repository's files.
384
+ * 3. Every OTHER finding gets `pathStatus` set from git's tracked/ignored
379
385
  * view, so a future false-positive triage is a log read.
380
386
  *
381
387
  * `collectTrackedFiles`/`collectUntrackedIgnoredIds` degrade to `undefined`
@@ -383,6 +389,32 @@ export class GitleaksClient extends SecurityScanClient {
383
389
  * keep `pathStatus: undefined` rather than a guessed value (fail-open,
384
390
  * matching `git-tracked-ignore.ts`'s own contract). Exported for unit tests.
385
391
  */
392
+ function isInsideNestedGitRepository(file, cwd) {
393
+ const root = path.resolve(cwd);
394
+ const absoluteFile = path.isAbsolute(file)
395
+ ? path.resolve(file)
396
+ : path.resolve(root, file);
397
+ const relative = path.relative(root, absoluteFile);
398
+ if (relative === ".." ||
399
+ relative.startsWith(`..${path.sep}`) ||
400
+ path.isAbsolute(relative))
401
+ return false;
402
+ let directory = path.dirname(absoluteFile);
403
+ while (directory !== root) {
404
+ try {
405
+ if (fs.existsSync(path.join(directory, ".git")))
406
+ return true;
407
+ }
408
+ catch {
409
+ return false;
410
+ }
411
+ const parent = path.dirname(directory);
412
+ if (parent === directory)
413
+ return false;
414
+ directory = parent;
415
+ }
416
+ return false;
417
+ }
386
418
  export async function classifyAndFilterFindings(findings, cwd) {
387
419
  if (findings.length === 0)
388
420
  return findings;
@@ -399,6 +431,10 @@ export async function classifyAndFilterFindings(findings, cwd) {
399
431
  const absPath = path.isAbsolute(finding.file)
400
432
  ? finding.file
401
433
  : path.resolve(cwd, finding.file);
434
+ if (isInsideNestedGitRepository(absPath, cwd)) {
435
+ classified.push({ ...finding, pathStatus: "nested-repository" });
436
+ continue;
437
+ }
402
438
  let pathStatus;
403
439
  if (trackedIds?.has(normalizeEphemeralMapKey(absPath))) {
404
440
  pathStatus = "tracked";
@@ -20,6 +20,8 @@ import * as fs from "node:fs";
20
20
  import * as os from "node:os";
21
21
  import * as path from "node:path";
22
22
  import { safeSpawnAsync } from "./safe-spawn.js";
23
+ import { probeToolAsync } from "./tool-probe.js";
24
+ import { firstOutputLine, spawnFailedWithNoOutput, } from "./dispatch/runners/utils/spawn-outcome.js";
23
25
  import { assertInstallAllowed } from "./project-trust.js";
24
26
  import { SecurityScanClient } from "./security-scan-client.js";
25
27
  import { classifyProbeFailure, describeProbeEvidence, INSTALL_TRANSIENT_MAX_ATTEMPTS, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
@@ -86,7 +88,7 @@ export class GovulncheckClient extends SecurityScanClient {
86
88
  let goOnPath;
87
89
  let goHostStallMs;
88
90
  try {
89
- goOnPath = await safeSpawnAsync("go", ["version"], {
91
+ goOnPath = await probeToolAsync("go", ["version"], {
90
92
  timeout: 5000,
91
93
  });
92
94
  }
@@ -219,7 +221,7 @@ export class GovulncheckClient extends SecurityScanClient {
219
221
  let reprobe;
220
222
  let reprobeHostStallMs;
221
223
  try {
222
- reprobe = await safeSpawnAsync("govulncheck", ["-version"], {
224
+ reprobe = await probeToolAsync("govulncheck", ["-version"], {
223
225
  timeout: 5000,
224
226
  });
225
227
  }
@@ -366,21 +368,60 @@ export class GovulncheckClient extends SecurityScanClient {
366
368
  const bin = this.binaryPath ?? "govulncheck";
367
369
  try {
368
370
  const result = await safeSpawnAsync(bin, ["-mode=source", "-format=json", "./..."], { cwd, timeout: SCAN_TIMEOUT_MS });
371
+ // #2154 v4 F1: this was the one runner client with no `result.error`
372
+ // check before its parse site. `safeSpawnAsync` reports a timeout or
373
+ // an ambient abort as `{status: null, error, stdout: <partial>}`, and
374
+ // the old guard (`!stdout.trim() && status !== 0 && status !== 3`)
375
+ // let that partial stream through as an authoritative scan. Same
376
+ // order as the seven sibling clients: the process first, its output
377
+ // second.
378
+ if (result.error) {
379
+ this.log(`Scan error: ${result.error.message}`);
380
+ return {
381
+ ...EMPTY_RESULT,
382
+ scannedAt,
383
+ summary: result.error.message.slice(0, 200),
384
+ };
385
+ }
369
386
  // govulncheck exits non-zero (status 3) when vulnerabilities are
370
- // found — that's success from our perspective. Genuine failures
371
- // produce empty stdout + a stderr message.
387
+ // found — that's success from our perspective. Any other exit code
388
+ // means the scan was not analysed, even if its config preamble or
389
+ // partial stream reached stdout before the failure.
372
390
  const rawStdout = result.stdout ?? "";
373
- if (!rawStdout.trim() && result.status !== 0 && result.status !== 3) {
374
- this.log(`Scan failed: ${(result.stderr ?? "").slice(0, 200)}`);
391
+ if (result.status !== 0 && result.status !== 3) {
392
+ return {
393
+ ...EMPTY_RESULT,
394
+ scannedAt,
395
+ summary: firstOutputLine(result.stderr) || "scan failed",
396
+ };
397
+ }
398
+ if (!rawStdout.trim()) {
399
+ // Nothing to parse. `govulncheck -format=json` always writes a
400
+ // stream (a `config` record at minimum), so an empty one is never
401
+ // evidence of "no vulnerabilities" — it must not retire a retained
402
+ // CVE finding, whatever the exit code. `spawnFailedWithNoOutput`
403
+ // is the same shared discriminator knip and dead-code use, in
404
+ // place of this file's hand-rolled status test.
405
+ if (spawnFailedWithNoOutput(result, rawStdout)) {
406
+ this.log(`Scan failed: ${(result.stderr ?? "").slice(0, 200)}`);
407
+ return {
408
+ ...EMPTY_RESULT,
409
+ scannedAt,
410
+ summary: firstOutputLine(result.stderr) || "scan failed",
411
+ };
412
+ }
375
413
  return {
376
414
  ...EMPTY_RESULT,
415
+ success: true,
377
416
  scannedAt,
378
- summary: (result.stderr ?? "").trim().split("\n")[0] || "scan failed",
417
+ summary: "govulncheck produced no output; nothing parsed",
379
418
  };
380
419
  }
381
420
  const findings = parseGovulncheckJson(rawStdout);
421
+ // #2154: the one govulncheck site that parsed a scan of this root.
382
422
  return {
383
423
  success: true,
424
+ analyzed: true,
384
425
  findings,
385
426
  scannedAt,
386
427
  };