@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
@@ -0,0 +1,135 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
9
+ import {
10
+ DEFAULT_REDUCER_MODEL,
11
+ DEFAULT_REDUCER_PROVIDER,
12
+ } from "./extensions/evidence-preserving-reducer/config.ts";
13
+
14
+ export const DEFAULT_CACHE_WRITE_READ_RATIO = 12.5;
15
+
16
+ export interface SolPiConfig {
17
+ readonly version: 1;
18
+ readonly actionFusion: boolean;
19
+ readonly observationPack: boolean;
20
+ readonly evidencePreservingReducer: boolean;
21
+ readonly evidencePreservingReducerModel: string;
22
+ readonly evidencePreservingReducerProvider: string;
23
+ readonly onlineContextCompact: boolean;
24
+ readonly cacheWriteReadRatio: number;
25
+ }
26
+
27
+ export const DEFAULT_CONFIG: SolPiConfig = Object.freeze({
28
+ version: 1,
29
+ actionFusion: false,
30
+ observationPack: false,
31
+ evidencePreservingReducer: false,
32
+ evidencePreservingReducerModel: DEFAULT_REDUCER_MODEL,
33
+ evidencePreservingReducerProvider: DEFAULT_REDUCER_PROVIDER,
34
+ onlineContextCompact: false,
35
+ cacheWriteReadRatio: DEFAULT_CACHE_WRITE_READ_RATIO,
36
+ });
37
+
38
+ const FEATURE_KEYS = [
39
+ "actionFusion",
40
+ "observationPack",
41
+ "evidencePreservingReducer",
42
+ "onlineContextCompact",
43
+ ] as const;
44
+ const STRING_KEYS = ["evidencePreservingReducerModel", "evidencePreservingReducerProvider"] as const;
45
+ const CONFIG_KEYS = new Set<string>(["version", ...FEATURE_KEYS, ...STRING_KEYS, "cacheWriteReadRatio"]);
46
+
47
+ export function findConfigPath(
48
+ cwd = process.cwd(),
49
+ agentDir = getAgentDir(),
50
+ allowProjectConfig = false,
51
+ ): string | undefined {
52
+ if (allowProjectConfig) {
53
+ const projectPath = join(cwd, CONFIG_DIR_NAME, "sol-pi.json");
54
+ if (existsSync(projectPath)) return projectPath;
55
+ }
56
+
57
+ const globalPath = join(agentDir, "sol-pi.json");
58
+ return existsSync(globalPath) ? globalPath : undefined;
59
+ }
60
+
61
+ export function loadSolPiConfig(
62
+ cwd = process.cwd(),
63
+ agentDir = getAgentDir(),
64
+ allowProjectConfig = false,
65
+ ): SolPiConfig {
66
+ const path = findConfigPath(cwd, agentDir, allowProjectConfig);
67
+ if (!path) return DEFAULT_CONFIG;
68
+
69
+ let parsed: unknown;
70
+ try {
71
+ parsed = JSON.parse(readFileSync(path, "utf8"));
72
+ } catch (error) {
73
+ const reason = error instanceof Error ? error.message : String(error);
74
+ throw new Error(`Unable to read SoL-Pi config ${path}: ${reason}`, { cause: error });
75
+ }
76
+
77
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
78
+ throw new Error(`SoL-Pi config must be a JSON object: ${path}`);
79
+ }
80
+
81
+ const record = parsed as Record<string, unknown>;
82
+ for (const key of Object.keys(record)) {
83
+ if (!CONFIG_KEYS.has(key)) throw new Error(`Unknown SoL-Pi config key: ${key}`);
84
+ }
85
+ if (record.version !== 1) throw new Error(`SoL-Pi config version must be 1: ${path}`);
86
+
87
+ for (const key of FEATURE_KEYS) {
88
+ if (record[key] !== undefined && typeof record[key] !== "boolean") {
89
+ throw new Error(`SoL-Pi config ${key} must be boolean: ${path}`);
90
+ }
91
+ }
92
+ const cacheWriteReadRatio = Object.hasOwn(record, "cacheWriteReadRatio")
93
+ ? record.cacheWriteReadRatio
94
+ : DEFAULT_CACHE_WRITE_READ_RATIO;
95
+ if (
96
+ typeof cacheWriteReadRatio !== "number" ||
97
+ !Number.isFinite(cacheWriteReadRatio) ||
98
+ cacheWriteReadRatio < 0
99
+ ) {
100
+ throw new Error(`SoL-Pi config cacheWriteReadRatio must be a finite non-negative number: ${path}`);
101
+ }
102
+ const evidencePreservingReducerModel = stringConfigValue(
103
+ record,
104
+ "evidencePreservingReducerModel",
105
+ DEFAULT_REDUCER_MODEL,
106
+ path,
107
+ );
108
+ const evidencePreservingReducerProvider = stringConfigValue(
109
+ record,
110
+ "evidencePreservingReducerProvider",
111
+ DEFAULT_REDUCER_PROVIDER,
112
+ path,
113
+ );
114
+
115
+ return Object.freeze({
116
+ ...DEFAULT_CONFIG,
117
+ ...record,
118
+ cacheWriteReadRatio,
119
+ evidencePreservingReducerModel,
120
+ evidencePreservingReducerProvider,
121
+ }) as SolPiConfig;
122
+ }
123
+
124
+ function stringConfigValue(
125
+ record: Record<string, unknown>,
126
+ key: (typeof STRING_KEYS)[number],
127
+ defaultValue: string,
128
+ path: string,
129
+ ): string {
130
+ const value = Object.hasOwn(record, key) ? record[key] : defaultValue;
131
+ if (typeof value !== "string" || value.trim().length === 0) {
132
+ throw new Error(`SoL-Pi config ${key} must be a non-empty string: ${path}`);
133
+ }
134
+ return value;
135
+ }
@@ -0,0 +1,74 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { realpath } from "node:fs/promises";
7
+ import { homedir } from "node:os";
8
+ import { basename, dirname, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const queueTails = new Map<string, Promise<void>>();
12
+
13
+ function stripToolPathPrefix(filePath: string): string {
14
+ return filePath.startsWith("@") ? filePath.slice(1) : filePath;
15
+ }
16
+
17
+ export function resolveToolPath(cwd: string, filePath: string): string {
18
+ const stripped = stripToolPathPrefix(filePath);
19
+ // Pi accepts file URLs; the queue and hash guard must use the same target.
20
+ const expanded = stripped.startsWith("file://") ? fileURLToPath(stripped) : stripped;
21
+ if (expanded === "~") return homedir();
22
+ if (expanded.startsWith("~/")) return resolve(homedir(), expanded.slice(2));
23
+ return resolve(cwd, expanded);
24
+ }
25
+
26
+ function isMissingPathError(error: unknown): boolean {
27
+ return (
28
+ typeof error === "object" &&
29
+ error !== null &&
30
+ "code" in error &&
31
+ (error.code === "ENOENT" || error.code === "ENOTDIR")
32
+ );
33
+ }
34
+
35
+ async function canonicalQueueKey(filePath: string): Promise<string> {
36
+ const resolvedPath = resolve(filePath);
37
+ let current = resolvedPath;
38
+ const missingSegments: string[] = [];
39
+
40
+ while (true) {
41
+ try {
42
+ return resolve(await realpath(current), ...missingSegments);
43
+ } catch (error) {
44
+ if (!isMissingPathError(error)) throw error;
45
+ const parent = dirname(current);
46
+ if (parent === current) return resolvedPath;
47
+ missingSegments.unshift(basename(current));
48
+ current = parent;
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Serialize fused operations for one canonical file path. This queue belongs
55
+ * to SoL-Pi and intentionally does not nest Pi's built-in mutation queue.
56
+ */
57
+ export async function withFusedFileQueue<T>(filePath: string, work: () => Promise<T>): Promise<T> {
58
+ const key = await canonicalQueueKey(filePath);
59
+ const previous = queueTails.get(key) ?? Promise.resolve();
60
+ let release!: () => void;
61
+ const owned = new Promise<void>((resolveOwned) => {
62
+ release = resolveOwned;
63
+ });
64
+ const tail = previous.then(() => owned);
65
+ queueTails.set(key, tail);
66
+
67
+ await previous;
68
+ try {
69
+ return await work();
70
+ } finally {
71
+ release();
72
+ if (queueTails.get(key) === tail) queueTails.delete(key);
73
+ }
74
+ }
@@ -0,0 +1,185 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ /**
6
+ * Action Fusion - fuse a file mutation and its follow-up command into one turn.
7
+ *
8
+ * Base pi rollouts repeatedly showed the same pair of turns: edit or write a
9
+ * file, then run a command to test, build, or start it. This extension replaces
10
+ * the built-in `edit` and `write` tools with versions that take an optional
11
+ * `then_run` object, apply the mutation, run the command, and return one
12
+ * combined observation. The model decision between the two turns disappears.
13
+ *
14
+ * Everything else about `edit` and `write` is inherited from the built-in
15
+ * definitions: their schemas, prompt text, argument shims, and renderers.
16
+ *
17
+ * This standalone version composes only Pi's public tool definitions.
18
+ */
19
+
20
+ import {
21
+ type BashToolOptions,
22
+ createEditToolDefinition,
23
+ createWriteToolDefinition,
24
+ type EditToolDetails,
25
+ type EditToolOptions,
26
+ type ExtensionAPI,
27
+ type ExtensionFactory,
28
+ type WriteToolOptions,
29
+ } from "@earendil-works/pi-coding-agent";
30
+ import { Type } from "typebox";
31
+ import { resolveToolPath } from "./file-queue.ts";
32
+ import { renderSolPiTool, showSolPiSavings } from "../../tui.ts";
33
+ import {
34
+ createThenRunSchema,
35
+ executeMutationThenRun,
36
+ THEN_RUN_SUCCEEDED,
37
+ type ThenRunInput,
38
+ } from "./then-run.ts";
39
+
40
+ const EDIT_THEN_RUN_DESCRIPTION =
41
+ "Command to run next on this file after the edit succeeds — e.g. run, build, start/restart, install, or check it; optional timeout in seconds. Skipped if the edit fails; a non-zero exit is reported but keeps the edit.";
42
+ const WRITE_THEN_RUN_DESCRIPTION =
43
+ "Command to run next on this file after the write succeeds — e.g. run, build, start/restart, install, or check it; optional timeout in seconds. Skipped if the write fails; a non-zero exit is reported but keeps the write.";
44
+
45
+ export interface ActionFusionOptions {
46
+ /** Optional programmatic bash overrides, primarily for tests and embedded runtimes. */
47
+ readonly bashOptions?: BashToolOptions;
48
+ /** Overrides for the underlying built-in `edit` tool. */
49
+ readonly editOptions?: EditToolOptions;
50
+ /** Overrides for the underlying built-in `write` tool. */
51
+ readonly writeOptions?: WriteToolOptions;
52
+ }
53
+
54
+ /**
55
+ * Built-in tool definitions capture their cwd in closures, so keep one per
56
+ * working directory instead of rebuilding them on every call and every redraw.
57
+ */
58
+ function memoizeByCwd<T>(create: (cwd: string) => T): (cwd: string) => T {
59
+ const cache = new Map<string, T>();
60
+ return (cwd) => {
61
+ const cached = cache.get(cwd);
62
+ if (cached) return cached;
63
+ const created = create(cwd);
64
+ cache.set(cwd, created);
65
+ return created;
66
+ };
67
+ }
68
+
69
+ function hasExternalToolOwner(pi: ExtensionAPI, name: string): boolean {
70
+ try {
71
+ const tool = pi.getAllTools().find((candidate) => candidate.name === name);
72
+ return Boolean(tool && tool.sourceInfo.source !== "builtin");
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ export function createActionFusionExtension(options: ActionFusionOptions = {}): ExtensionFactory {
79
+ const baseEdit = memoizeByCwd((cwd: string) => createEditToolDefinition(cwd, options.editOptions));
80
+ const baseWrite = memoizeByCwd((cwd: string) => createWriteToolDefinition(cwd, options.writeOptions));
81
+
82
+ return (pi: ExtensionAPI) => {
83
+ // then_run executes Bash inside the mutation tool, so it cannot pass through
84
+ // Pi's public tool_call permission handlers. Leave it to automode instead of
85
+ // creating a nested shell escape hatch.
86
+ if (hasExternalToolOwner(pi, "automode_inspect")) return;
87
+
88
+ const editTemplate = baseEdit(process.cwd());
89
+ const writeTemplate = baseWrite(process.cwd());
90
+
91
+ const editParameters = Type.Object({
92
+ ...editTemplate.parameters.properties,
93
+ then_run: createThenRunSchema(EDIT_THEN_RUN_DESCRIPTION),
94
+ });
95
+ const writeParameters = Type.Object({
96
+ ...writeTemplate.parameters.properties,
97
+ then_run: createThenRunSchema(WRITE_THEN_RUN_DESCRIPTION),
98
+ });
99
+
100
+ pi.registerTool<typeof editParameters, EditToolDetails | undefined>({
101
+ ...editTemplate,
102
+ parameters: editParameters,
103
+ async execute(toolCallId, input, signal, onUpdate, ctx) {
104
+ const { then_run, ...editInput } = input as typeof input & { then_run?: ThenRunInput };
105
+ const result = await executeMutationThenRun({
106
+ toolCallId,
107
+ absolutePath: resolveToolPath(ctx.cwd, input.path),
108
+ thenRun: then_run,
109
+ bashOptions: options.bashOptions,
110
+ signal,
111
+ ctx,
112
+ mutate: () => baseEdit(ctx.cwd).execute(toolCallId, editInput, signal, onUpdate, ctx),
113
+ });
114
+ if (
115
+ then_run &&
116
+ result.content.some((block) => block.type === "text" && block.text.includes(THEN_RUN_SUCCEEDED))
117
+ ) {
118
+ showSolPiSavings(ctx, "Action Fusion", "1 model round-trip avoided");
119
+ }
120
+ return result;
121
+ },
122
+ renderCall: (args, theme, context) => {
123
+ const base = baseEdit(context.cwd).renderCall!(args, theme, context);
124
+ return args.then_run ? renderSolPiTool(theme, "Action Fusion", "1 model round-trip avoided", base) : base;
125
+ },
126
+ renderResult: (result, resultOptions, theme, context) => {
127
+ const base = baseEdit(context.cwd).renderResult!(result, resultOptions, theme, context);
128
+ return context.args.then_run
129
+ ? renderSolPiTool(theme, "Action Fusion", "1 model round-trip avoided", base)
130
+ : base;
131
+ },
132
+ });
133
+
134
+ if (hasExternalToolOwner(pi, "write")) return;
135
+
136
+ pi.registerTool<typeof writeParameters, undefined>({
137
+ ...writeTemplate,
138
+ parameters: writeParameters,
139
+ async execute(toolCallId, input, signal, onUpdate, ctx) {
140
+ const { then_run, ...writeInput } = input as typeof input & { then_run?: ThenRunInput };
141
+ const result = await executeMutationThenRun({
142
+ toolCallId,
143
+ absolutePath: resolveToolPath(ctx.cwd, input.path),
144
+ thenRun: then_run,
145
+ bashOptions: options.bashOptions,
146
+ signal,
147
+ ctx,
148
+ mutate: () => baseWrite(ctx.cwd).execute(toolCallId, writeInput, signal, onUpdate, ctx),
149
+ });
150
+ if (
151
+ then_run &&
152
+ result.content.some((block) => block.type === "text" && block.text.includes(THEN_RUN_SUCCEEDED))
153
+ ) {
154
+ showSolPiSavings(ctx, "Action Fusion", "1 model round-trip avoided");
155
+ }
156
+ return result;
157
+ },
158
+ renderCall: (args, theme, context) => {
159
+ const base = baseWrite(context.cwd).renderCall!(args, theme, context);
160
+ return args.then_run ? renderSolPiTool(theme, "Action Fusion", "1 model round-trip avoided", base) : base;
161
+ },
162
+ renderResult: (result, resultOptions, theme, context) => {
163
+ const base = baseWrite(context.cwd).renderResult!(result, resultOptions, theme, context);
164
+ return context.args.then_run
165
+ ? renderSolPiTool(theme, "Action Fusion", "1 model round-trip avoided", base)
166
+ : base;
167
+ },
168
+ });
169
+ };
170
+ }
171
+
172
+ export type { ThenRunInput } from "./then-run.ts";
173
+ export {
174
+ assertUnchangedBeforeCommand,
175
+ executeMutationThenRun,
176
+ THEN_RUN_FAILED,
177
+ THEN_RUN_SKIPPED,
178
+ THEN_RUN_SUCCEEDED,
179
+ } from "./then-run.ts";
180
+
181
+ export function registerActionFusion(pi: ExtensionAPI): void {
182
+ createActionFusionExtension()(pi);
183
+ }
184
+
185
+ export default registerActionFusion;
@@ -0,0 +1,128 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ import { createHash } from "node:crypto";
6
+ import { readFile } from "node:fs/promises";
7
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
8
+ import { type BashToolOptions, createBashToolDefinition, type ExtensionContext } from "@earendil-works/pi-coding-agent";
9
+ import { Type } from "typebox";
10
+ import { withFusedFileQueue } from "./file-queue.ts";
11
+
12
+ export const THEN_RUN_SUCCEEDED = "[then_run:succeeded]";
13
+ export const THEN_RUN_FAILED = "[then_run:failed]";
14
+ export const THEN_RUN_SKIPPED = "[then_run:skipped]";
15
+
16
+ export interface ThenRunInput {
17
+ command: string;
18
+ timeout?: number;
19
+ }
20
+
21
+ export function createThenRunSchema(description: string) {
22
+ return Type.Optional(
23
+ Type.Object(
24
+ {
25
+ command: Type.String({ description: "Bash command to run" }),
26
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
27
+ },
28
+ { description },
29
+ ),
30
+ );
31
+ }
32
+
33
+ function errorText(error: unknown): string {
34
+ return error instanceof Error ? error.message : String(error);
35
+ }
36
+
37
+ function resultText(result: AgentToolResult<unknown>): string {
38
+ return result.content
39
+ .filter((block) => block.type === "text")
40
+ .map((block) => block.text)
41
+ .join("\n");
42
+ }
43
+
44
+ function thenRunSkippedError(error: unknown): Error {
45
+ return new Error(
46
+ `${errorText(error)}\n\n${THEN_RUN_SKIPPED} The file mutation did not complete successfully; the command was not run.`,
47
+ );
48
+ }
49
+
50
+ async function fileSha256(path: string): Promise<string> {
51
+ return createHash("sha256").update(await readFile(path)).digest("hex");
52
+ }
53
+
54
+ export async function assertUnchangedBeforeCommand(
55
+ path: string,
56
+ yieldForInterference: () => Promise<void> = () => new Promise<void>((resolve) => setImmediate(resolve)),
57
+ ): Promise<void> {
58
+ try {
59
+ const mutationHash = await fileSha256(path);
60
+ await yieldForInterference();
61
+ const commandHash = await fileSha256(path);
62
+ if (mutationHash !== commandHash) {
63
+ throw new Error("target content changed after the fused mutation");
64
+ }
65
+ } catch (error) {
66
+ throw new Error(`${THEN_RUN_SKIPPED} ${errorText(error)}; the command was not run.`, { cause: error });
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Apply a file mutation and, when the model asked for one, run its follow-up
72
+ * command before returning a single observation.
73
+ *
74
+ * Both steps run inside one SoL-Pi queue slot for `absolutePath`, so another
75
+ * fused mutation of the same file cannot interleave. Pi's built-in mutation
76
+ * tool keeps its own queue; the two queues are not nested.
77
+ */
78
+ export async function executeMutationThenRun<TDetails>({
79
+ toolCallId,
80
+ absolutePath,
81
+ thenRun,
82
+ mutate,
83
+ bashOptions,
84
+ signal,
85
+ ctx,
86
+ }: {
87
+ toolCallId: string;
88
+ absolutePath: string;
89
+ thenRun: ThenRunInput | undefined;
90
+ mutate: () => Promise<AgentToolResult<TDetails>>;
91
+ bashOptions: BashToolOptions | undefined;
92
+ signal: AbortSignal | undefined;
93
+ ctx: ExtensionContext;
94
+ }): Promise<AgentToolResult<TDetails>> {
95
+ return withFusedFileQueue(absolutePath, async () => {
96
+ let mutationResult: AgentToolResult<TDetails>;
97
+ try {
98
+ mutationResult = await mutate();
99
+ } catch (error) {
100
+ if (thenRun !== undefined) {
101
+ throw thenRunSkippedError(error);
102
+ }
103
+ throw error;
104
+ }
105
+
106
+ if (thenRun === undefined) {
107
+ return mutationResult;
108
+ }
109
+
110
+ await assertUnchangedBeforeCommand(absolutePath);
111
+ const bash = createBashToolDefinition(ctx.cwd, bashOptions);
112
+ try {
113
+ // pi-lens-ignore: sql-injection
114
+ const bashResult = await bash.execute(`${toolCallId}:then_run`, thenRun, signal, undefined, ctx);
115
+ const output = resultText(bashResult);
116
+ return {
117
+ ...mutationResult,
118
+ content: [
119
+ ...mutationResult.content,
120
+ { type: "text", text: output ? `${THEN_RUN_SUCCEEDED}\n${output}` : THEN_RUN_SUCCEEDED },
121
+ ],
122
+ };
123
+ } catch (error) {
124
+ const mutationOutput = resultText(mutationResult);
125
+ throw new Error([mutationOutput, THEN_RUN_FAILED, errorText(error)].filter(Boolean).join("\n\n"), { cause: error });
126
+ }
127
+ });
128
+ }
@@ -0,0 +1,53 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ import { isRecord, type ReducerConfig, sha256 } from "./config.ts";
8
+
9
+ export interface ArchiveObject {
10
+ readonly hash: string;
11
+ readonly bytes: number;
12
+ readonly chars: number;
13
+ readonly lines: number;
14
+ readonly path: string;
15
+ }
16
+
17
+ /**
18
+ * Archived logs live under SoL-Pi's session-derived runtime directory.
19
+ */
20
+ export function archiveRoot(config: ReducerConfig): string {
21
+ return config.storeRoot;
22
+ }
23
+
24
+ /**
25
+ * Store the raw log under its own content hash.
26
+ *
27
+ * Every quote in a receipt is checked against this archive, and the receipt
28
+ * points the frontier agent back at this path for exact readback. An existing
29
+ * object with the same name but different bytes is an integrity failure, not a
30
+ * cache hit.
31
+ */
32
+ export async function archiveBody(root: string, body: string): Promise<ArchiveObject> {
33
+ const hash = sha256(body);
34
+ const objectDir = join(root, "objects", hash.slice(0, 2));
35
+ const path = join(objectDir, `${hash}.txt`);
36
+ await mkdir(objectDir, { recursive: true, mode: 0o700 });
37
+ try {
38
+ await writeFile(path, body, { encoding: "utf8", flag: "wx", mode: 0o600 });
39
+ } catch (error) {
40
+ if (!isRecord(error) || error.code !== "EEXIST") throw error;
41
+ const existing = await readFile(path, "utf8");
42
+ if (existing !== body || sha256(existing) !== hash) {
43
+ throw new Error(`Reducer archive integrity failure: ${path}`, { cause: error });
44
+ }
45
+ }
46
+ return {
47
+ hash,
48
+ bytes: Buffer.byteLength(body, "utf8"),
49
+ chars: body.length,
50
+ lines: body.length === 0 ? 0 : body.split("\n").length,
51
+ path,
52
+ };
53
+ }
@@ -0,0 +1,101 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ import { lstat, readFile, realpath } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
7
+ import { basename, dirname } from "node:path";
8
+ import type { ToolResultEvent } from "@earendil-works/pi-coding-agent";
9
+ import { recordValue } from "./config.ts";
10
+
11
+ /** Markers written by the action-fusion extension around a fused command's output. */
12
+ const THEN_RUN_SUCCEEDED = "[then_run:succeeded]";
13
+ const THEN_RUN_FAILED = "[then_run:failed]";
14
+
15
+ export interface ReducibleToolResult {
16
+ readonly command: string;
17
+ readonly body: string;
18
+ /** Put the receipt back where the raw output was, leaving the rest of the result alone. */
19
+ readonly projectReceipt: (receipt: string) => ToolResultEvent["content"];
20
+ }
21
+
22
+ function textContent(event: ToolResultEvent): string {
23
+ return event.content
24
+ .filter((item): item is { type: "text"; text: string } => item.type === "text")
25
+ .map((item) => item.text)
26
+ .join("\n");
27
+ }
28
+
29
+ export function detailsFullOutputPath(details: unknown): string | undefined {
30
+ const value = recordValue(details, "fullOutputPath");
31
+ return typeof value === "string" ? value : undefined;
32
+ }
33
+
34
+ async function safePiBashTempPath(path: string | undefined): Promise<boolean> {
35
+ if (!path || !/^pi-bash-[^/\\]+\.log$/u.test(basename(path))) return false;
36
+ try {
37
+ const [candidate, root, status] = await Promise.all([realpath(path), realpath(tmpdir()), lstat(path)]);
38
+ return status.isFile() && !status.isSymbolicLink() && dirname(candidate) === root;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Prefer the untruncated file pi wrote for a large bash result, so evidence is
46
+ * checked against the exact bytes the command produced rather than a preview.
47
+ */
48
+ async function exactBodyFromInline(inline: string, details: unknown): Promise<string> {
49
+ const detailsPath = detailsFullOutputPath(details);
50
+ const inlineMatch = inline.match(/Full output:\s*([^\]\r\n]+)/u);
51
+ const candidate = detailsPath ?? inlineMatch?.[1]?.trim();
52
+ if (!candidate || !(await safePiBashTempPath(candidate))) return inline;
53
+ try {
54
+ return await readFile(candidate, "utf8");
55
+ } catch {
56
+ return inline;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Identify the log inside a tool result: either a plain bash result, or the
62
+ * command output appended by a fused `edit`/`write` call.
63
+ */
64
+ export async function reducibleToolResult(event: ToolResultEvent): Promise<ReducibleToolResult | undefined> {
65
+ if (event.toolName === "bash") {
66
+ const command = typeof event.input.command === "string" ? event.input.command : "";
67
+ if (!command) return undefined;
68
+ const inline = textContent(event);
69
+ return {
70
+ command,
71
+ body: await exactBodyFromInline(inline, event.details),
72
+ projectReceipt: (receipt) => [{ type: "text", text: receipt }],
73
+ };
74
+ }
75
+ if (event.toolName !== "write" && event.toolName !== "edit") return undefined;
76
+ const thenRun = recordValue(event.input, "then_run");
77
+ const commandValue = recordValue(thenRun, "command");
78
+ if (typeof commandValue !== "string" || !commandValue) return undefined;
79
+ const marker = event.isError ? THEN_RUN_FAILED : THEN_RUN_SUCCEEDED;
80
+ for (let index = 0; index < event.content.length; index++) {
81
+ const block = event.content[index];
82
+ if (!block || block.type !== "text") continue;
83
+ const markerIndex = block.text.indexOf(marker);
84
+ if (markerIndex < 0) continue;
85
+ const suffixStart = markerIndex + marker.length;
86
+ const suffix = block.text.slice(suffixStart);
87
+ const separator = suffix.match(/^(?:\r?\n)+/u)?.[0] ?? "\n";
88
+ const inline = suffix.slice(separator === "\n" && !suffix.startsWith("\n") ? 0 : separator.length);
89
+ return {
90
+ command: commandValue,
91
+ body: await exactBodyFromInline(inline, event.details),
92
+ projectReceipt: (receipt) =>
93
+ event.content.map((content, contentIndex) =>
94
+ contentIndex === index && content.type === "text"
95
+ ? { ...content, text: `${content.text.slice(0, suffixStart)}${separator}${receipt}` }
96
+ : content,
97
+ ),
98
+ };
99
+ }
100
+ return undefined;
101
+ }