@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
@@ -50,6 +50,8 @@ import { logLatency } from "./latency-logger.js";
50
50
  * must not also be written by a raw `logLatency` call somewhere else.
51
51
  */
52
52
  export const BOUNDED_TELEMETRY_PHASES = [
53
+ /** #2890: a duplicate pi RPC session_start was admitted but suppressed. */
54
+ "session_start_duplicate_suppressed",
53
55
  /**
54
56
  * #2467: a demand for the analyzer bootstrap clients was not served and
55
57
  * the caller proceeded without them. Rising-edge per demand reason, with
@@ -176,9 +176,50 @@ function newAttributionState() {
176
176
  injectedCharsSinceLastUsage: 0,
177
177
  newTranscriptCharsSinceLastUsage: 0,
178
178
  attributionCharsCapped: false,
179
+ injectedBytes: emptyInjectedBytes(),
180
+ injectedFindingsRepeated: 0,
181
+ findingIdentities: new Set(),
182
+ findingIdentityCapRecorded: false,
183
+ turnEndAdvisoryBytes: 0,
184
+ toolResultBytes: 0,
185
+ toolResultsTruncated: 0,
179
186
  summary: newCacheUsageSummary(),
180
187
  };
181
188
  }
189
+ const MAX_FINDING_IDENTITIES = 2_000;
190
+ function emptyInjectedBytes() {
191
+ return {
192
+ sessionGuidance: 0,
193
+ turnFindings: 0,
194
+ testFindings: 0,
195
+ agentNudge: 0,
196
+ turnEndAdvisory: 0,
197
+ other: 0,
198
+ };
199
+ }
200
+ function sourceKey(source) {
201
+ return {
202
+ "session-guidance": "sessionGuidance",
203
+ "turn-findings": "turnFindings",
204
+ "test-findings": "testFindings",
205
+ "agent-nudge": "agentNudge",
206
+ }[source];
207
+ }
208
+ /** Best-effort, language-neutral identity extraction from rendered findings. */
209
+ function findingIdentities(text) {
210
+ const out = [];
211
+ for (const line of text.split(/\r?\n/)) {
212
+ const location = line.match(/(?:^|\s)([^\s:]+):(\d+)(?::\d+)?/);
213
+ if (!location)
214
+ continue;
215
+ const rule = line.match(/(?:rule|code)\s*[:=]\s*([\w./-]+)/i)?.[1] ??
216
+ line.match(/\(([\w./-]+)\)\s*$/)?.[1] ??
217
+ line.match(/:\d+(?::\d+)?\s+([\w./-]+)/)?.[1];
218
+ if (rule)
219
+ out.push(`${location[1]}:${location[2]}:${rule}`);
220
+ }
221
+ return out;
222
+ }
182
223
  /**
183
224
  * Fetch (creating if absent) one session's attribution state, refreshing LRU
184
225
  * recency and evicting the oldest entries past the cap — same bounding as
@@ -688,10 +729,37 @@ export function observeCacheContext(args) {
688
729
  countsCapped: sliceSize.capped,
689
730
  };
690
731
  });
732
+ const state = attributionFor(attributionKey(args.sessionId, args.sessionRole));
733
+ for (const slice of injectionSlices) {
734
+ const key = sourceKey(slice.source);
735
+ const bytes = measureInjectedMessages(slice.messages).bytes;
736
+ state.injectedBytes[key] += bytes;
737
+ for (const message of slice.messages) {
738
+ for (const identity of findingIdentities(typeof message.content === "string" ? message.content : "")) {
739
+ if (state.findingIdentities.has(identity)) {
740
+ state.injectedFindingsRepeated += 1;
741
+ continue;
742
+ }
743
+ if (state.findingIdentities.size >= MAX_FINDING_IDENTITIES) {
744
+ if (!state.findingIdentityCapRecorded) {
745
+ state.findingIdentityCapRecorded = true;
746
+ logLatency({
747
+ type: "phase",
748
+ filePath: "<pi-lens>",
749
+ phase: "injected_finding_identity_cap",
750
+ durationMs: 0,
751
+ metadata: { cap: MAX_FINDING_IDENTITIES },
752
+ });
753
+ }
754
+ continue;
755
+ }
756
+ state.findingIdentities.add(identity);
757
+ }
758
+ }
759
+ }
691
760
  const messageCountCapped = existingMessages.length > MAX_REPORTED_MESSAGES ||
692
761
  resultMessages.length > MAX_REPORTED_MESSAGES;
693
762
  recordContextAttribution(attributionKey(args.sessionId, args.sessionRole), existingMessages, sizes);
694
- const state = attributionFor(attributionKey(args.sessionId, args.sessionRole));
695
763
  state.sequenceHashTruncatedSinceLastUsage ||=
696
764
  beforeSequence.truncated ||
697
765
  beforeSequence.contentTruncated ||
@@ -721,6 +789,10 @@ export function observeCacheContext(args) {
721
789
  injectedMessageCountCapped: injectedMessages.length > MAX_REPORTED_MESSAGES,
722
790
  injectedChars: sizes.chars,
723
791
  injectedBytes: sizes.bytes,
792
+ injectedBytesBySource: Object.fromEntries(sourceBreakdown.map((entry) => [
793
+ sourceKey(entry.source),
794
+ entry.bytes,
795
+ ])),
724
796
  injectedEstimatedTokens: estimateTokens(sizes.chars),
725
797
  injectedTokenBasis: "chars-per-token-4-estimate-not-provider-measured",
726
798
  injectedCountsCapped: sizes.capped,
@@ -881,6 +953,13 @@ export function logCacheUsage(message, dbg, context) {
881
953
  const injectedCharsSinceLastTurn = state.injectedCharsSinceLastUsage;
882
954
  const newTranscriptCharsSinceLastTurn = state.newTranscriptCharsSinceLastUsage;
883
955
  const attributionCharsCapped = state.attributionCharsCapped;
956
+ const injectedBytes = {
957
+ ...state.injectedBytes,
958
+ turnEndAdvisory: state.turnEndAdvisoryBytes + state.injectedBytes.turnEndAdvisory,
959
+ };
960
+ const injectedFindingsRepeated = state.injectedFindingsRepeated;
961
+ const toolResultBytes = state.toolResultBytes;
962
+ const toolResultsTruncated = state.toolResultsTruncated;
884
963
  // This record is the turn boundary: reset the per-turn accumulators and
885
964
  // re-arm the prefix-break flag so the next verdict describes the NEXT gap.
886
965
  state.lastUsageAtMs = nowMs;
@@ -898,6 +977,11 @@ export function logCacheUsage(message, dbg, context) {
898
977
  state.injectedCharsSinceLastUsage = 0;
899
978
  state.newTranscriptCharsSinceLastUsage = 0;
900
979
  state.attributionCharsCapped = false;
980
+ state.injectedBytes = emptyInjectedBytes();
981
+ state.injectedFindingsRepeated = 0;
982
+ state.turnEndAdvisoryBytes = 0;
983
+ state.toolResultBytes = 0;
984
+ state.toolResultsTruncated = 0;
901
985
  // Clear the request stamp too: the next turn measures from ITS request, and
902
986
  // a turn whose `context` call pi-lens never saw must fall back rather than
903
987
  // reuse this one.
@@ -939,6 +1023,13 @@ export function logCacheUsage(message, dbg, context) {
939
1023
  injectedCharsSinceLastTurn,
940
1024
  newTranscriptCharsSinceLastTurn,
941
1025
  attributionCharsCapped,
1026
+ injectedBytes,
1027
+ injectedFindingsRepeated,
1028
+ // Delivered tool-result aggregation over the turn (#2800 item 7):
1029
+ // the sum of each delivered footer's bytes= figure and the count
1030
+ // of results whose bound fired.
1031
+ toolResultBytes,
1032
+ toolResultsTruncated,
942
1033
  ...(context
943
1034
  ? {
944
1035
  // MessageEndEvent has no request/context id in the host API. These
@@ -1126,6 +1217,29 @@ export function clearCachePrefixSession(sessionId, sessionRole) {
1126
1217
  // a stale gap baseline and a stale prefix-break flag.
1127
1218
  attributionBySession.delete(key);
1128
1219
  }
1220
+ /** Record the exact quiet-window summary content handed to pi.sendMessage. */
1221
+ export function recordTurnEndAdvisoryBytes(sessionId, bytes, sessionRole = "primary") {
1222
+ const state = attributionFor(attributionKey(sessionId, sessionRole));
1223
+ state.turnEndAdvisoryBytes = Math.min(MAX_INJECTED_BYTES, state.turnEndAdvisoryBytes + Math.max(0, bytes));
1224
+ }
1225
+ /**
1226
+ * Fold one delivered tool result into the per-turn aggregation the
1227
+ * `cache_usage` row reports as `toolResultBytes` / `toolResultsTruncated`
1228
+ * (#2800 item 7). `bytes` is the delivered payload's UTF-8 byte count as
1229
+ * stamped in the result's own footer, and `truncated` is the footer's
1230
+ * truncated flag. Called from each host adapter's delivery seam; the row is
1231
+ * emitted by `logCacheUsage`, which resets both figures at the turn boundary.
1232
+ * The byte sum saturates at the attribution bound so the row stays bounded.
1233
+ */
1234
+ export function recordToolResultDelivery(args) {
1235
+ const state = attributionFor(attributionKey(args.sessionId, args.sessionRole));
1236
+ const bytes = Number.isFinite(args.bytes)
1237
+ ? Math.max(0, Math.floor(args.bytes))
1238
+ : 0;
1239
+ state.toolResultBytes = Math.min(MAX_ATTRIBUTION_BYTES, state.toolResultBytes + bytes);
1240
+ if (args.truncated === true)
1241
+ state.toolResultsTruncated += 1;
1242
+ }
1129
1243
  /**
1130
1244
  * Emit one bounded session summary and retire its attribution state. The
1131
1245
  * summary contains fixed-key counters only; it never serializes transcript or
@@ -1165,6 +1279,13 @@ export function resetCachePrefixObservation() {
1165
1279
  prefixHashBySession.clear();
1166
1280
  attributionBySession.clear();
1167
1281
  }
1282
+ export function resetCacheFindingIdentitiesSession(sessionId, sessionRole) {
1283
+ const state = attributionBySession.get(attributionKey(sessionId, sessionRole));
1284
+ if (!state)
1285
+ return;
1286
+ state.findingIdentities.clear();
1287
+ state.findingIdentityCapRecorded = false;
1288
+ }
1168
1289
  export function _attributionBySessionHasForTests(key) {
1169
1290
  return attributionBySession.has(key);
1170
1291
  }
@@ -13,6 +13,7 @@
13
13
  import { createSubsystemLogger } from "./extension-log.js";
14
14
  import * as fs from "node:fs";
15
15
  import * as path from "node:path";
16
+ import { calcCyclomaticComplexity } from "./dispatch/facts/function-facts.js";
16
17
  import { firstChildOfType, withTreeSitterRoot, isComplexityLanguageId, isComplexitySupportedFile, resolveTreeSitterLanguage, walk, } from "./tree-sitter-shared.js";
17
18
  // JS/TS/JSX share one grammar shape.
18
19
  const JSTS = {
@@ -190,6 +191,9 @@ function isLogicalOp(node, nodes) {
190
191
  }
191
192
  /** Cyclomatic contribution of a subtree: decision points + logical operators. */
192
193
  function subtreeCyclomatic(root, nodes) {
194
+ // Dispatch uses 1-based McCabe; this client exposes 0-based contributions (#2697).
195
+ if (nodes === JSTS)
196
+ return calcCyclomaticComplexity(root) - 1;
193
197
  let cc = 0;
194
198
  walk(root, (n) => {
195
199
  if (nodes.decision.has(n.type))
@@ -438,6 +442,7 @@ export class ComplexityClient {
438
442
  aiCommentPatterns: countAICommentPatterns(content),
439
443
  singleUseFunctions: countSingleUseFunctions(functions),
440
444
  tryCatchCount: countTryCatch(root, nodes),
445
+ functions,
441
446
  };
442
447
  }
443
448
  aggregateFunctionStats(functions) {
@@ -171,6 +171,8 @@ export const CONFIG_DIAGNOSTIC_CODES = {
171
171
  * suppressed" about a configuration that applied nothing at all.
172
172
  */
173
173
  PILENS_CFG_0008: "config resolution failed; whole configuration ignored",
174
+ /** A tool key is unknown or names a required, non-disableable tool. */
175
+ PILENS_CFG_0009: "unknown or non-disableable tool config key ignored",
174
176
  };
175
177
  /** The shape every code in the namespace must match. */
176
178
  export const CONFIG_DIAGNOSTIC_CODE_PATTERN = /^PILENS_CFG_\d{4}$/;
@@ -740,8 +740,8 @@ export function reportPiLensConfigRecords(records) {
740
740
  */
741
741
  export function ignoredRecordCollector(configPath, tier) {
742
742
  const noted = [];
743
- const note = (reason) => {
744
- noted.push({
743
+ const note = (reason, code = "PILENS_CFG_0001") => {
744
+ let notedRecord = {
745
745
  code: "PILENS_CFG_0001",
746
746
  file: configPath,
747
747
  key: "",
@@ -752,7 +752,10 @@ export function ignoredRecordCollector(configPath, tier) {
752
752
  sourceText: reason.sourceText,
753
753
  }),
754
754
  tier,
755
- });
755
+ };
756
+ if (code !== "PILENS_CFG_0001")
757
+ notedRecord.code = code;
758
+ noted.push(notedRecord);
756
759
  };
757
760
  return {
758
761
  note,
@@ -42,6 +42,7 @@ import { LEGACY_ROOT_LSP_KEYS, LSP_NAMESPACE_KEY } from "./config-locations.js";
42
42
  // every config loader's import graph for one type alias.
43
43
  import { DENY_KEY, } from "./config-core/schema.js";
44
44
  import { flagConfigSectionKeys, GLOBAL_NON_FLAG_CONFIG_SECTIONS, LENS_FLAGS, PROJECT_FOREIGN_CONFIG_NAMESPACES, PROJECT_NON_FLAG_CONFIG_SECTIONS, } from "./lens-flag-registry.js";
45
+ import { LENS_TOOL_NAMES } from "./tool-config.js";
45
46
  /** The JSON Schema dialect the published artifact declares. */
46
47
  const CONFIG_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
47
48
  /**
@@ -154,6 +155,22 @@ function buildConfigSchema() {
154
155
  ]) {
155
156
  properties[key] = opaque("stable");
156
157
  }
158
+ properties.tools = {
159
+ type: "object",
160
+ additionalProperties: true,
161
+ properties: Object.fromEntries(LENS_TOOL_NAMES.map((name) => [
162
+ name,
163
+ {
164
+ type: "object",
165
+ additionalProperties: true,
166
+ properties: {
167
+ enabled: { type: "boolean", [STABILITY_TIER_KEY]: "experimental" },
168
+ },
169
+ [STABILITY_TIER_KEY]: "experimental",
170
+ },
171
+ ])),
172
+ [STABILITY_TIER_KEY]: "stable",
173
+ };
157
174
  // Namespaces owned by another tool that ride in the same file (`trivy`,
158
175
  // `helm`). Reserved so they survive validation, `experimental` because their
159
176
  // shape belongs to those runners, not to this schema.
@@ -22,7 +22,7 @@
22
22
  * extraction — the prose is not API (the code is), but silently rewording a
23
23
  * warning inside a refactor is still not a thing this PR does.
24
24
  */
25
- import { DEPRECATED_CONFIG_SURFACES, } from "./config-diagnostic-codes.js";
25
+ import { DEPRECATED_CONFIG_SURFACES, withConfigDiagnosticCode, } from "./config-diagnostic-codes.js";
26
26
  import { recordDegradationOnce } from "./degradation-ledger.js";
27
27
  import { errorClassName } from "./error-class.js";
28
28
  import { logExtension } from "./extension-log.js";
@@ -378,7 +378,7 @@ export function warnIgnoredConfigOnce(options) {
378
378
  logExtension({
379
379
  subsystem,
380
380
  level: "warn",
381
- message,
381
+ message: withConfigDiagnosticCode(message, code),
382
382
  metadata: { configPath: file, reason, code, ...(key ? { key } : {}) },
383
383
  });
384
384
  // HUMAN-audience too: a config the user wrote is being ignored. Routed
@@ -19,6 +19,7 @@ import { findLocalBinsAt, VENV_BIN_DIRS } from "./package-manager.js";
19
19
  import { findNearestMarkerRoot } from "./path-utils.js";
20
20
  import { getScratchTreeFnmatchPatterns } from "./scratch-tree-policy.js";
21
21
  import { safeSpawnAsync } from "./safe-spawn.js";
22
+ import { probeToolAsync } from "./tool-probe.js";
22
23
  import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
23
24
  import { firstOutputLine, spawnFailedWithNoOutput, } from "./dispatch/runners/utils/spawn-outcome.js";
24
25
  import { formatToolFailure } from "./dispatch/runners/utils/tool-failure.js";
@@ -229,7 +230,7 @@ export class PythonDeadCodeClient {
229
230
  let probe;
230
231
  let hostStallMs;
231
232
  try {
232
- probe = await safeSpawnAsync(c.cmd, [...c.prefix, "--version"], {
233
+ probe = await probeToolAsync(c.cmd, [...c.prefix, "--version"], {
233
234
  timeout: 5000,
234
235
  });
235
236
  }
@@ -415,9 +416,13 @@ export class PythonDeadCodeClient {
415
416
  durationMs,
416
417
  };
417
418
  }
419
+ // vulture's verified exit-code table (above): exit 0 with empty
420
+ // stdout IS the clean-run signal, so this ran over the root
421
+ // (#2154) — unlike knip, whose clean run always prints JSON.
418
422
  return {
419
423
  ...emptyResult(this.language),
420
424
  success: true,
425
+ analyzed: true,
421
426
  summary: "No dead code found",
422
427
  durationMs,
423
428
  };
@@ -430,6 +435,8 @@ export class PythonDeadCodeClient {
430
435
  return {
431
436
  ...emptyResult(this.language),
432
437
  success: true,
438
+ // #2154: parsed vulture output for this root.
439
+ analyzed: true,
433
440
  unusedExports,
434
441
  summary: total === 0
435
442
  ? "No dead code found"
@@ -815,7 +815,11 @@ export class DependencyChecker {
815
815
  }
816
816
  }
817
817
  this.publishState(gen, circular, circularFiles);
818
- return { circular, count: circular.length };
818
+ // #2154: the one madge site that parsed a graph for this root. Every
819
+ // other return here (missing root, no top-level source file, madge
820
+ // unavailable, spawn error, parse throw) is the SAME empty shape and
821
+ // must not be read as "no cycles in this project".
822
+ return { circular, count: circular.length, analyzed: true };
819
823
  }
820
824
  catch (err) {
821
825
  this.log(`Scan error: ${err.message}`);
@@ -29,7 +29,7 @@ import { isSpawnableCommand } from "../installer/index.js";
29
29
  import { isAbsoluteFilePath, normalizeEphemeralMapKey, normalizeMapKey, } from "../path-utils.js";
30
30
  import { loadPiLensProjectConfig } from "../project-lens-config.js";
31
31
  import { RUNTIME_CONFIG, getRunnerTimeoutFloorMs } from "../runtime-config.js";
32
- import { safeSpawnAsync } from "../safe-spawn.js";
32
+ import { probeToolAsync } from "../tool-probe.js";
33
33
  import { classifyDiagnostic } from "./diagnostic-taxonomy.js";
34
34
  import { classifyProbeFailure, logAvailabilityDecision, startHostStallSampler, transientRetryDelayMs, } from "./runners/utils/availability-policy.js";
35
35
  import { recordAvailabilityProbeOverrun, getDispatchAvailabilityGeneration, } from "./runners/utils/runner-helpers.js";
@@ -147,7 +147,7 @@ export async function checkToolAvailability(command, facts) {
147
147
  let hostStallMs;
148
148
  let probeJoined = false;
149
149
  try {
150
- const shared = dispatcherProbeFlights.run(`dispatcher:${key}`, () => safeSpawnAsync(command, ["--version"], {
150
+ const shared = dispatcherProbeFlights.run(`dispatcher:${key}`, () => probeToolAsync(command, ["--version"], {
151
151
  timeout: TOOL_PROBE_TIMEOUT_MS,
152
152
  }));
153
153
  probeJoined = shared.joined;
@@ -285,11 +285,13 @@ telemetryModel, telemetryProvider) {
285
285
  async hasTool(command) {
286
286
  return checkToolAvailability(command, facts);
287
287
  },
288
- log(message) {
288
+ log(message, level) {
289
289
  // #1333: pi owns the terminal — a runner advisory must never be a raw
290
290
  // write. Every DispatchContext.log line lands in extension.log instead.
291
291
  logExtension({
292
292
  subsystem: "dispatch",
293
+ // exactOptionalPropertyTypes: an omitted level must stay omitted.
294
+ ...(level !== undefined && { level }),
293
295
  message,
294
296
  metadata: { filePath: normalizedFilePath, kind },
295
297
  });
@@ -446,31 +448,49 @@ function buildCoverageNotice(ctx, runnerLatencies) {
446
448
  ...new Set(relevant.flatMap((r) => r.unconfirmedServerIds ?? [])),
447
449
  ];
448
450
  if (unconfirmedServerIds.length > 0) {
449
- // The marker describes this exact silent-scanner set. A scanner can
450
- // recover while another goes dark on the same file, so the set belongs
451
- // in the session dedupe identity rather than only kind and path.
451
+ const deferredIds = new Set(relevant.flatMap((r) => r.deferredServerIds ?? []));
452
+ const markerFor = (ids) => {
453
+ const shown = ids.slice(0, 4);
454
+ const remainder = ids.length - shown.length;
455
+ return `${shown.join(", ")}${remainder > 0 ? ` +${remainder}` : ""}`;
456
+ };
457
+ const deferredServerIds = unconfirmedServerIds.filter((id) => deferredIds.has(id));
458
+ const silentServerIds = unconfirmedServerIds.filter((id) => !deferredIds.has(id));
459
+ // The marker describes this exact scanner set. A scanner can recover
460
+ // while another goes dark on the same file, so the set belongs in the
461
+ // session dedupe identity rather than only kind and path.
452
462
  // #2016: these are SCANNER IDS, not filesystem paths. `normalizeMapKey`
453
463
  // would realpath each one; on Windows that fails, falls through to
454
464
  // `resolveNonExisting`, and resolves the id against the CURRENT process
455
465
  // cwd, so the dedupe key differed by platform and by cwd (the #2219
456
466
  // non-path-sentinel class). The cheap syntactic fold is what this
457
- // session-scoped dedupe key actually needs.
458
- const silentScannerSet = [...new Set(unconfirmedServerIds)]
467
+ // session-scoped dedupe key actually needs. Code-unit comparator: the
468
+ // sorted set is a KEY, so ordering must be deterministic across locales
469
+ // — localeCompare is not.
470
+ const dedupeSet = (ids) => [...new Set(ids)]
459
471
  .map(normalizeEphemeralMapKey)
460
- // Code-unit comparator: the sorted set is a dedupe KEY, so ordering
461
- // must be deterministic across locales — localeCompare is not.
462
472
  .sort((a, b) => Number(a > b) - Number(a < b))
463
473
  .join(",");
464
- const onceKey = `${ctx.kind}:${ctx.filePath}:${silentScannerSet}`;
474
+ // #2810 round 4: the PARTITION is part of the identity, not just the
475
+ // scanner set. The same scanner can be silent on one edit and marked for
476
+ // late delivery on the next; keying on the unconfirmed set alone showed
477
+ // the session whichever message came first and suppressed the other, so a
478
+ // scanner that recovered a delivery path (or lost one) kept the stale
479
+ // wording for the rest of the session.
480
+ const onceKey = `${ctx.kind}:${ctx.filePath}:${dedupeSet(silentServerIds)}|${dedupeSet(deferredServerIds)}`;
465
481
  if (coverageNoticeSeen.has(onceKey))
466
482
  return undefined;
467
483
  coverageNoticeSeen.add(onceKey);
468
- const shown = unconfirmedServerIds.slice(0, 4);
469
- const remainder = unconfirmedServerIds.length - shown.length;
470
- const marker = `${shown.join(", ")}${remainder > 0 ? ` +${remainder}` : ""}`;
484
+ const coverageParts = [];
485
+ if (deferredServerIds.length > 0) {
486
+ coverageParts.push(`coverage: ${markerFor(deferredServerIds)} deferred diagnostics are incomplete; findings arrive at turn end if the scan lands.`);
487
+ }
488
+ if (silentServerIds.length > 0) {
489
+ coverageParts.push(`coverage: ${markerFor(silentServerIds)} silent — diagnostics are incomplete (not a clean result).`);
490
+ }
471
491
  return {
472
492
  id: `coverage-partial:${ctx.kind}:${path.basename(ctx.filePath)}`,
473
- message: `coverage: ${marker} silent — diagnostics are incomplete (not a clean result).`,
493
+ message: coverageParts.join("\n"),
474
494
  filePath: ctx.filePath,
475
495
  severity: "warning",
476
496
  semantic: "warning",
@@ -837,6 +857,9 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
837
857
  ...(result.unconfirmedServerIds !== undefined && {
838
858
  unconfirmedServerIds: result.unconfirmedServerIds,
839
859
  }),
860
+ ...(result.deferredServerIds !== undefined && {
861
+ deferredServerIds: result.deferredServerIds,
862
+ }),
840
863
  });
841
864
  logLatency({
842
865
  type: "runner",
@@ -135,7 +135,7 @@ function isCallPassThrough(stmt, paramNames) {
135
135
  }
136
136
  return { pass: true, target: (expr.children ?? [])[0]?.text };
137
137
  }
138
- function calcCyclomaticComplexity(body) {
138
+ export function calcCyclomaticComplexity(body) {
139
139
  let cc = 1;
140
140
  walk(body, (node) => {
141
141
  if (COMPLEXITY_TYPES.has(node.type)) {
@@ -1,6 +1,10 @@
1
1
  import { logLatency } from "../../latency-logger.js";
2
2
  import { isJstsFactFile } from "../../file-kinds.js";
3
3
  import { childrenOfType, firstChildOfType, withFactTree, walk, } from "./tree-sitter-facts.js";
4
+ /** Whether the import-facts provider applies to a source path. */
5
+ export function importFactsApplyTo(filePath) {
6
+ return isJstsFactFile(filePath);
7
+ }
4
8
  function stripQuotes(raw) {
5
9
  return raw.replace(/^["'`]+|["'`]+$/g, "");
6
10
  }
@@ -69,7 +73,7 @@ export const importFactProvider = {
69
73
  provides: ["file.imports", "file.reexports", "file.importFactsCoverage"],
70
74
  requires: ["file.content"],
71
75
  appliesTo(ctx) {
72
- return isJstsFactFile(ctx.filePath);
76
+ return importFactsApplyTo(ctx.filePath);
73
77
  },
74
78
  async run(ctx, store) {
75
79
  const content = store.getFileFact(ctx.filePath, "file.content");
@@ -1,5 +1,6 @@
1
1
  import 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
  const actionlint = createAvailabilityChecker("actionlint", ".exe");
@@ -66,7 +67,7 @@ const actionlintRunner = {
66
67
  return isGitHubWorkflowFile(ctx.filePath);
67
68
  },
68
69
  async run(ctx) {
69
- const cwd = ctx.cwd || process.cwd();
70
+ const cwd = resolveRunnerCwd(ctx, "actionlint");
70
71
  let cmd = null;
71
72
  if (await actionlint.isAvailableAsync(cwd)) {
72
73
  cmd = actionlint.getCommand(cwd);
@@ -5,13 +5,12 @@
5
5
  * Autofix is handled earlier by the post-write pipeline to avoid
6
6
  * mutating files mid-dispatch after LSP sync has already happened.
7
7
  */
8
- import * as path from "node:path";
9
8
  import { incrementDegradationCount } from "../../degradation-ledger.js";
10
9
  import { mapWithConcurrency } from "../../dependency-checker.js";
11
10
  import { safeSpawnAsync } from "../../safe-spawn.js";
12
11
  import { biomeConfigArgs, getAutofixCapability, getJstsLintPolicyForCwd, } from "../../tool-policy.js";
13
12
  import { PRIORITY } from "../priorities.js";
14
- import { resolveToolCommandWithInstallFallback } from "./utils/runner-helpers.js";
13
+ import { resolveRunnerCwd, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
15
14
  import { finishParsedRun } from "./utils/tool-failure.js";
16
15
  /**
17
16
  * Map biome's own severity vocabulary onto the four-tier `Diagnostic.severity`
@@ -198,7 +197,7 @@ const biomeCheckJsonRunner = {
198
197
  appliesTo: ["jsts"],
199
198
  priority: PRIORITY.FORMAT_AND_LINT_PRIMARY,
200
199
  async run(ctx) {
201
- const cwd = ctx.cwd || path.dirname(ctx.filePath);
200
+ const cwd = resolveRunnerCwd(ctx, "biome");
202
201
  const policy = getJstsLintPolicyForCwd(cwd);
203
202
  // Defer to ESLint/oxlint if the project has explicitly configured one —
204
203
  // biome runs as the default linter only when no alternative is present.
@@ -1,6 +1,8 @@
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 { probeToolAsync } from "../../tool-probe.js";
5
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
4
6
  import { PRIORITY } from "../priorities.js";
5
7
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
6
8
  // Per-compiler availability checkers — each one caches per-cwd and
@@ -100,8 +102,7 @@ async function resolveCompiler(absPath, cwd) {
100
102
  // Mirrors the deliberate global-PATH probes in utils/runner-helpers.ts:
101
103
  // this never resolves a config file or a target path, so there is no cwd
102
104
  // for it to get wrong.
103
- // cwd-exempt: presence probe only -- `cl` with no args and no target file, used solely to detect whether MSVC is on PATH
104
- const clProbe = await safeSpawnAsync("cl", [], { timeout: 5000 });
105
+ const clProbe = await probeToolAsync("cl", [], { timeout: 5000 });
105
106
  if (!clProbe.error && clProbe.status !== null) {
106
107
  return {
107
108
  command: "cl",
@@ -181,7 +182,7 @@ const cppCheckRunner = {
181
182
  priority: PRIORITY.GENERAL_ANALYSIS,
182
183
  skipTestFiles: false,
183
184
  async run(ctx) {
184
- const cwd = ctx.cwd || process.cwd();
185
+ const cwd = resolveRunnerCwd(ctx, "cpp-check");
185
186
  const absPath = path.resolve(cwd, ctx.filePath);
186
187
  const compiler = await resolveCompiler(absPath, cwd);
187
188
  if (!compiler) {
@@ -1,6 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import * as fs from "node:fs";
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 { createCwdCachedProbe } from "./utils/runner-helpers.js";
6
7
  const CREDO_PROBE_BUDGET_MS = 10_000;
@@ -50,7 +51,7 @@ const credoRunner = {
50
51
  priority: PRIORITY.GENERAL_ANALYSIS,
51
52
  skipTestFiles: false,
52
53
  async run(ctx) {
53
- const cwd = ctx.cwd || process.cwd();
54
+ const cwd = resolveRunnerCwd(ctx, "credo");
54
55
  if (!hasMixExs(cwd)) {
55
56
  return { status: "skipped", diagnostics: [], semantic: "none" };
56
57
  }
@@ -104,6 +104,7 @@
104
104
  import * as fs from "node:fs";
105
105
  import * as path from "node:path";
106
106
  import { safeSpawnAsync } from "../../safe-spawn.js";
107
+ import { resolveRunnerCwd } from "../../tool-cwd.js";
107
108
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
108
109
  import { spawnFailedWithNoOutput } from "./utils/spawn-outcome.js";
109
110
  import { PRIORITY } from "../priorities.js";
@@ -281,7 +282,7 @@ const cueVetRunner = {
281
282
  skipTestFiles: false,
282
283
  timeoutMs: 30_000,
283
284
  async run(ctx) {
284
- const cwd = ctx.cwd || process.cwd();
285
+ const cwd = resolveRunnerCwd(ctx, "cue-vet");
285
286
  let cmd = null;
286
287
  if (await cue.isAvailableAsync(cwd)) {
287
288
  cmd = cue.getCommand(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 { createAvailabilityChecker } from "./utils/runner-helpers.js";
4
5
  import { PRIORITY } from "../priorities.js";
5
6
  const dart = createAvailabilityChecker("dart", ".exe");
@@ -147,7 +148,7 @@ const dartAnalyzeRunner = {
147
148
  priority: PRIORITY.GENERAL_ANALYSIS,
148
149
  skipTestFiles: false,
149
150
  async run(ctx) {
150
- const cwd = ctx.cwd || process.cwd();
151
+ const cwd = resolveRunnerCwd(ctx, "dart-analyze");
151
152
  const absPath = path.resolve(cwd, ctx.filePath);
152
153
  const dartAvailable = await dart.isAvailableAsync(cwd);
153
154
  const flutterAvailable = !dartAvailable && (await flutter.isAvailableAsync(cwd));
@@ -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 { getLinterPolicyForCwd } from "../../tool-policy.js";
5
6
  import { PRIORITY } from "../priorities.js";
6
7
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
@@ -129,7 +130,7 @@ const detektRunner = {
129
130
  timeoutMs: 90_000,
130
131
  skipTestFiles: false,
131
132
  async run(ctx) {
132
- const cwd = ctx.cwd || process.cwd();
133
+ const cwd = resolveRunnerCwd(ctx, "detekt");
133
134
  const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
134
135
  if (policy && !policy.preferredRunners.includes("detekt")) {
135
136
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -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 } from "./utils/runner-helpers.js";
6
7
  const dotnet = createAvailabilityChecker("dotnet", ".exe");
@@ -118,7 +119,7 @@ const dotnetBuildRunner = {
118
119
  timeoutMs: 90_000,
119
120
  skipTestFiles: false,
120
121
  async run(ctx) {
121
- const cwd = ctx.cwd || process.cwd();
122
+ const cwd = resolveRunnerCwd(ctx, "dotnet-build");
122
123
  if (!(await dotnet.isAvailableAsync(cwd))) {
123
124
  return { status: "skipped", diagnostics: [], semantic: "none" };
124
125
  }