@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
@@ -13,6 +13,7 @@ import * as path from "node:path";
13
13
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./dispatch/runners/utils/runner-helpers.js";
14
14
  import { isFileKind } from "./file-kinds.js";
15
15
  import { safeSpawnAsync } from "./safe-spawn.js";
16
+ import { resolveToolCwd } from "./tool-cwd.js";
16
17
  import { ruffConfigArgs } from "./tool-policy.js";
17
18
  // --- Client ---
18
19
  const ruffAvailability = createAvailabilityChecker("ruff", ".exe");
@@ -44,8 +45,11 @@ export class RuffClient {
44
45
  }
45
46
  /**
46
47
  * Async auto-fix variant for pipeline use (non-blocking spawn).
47
- * `cwd` is the dispatch language root (used for config discovery); when
48
- * omitted it defaults to the file's directory.
48
+ * `cwd` is the dispatch language root. It is the seam's starting point,
49
+ * not the answer: `resolveToolCwd` walks from the file to the nearest
50
+ * `pyproject.toml`/`ruff.toml`/`.ruff.toml` at or above it, capped inside
51
+ * `cwd` (and at `$HOME` when there is none), so a file in a nested package
52
+ * gets that package's config rather than the workspace-root one.
49
53
  */
50
54
  async fixFileAsync(filePath, cwd) {
51
55
  if (!(await this.ensureAvailable())) {
@@ -67,15 +71,20 @@ export class RuffClient {
67
71
  }
68
72
  try {
69
73
  const before = await fs.promises.readFile(absolutePath, "utf-8");
74
+ // #2894: the root ruff resolves its config from now comes from the
75
+ // shared seam, not a local `cwd ?? path.dirname(file)`. `tool-policy.ts`
76
+ // requires the lint runner (`ruff.ts`, on `resolveRunnerCwd`) and this
77
+ // autofix path to consume the SAME policy; deriving the root two
78
+ // different ways is how they drift, and the hand-rolled form ignored a
79
+ // nested `pyproject.toml` whenever the caller passed a workspace root.
80
+ const ruffCwd = resolveToolCwd("runner", "ruff", absolutePath, {
81
+ ...(cwd !== undefined && { cwd }),
82
+ });
70
83
  // Shared config-args seam (#1247): the lint runner consumes the same
71
84
  // builder, so `check --fix` can never drift to ruff's default rule
72
85
  // set when the project lacks its own config and the package-owned
73
86
  // core.toml fallback applies.
74
- const configArgs = ruffConfigArgs(cwd ?? path.dirname(absolutePath));
75
- const spawnOpts = {
76
- timeout: 10000,
77
- cwd: cwd ?? path.dirname(absolutePath),
78
- };
87
+ const configArgs = ruffConfigArgs(ruffCwd);
79
88
  const pre = await safeSpawnAsync(this.ruffCommand, [
80
89
  "check",
81
90
  "--output-format",
@@ -84,12 +93,12 @@ export class RuffClient {
84
93
  "py310",
85
94
  ...configArgs,
86
95
  absolutePath,
87
- ], spawnOpts);
96
+ ], { timeout: 10000, cwd: ruffCwd });
88
97
  const beforeDiags = pre.stdout?.trim()
89
98
  ? this.parseOutput(pre.stdout, absolutePath)
90
99
  : [];
91
100
  const fixableCount = beforeDiags.filter((d) => d.fixable).length;
92
- const fix = await safeSpawnAsync(this.ruffCommand, ["check", "--fix", ...configArgs, absolutePath], { timeout: 15000, cwd: cwd ?? path.dirname(absolutePath) });
101
+ const fix = await safeSpawnAsync(this.ruffCommand, ["check", "--fix", ...configArgs, absolutePath], { timeout: 15000, cwd: ruffCwd });
93
102
  if (fix.error) {
94
103
  return {
95
104
  success: false,
@@ -39,6 +39,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
39
39
  // to fall back to "claim as orphaned" (see claimDeferredFormatFiles).
40
40
  const { claimed, staleClaimed, deferredToOwner, droppedOrphans } = runtime.claimDeferredMutations(currentSessionId, Date.now(), staleAfterMs, ctxCwd ?? runtime.projectRoot);
41
41
  const records = [...claimed, ...staleClaimed];
42
+ const scheduledTurnId = records[0]?.queuedTurnId;
42
43
  const requeuedKinds = new Set();
43
44
  // S2d (gap 4, #1432 review): the aggregate `agent_end_deferred_mutation_drain`
44
45
  // row only carried a coalesced `requeuedKinds` set with no reason, per-call
@@ -65,6 +66,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
65
66
  toolName: "agent_end",
66
67
  filePath: ctxCwd ?? runtime.projectRoot,
67
68
  phase: "agent_end_deferred_mutation_requeue",
69
+ turnId: pending[0]?.queuedTurnId,
68
70
  durationMs: 0,
69
71
  metadata: {
70
72
  reason,
@@ -170,6 +172,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
170
172
  toolName: "agent_end",
171
173
  filePath: ctxCwd ?? runtime.projectRoot,
172
174
  phase: "agent_end_deferred_format_stale_claim",
175
+ turnId: scheduledTurnId,
173
176
  durationMs: 0,
174
177
  metadata: {
175
178
  fileCount: staleClaimed.length,
@@ -307,6 +310,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
307
310
  toolName: "agent_end",
308
311
  filePath: ctxCwd ?? runtime.projectRoot,
309
312
  phase: "agent_end_deferred_format_start",
313
+ turnId: scheduledTurnId,
310
314
  durationMs: 0,
311
315
  metadata: {
312
316
  fileCount: records.length,
@@ -491,6 +495,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
491
495
  toolName: "agent_end",
492
496
  filePath,
493
497
  phase: "deferred_format_file",
498
+ turnId: record.queuedTurnId,
494
499
  durationMs: Date.now() - fileStart,
495
500
  metadata: {
496
501
  changed: result.formatChanged,
@@ -635,6 +640,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
635
640
  toolName: "agent_end",
636
641
  filePath: ctxCwd ?? runtime.projectRoot,
637
642
  phase: "actionable_warnings_autofix",
643
+ turnId: scheduledTurnId,
638
644
  durationMs: Date.now() - fixStart,
639
645
  metadata: {
640
646
  considered: fixSummary.considered,
@@ -657,6 +663,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
657
663
  toolName: "agent_end",
658
664
  filePath: ctxCwd ?? runtime.projectRoot,
659
665
  phase: "agent_end_deferred_mutation_drain",
666
+ turnId: scheduledTurnId,
660
667
  durationMs: Date.now() - startedAt,
661
668
  metadata: {
662
669
  autofixRecords: autofixRecords.length,
@@ -671,6 +678,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
671
678
  filePath: ctxCwd ?? runtime.projectRoot,
672
679
  durationMs: Date.now() - startedAt,
673
680
  result: "deferred_format_complete",
681
+ turnId: scheduledTurnId,
674
682
  metadata: {
675
683
  queued: summary.queued,
676
684
  formatted: summary.formatted,
@@ -685,6 +693,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
685
693
  toolName: "agent_end",
686
694
  filePath: ctxCwd ?? runtime.projectRoot,
687
695
  phase: "agent_end_deferred_format_done",
696
+ turnId: scheduledTurnId,
688
697
  durationMs: Date.now() - startedAt,
689
698
  metadata: {
690
699
  formatted: summary.formatted,
@@ -11,6 +11,7 @@ import { ReadGuard } from "./read-guard.js";
11
11
  import { RUNTIME_CONFIG } from "./runtime-config.js";
12
12
  import { TurnSummaryCollector } from "./turn-summary.js";
13
13
  import { deriveProviderFromModelId } from "./model-provider.js";
14
+ import { beginTurnContext, setTurnContextSession } from "./turn-context.js";
14
15
  /**
15
16
  * Bounded receipt ring capacity (defect shape 9: bound the axis that grows —
16
17
  * per-mutation receipts are unbounded across a long session). Oldest receipts
@@ -259,6 +260,7 @@ export class RuntimeCoordinator {
259
260
  // by resetForSession().
260
261
  this._turnStartProjectSeq = this._projectSeq;
261
262
  this._turnIndex += 1;
263
+ beginTurnContext(this._telemetrySessionId);
262
264
  this._writeIndex = 0;
263
265
  this._reportedThisTurn.clear();
264
266
  this._writtenThisTurn.clear();
@@ -362,6 +364,7 @@ export class RuntimeCoordinator {
362
364
  setTelemetryIdentity(identity) {
363
365
  if (identity.sessionId && identity.sessionId.trim()) {
364
366
  this._telemetrySessionId = identity.sessionId.trim();
367
+ setTurnContextSession(this._telemetrySessionId);
365
368
  }
366
369
  const model = identity.model?.trim();
367
370
  const provider = identity.provider?.trim();
@@ -1029,6 +1032,7 @@ export class RuntimeCoordinator {
1029
1032
  existing.toolNames.add(toolName);
1030
1033
  existing.kinds.add(kind);
1031
1034
  existing.queuedTurnIndex = this._turnIndex;
1035
+ existing.queuedTurnId = `${this._telemetrySessionId}:${this._turnIndex}`;
1032
1036
  existing.ownerSessionId = ownerSessionId;
1033
1037
  existing.originCwd = resolvedOriginCwd;
1034
1038
  return addedKind;
@@ -1042,6 +1046,7 @@ export class RuntimeCoordinator {
1042
1046
  toolNames: new Set([toolName]),
1043
1047
  kinds: new Set([kind]),
1044
1048
  queuedTurnIndex: this._turnIndex,
1049
+ queuedTurnId: `${this._telemetrySessionId}:${this._turnIndex}`,
1045
1050
  ownerSessionId,
1046
1051
  originCwd: resolvedOriginCwd,
1047
1052
  });
@@ -42,6 +42,7 @@ import { resetSafeSpawnWindowsCommandCache } from "./safe-spawn.js";
42
42
  import { resetAnalyzerBootstrapSessionState, } from "./bootstrap.js";
43
43
  import { getSlowFsVerdict, isSlowFs, slowFsDegradationNotice, } from "./slow-fs.js";
44
44
  import { countRecentSmells, formatSmellsSessionStartLine, resetSmellsSessionState, } from "./smells-rollup.js";
45
+ import { resetSituationalToolTelemetry } from "./situational-tool-telemetry.js";
45
46
  import { findNearestProjectRoot, getStartupScanMaxEntries, isStartupScanVerdictFresh, resolveStartupScanContext, } from "./startup-scan.js";
46
47
  import { getSubagentIdentity, isSubagentSession, subagentLightModeNotice, } from "./subagent-mode.js";
47
48
  import { TrivyClient } from "./trivy-client.js";
@@ -1361,17 +1362,16 @@ function scheduleDeferredToolProbesWithClients(deps, languageProfile, startupDef
1361
1362
  /**
1362
1363
  * Session-start orientation prepended as a context message (gated by the
1363
1364
  * context-injection toggle). Deliberately lean: it names the high-value tools
1364
- * and the one non-obvious behaviour (mode=all resurfaces stale blocking errors)
1365
+ * and the distinction between cached reporting and active verification
1365
1366
  * — per-tool argument detail lives in each tool's own registered description, so
1366
1367
  * re-documenting it here would just pay the tokens twice every session.
1367
1368
  */
1368
1369
  export const SESSION_START_GUIDANCE = [
1369
1370
  "📌 pi-lens active — automated checks run on every edit/write; blocking errors (including pre-existing) show inline and must be fixed.\n" +
1370
1371
  "Key tools (see each tool's own description for args):\n" +
1371
- "• lens_diagnostics — session-wide diagnostic state; mode=all resurfaces stale blocking errors that dropped from turn context.\n" +
1372
+ "• lens_diagnostics — source=session reads cache; empty cache ≠ clean; use source=lsp scope=paths for changed files with absent or stale findings.\n" +
1372
1373
  "• symbol_search → module_report → read_symbol/read_enclosing — ranked identifier search, then navigable outline/callback handles + exact body reads; cheaper than reading a whole file before editing.\n" +
1373
- "• lsp_diagnostics probe LSP for errors in a file/folder/workspace.\n" +
1374
- "• Situational (activate via pi_lens_activate_tools): lsp_navigation, ast_grep_search, ast_grep_replace, ast_grep_dump.",
1374
+ "• Situational (activate via pi_lens_activate_tools): lsp_navigation, ast_grep_search, ast_grep_replace. Use ast_grep_search with dump=true to inspect AST nodes.",
1375
1375
  ];
1376
1376
  export async function handleSessionStart(deps) {
1377
1377
  resetDegradationLedger();
@@ -1814,6 +1814,15 @@ export async function handleSessionStart(deps) {
1814
1814
  // #1123 item 3: a fresh session can re-report smells that a prior session
1815
1815
  // already surfaced once (see `checkSmellsAndNoteOnce`'s once-per-session gate).
1816
1816
  resetSmellsSessionState();
1817
+ // #2800 item 8: the situational-tool dead-weight observation (which tools
1818
+ // this session never activated or called) is session-scoped, so its sets
1819
+ // and once-latch re-arm here beside the other registered resets. Both hosts
1820
+ // open the session's row through startSituationalToolTelemetrySession()
1821
+ // BEFORE this handler runs, and a repeated MCP session_start refresh
1822
+ // legitimately re-runs this handler — so the reset only acts when no
1823
+ // telemetry session is open; clearing a live session here would wipe the
1824
+ // calls recorded before the refresh.
1825
+ resetSituationalToolTelemetry();
1817
1826
  // #1782: re-arm the workspace-diagnostics cache session clock. Entries
1818
1827
  // written before this instant assert findings from a session that is over,
1819
1828
  // so they must revalidate before they can be served as current again.
@@ -21,7 +21,7 @@ import { replayThroughMutationBridge, storedLineHashesFor, } from "./observed-mu
21
21
  import { getAmbientAbortSignal } from "./safe-spawn.js";
22
22
  import { resolveToolCallCorrelationId } from "./tool-event.js";
23
23
  import { boundedIndexesForCount, createReadGuardEditBatchSummary, getReadGuardCorrelationId, logReadGuardEvent, } from "./read-guard-logger.js";
24
- import { notifyExternalFileChange } from "./lsp/index.js";
24
+ import { notifyExternalFileChange, resyncGitChangedFiles, } from "./lsp/index.js";
25
25
  import { runPipeline } from "./pipeline.js";
26
26
  import { renderPostAutofixNotice, } from "./post-autofix-notice.js";
27
27
  import { syncGitGuardRecord } from "./git-guard.js";
@@ -897,6 +897,9 @@ export async function handleToolResult(deps) {
897
897
  durationMs: Date.now() - started,
898
898
  result: `changed:${opaquePaths.length}`,
899
899
  });
900
+ void resyncGitChangedFiles(opaquePaths).catch((err) => {
901
+ dbg(`git-change LSP resync failed: ${err}`);
902
+ });
900
903
  }
901
904
  }
902
905
  // wp iterates opaquePaths VERBATIM (already normalizeMapKey keys), so the
@@ -13,6 +13,7 @@ import { getKnipIgnorePatterns } from "./file-utils.js";
13
13
  import { formatCacheAgeLabel } from "./finding-delivery-gate.js";
14
14
  import { getFullScanWallClockMs, isWorkspaceSweepActive, runWhenWorkspaceSweepIdle, SWEEP_IDLE_SAFETY_MARGIN_MS, } from "./lsp/workspace-sweep-hold.js";
15
15
  import { isTestRoleCollateral } from "./collateral-test-role.js";
16
+ import { classifyAndFilterFindings, } from "./gitleaks-client.js";
16
17
  import { dedupeSecretFindings, fromAstGrepWarnings, fromGitleaks, fromTrivySecrets, isSecretWarning, secretLocationKey, } from "./secret-findings.js";
17
18
  import { deadCodeIssueKey, deadCodeIssues, formatDeadCodeDelta, stableFindingKey, } from "./dead-code-client.js";
18
19
  import { logDeadCodeScan } from "./dead-code-logger.js";
@@ -49,6 +50,8 @@ import { emitBounded } from "./bounded-telemetry.js";
49
50
  import { degradeDemotedFindingBody, formatDeliveryCapNote, formatRetirementNote, } from "./demoted-finding-render.js";
50
51
  import { STALE_LINE_MARKER } from "./stale-marker.js";
51
52
  import { getActiveSessionId } from "./session-lifecycle.js";
53
+ import { bounded } from "./deadline-utils.js";
54
+ import { HOOK_WALL_BUDGET_MS } from "./hook-budgets.js";
52
55
  import { drainRenderedDependencyDriftFilePaths, getWidgetBlockingFilesForSweep, incrementWidgetDependencyDriftDelivery, markWidgetFileBlockersStale, recordRunner, retireWidgetDependencyDriftBlockers, } from "./widget-state.js";
53
56
  /** Maximum detailed notify-stall coverage-gap rows emitted in one turn. */
54
57
  const LATE_AUX_COVERAGE_GAP_DETAIL_CAP_PER_TURN = 20;
@@ -1483,6 +1486,26 @@ export async function handleTurnEnd(deps) {
1483
1486
  // provenance) — a blocker, since credentials need rotation before merge.
1484
1487
  const gitleaksData = cacheManager.readCache("gitleaks", cwd)?.data;
1485
1488
  const trivySecretsData = trivyCacheEntry?.data;
1489
+ // Gitleaks deliberately scans gitignored local files and nested repositories
1490
+ // so an explicit security audit can still inspect them. The adapter is the
1491
+ // source of truth for whether a finding belongs in a blocking delivery lane;
1492
+ // filter here before freshness handling so demoted findings cannot leak into
1493
+ // either the blocker or stale-secret turn context.
1494
+ const boundedClassification = await bounded(classifyAndFilterFindings(gitleaksData?.findings ?? [], cwd), {
1495
+ ms: HOOK_WALL_BUDGET_MS.turn_end,
1496
+ signal: deps.signal,
1497
+ hook: "turn_end",
1498
+ label: "classifyAndFilterFindings",
1499
+ });
1500
+ const classifiedGitleaksFindings = boundedClassification ?? gitleaksData?.findings ?? [];
1501
+ if (boundedClassification === undefined) {
1502
+ recordDegradationOnce({
1503
+ kind: "gitleaks_classification_timeout",
1504
+ subject: cwd,
1505
+ reason: "gitleaks classification exceeded the turn_end budget; retained raw findings to fail open",
1506
+ });
1507
+ }
1508
+ const blockingGitleaksFindings = classifiedGitleaksFindings.filter((finding) => gitleaksFindingToProjectDiagnostic(cwd, finding).semantic === "blocking");
1486
1509
  // #1461 slice 1 (#1460): the gitleaks cache is TTL-only, so a finding for a
1487
1510
  // file deleted after the scan is still served as a 🔴 blocker for the rest
1488
1511
  // of the 30-minute window — the live case, and 119 of 126 findings in
@@ -1497,7 +1520,7 @@ export async function handleTurnEnd(deps) {
1497
1520
  // would let any edit — malicious or accidental — mute a real secret.
1498
1521
  const gitleaksGate = gateFindingsByPathFreshness({
1499
1522
  store: "gitleaks",
1500
- findings: gitleaksData?.findings ?? [],
1523
+ findings: blockingGitleaksFindings,
1501
1524
  cwd,
1502
1525
  scannedAt: gitleaksData?.scannedAt,
1503
1526
  citedPath: (finding) => finding.file,
@@ -2882,6 +2905,7 @@ export async function handleTurnEnd(deps) {
2882
2905
  let lateAuxCoverageGapDropCount = 0;
2883
2906
  const lateAuxStuckPairs = [];
2884
2907
  if (drainedPairs.length > 0) {
2908
+ const lateObserverDeadline = Date.now() + HOOK_WALL_BUDGET_MS.turn_end;
2885
2909
  const byFile = new Map();
2886
2910
  for (const pair of drainedPairs) {
2887
2911
  const list = byFile.get(pair.filePath);
@@ -2993,6 +3017,19 @@ export async function handleTurnEnd(deps) {
2993
3017
  }
2994
3018
  continue;
2995
3019
  }
3020
+ // A demoted auxiliary still answers through this late path. Feed the
3021
+ // publication-minus-mark interval into the re-promotion streak. This is
3022
+ // delivery latency observed by the drain, not the scanner's total scan
3023
+ // latency. Cache priming is
3024
+ // below the freshness gate so a changed file cannot resurrect stale data.
3025
+ if (typeof service.observeLateAuxiliaryAnswer === "function") {
3026
+ await bounded(service.observeLateAuxiliaryAnswer(lateAuxPath, pair.serverId, cachedEntry.publishedAt - pair.markedAtMs), {
3027
+ ms: Math.max(1, lateObserverDeadline - Date.now()),
3028
+ signal: deps.signal /* late observer */,
3029
+ hook: "turn_end",
3030
+ label: "observeLateAuxiliaryAnswer",
3031
+ });
3032
+ }
2996
3033
  if (rawDiags.length === 0) {
2997
3034
  lateAuxCleanConfirmed += 1;
2998
3035
  continue;
@@ -3045,6 +3082,16 @@ export async function handleTurnEnd(deps) {
3045
3082
  }
3046
3083
  continue;
3047
3084
  }
3085
+ // #2810 round 4: this drain does NOT write the hash-bound
3086
+ // last-known record. The prime it used to call could only fire when
3087
+ // a record already existed at the pair's hash — which requires a
3088
+ // FULLY covered touch of those exact bytes, the one case where the
3089
+ // scanner's findings are already in the record — so it was a no-op
3090
+ // in the demoted steady state it was added for, and a #570/#1470
3091
+ // hazard everywhere else (an auxiliary-only array replacing the
3092
+ // merged one). Late findings reach the agent as the gated advisory
3093
+ // below; the turn-end hash-guarded fast path stays cold for a file
3094
+ // whose touch was partial, which is exactly what #1470 requires.
3048
3095
  const lines = gate.live.map((f) => ` ${displayLateAuxPath}:${f.line}:${f.column} [${f.rule}] ${f.message}`);
3049
3096
  lateAuxDelivered += gate.live.length;
3050
3097
  lateAuxAnswered += 1;
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import * as path from "node:path";
16
16
  import { createSubsystemLogger } from "./extension-log.js";
17
- import { safeSpawnAsync } from "./safe-spawn.js";
17
+ import { probeToolAsync } from "./tool-probe.js";
18
18
  import { classifyProbeFailure, createAvailabilityLatch, describeInstallAttempt, describeProbeEvidence, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
19
19
  import { createAvailabilityProbeFlight } from "./availability-probe-flight.js";
20
20
  import { createSingleFlight } from "./single-flight.js";
@@ -132,7 +132,7 @@ export class SecurityScanClient {
132
132
  const { findManagedToolBinary } = await import("./installer/index.js");
133
133
  const managed = await findManagedToolBinary(this.toolName);
134
134
  return {
135
- probe: await safeSpawnAsync(managed ?? this.toolName, versionArgs, {
135
+ probe: await probeToolAsync(managed ?? this.toolName, versionArgs, {
136
136
  timeout: 5000,
137
137
  }),
138
138
  binaryPath: managed ?? null,
@@ -64,7 +64,22 @@
64
64
  * wrapper narrows nothing else: a handler bug must stay as loud as it was.
65
65
  */
66
66
  import { emitBounded } from "./bounded-telemetry.js";
67
+ import { recordDegradationOnce } from "./degradation-ledger.js";
67
68
  import { probeCtxActive } from "./session-lifecycle.js";
69
+ import { runWithTurnContext } from "./turn-context.js";
70
+ function stableSessionId(ctx) {
71
+ try {
72
+ return ctx?.sessionManager?.getSessionId?.();
73
+ }
74
+ catch {
75
+ recordDegradationOnce({
76
+ kind: "turn-context-identity-fallback",
77
+ subject: "session-event-guard",
78
+ reason: "stable session identity resolution failed; using detached turn context",
79
+ });
80
+ return undefined;
81
+ }
82
+ }
68
83
  /**
69
84
  * The pi SDK invalidates a captured `pi`/command ctx after a session
70
85
  * replacement or reload; every later `pi.*` call then throws with this
@@ -99,6 +114,59 @@ function recordStaleSkip(eventName, detectedAt) {
99
114
  reason: `${eventName} skipped: extension ctx is stale (${detectedAt})`,
100
115
  });
101
116
  }
117
+ /**
118
+ * The one policy for "a pi hook handler threw and production swallows it"
119
+ * (#2884).
120
+ *
121
+ * Nine catch sites in `index.ts` absorb a crashed handler so a pi-lens bug
122
+ * can never take down the host's session. Each of them used to write only
123
+ * `dbg(...)`, and `dbg` writes nothing under vitest — so a crashed handler was
124
+ * indistinguishable from a completed one. #2859 is what that costs: fourteen
125
+ * `session_start` awaits in `tests/index-integration.test.ts` rejected into
126
+ * one of those catches, every assertion after them was vacuous, and the file
127
+ * stayed green. #2866 closed the hole for `session_start` with an inline
128
+ * `if (process.env.VITEST) throw`; this function is that guard folded into one
129
+ * place so the remaining eight cannot drift from it.
130
+ *
131
+ * Two things happen on every crash, in this order:
132
+ *
133
+ * 1. **The production record.** One bounded `hook-handler-crash` row per
134
+ * handler per session (`recordDegradationOnce`), so a handler that crashes
135
+ * on every turn leaves one durable row and an exact ledger tally instead of
136
+ * a silent no-op. It is written BEFORE the rethrow, so a test can assert the
137
+ * production observability the runner path would otherwise hide.
138
+ * 2. **The runner rethrow.** Under vitest the crash is rethrown, so the test
139
+ * whose `await` caused it fails with the real error instead of resolving.
140
+ * In production nothing is rethrown and the caller's swallow stands.
141
+ *
142
+ * Deliberately `process.env.VITEST` and not `isTestMode()` (kept from #2859):
143
+ * the question is whether a TEST is awaiting this handler, and the #2815 R7
144
+ * case runs under vitest with `PI_LENS_TEST_MODE=0`.
145
+ *
146
+ * The stale-ctx class is NOT this function's business. Callers that classify
147
+ * it (`session_start`, `agent_end`, `turn_end`, the `agent_settled` drain)
148
+ * rethrow `isStaleExtensionCtxError` themselves BEFORE calling in, so a benign
149
+ * session swap keeps its own single record and never lands here as a crash.
150
+ * The quiet-window site is intentionally the exception to the test-runner
151
+ * rethrow: it is fire-and-forget, so a rethrow would be an unhandled rejection
152
+ * that can terminate the pi host before its caller can observe the failure.
153
+ */
154
+ export function surfaceHandlerCrash(handler, err, options = {}) {
155
+ try {
156
+ options.dbg?.(`${handler} crashed: ${err}`);
157
+ options.dbg?.(`${handler} crash stack: ${err?.stack}`);
158
+ }
159
+ catch {
160
+ // A debug sink must never decide whether a crash is recorded.
161
+ }
162
+ recordDegradationOnce({
163
+ kind: "hook-handler-crash",
164
+ subject: handler,
165
+ reason: `${handler} handler crashed and was swallowed: ${String(err)}`,
166
+ });
167
+ if (process.env.VITEST && options.rethrow !== false)
168
+ throw err;
169
+ }
102
170
  function isThenable(value) {
103
171
  return (typeof value?.then === "function");
104
172
  }
@@ -131,7 +199,7 @@ function guardSessionEvent(eventName, handler, onStaleResult, options) {
131
199
  // below.
132
200
  return skip(event, "pre-dispatch");
133
201
  try {
134
- const result = handler(event, ctx);
202
+ const result = runWithTurnContext(stableSessionId(ctx), () => handler(event, ctx));
135
203
  if (isThenable(result)) {
136
204
  // Recover the rejection in place. The host awaits the same promise
137
205
  // it would have awaited anyway; it just resolves instead.
@@ -12,6 +12,7 @@ import { getSgCommand, resolveManagedToolClient, } from "./dispatch/runners/util
12
12
  import { getProjectIgnoreGlobs } from "./file-utils.js";
13
13
  import { findGlobalBinary } from "./package-manager.js";
14
14
  import { safeSpawnAsync } from "./safe-spawn.js";
15
+ import { probeToolAsync } from "./tool-probe.js";
15
16
  import { truncatedByOutputCap } from "./spawn-output-cap.js";
16
17
  import { createSingleFlight } from "./single-flight.js";
17
18
  import { classifyProbeFailure, describeInstallAttempt, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
@@ -459,7 +460,7 @@ export class SgRunner {
459
460
  let result;
460
461
  let hostStallMs;
461
462
  try {
462
- result = await safeSpawnAsync(cmd, [...argsPrefix, "--version"], {
463
+ result = await probeToolAsync(cmd, [...argsPrefix, "--version"], {
463
464
  timeout: PROBE_TIMEOUT_MS,
464
465
  });
465
466
  }
@@ -0,0 +1,108 @@
1
+ import { logExtension } from "./extension-log.js";
2
+ import { TOOL_REGISTRY } from "./tool-config.js";
3
+ const situationalTools = TOOL_REGISTRY.filter((tool) => "situational" in tool && tool.situational === true).map((tool) => tool.name);
4
+ const situationalToolSet = new Set(situationalTools);
5
+ const activated = new Set();
6
+ const called = new Set();
7
+ let sessionStarted = false;
8
+ let emitted = false;
9
+ // MCP-only connection-terminal latch: once the connection's row is recorded,
10
+ // repeated initialize/tool-call starts must not reopen the session.
11
+ let connectionEnded = false;
12
+ // Which host owns the open session; only MCP may arm connectionEnded. Pi state
13
+ // belongs to the conversation and survives in-process session rebuilds.
14
+ let sessionHost = "mcp";
15
+ function observe(set, name) {
16
+ if (situationalToolSet.has(name)) {
17
+ set.add(name);
18
+ }
19
+ }
20
+ function clearObservations() {
21
+ activated.clear();
22
+ called.clear();
23
+ }
24
+ export function observeSituationalToolActivation(names) {
25
+ for (const name of names)
26
+ observe(activated, name);
27
+ }
28
+ export function observeSituationalToolCall(name) {
29
+ observe(called, name);
30
+ }
31
+ export function resetSituationalToolTelemetry() {
32
+ // A live telemetry session owns its observation sets: both hosts open the
33
+ // session (which resets the sets) BEFORE handleSessionStart runs, and a
34
+ // repeated MCP session_start refresh legitimately re-runs that handler —
35
+ // clearing here would wipe the calls recorded before the refresh. This
36
+ // registered reset therefore only acts when no session is open.
37
+ if (sessionStarted)
38
+ return;
39
+ clearObservations();
40
+ emitted = false;
41
+ }
42
+ /**
43
+ * Open the telemetry session for one host.
44
+ *
45
+ * Pi owns one observation set per session file. A shutdown with a
46
+ * `targetSessionFile` emits the conversation that is ending before the host
47
+ * opens the replacement. Reload keeps the same file, so its factory rebuild
48
+ * does not split the row. A process restart opens an empty set and recovers nothing:
49
+ * pi-lens's own activation memory (`rememberedLazyTools` in `index.ts`) is
50
+ * empty in a new process, so the restore deactivates every situational tool,
51
+ * and the host's restored active set is evidence of REGISTRATION, not of
52
+ * model activation (#2866 review F1). MCP remains connection-scoped.
53
+ */
54
+ export function startSituationalToolTelemetrySession(host) {
55
+ if (host === "mcp") {
56
+ if (connectionEnded)
57
+ return;
58
+ if (sessionStarted)
59
+ return;
60
+ sessionHost = "mcp";
61
+ clearObservations();
62
+ emitted = false;
63
+ sessionStarted = true;
64
+ return;
65
+ }
66
+ if (sessionStarted) {
67
+ sessionHost = "pi";
68
+ return;
69
+ }
70
+ sessionHost = "pi";
71
+ clearObservations();
72
+ emitted = false;
73
+ sessionStarted = true;
74
+ }
75
+ /** Emit the one session-end row and make repeated shutdown calls harmless. */
76
+ export function endSituationalToolTelemetry() {
77
+ if (!sessionStarted)
78
+ return;
79
+ emitSituationalDeadWeight();
80
+ if (sessionHost === "mcp")
81
+ connectionEnded = true;
82
+ clearObservations();
83
+ emitted = false;
84
+ sessionStarted = false;
85
+ }
86
+ export function emitSituationalDeadWeight() {
87
+ if (emitted)
88
+ return;
89
+ emitted = true;
90
+ const used = new Set([...activated, ...called]);
91
+ logExtension({
92
+ subsystem: "tools",
93
+ level: "debug",
94
+ message: "situational tool dead weight",
95
+ metadata: {
96
+ tools: situationalTools.filter((name) => !used.has(name)),
97
+ },
98
+ });
99
+ }
100
+ /** Test-only view of the module's state, for the #1635 session-state registry probe. */
101
+ export function _getSituationalToolTelemetryStateForTests() {
102
+ return {
103
+ activated: activated.size,
104
+ called: called.size,
105
+ emitted,
106
+ sessionStarted,
107
+ };
108
+ }