@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
@@ -26,6 +26,8 @@ import { logSessionStart } from "../sessionstart-logger.js";
26
26
  import { findLocalSgconfig, resolveBaselineSgconfig } from "../sgconfig.js";
27
27
  import { findLocalTyposConfig } from "../typos-config.js";
28
28
  import { resolvePackagePath } from "../package-root.js";
29
+ import { resolveToolCwd } from "../tool-cwd.js";
30
+ import { recordDegradationOnce } from "../degradation-ledger.js";
29
31
  import { hasCargoWorkspaceTable, readCargoWorkspaceExclude, readCargoWorkspaceMembers, readTextFileOrUndefined, } from "../cargo-manifest.js";
30
32
  import { resolveAstGrepNativeExe } from "./wait-policy/index.js";
31
33
  import { hasSpawnFailureKind, isCommandAvailableAsync, safeSpawnAsync, } from "../safe-spawn.js";
@@ -36,6 +38,48 @@ import { normalizeMapKey } from "./path-utils.js";
36
38
  import { getRubyVersionDirNamesSync } from "./ruby-drive-dirs.js";
37
39
  import { getProcessSingleton } from "../process-singletons.js";
38
40
  import { createGenerationSource, } from "../generation-guard.js";
41
+ function withRootMarkers(root, markers) {
42
+ root.rootMarkers = markers;
43
+ return root;
44
+ }
45
+ /** Resolve a server identity cwd through the shared tool-cwd seam. */
46
+ export async function resolveLspServerCwd(server, filePath, sessionCwd, onRootFailure) {
47
+ const rootMarkers = server.rootMarkers ?? server.root.rootMarkers;
48
+ let serverRoot;
49
+ let rootFailed = false;
50
+ try {
51
+ serverRoot = await server.root(filePath);
52
+ }
53
+ catch (error) {
54
+ const reason = error instanceof Error ? error.message : String(error);
55
+ rootFailed = true;
56
+ onRootFailure?.(reason);
57
+ }
58
+ const isFileDirFallback = serverRoot !== undefined &&
59
+ rootMarkers?.length &&
60
+ path.resolve(serverRoot) === path.resolve(path.dirname(filePath));
61
+ if (!serverRoot || isFileDirFallback) {
62
+ if (!serverRoot) {
63
+ recordDegradationOnce({
64
+ kind: "tool-cwd-resolution",
65
+ subject: server.id,
66
+ reason: `lsp:server-root-${rootFailed ? "failed" : "fallback"}:${filePath}`,
67
+ });
68
+ }
69
+ if (!rootMarkers?.length)
70
+ return undefined;
71
+ return resolveToolCwd("lsp", server.id, filePath, {
72
+ cwd: path.dirname(path.resolve(filePath)),
73
+ rootMarkers,
74
+ });
75
+ }
76
+ const boundedServerRoot = enforceLspRootCeiling(serverRoot, sessionCwd, filePath);
77
+ return resolveToolCwd("lsp", server.id, filePath, {
78
+ cwd: sessionCwd,
79
+ rootMarkers,
80
+ serverRoot: boundedServerRoot,
81
+ });
82
+ }
39
83
  const FIXTURE_ROOT_SEGMENTS = new Set(["__fixtures__", "testdata"]);
40
84
  const FALLBACK_PROJECT_MARKERS = [
41
85
  ".git",
@@ -845,12 +889,12 @@ function normalizeRootKey(root) {
845
889
  }
846
890
  function IgnoreHomeRoot(primary) {
847
891
  const homeKey = normalizeRootKey(os.homedir());
848
- return async (file) => {
892
+ return withRootMarkers(async (file) => {
849
893
  const root = await primary(file);
850
894
  if (!root)
851
895
  return undefined;
852
896
  return normalizeRootKey(root) === homeKey ? undefined : root;
853
- };
897
+ }, primary.rootMarkers ?? []);
854
898
  }
855
899
  function rubyBinCandidates(baseName) {
856
900
  const candidates = [];
@@ -879,6 +923,7 @@ function createInteractiveServer(spec) {
879
923
  name: spec.name,
880
924
  extensions: spec.extensions,
881
925
  root: spec.root,
926
+ rootMarkers: spec.root.rootMarkers,
882
927
  fallbackFor: spec.fallbackFor,
883
928
  availabilityKey: typeof spec.command === "string" && isSimpleCommand(spec.command)
884
929
  ? spec.command
@@ -917,29 +962,29 @@ function createInteractiveServer(spec) {
917
962
  }
918
963
  export function PriorityRoot(markerGroups, excludePatterns, stopDir) {
919
964
  const resolvers = markerGroups.map((markers) => NearestRoot(markers, excludePatterns, stopDir));
920
- return async (file) => {
965
+ return withRootMarkers(async (file) => {
921
966
  for (const resolve of resolvers) {
922
967
  const root = await resolve(file);
923
968
  if (root)
924
969
  return root;
925
970
  }
926
971
  return undefined;
927
- };
972
+ }, markerGroups.flat());
928
973
  }
929
974
  export const FileDirRoot = async (file) => {
930
975
  const candidate = path.resolve(path.dirname(file));
931
976
  return nearestNonExcludedFallbackRoot(candidate);
932
977
  };
933
978
  export function RootWithFallback(primary, fallback = FileDirRoot) {
934
- return async (file) => {
979
+ return withRootMarkers(async (file) => {
935
980
  const primaryRoot = await primary(file);
936
981
  if (primaryRoot)
937
982
  return primaryRoot;
938
983
  return fallback(file);
939
- };
984
+ }, primary.rootMarkers ?? []);
940
985
  }
941
986
  export function WorkspacePriorityRoot(markerGroups, excludePatterns) {
942
- return async (file) => PriorityRoot(markerGroups, excludePatterns, process.cwd())(file);
987
+ return withRootMarkers(async (file) => PriorityRoot(markerGroups, excludePatterns, process.cwd())(file), markerGroups.flat());
943
988
  }
944
989
  function isPermissionFsError(err) {
945
990
  const code = err?.code;
@@ -1010,7 +1055,7 @@ export function NearestRoot(includePatterns, excludePatterns, stopDir) {
1010
1055
  // re-walk cost for configless repos is a known trade-off; bounding the
1011
1056
  // walk with stopDir for in-cwd files is the tracked optimization (#1412).
1012
1057
  const inFlight = new Map();
1013
- return async (file) => {
1058
+ return withRootMarkers(async (file) => {
1014
1059
  // Cache key is the resolved directory — all files in the same dir share a root.
1015
1060
  const startDir = path.resolve(path.dirname(file));
1016
1061
  const dirKey = normalizeMapKey(startDir);
@@ -1052,6 +1097,7 @@ export function NearestRoot(includePatterns, excludePatterns, stopDir) {
1052
1097
  // glob markers like `*.csproj` match real project filenames (#201).
1053
1098
  for (const pattern of includePatterns) {
1054
1099
  if ((await markerExists(currentDir, pattern)) &&
1100
+ (pattern !== ".git" || currentDir !== os.tmpdir()) &&
1055
1101
  !(await isExcludedLspRoot(currentDir))) {
1056
1102
  return enforceLspRootCeiling(currentDir, process.cwd(), file);
1057
1103
  }
@@ -1073,7 +1119,7 @@ export function NearestRoot(includePatterns, excludePatterns, stopDir) {
1073
1119
  finally {
1074
1120
  inFlight.delete(dirKey);
1075
1121
  }
1076
- };
1122
+ }, includePatterns);
1077
1123
  }
1078
1124
  /** Alias kept for backward compatibility */
1079
1125
  export const createRootDetector = NearestRoot;
@@ -1579,7 +1625,7 @@ async function hasAgentLevelProjectMarker(extensionRootKey) {
1579
1625
  }
1580
1626
  return false;
1581
1627
  }
1582
- const TypeScriptRoot = DenoExcludeRoot(async (file) => {
1628
+ const TypeScriptRoot = withRootMarkers(DenoExcludeRoot(async (file) => {
1583
1629
  const extensionRootKey = piAgentExtensionsRootKey(file);
1584
1630
  if (extensionRootKey) {
1585
1631
  // Bounded walk so we never adopt a parent (e.g. ~/.pi/agent/) as the
@@ -1603,13 +1649,14 @@ const TypeScriptRoot = DenoExcludeRoot(async (file) => {
1603
1649
  if (projectRoot)
1604
1650
  return projectRoot;
1605
1651
  return FileDirRoot(file);
1606
- });
1652
+ }), [...TS_CONFIG_MARKERS, ...TS_TOOLING_MARKERS]);
1607
1653
  export const TypeScriptServer = {
1608
1654
  id: "typescript",
1609
1655
  name: "TypeScript Language Server",
1610
1656
  extensions: JS_TS_LSP_EXTENSIONS,
1611
1657
  autoPropagateDiagnostics: true,
1612
1658
  root: TypeScriptRoot,
1659
+ rootMarkers: TypeScriptRoot.rootMarkers,
1613
1660
  async spawn(root, options) {
1614
1661
  const fs = await import("node:fs/promises");
1615
1662
  const nativeLsp = await findNativeTypeScriptLsp(root);
@@ -1919,7 +1966,7 @@ function sessionHoistCeiling(sessionCwd, file) {
1919
1966
  }
1920
1967
  function RustWorkspaceRoot() {
1921
1968
  const crateRoot = createRootDetector(["Cargo.toml", "Cargo.lock"]);
1922
- return async (file) => {
1969
+ return withRootMarkers(async (file) => {
1923
1970
  const root = await crateRoot(file);
1924
1971
  if (!root)
1925
1972
  return undefined;
@@ -1958,7 +2005,7 @@ function RustWorkspaceRoot() {
1958
2005
  current = parent;
1959
2006
  }
1960
2007
  return root;
1961
- };
2008
+ }, crateRoot.rootMarkers ?? []);
1962
2009
  }
1963
2010
  /**
1964
2011
  * Resolve whether `parentPomPath`'s <modules> block declares `childDir` as one
@@ -2011,7 +2058,7 @@ function JavaWorkspaceRoot() {
2011
2058
  "build.gradle",
2012
2059
  ".classpath",
2013
2060
  ]);
2014
- return async (file) => {
2061
+ return withRootMarkers(async (file) => {
2015
2062
  const root = await moduleRoot(file);
2016
2063
  if (!root)
2017
2064
  return undefined;
@@ -2051,7 +2098,7 @@ function JavaWorkspaceRoot() {
2051
2098
  current = parent;
2052
2099
  }
2053
2100
  return enforceLspRootCeiling(lastHop, sessionCwd, file);
2054
- };
2101
+ }, moduleRoot.rootMarkers ?? []);
2055
2102
  }
2056
2103
  export const RustServer = {
2057
2104
  id: "rust",
@@ -8,6 +8,7 @@ import { normalizeMapKey } from "../path-utils.js";
8
8
  import { loadReverseDependencyIndexFromSnapshot } from "../reverse-deps.js";
9
9
  import { freshnessFromMtime } from "../freshness.js";
10
10
  import { logLatency } from "../latency-logger.js";
11
+ import { recordDegradationOnce } from "../degradation-ledger.js";
11
12
  import { compareOrdinal } from "../string-utils.js";
12
13
  import { workspaceDiagnosticsCacheSessionStart } from "./workspace-diagnostics-session.js";
13
14
  import { createDiskBindingCache, } from "./diagnostic-binding.js";
@@ -31,12 +32,17 @@ import { createDiskBindingCache, } from "./diagnostic-binding.js";
31
32
  * `LSPWorkspaceDiagnosticResult.timedOut` in `./index.ts`) into
32
33
  * `WorkspaceDiagnosticsCacheEntry`.
33
34
  */
35
+ // v3 (#2776): cached diagnostics must carry per-diagnostic `serverId`
36
+ // provenance before replay, because primary/auxiliary partitioning is
37
+ // server-owned rather than source-owned. v2 entries predate that contract and
38
+ // are rejected so the owning server can re-collect them instead of silently
39
+ // demoting old primary findings to auxiliary.
34
40
  // v2 (#1095): entries may carry an optional `contentHash` fingerprint of the
35
41
  // file bytes the cached diagnostics were computed against, so a `lookup` can
36
42
  // surface a content `binding` (boundToCurrentDisk) beyond the mtime proxy.
37
43
  // Legacy v1 entries are rejected by the version guard and re-scanned — a
38
44
  // deliberately clean break so no entry lacking the field is ever served.
39
- export const WORKSPACE_DIAGNOSTICS_CACHE_VERSION = 2;
45
+ export const WORKSPACE_DIAGNOSTICS_CACHE_VERSION = 3;
40
46
  const CACHE_FILE = "lsp-workspace-diagnostics.json";
41
47
  function cachePath(cwd) {
42
48
  return path.join(getProjectDataDir(cwd), "cache", CACHE_FILE);
@@ -161,11 +167,34 @@ export function loadWorkspaceDiagnosticsCache(cwd) {
161
167
  if (!parsed || typeof parsed !== "object")
162
168
  return undefined;
163
169
  const cache = parsed;
164
- if (cache.version !== WORKSPACE_DIAGNOSTICS_CACHE_VERSION)
170
+ if (cache.version !== WORKSPACE_DIAGNOSTICS_CACHE_VERSION) {
171
+ if (cache.version === 1 || cache.version === 2) {
172
+ recordDegradationOnce({
173
+ kind: "lsp-workspace-cache-migration",
174
+ subject: path.resolve(cwd),
175
+ reason: `rejected v${cache.version} cache with ${cache.entries && typeof cache.entries === "object"
176
+ ? Object.keys(cache.entries).length
177
+ : 0} entries during provenance migration`,
178
+ });
179
+ }
165
180
  return undefined;
181
+ }
166
182
  if (!cache.entries || typeof cache.entries !== "object")
167
183
  return undefined;
168
- return cache;
184
+ const validEntries = {};
185
+ for (const [key, entry] of Object.entries(cache.entries)) {
186
+ if (!entry || typeof entry !== "object")
187
+ continue;
188
+ if (!Array.isArray(entry.diagnostics) ||
189
+ entry.diagnostics.some((diagnostic) => !diagnostic ||
190
+ typeof diagnostic !== "object" ||
191
+ typeof diagnostic.serverId !== "string" ||
192
+ diagnostic.serverId.length === 0)) {
193
+ continue;
194
+ }
195
+ validEntries[key] = entry;
196
+ }
197
+ return { ...cache, entries: validEntries };
169
198
  });
170
199
  }
171
200
  export function saveWorkspaceDiagnosticsCache(cwd, cache) {
@@ -470,6 +499,9 @@ export function createWorkspaceDiagnosticsCacheContext(cwd) {
470
499
  depIndexColdRefusalCount = 0;
471
500
  };
472
501
  return {
502
+ importsFor(filePath) {
503
+ return getImports(filePath);
504
+ },
473
505
  lookup(filePath, scopeKey) {
474
506
  // #1669 review N4: the epoch guarded `persist()` but not `lookup()` —
475
507
  // an in-flight sweep kept REPLAYING disowned entries into its OWN
@@ -552,11 +584,12 @@ export function createWorkspaceDiagnosticsCacheContext(cwd) {
552
584
  };
553
585
  },
554
586
  record(filePath, scopeKey, diagnostics, mtimeMs, contentHash, sizeBytes) {
587
+ const scanGeneration = Date.now();
555
588
  entries[cacheKeyFor(filePath)] = {
556
589
  diagnostics,
557
590
  count: diagnostics.length,
558
591
  mtimeMs,
559
- scannedAt: Date.now(),
592
+ scannedAt: scanGeneration,
560
593
  scopeKey,
561
594
  // #1793: stamp whether THIS FILE actually had dependency
562
595
  // knowledge this sweep (not just whether SOME index was
@@ -367,6 +367,9 @@ export async function analyzeFile(filePath, cwd, options = {}) {
367
367
  const lspRunner = latencyReport?.runners.find((runner) => runner.runnerId === "lsp");
368
368
  const lsp = lspRunner
369
369
  ? {
370
+ // `deferred` means the runner entered the LSP path but its auxiliary
371
+ // coverage is delivered later, so it still counts as ran here. The
372
+ // status remains available to surfaces that need the coverage detail.
370
373
  ran: lspRunner.status !== "skipped" &&
371
374
  lspRunner.status !== "when_skipped" &&
372
375
  lspRunner.status !== "test_file_skipped",
@@ -30,6 +30,7 @@ import { RuntimeCoordinator } from "../runtime-coordinator.js";
30
30
  import { handleSessionStart } from "../runtime-session.js";
31
31
  import { handleTurnEnd } from "../runtime-turn.js";
32
32
  import { createMcpHost } from "./host-shim.js";
33
+ import { startSituationalToolTelemetrySession } from "../situational-tool-telemetry.js";
33
34
  let contextPromise;
34
35
  let resolvedRuntime;
35
36
  /**
@@ -79,6 +80,7 @@ function joinMessages(consumed) {
79
80
  * `pilens_health` afterwards for the scan results as they land.
80
81
  */
81
82
  export async function runSessionStart(cwd) {
83
+ startSituationalToolTelemetrySession("mcp");
82
84
  const ctx = await getMcpSessionContext();
83
85
  const host = createMcpHost(undefined, cwd);
84
86
  await handleSessionStart({
@@ -148,8 +148,10 @@ export class OpengrepClient extends SecurityScanClient {
148
148
  };
149
149
  }
150
150
  const findings = parseOpengrepReport(fs.readFileSync(reportPath, "utf-8"));
151
+ // #2154: the one opengrep site that parsed a scan of this root.
151
152
  return {
152
153
  success: true,
154
+ analyzed: true,
153
155
  findings,
154
156
  scannedAt,
155
157
  };
@@ -18,6 +18,7 @@ import * as os from "node:os";
18
18
  import * as path from "node:path";
19
19
  import { classifyProbeFailure, createAvailabilityLatch, isTransientDecision, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
20
20
  import { safeSpawnAsync } from "./safe-spawn.js";
21
+ import { probeToolAsync } from "./tool-probe.js";
21
22
  import { createAvailabilityProbeFlight } from "./availability-probe-flight.js";
22
23
  import { createGenerationSource, } from "./generation-guard.js";
23
24
  import { isAtOrAboveHomeDir, walkUpDirs } from "./path-utils.js";
@@ -114,7 +115,7 @@ async function probeAvailability(pm) {
114
115
  let result;
115
116
  let hostStallMs;
116
117
  try {
117
- result = await safeSpawnAsync(finder, [pm], { timeout: PROBE_TIMEOUT_MS });
118
+ result = await probeToolAsync(finder, [pm], { timeout: PROBE_TIMEOUT_MS });
118
119
  }
119
120
  finally {
120
121
  hostStallMs = sampler.stop();
@@ -91,8 +91,9 @@
91
91
  * gate says "config exists" while the carriage says "not found", silently
92
92
  * dropping the `--config` argv this module exists to carry.
93
93
  */
94
+ import { existsSync } from "node:fs";
94
95
  import * as path from "node:path";
95
- import { findLocalToolConfig } from "./path-utils.js";
96
+ import { resolveToolCwd } from "./tool-cwd.js";
96
97
  /**
97
98
  * In `computeConfigFiles()`'s own precedence order — `.php-cs-fixer.php`
98
99
  * before `.php-cs-fixer.dist.php` — so a same-directory precedence check can
@@ -109,6 +110,10 @@ const PHP_CS_FIXER_CONFIG_NAMES = [
109
110
  * agreeing with this tool's own (unceilinged) detection gates.
110
111
  */
111
112
  export function resolvePhpCsFixerConfig(filePath) {
112
- const startDir = path.dirname(path.resolve(filePath));
113
- return findLocalToolConfig(startDir, PHP_CS_FIXER_CONFIG_NAMES);
113
+ const absolute = path.resolve(filePath);
114
+ const startDir = resolveToolCwd("formatter", "php-cs-fixer", absolute, {
115
+ cwd: path.parse(absolute).root,
116
+ allowHomeMarker: true,
117
+ });
118
+ return PHP_CS_FIXER_CONFIG_NAMES.map((name) => path.join(startDir, name)).find(existsSync);
114
119
  }
@@ -34,6 +34,7 @@ import { clearGraphCache } from "./review-graph/builder.js";
34
34
  import { BoundedLruCache } from "./bounded-cache.js";
35
35
  import { RUNTIME_CONFIG } from "./runtime-config.js";
36
36
  import { getAmbientAbortSignal, safeSpawnAsync } from "./safe-spawn.js";
37
+ import { probeToolAsync } from "./tool-probe.js";
37
38
  import { bounded } from "./deadline-utils.js";
38
39
  import { enabledAuxiliaryLspServerIds } from "./dispatch/auxiliary-lsp.js";
39
40
  import { recordDegradationOnce } from "./degradation-ledger.js";
@@ -356,7 +357,7 @@ async function tryOxlintFix(filePath, cwd) {
356
357
  return detectFileChangedAfterCommand(filePath, cmd, ["--fix", filePath], cwd, [1]);
357
358
  }
358
359
  async function tryRustClippyFix(filePath) {
359
- const check = await safeSpawnAsync("cargo", ["--version"], { timeout: 5000 });
360
+ const check = await probeToolAsync("cargo", ["--version"], { timeout: 5000 });
360
361
  if (check.error || check.status !== 0)
361
362
  return [];
362
363
  const cargoDir = findNearestContaining(path.dirname(path.resolve(filePath)), [
@@ -371,7 +372,7 @@ async function tryRustClippyFix(filePath) {
371
372
  return diffProjectSnapshot(cargoDir, before);
372
373
  }
373
374
  async function tryDartFix(filePath) {
374
- const check = await safeSpawnAsync("dart", ["--version"], { timeout: 5000 });
375
+ const check = await probeToolAsync("dart", ["--version"], { timeout: 5000 });
375
376
  if (check.error || check.status !== 0)
376
377
  return [];
377
378
  const pubspecDir = findNearestContaining(path.dirname(path.resolve(filePath)), ["pubspec.yaml"]);
@@ -149,6 +149,7 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
149
149
  return {
150
150
  diagnostics: [],
151
151
  runners: [],
152
+ analyzed: [],
152
153
  cold: [...ANALYZER_IDS],
153
154
  coldReasons: Object.fromEntries(ANALYZER_IDS.map((id) => [id, unsafeRootReason])),
154
155
  failed: [],
@@ -158,6 +159,7 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
158
159
  }
159
160
  const diagnostics = [];
160
161
  const runners = [];
162
+ const analyzed = [];
161
163
  const cold = [];
162
164
  // #1623: the specific reason each `cold` id was skipped, captured at the
163
165
  // gate that decided it — see FreshProjectDiagnosticsResult.coldReasons.
@@ -188,7 +190,17 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
188
190
  pushUnique(cold, id);
189
191
  coldReasons[id] = reason;
190
192
  }
191
- function record(id, adapted, elapsedMs) {
193
+ /**
194
+ * `analysedRoot` is the client's own opt-in `AnalysedRootSignal` (#2154),
195
+ * never a property of reaching this function: `success: true` is also what
196
+ * a skipped, crashed-before-reporting or memoised run returns, and only the
197
+ * client knows which of those it is. Findings are recorded either way — an
198
+ * id that did not analyse the root simply carries no authority to retire a
199
+ * retained finding.
200
+ */
201
+ function record(id, adapted, elapsedMs, analysedRoot) {
202
+ if (analysedRoot)
203
+ pushUnique(analyzed, id);
192
204
  timings[id] = (timings[id] ?? 0) + elapsedMs;
193
205
  const kept = applyDispositionsMultiFile(adapted, analysisRoot, (d) => d.filePath);
194
206
  const suppressedHere = adapted.length - kept.length;
@@ -223,10 +235,17 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
223
235
  recordFailed("knip", result);
224
236
  return;
225
237
  }
238
+ // knip reports `success: true` for a run that never happened (no
239
+ // project root, a memo hit, an unparseable exit-0). Those belong in
240
+ // the honest `cold` channel, not in "fetched fresh this call".
241
+ if (result.analyzed !== true) {
242
+ markCold("knip", result.summary ?? "knip did not analyse this root");
243
+ return;
244
+ }
226
245
  cacheManager.writeCache("knip", result, analysisRoot, {
227
246
  scanDurationMs: Date.now() - startMs,
228
247
  });
229
- record("knip", knipIssuesToProjectDiagnostics(analysisRoot, result.issues ?? []), Date.now() - startMs);
248
+ record("knip", knipIssuesToProjectDiagnostics(analysisRoot, result.issues ?? []), Date.now() - startMs, true);
230
249
  }),
231
250
  // jscpd — duplicate code detection. Cache key varies with TS-project
232
251
  // detection, exactly mirroring session_start's own logic.
@@ -243,10 +262,14 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
243
262
  recordFailed("jscpd", result);
244
263
  return;
245
264
  }
265
+ if (result.analyzed !== true) {
266
+ markCold("jscpd", "jscpd did not analyse this root (no source files, or the scan produced no report)");
267
+ return;
268
+ }
246
269
  cacheManager.writeCache(scannerKey, result, analysisRoot, {
247
270
  scanDurationMs: Date.now() - startMs,
248
271
  });
249
- record("jscpd", jscpdResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs);
272
+ record("jscpd", jscpdResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs, true);
250
273
  }),
251
274
  // madge — circular-dependency detection.
252
275
  task("madge", async () => {
@@ -259,7 +282,7 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
259
282
  cacheManager.writeCache("madge", result, analysisRoot, {
260
283
  scanDurationMs: Date.now() - startMs,
261
284
  });
262
- record("madge", circularDepsToProjectDiagnostics(analysisRoot, result.circular ?? []), Date.now() - startMs);
285
+ record("madge", circularDepsToProjectDiagnostics(analysisRoot, result.circular ?? []), Date.now() - startMs, result.analyzed === true);
263
286
  }),
264
287
  // gitleaks — committed-secrets detection. session_start/per-edit stay
265
288
  // config-gated per #130's strict default (GitleaksClient.hasGitleaksSignal),
@@ -290,7 +313,7 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
290
313
  cacheManager.writeCache("gitleaks", result, analysisRoot, {
291
314
  scanDurationMs: Date.now() - startMs,
292
315
  });
293
- record("gitleaks", gitleaksResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs);
316
+ record("gitleaks", gitleaksResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs, result.analyzed === true);
294
317
  }),
295
318
  // govulncheck — Go module CVE detection. Go-module-gated per #132.
296
319
  task("govulncheck", async () => {
@@ -322,7 +345,7 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
322
345
  cacheManager.writeCache("govulncheck", result, analysisRoot, {
323
346
  scanDurationMs: Date.now() - startMs,
324
347
  });
325
- record("govulncheck", govulncheckResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs);
348
+ record("govulncheck", govulncheckResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs, result.analyzed === true);
326
349
  }),
327
350
  // opengrep — full-workspace semgrep-grade security/quality findings via a
328
351
  // single project-wide CLI scan (#584). Structurally always-on: mirrors
@@ -352,7 +375,7 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
352
375
  cacheManager.writeCache("opengrep", result, analysisRoot, {
353
376
  scanDurationMs: Date.now() - startMs,
354
377
  });
355
- record("opengrep", opengrepResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs);
378
+ record("opengrep", opengrepResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs, result.analyzed === true);
356
379
  }),
357
380
  // trivy — dependency CVE detection. Explicit opt-in per #131.
358
381
  task("trivy", async () => {
@@ -373,7 +396,7 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
373
396
  cacheManager.writeCache("trivy", result, analysisRoot, {
374
397
  scanDurationMs: Date.now() - startMs,
375
398
  });
376
- record("trivy", trivyResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs);
399
+ record("trivy", trivyResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs, result.analyzed === true);
377
400
  }),
378
401
  // dead-code — cross-file dead-code for non-JS/TS languages (#127).
379
402
  // Each client self-gates via detect(); only matching-language projects
@@ -395,7 +418,7 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
395
418
  cacheManager.writeCache(cacheKey, result, analysisRoot, {
396
419
  scanDurationMs: Date.now() - startMs,
397
420
  });
398
- record("dead-code", deadCodeResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs);
421
+ record("dead-code", deadCodeResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs, result.analyzed === true);
399
422
  }));
400
423
  }),
401
424
  // test-runner — CACHE-READ only, unlike every task above (#1004). Its
@@ -440,7 +463,10 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
440
463
  // looking indistinguishable from a lane that just executed.
441
464
  cachedAgeMs["test-runner"] =
442
465
  Date.now() - new Date(cached.meta.timestamp).getTime();
443
- record("test-runner", testRunnerFindingsToProjectDiagnostics(cached.data, analysisRoot, options.runtime), Date.now() - startMs);
466
+ record("test-runner", testRunnerFindingsToProjectDiagnostics(cached.data, analysisRoot, options.runtime), Date.now() - startMs,
467
+ // Never authoritative: this lane reads the cache turn_end wrote,
468
+ // it never runs a suite over the root this call (#2154).
469
+ false);
444
470
  }),
445
471
  ];
446
472
  // Swallow any later rejection so an aborted-and-abandoned task can never
@@ -470,6 +496,7 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
470
496
  return {
471
497
  diagnostics,
472
498
  runners,
499
+ analyzed,
473
500
  cold,
474
501
  coldReasons,
475
502
  failed,
@@ -484,6 +511,7 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
484
511
  return {
485
512
  diagnostics,
486
513
  runners,
514
+ analyzed,
487
515
  cold,
488
516
  coldReasons,
489
517
  failed,
@@ -3,12 +3,11 @@ import * as path from "node:path";
3
3
  * A gitleaks finding is a leaked secret at a concrete `file:startLine`. Secrets
4
4
  * are treated as **blocking** — a committed credential is not a style nit.
5
5
  *
6
- * Exception: `pathStatus: "scratch"` (#1562 review-round F2) a finding
7
- * `classifyAndFilterFindings` demoted rather than dropped, so the observability
8
- * criterion ("record carries tracked/ignored/scratch status") stays honest
9
- * about a fourth REACHABLE value instead of silently deleting it. Demoted to
10
- * `severity: "info"`/`semantic: "none"` so it can never read as a blocking
11
- * "leaked secret" alarm, while remaining visible for an audit read.
6
+ * Findings in scratch trees, gitignored files, and nested repositories are
7
+ * retained for explicit audit but demoted to `severity: "info"` / `semantic:
8
+ * "none"`. They are outside the current repository's shippable source, so they
9
+ * must never read as blocking leaked-secret alarms. Tracked files and ordinary
10
+ * untracked files remain blocking.
12
11
  */
13
12
  export function gitleaksFindingToProjectDiagnostic(cwd, finding) {
14
13
  // #1562 observability criterion: surface the git status right in the
@@ -17,14 +16,16 @@ export function gitleaksFindingToProjectDiagnostic(cwd, finding) {
17
16
  const statusSuffix = finding.pathStatus
18
17
  ? ` [git: ${finding.pathStatus}]`
19
18
  : "";
20
- const isScratch = finding.pathStatus === "scratch";
19
+ const isBlocking = finding.pathStatus !== "scratch" &&
20
+ finding.pathStatus !== "ignored" &&
21
+ finding.pathStatus !== "nested-repository";
21
22
  return {
22
23
  filePath: path.isAbsolute(finding.file)
23
24
  ? finding.file
24
25
  : path.resolve(cwd, finding.file),
25
26
  line: finding.startLine,
26
- severity: isScratch ? "info" : "error",
27
- semantic: isScratch ? "none" : "blocking",
27
+ severity: isBlocking ? "error" : "info",
28
+ semantic: isBlocking ? "blocking" : "none",
28
29
  tool: "gitleaks",
29
30
  runner: "gitleaks",
30
31
  rule: `gitleaks:${finding.ruleId}`,
@@ -69,6 +69,7 @@ import { deprecationRecords, ignoredRecordCollector, projectLocationFor, readCon
69
69
  import { finalizeRecords, } from "./config-core/records.js";
70
70
  import { homeRelativePath, isAtOrAboveHomeDir, walkUpDirs, } from "./path-utils.js";
71
71
  import { findPiLensConfigMarkerInDir } from "./workspace-topology.js";
72
+ import { readToolConfig } from "./tool-config.js";
72
73
  /**
73
74
  * Project config basenames, in DESCENDING precedence (first match wins), for
74
75
  * this loader's own discovery.
@@ -563,6 +564,7 @@ function parseConfigFile(configPath) {
563
564
  for (const spec of PROJECT_SCOPED_LENS_FLAGS) {
564
565
  assignFlagConfigSection(obj, mutations, spec.configKey, note);
565
566
  }
567
+ const toolConfig = readToolConfig(obj, note);
566
568
  const rules = {};
567
569
  if (obj.rules && typeof obj.rules === "object" && !Array.isArray(obj.rules)) {
568
570
  const rawRules = obj.rules;
@@ -738,6 +740,7 @@ function parseConfigFile(configPath) {
738
740
  }
739
741
  return {
740
742
  config: {
743
+ ...(toolConfig === undefined ? {} : { tools: toolConfig }),
741
744
  ...(startup === undefined ? {} : { startup }),
742
745
  ignore,
743
746
  rules,
@@ -3,6 +3,7 @@ import { isTestMode } from "./env-utils.js";
3
3
  import { getGlobalPiLensLogDir } from "./probe-home-state.js";
4
4
  import { createNdjsonLogger } from "./ndjson-logger.js";
5
5
  import { normalizeFilePath } from "./path-utils.js";
6
+ import { getTurnId } from "./turn-context.js";
6
7
  const READ_GUARD_LOG_DIR = getGlobalPiLensLogDir();
7
8
  const READ_GUARD_LOG_FILE = path.join(READ_GUARD_LOG_DIR, "read-guard.log");
8
9
  const READ_GUARD_LOG_BACKUP_FILE = path.join(READ_GUARD_LOG_DIR, "read-guard.log.1");
@@ -236,6 +237,7 @@ export function logReadGuardEvent(entry) {
236
237
  writer.log({
237
238
  ts: new Date().toISOString(),
238
239
  ...logEntry,
240
+ turnId: getTurnId(),
239
241
  filePath: normalizeFilePath(logEntry.filePath),
240
242
  metadata,
241
243
  });
@@ -4,6 +4,7 @@ import { getGlobalPiLensLogDir } from "./probe-home-state.js";
4
4
  import { getMaxLogSizeMB } from "./log-cleanup.js";
5
5
  import { createNdjsonLogger } from "./ndjson-logger.js";
6
6
  import { normalizeFilePath } from "./path-utils.js";
7
+ import { getTurnId } from "./turn-context.js";
7
8
  const REVIEW_GRAPH_LOG_DIR = getGlobalPiLensLogDir();
8
9
  const REVIEW_GRAPH_LOG_FILE = path.join(REVIEW_GRAPH_LOG_DIR, "review-graph.log");
9
10
  // Bounded like every other global sink (#2505) — see sessionstart-logger.
@@ -64,6 +65,7 @@ export function logReviewGraph(entry) {
64
65
  ...entry,
65
66
  cwd: normalizeFilePath(entry.cwd),
66
67
  pid: process.pid,
68
+ turnId: getTurnId(),
67
69
  });
68
70
  }
69
71
  export function getReviewGraphLogPath() {