@fitchmultz/pi-subagents 0.0.0-stage → 0.44.5

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 (347) hide show
  1. package/CHANGELOG.md +1654 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1533 -2
  4. package/agents/context-builder.md +97 -0
  5. package/agents/debugger.md +51 -0
  6. package/agents/delegate.md +13 -0
  7. package/agents/fixer.md +37 -0
  8. package/agents/oracle.md +71 -0
  9. package/agents/planner.md +72 -0
  10. package/agents/researcher.md +51 -0
  11. package/agents/reviewer-claude.md +69 -0
  12. package/agents/reviewer-gpt.md +68 -0
  13. package/agents/reviewer-ponytail.md +82 -0
  14. package/agents/reviewer-security.md +81 -0
  15. package/agents/reviewer.md +69 -0
  16. package/agents/scout.md +66 -0
  17. package/agents/ui-designer.md +51 -0
  18. package/agents/watcher.md +26 -0
  19. package/agents/worker.md +46 -0
  20. package/agents/writer.md +36 -0
  21. package/dist/agents/agent-management.js +923 -0
  22. package/dist/agents/agent-scope.js +7 -0
  23. package/dist/agents/agent-selection.js +20 -0
  24. package/dist/agents/agent-serializer.js +100 -0
  25. package/dist/agents/agents.js +752 -0
  26. package/dist/agents/chain-serializer.js +367 -0
  27. package/dist/agents/frontmatter.js +21 -0
  28. package/dist/agents/identity.js +29 -0
  29. package/dist/agents/skills.js +570 -0
  30. package/dist/extension/build-info.js +1 -0
  31. package/dist/extension/compact-tools.js +112 -0
  32. package/dist/extension/config.js +22 -0
  33. package/dist/extension/control-notices.js +29 -0
  34. package/dist/extension/doctor.js +143 -0
  35. package/dist/extension/fanout-child.js +326 -0
  36. package/dist/extension/index.js +577 -0
  37. package/dist/extension/schemas.js +416 -0
  38. package/dist/extension/tool-input.js +20 -0
  39. package/dist/extension/tool-result.js +14 -0
  40. package/dist/history/canonical-result.js +81 -0
  41. package/dist/history/database.js +72 -0
  42. package/dist/history/index.js +199 -0
  43. package/dist/history/ingest.js +468 -0
  44. package/dist/history/queries.js +350 -0
  45. package/dist/history/store.js +216 -0
  46. package/dist/history/types.js +4 -0
  47. package/dist/history/worker.js +466 -0
  48. package/dist/intercom/intercom-bridge.js +112 -0
  49. package/dist/intercom/live-intercom.js +94 -0
  50. package/dist/intercom/result-intercom.js +373 -0
  51. package/dist/pi-intercom/agent-dir.js +10 -0
  52. package/dist/pi-intercom/bridge.js +750 -0
  53. package/dist/pi-intercom/broker/broker.js +426 -0
  54. package/dist/pi-intercom/broker/client.js +492 -0
  55. package/dist/pi-intercom/broker/framing.js +65 -0
  56. package/dist/pi-intercom/broker/paths.js +42 -0
  57. package/dist/pi-intercom/broker/pid.js +66 -0
  58. package/dist/pi-intercom/broker/spawn.js +221 -0
  59. package/dist/pi-intercom/config.js +102 -0
  60. package/dist/pi-intercom/index.js +2942 -0
  61. package/dist/pi-intercom/reply-tracker.js +182 -0
  62. package/dist/pi-intercom/session-targets.js +128 -0
  63. package/dist/pi-intercom/subagent-live-events.js +132 -0
  64. package/dist/pi-intercom/topics.js +157 -0
  65. package/dist/pi-intercom/types.js +163 -0
  66. package/dist/pi-intercom/ui/compose.js +269 -0
  67. package/dist/pi-intercom/ui/inline-message.js +80 -0
  68. package/dist/pi-intercom/ui/session-list.js +124 -0
  69. package/dist/runs/background/async-control.js +95 -0
  70. package/dist/runs/background/async-execution.js +676 -0
  71. package/dist/runs/background/async-job-tracker.js +383 -0
  72. package/dist/runs/background/async-result-file.js +58 -0
  73. package/dist/runs/background/async-resume.js +305 -0
  74. package/dist/runs/background/async-status.js +442 -0
  75. package/dist/runs/background/completion-dedupe.js +69 -0
  76. package/dist/runs/background/completion-delivery.js +245 -0
  77. package/dist/runs/background/notify.js +71 -0
  78. package/dist/runs/background/parallel-groups.js +55 -0
  79. package/dist/runs/background/result-watcher.js +426 -0
  80. package/dist/runs/background/run-id-resolver.js +64 -0
  81. package/dist/runs/background/run-status.js +382 -0
  82. package/dist/runs/background/stale-run-reconciler.js +335 -0
  83. package/dist/runs/background/subagent-runner-launcher.js +55 -0
  84. package/dist/runs/background/subagent-runner.js +1872 -0
  85. package/dist/runs/background/top-level-async.js +5 -0
  86. package/dist/runs/foreground/chain-clarify.js +1152 -0
  87. package/dist/runs/foreground/clarify-invocation.js +72 -0
  88. package/dist/runs/foreground/execution-input.js +423 -0
  89. package/dist/runs/foreground/foreground-control.js +817 -0
  90. package/dist/runs/foreground/question-control.js +88 -0
  91. package/dist/runs/foreground/run-async-path.js +205 -0
  92. package/dist/runs/foreground/subagent-executor.js +442 -0
  93. package/dist/runs/foreground/subagent-params.js +165 -0
  94. package/dist/runs/foreground/wait-run.js +141 -0
  95. package/dist/runs/shared/acceptance-contract.js +253 -0
  96. package/dist/runs/shared/acceptance-evaluation.js +256 -0
  97. package/dist/runs/shared/acceptance-finalization.js +280 -0
  98. package/dist/runs/shared/acceptance-reports.js +147 -0
  99. package/dist/runs/shared/acceptance.js +4 -0
  100. package/dist/runs/shared/chain-outputs.js +110 -0
  101. package/dist/runs/shared/child-attempt.js +693 -0
  102. package/dist/runs/shared/child-execution-cwd.js +96 -0
  103. package/dist/runs/shared/child-observations.js +114 -0
  104. package/dist/runs/shared/claude-code.js +199 -0
  105. package/dist/runs/shared/completion-guard.js +31 -0
  106. package/dist/runs/shared/dynamic-fanout.js +274 -0
  107. package/dist/runs/shared/history-index.js +72 -0
  108. package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
  109. package/dist/runs/shared/model-fallback.js +185 -0
  110. package/dist/runs/shared/mutating-tool-guard.js +170 -0
  111. package/dist/runs/shared/native-finalization.js +101 -0
  112. package/dist/runs/shared/native-usage.js +153 -0
  113. package/dist/runs/shared/nested-events.js +700 -0
  114. package/dist/runs/shared/nested-path.js +48 -0
  115. package/dist/runs/shared/nested-render.js +108 -0
  116. package/dist/runs/shared/parallel-utils.js +61 -0
  117. package/dist/runs/shared/parent-receipts.js +68 -0
  118. package/dist/runs/shared/parent-usage.js +121 -0
  119. package/dist/runs/shared/pi-args.js +310 -0
  120. package/dist/runs/shared/pi-spawn.js +52 -0
  121. package/dist/runs/shared/run-history-queries.js +45 -0
  122. package/dist/runs/shared/run-history.js +110 -0
  123. package/dist/runs/shared/run-id-ambiguity.js +4 -0
  124. package/dist/runs/shared/run-records.js +679 -0
  125. package/dist/runs/shared/session-cwd-preload.js +33 -0
  126. package/dist/runs/shared/single-output.js +222 -0
  127. package/dist/runs/shared/streaming-text.js +15 -0
  128. package/dist/runs/shared/structured-output.js +55 -0
  129. package/dist/runs/shared/subagent-control.js +179 -0
  130. package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
  131. package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
  132. package/dist/runs/shared/supervisor-questions.js +350 -0
  133. package/dist/runs/shared/workflow-graph.js +214 -0
  134. package/dist/runs/shared/workflow-policy.js +84 -0
  135. package/dist/runs/shared/worktree.js +525 -0
  136. package/dist/shared/agent-context-policy.js +126 -0
  137. package/dist/shared/artifacts.js +96 -0
  138. package/dist/shared/atomic-json.js +13 -0
  139. package/dist/shared/cooperative.js +20 -0
  140. package/dist/shared/execution-cwd.js +14 -0
  141. package/dist/shared/file-coalescer.js +21 -0
  142. package/dist/shared/fork-context.js +49 -0
  143. package/dist/shared/formatters.js +85 -0
  144. package/dist/shared/journal-reader.js +407 -0
  145. package/dist/shared/lazy-tools.js +32 -0
  146. package/dist/shared/model-info.js +68 -0
  147. package/dist/shared/native-session.js +15 -0
  148. package/dist/shared/native-tui.js +18 -0
  149. package/dist/shared/native-typebox.js +20 -0
  150. package/dist/shared/post-exit-stdio-guard.js +197 -0
  151. package/dist/shared/prompt-sections.js +8 -0
  152. package/dist/shared/session-entries.js +52 -0
  153. package/dist/shared/session-identity.js +14 -0
  154. package/dist/shared/settings.js +254 -0
  155. package/dist/shared/status-format.js +111 -0
  156. package/dist/shared/temp-root.js +114 -0
  157. package/dist/shared/types.js +207 -0
  158. package/dist/shared/ui-mode.js +3 -0
  159. package/dist/shared/utils.js +334 -0
  160. package/dist/slash/prompt-template-bridge.js +298 -0
  161. package/dist/slash/slash-bridge.js +116 -0
  162. package/dist/slash/slash-commands.js +607 -0
  163. package/dist/slash/slash-live-state.js +246 -0
  164. package/dist/tui/action-hints.js +75 -0
  165. package/dist/tui/agent-history.js +197 -0
  166. package/dist/tui/agent-view.js +1656 -0
  167. package/dist/tui/render.js +1515 -0
  168. package/docs/intercom-bridge.md +212 -0
  169. package/docs/intercom.md +581 -0
  170. package/docs/performance.md +121 -0
  171. package/package.json +102 -4
  172. package/prompts/gather-context-and-clarify.md +14 -0
  173. package/prompts/parallel-cleanup.md +62 -0
  174. package/prompts/parallel-context-build.md +56 -0
  175. package/prompts/parallel-handoff-plan.md +62 -0
  176. package/prompts/parallel-research.md +51 -0
  177. package/prompts/parallel-review.md +59 -0
  178. package/prompts/review-loop.md +42 -0
  179. package/scripts/build.mjs +134 -0
  180. package/scripts/check-compat.mjs +48 -0
  181. package/scripts/compat-host.mjs +19 -0
  182. package/scripts/intercom-bridge-credentials.mjs +143 -0
  183. package/scripts/intercom-remote.mjs +103 -0
  184. package/scripts/linux-smoke.sh +51 -0
  185. package/scripts/local-install-smoke.mjs +66 -0
  186. package/scripts/native-package-smoke.mjs +82 -0
  187. package/scripts/package-smoke.mjs +209 -0
  188. package/scripts/prepare.mjs +67 -0
  189. package/scripts/real-pi-smoke.mjs +415 -0
  190. package/scripts/run-tests.mjs +149 -0
  191. package/skills/pi-intercom/SKILL.md +113 -0
  192. package/skills/pi-intercom/evals/evals.json +29 -0
  193. package/skills/pi-intercom/evals/trigger-evals.json +47 -0
  194. package/skills/pi-intercom/references/peer-sessions.md +69 -0
  195. package/skills/pi-subagents/SKILL.md +76 -0
  196. package/skills/pi-subagents/evals/evals.json +29 -0
  197. package/skills/pi-subagents/evals/trigger-evals.json +42 -0
  198. package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
  199. package/src/agents/agent-management.ts +813 -0
  200. package/src/agents/agent-scope.ts +7 -0
  201. package/src/agents/agent-selection.ts +23 -0
  202. package/src/agents/agent-serializer.ts +99 -0
  203. package/src/agents/agents.ts +916 -0
  204. package/src/agents/chain-serializer.ts +329 -0
  205. package/src/agents/frontmatter.ts +21 -0
  206. package/src/agents/identity.ts +30 -0
  207. package/src/agents/skills.ts +637 -0
  208. package/src/extension/build-info.ts +2 -0
  209. package/src/extension/compact-tools.ts +114 -0
  210. package/src/extension/config.ts +23 -0
  211. package/src/extension/control-notices.ts +47 -0
  212. package/src/extension/doctor.ts +171 -0
  213. package/src/extension/fanout-child.ts +312 -0
  214. package/src/extension/index.ts +649 -0
  215. package/src/extension/schemas.ts +444 -0
  216. package/src/extension/tool-input.ts +19 -0
  217. package/src/extension/tool-result.ts +16 -0
  218. package/src/history/canonical-result.ts +62 -0
  219. package/src/history/database.ts +59 -0
  220. package/src/history/index.ts +156 -0
  221. package/src/history/ingest.ts +349 -0
  222. package/src/history/queries.ts +241 -0
  223. package/src/history/store.ts +203 -0
  224. package/src/history/types.ts +166 -0
  225. package/src/history/worker.ts +321 -0
  226. package/src/intercom/intercom-bridge.ts +123 -0
  227. package/src/intercom/live-intercom.ts +104 -0
  228. package/src/intercom/result-intercom.ts +429 -0
  229. package/src/pi-intercom/agent-dir.ts +9 -0
  230. package/src/pi-intercom/bridge.ts +668 -0
  231. package/src/pi-intercom/broker/broker.ts +463 -0
  232. package/src/pi-intercom/broker/client.ts +558 -0
  233. package/src/pi-intercom/broker/framing.ts +77 -0
  234. package/src/pi-intercom/broker/paths.ts +47 -0
  235. package/src/pi-intercom/broker/pid.ts +59 -0
  236. package/src/pi-intercom/broker/spawn.ts +248 -0
  237. package/src/pi-intercom/config.ts +142 -0
  238. package/src/pi-intercom/index.ts +2929 -0
  239. package/src/pi-intercom/reply-tracker.ts +211 -0
  240. package/src/pi-intercom/session-targets.ts +172 -0
  241. package/src/pi-intercom/subagent-live-events.ts +133 -0
  242. package/src/pi-intercom/topics.ts +125 -0
  243. package/src/pi-intercom/types.ts +271 -0
  244. package/src/pi-intercom/ui/compose.ts +289 -0
  245. package/src/pi-intercom/ui/inline-message.ts +92 -0
  246. package/src/pi-intercom/ui/session-list.ts +139 -0
  247. package/src/runs/background/async-control.ts +89 -0
  248. package/src/runs/background/async-execution.ts +850 -0
  249. package/src/runs/background/async-job-tracker.ts +385 -0
  250. package/src/runs/background/async-result-file.ts +62 -0
  251. package/src/runs/background/async-resume.ts +333 -0
  252. package/src/runs/background/async-status.ts +481 -0
  253. package/src/runs/background/completion-dedupe.ts +79 -0
  254. package/src/runs/background/completion-delivery.ts +195 -0
  255. package/src/runs/background/notify.ts +123 -0
  256. package/src/runs/background/parallel-groups.ts +53 -0
  257. package/src/runs/background/result-watcher.ts +423 -0
  258. package/src/runs/background/run-id-resolver.ts +69 -0
  259. package/src/runs/background/run-status.ts +384 -0
  260. package/src/runs/background/stale-run-reconciler.ts +369 -0
  261. package/src/runs/background/subagent-runner-launcher.ts +56 -0
  262. package/src/runs/background/subagent-runner.ts +2053 -0
  263. package/src/runs/background/top-level-async.ts +13 -0
  264. package/src/runs/foreground/chain-clarify.ts +1340 -0
  265. package/src/runs/foreground/clarify-invocation.ts +77 -0
  266. package/src/runs/foreground/execution-input.ts +467 -0
  267. package/src/runs/foreground/foreground-control.ts +821 -0
  268. package/src/runs/foreground/question-control.ts +82 -0
  269. package/src/runs/foreground/run-async-path.ts +243 -0
  270. package/src/runs/foreground/subagent-executor.ts +501 -0
  271. package/src/runs/foreground/subagent-params.ts +320 -0
  272. package/src/runs/foreground/wait-run.ts +124 -0
  273. package/src/runs/shared/acceptance-contract.ts +257 -0
  274. package/src/runs/shared/acceptance-evaluation.ts +275 -0
  275. package/src/runs/shared/acceptance-finalization.ts +353 -0
  276. package/src/runs/shared/acceptance-reports.ts +152 -0
  277. package/src/runs/shared/acceptance.ts +33 -0
  278. package/src/runs/shared/chain-outputs.ts +114 -0
  279. package/src/runs/shared/child-attempt.ts +623 -0
  280. package/src/runs/shared/child-execution-cwd.ts +91 -0
  281. package/src/runs/shared/child-observations.ts +99 -0
  282. package/src/runs/shared/claude-code.ts +243 -0
  283. package/src/runs/shared/completion-guard.ts +35 -0
  284. package/src/runs/shared/dynamic-fanout.ts +295 -0
  285. package/src/runs/shared/history-index.ts +69 -0
  286. package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
  287. package/src/runs/shared/model-fallback.ts +219 -0
  288. package/src/runs/shared/mutating-tool-guard.ts +194 -0
  289. package/src/runs/shared/native-finalization.ts +128 -0
  290. package/src/runs/shared/native-usage.ts +113 -0
  291. package/src/runs/shared/nested-events.ts +752 -0
  292. package/src/runs/shared/nested-path.ts +52 -0
  293. package/src/runs/shared/nested-render.ts +119 -0
  294. package/src/runs/shared/parallel-utils.ts +153 -0
  295. package/src/runs/shared/parent-receipts.ts +59 -0
  296. package/src/runs/shared/parent-usage.ts +121 -0
  297. package/src/runs/shared/pi-args.ts +350 -0
  298. package/src/runs/shared/pi-spawn.ts +52 -0
  299. package/src/runs/shared/run-history-queries.ts +44 -0
  300. package/src/runs/shared/run-history.ts +100 -0
  301. package/src/runs/shared/run-id-ambiguity.ts +4 -0
  302. package/src/runs/shared/run-records.ts +607 -0
  303. package/src/runs/shared/session-cwd-preload.ts +27 -0
  304. package/src/runs/shared/single-output.ts +267 -0
  305. package/src/runs/shared/streaming-text.ts +18 -0
  306. package/src/runs/shared/structured-output.ts +68 -0
  307. package/src/runs/shared/subagent-control.ts +219 -0
  308. package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
  309. package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
  310. package/src/runs/shared/supervisor-questions.ts +415 -0
  311. package/src/runs/shared/workflow-graph.ts +220 -0
  312. package/src/runs/shared/workflow-policy.ts +125 -0
  313. package/src/runs/shared/worktree.ts +635 -0
  314. package/src/shared/agent-context-policy.ts +184 -0
  315. package/src/shared/artifacts.ts +95 -0
  316. package/src/shared/atomic-json.ts +16 -0
  317. package/src/shared/cooperative.ts +21 -0
  318. package/src/shared/execution-cwd.ts +14 -0
  319. package/src/shared/file-coalescer.ts +29 -0
  320. package/src/shared/fork-context.ts +68 -0
  321. package/src/shared/formatters.ts +84 -0
  322. package/src/shared/journal-reader.ts +333 -0
  323. package/src/shared/lazy-tools.ts +36 -0
  324. package/src/shared/model-info.ts +81 -0
  325. package/src/shared/native-session.ts +8 -0
  326. package/src/shared/native-tui.ts +10 -0
  327. package/src/shared/native-typebox.ts +14 -0
  328. package/src/shared/post-exit-stdio-guard.ts +204 -0
  329. package/src/shared/prompt-sections.ts +10 -0
  330. package/src/shared/session-entries.ts +49 -0
  331. package/src/shared/session-identity.ts +18 -0
  332. package/src/shared/settings.ts +384 -0
  333. package/src/shared/status-format.ts +123 -0
  334. package/src/shared/temp-root.ts +107 -0
  335. package/src/shared/types.ts +1260 -0
  336. package/src/shared/ui-mode.ts +5 -0
  337. package/src/shared/utils.ts +342 -0
  338. package/src/slash/prompt-template-bridge.ts +408 -0
  339. package/src/slash/slash-bridge.ts +178 -0
  340. package/src/slash/slash-commands.ts +617 -0
  341. package/src/slash/slash-live-state.ts +292 -0
  342. package/src/tui/action-hints.ts +69 -0
  343. package/src/tui/agent-history.ts +210 -0
  344. package/src/tui/agent-view.ts +1323 -0
  345. package/src/tui/render.ts +1550 -0
  346. package/tsconfig.build.json +12 -0
  347. package/tsconfig.json +19 -0
@@ -0,0 +1,194 @@
1
+ interface FailedMutatingAttempt {
2
+ tool: string;
3
+ path?: string;
4
+ error: string;
5
+ ts: number;
6
+ }
7
+
8
+ interface MutatingFailureState {
9
+ consecutiveFailures: number;
10
+ lastFailureAt?: number;
11
+ recentFailures: FailedMutatingAttempt[];
12
+ lastMutatingPath?: string;
13
+ repeatedPathFailures: number;
14
+ }
15
+
16
+ const MUTATING_BASH_PATTERNS = [
17
+ /(^|[;&|()\s])rm\s+/,
18
+ /(^|[;&|()\s])mv\s+/,
19
+ /(^|[;&|()\s])cp\s+/,
20
+ /(^|[;&|()\s])mkdir\s+/,
21
+ /(^|[;&|()\s])touch\s+/,
22
+ /(^|[;&|()\s])git\s+apply\b/,
23
+ /(^|[;&|()\s])patch\s+/,
24
+ /(^|[;&|()\s])sed\s+[^\n;&|]*\s-i\b/,
25
+ /(^|[;&|()\s])perl\s+[^\n;&|]*\s-pi\b/,
26
+ /(^|[;&|()]|\n)\s*tee\s+[^|&;]+/,
27
+ /\b(writeFile|writeFileSync|appendFile|appendFileSync)\b/,
28
+ /\bwrite_text\s*\(/,
29
+ /\bopen\s*\([^)]*,\s*["'][wa]/,
30
+ ];
31
+
32
+ export function resolveCurrentPath(toolName: string | undefined, args: Record<string, unknown> | undefined): string | undefined {
33
+ if (!toolName || !args) return undefined;
34
+ const direct = ["path", "file", "filename", "target", "cwd"];
35
+ for (const key of direct) {
36
+ const value = args[key];
37
+ if (typeof value === "string" && value.trim()) return value.trim();
38
+ }
39
+ if (toolName === "bash") {
40
+ const command = typeof args.command === "string" ? args.command : undefined;
41
+ if (!command) return undefined;
42
+ const redirect = command.match(/(?:>>?|tee\s+)\s*([^&>\s][^\s;&|]*)/);
43
+ if (redirect?.[1]) return redirect[1];
44
+ }
45
+ return undefined;
46
+ }
47
+
48
+ function hasUnquotedFileRedirection(command: string): boolean {
49
+ let inSingle = false;
50
+ let inDouble = false;
51
+ for (let i = 0; i < command.length; i++) {
52
+ const char = command[i]!;
53
+ if (char === "'" && !inDouble) {
54
+ inSingle = !inSingle;
55
+ continue;
56
+ }
57
+ if (char === '"' && !inSingle) {
58
+ inDouble = !inDouble;
59
+ continue;
60
+ }
61
+ if (inSingle || inDouble) continue;
62
+ if (char !== ">") continue;
63
+ if (command[i - 1] === "-") continue;
64
+ const isDouble = command[i + 1] === ">";
65
+ let cursor = i + (isDouble ? 2 : 1);
66
+ while (cursor < command.length && /\s/.test(command[cursor]!)) cursor++;
67
+ if (cursor >= command.length) continue;
68
+ const targetStart = command[cursor]!;
69
+ if (targetStart === "&" || targetStart === "|" || targetStart === ";") continue;
70
+ if (targetStart === "(" || targetStart === ")") continue;
71
+ return true;
72
+ }
73
+ return false;
74
+ }
75
+
76
+ export function isMutatingBashCommand(command: string): boolean {
77
+ return hasUnquotedFileRedirection(command) || MUTATING_BASH_PATTERNS.some((pattern) => pattern.test(command));
78
+ }
79
+
80
+ export function isMutatingTool(toolName: string | undefined, args: Record<string, unknown> | undefined): boolean {
81
+ if (!toolName || args?.preview === true) return false;
82
+ if (["edit", "write", "apply_edits", "apply_patch", "replace_text", "write_files"].includes(toolName)) return true;
83
+ if (toolName !== "bash") return false;
84
+ const command = typeof args?.command === "string" ? args.command : "";
85
+ if (!command.trim()) return false;
86
+ return isMutatingBashCommand(command);
87
+ }
88
+
89
+ export interface PendingMutationTool {
90
+ id?: string;
91
+ tool: string;
92
+ path?: string;
93
+ mutates: boolean;
94
+ startedAt?: number;
95
+ }
96
+
97
+ export interface MutationToolResult extends PendingMutationTool {
98
+ completedMutation: boolean;
99
+ errored: boolean;
100
+ }
101
+
102
+ export function createMutationCompletionTracker(): {
103
+ recordToolStart(input: { id?: string; toolName?: string; args?: Record<string, unknown>; path?: string; mutates?: boolean; startedAt?: number }): PendingMutationTool;
104
+ recordToolResult(input?: { toolCallId?: unknown; toolName?: unknown; isError?: unknown; details?: unknown }): MutationToolResult | undefined;
105
+ } {
106
+ const pending: PendingMutationTool[] = [];
107
+ const take = (input?: { toolCallId?: unknown; toolName?: unknown }): PendingMutationTool | undefined => {
108
+ const id = typeof input?.toolCallId === "string" ? input.toolCallId : undefined;
109
+ if (id) {
110
+ const index = pending.findIndex((tool) => tool.id === id);
111
+ if (index >= 0) return pending.splice(index, 1)[0];
112
+ }
113
+ const name = typeof input?.toolName === "string" ? input.toolName : undefined;
114
+ if (name) {
115
+ const index = pending.findIndex((tool) => tool.tool === name);
116
+ if (index >= 0) return pending.splice(index, 1)[0];
117
+ }
118
+ return pending.shift();
119
+ };
120
+ return {
121
+ recordToolStart(input) {
122
+ const tool = input.toolName ?? "tool";
123
+ const entry = {
124
+ id: input.id,
125
+ tool,
126
+ path: input.path,
127
+ mutates: input.mutates ?? isMutatingTool(input.toolName, input.args),
128
+ startedAt: input.startedAt,
129
+ };
130
+ pending.push(entry);
131
+ return entry;
132
+ },
133
+ recordToolResult(input) {
134
+ const entry = take(input);
135
+ if (!entry) return undefined;
136
+ const errored = input?.isError === true;
137
+ const details = input?.details && typeof input.details === "object" ? input.details as { preview?: unknown; modifiedFiles?: unknown } : undefined;
138
+ const committed = Array.isArray(details?.modifiedFiles)
139
+ ? details.modifiedFiles.some((file) => typeof file === "string" && file.trim().length > 0)
140
+ : !errored && !["apply_patch", "replace_text", "write_files"].includes(entry.tool);
141
+ return { ...entry, errored, completedMutation: entry.mutates && details?.preview !== true && committed };
142
+ },
143
+ };
144
+ }
145
+
146
+ export function resetMutatingFailureState(state: MutatingFailureState): void {
147
+ state.consecutiveFailures = 0;
148
+ state.lastFailureAt = undefined;
149
+ state.recentFailures = [];
150
+ state.lastMutatingPath = undefined;
151
+ state.repeatedPathFailures = 0;
152
+ }
153
+
154
+ export function createMutatingFailureState(): MutatingFailureState {
155
+ return {
156
+ consecutiveFailures: 0,
157
+ recentFailures: [],
158
+ repeatedPathFailures: 0,
159
+ };
160
+ }
161
+
162
+ export function recordMutatingFailure(
163
+ state: MutatingFailureState,
164
+ input: FailedMutatingAttempt,
165
+ windowMs: number,
166
+ ): void {
167
+ if (state.lastFailureAt === undefined || input.ts - state.lastFailureAt > windowMs) {
168
+ state.consecutiveFailures = 0;
169
+ state.recentFailures = [];
170
+ state.repeatedPathFailures = 0;
171
+ state.lastMutatingPath = undefined;
172
+ }
173
+ state.lastFailureAt = input.ts;
174
+ state.consecutiveFailures += 1;
175
+ if (input.path && state.lastMutatingPath === input.path) {
176
+ state.repeatedPathFailures += 1;
177
+ } else if (input.path) {
178
+ state.lastMutatingPath = input.path;
179
+ state.repeatedPathFailures = 1;
180
+ }
181
+ state.recentFailures.push(input);
182
+ if (state.recentFailures.length > 3) state.recentFailures.shift();
183
+ }
184
+
185
+ export function shouldEscalateMutatingFailures(state: MutatingFailureState, threshold: number): boolean {
186
+ return state.consecutiveFailures >= threshold || state.repeatedPathFailures >= threshold;
187
+ }
188
+
189
+ export function summarizeRecentMutatingFailures(state: MutatingFailureState): string | undefined {
190
+ if (state.recentFailures.length === 0) return undefined;
191
+ return state.recentFailures
192
+ .map((entry) => `${entry.tool}${entry.path ? `(${entry.path})` : ""}: ${entry.error}`)
193
+ .join(" | ");
194
+ }
@@ -0,0 +1,128 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import type { Message } from "@earendil-works/pi-ai";
6
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+ import { Type } from "../../shared/native-typebox.ts";
8
+ import type { AcceptanceLedger, ResolvedAcceptanceConfig } from "../../shared/types.ts";
9
+ import { setPromptSection } from "../../shared/prompt-sections.ts";
10
+ import { detectSubagentError, getFinalOutput } from "../../shared/utils.ts";
11
+ import {
12
+ acceptanceFailureMessage, acceptanceSelfReviewConfig, createFinalizationReportRuntime,
13
+ evaluateAcceptance, formatAcceptanceFinalizationPrompt, readFinalizationReport,
14
+ resolveFinalizationOutput, stripAcceptanceReport,
15
+ } from "./acceptance.ts";
16
+ import { captureSingleOutputSnapshot, resolveSingleOutput, type SingleOutputSnapshot } from "./single-output.ts";
17
+ import { readStructuredOutput, validateStructuredOutputValue, type StructuredOutputRuntime } from "./structured-output.ts";
18
+ import { compactObservedMessage } from "./child-observations.ts";
19
+
20
+ const CONFIG_ENV = "PI_SUBAGENT_FINALIZATION_CONFIG";
21
+ export const FINALIZATION_EVENT = "subagent.finalization";
22
+
23
+ export interface NativeFinalizationConfig {
24
+ nonce: string;
25
+ acceptance: ResolvedAcceptanceConfig;
26
+ reportRuntime: ReturnType<typeof createFinalizationReportRuntime>;
27
+ publicOutput?: StructuredOutputRuntime;
28
+ outputPath?: string;
29
+ outputSnapshot?: SingleOutputSnapshot;
30
+ }
31
+
32
+ export interface NativeFinalizationEvent {
33
+ type: typeof FINALIZATION_EVENT;
34
+ nonce: string;
35
+ turn: number;
36
+ lastEntryId?: string;
37
+ messageCount: number;
38
+ at: number;
39
+ submission: ReturnType<typeof readFinalizationReport> & { error?: string };
40
+ acceptance?: AcceptanceLedger;
41
+ resolvedOutput: ReturnType<typeof resolveSingleOutput>;
42
+ nextPrompt?: string;
43
+ }
44
+
45
+ export function createNativeFinalization(acceptance: ResolvedAcceptanceConfig, publicOutput?: StructuredOutputRuntime, outputPath?: string): NativeFinalizationConfig {
46
+ return { nonce: randomUUID(), acceptance, reportRuntime: createFinalizationReportRuntime(publicOutput?.schema), publicOutput,
47
+ outputPath, outputSnapshot: captureSingleOutputSnapshot(outputPath) };
48
+ }
49
+
50
+ export function nativeFinalizationLaunch(config: NativeFinalizationConfig): { extension: string; env: Record<string, string> } {
51
+ const configPath = path.join(path.dirname(config.reportRuntime.schemaPath), "finalization.json");
52
+ fs.writeFileSync(configPath, JSON.stringify(config), { mode: 0o600 });
53
+ return { extension: fileURLToPath(new URL(`native-finalization${import.meta.url.endsWith(".ts") ? ".ts" : ".js"}`, import.meta.url)), env: { [CONFIG_ENV]: configPath } };
54
+ }
55
+
56
+ /** Self-review includes structural and workspace checks; configured verification stays owner-run. */
57
+ export default function registerNativeFinalization(pi: ExtensionAPI): void {
58
+ const configPath = process.env[CONFIG_ENV];
59
+ if (!configPath) return;
60
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as NativeFinalizationConfig;
61
+ const { acceptance, reportRuntime } = config;
62
+ const selfReview = acceptanceSelfReviewConfig(acceptance);
63
+ const messages: Message[] = [];
64
+ let messageOffset = 0;
65
+ let turn = 0;
66
+ let initialOutput = "";
67
+ let latestOutput = "";
68
+ let initialLedger: AcceptanceLedger | undefined;
69
+ let resolvedOutput: ReturnType<typeof resolveSingleOutput> = { fullOutput: "" };
70
+ const registerOutput = (runtime: StructuredOutputRuntime) => pi.registerTool({
71
+ name: "structured_output", label: "Structured Output", description: "Submit the complete output matching the current schema.",
72
+ parameters: Type.Object({ value: Type.Unsafe(runtime.schema) }, { additionalProperties: false }),
73
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
74
+ execute: async (_id, args: { value: unknown }) => {
75
+ const validation = validateStructuredOutputValue(runtime.schema, args.value);
76
+ if (validation.status === "invalid") throw new Error(validation.message);
77
+ fs.writeFileSync(runtime.outputPath, JSON.stringify(args.value), { mode: 0o600 });
78
+ return { content: [{ type: "text", text: "Structured output captured." }], details: {}, terminate: true };
79
+ },
80
+ });
81
+ if (config.publicOutput) {
82
+ registerOutput(config.publicOutput);
83
+ pi.on("before_agent_start", (event) => setPromptSection(event.systemPromptOptions, "subagent_output",
84
+ "Your final action must call structured_output with JSON matching the current schema. Prose alone does not complete this output contract."));
85
+ }
86
+ pi.on("message_end", (event) => {
87
+ if (["assistant", "user", "toolResult"].includes(event.message.role)) {
88
+ latestOutput = getFinalOutput([event.message as Message]) || latestOutput;
89
+ messages.push(compactObservedMessage(event.message as Message));
90
+ }
91
+ });
92
+ pi.on("agent_before_settle", async (event, ctx) => {
93
+ if (event.outcome !== "completed") return;
94
+ const submission: NativeFinalizationEvent["submission"] = turn === 0
95
+ ? { output: latestOutput }
96
+ : readFinalizationReport(messages, reportRuntime, { messageOffset });
97
+ if (turn === 0) {
98
+ initialOutput = submission.output;
99
+ const hiddenError = detectSubagentError(messages);
100
+ if (hiddenError.hasError) submission.error = hiddenError.details ?? `${hiddenError.errorType} failed`;
101
+ if (config.publicOutput) submission.error ??= readStructuredOutput(config.publicOutput).error;
102
+ }
103
+ const ledger = await evaluateAcceptance({ acceptance: selfReview, governing: acceptance, cwd: ctx.cwd,
104
+ output: submission.reportSubmissionError ? "" : submission.output, report: submission.reportSubmissionError ? undefined : submission.report });
105
+ initialLedger ??= ledger;
106
+ if (!submission.error && !submission.reportSubmissionError) {
107
+ const output = turn === 0 ? stripAcceptanceReport(submission.output) : resolveFinalizationOutput(submission.output, resolvedOutput.fullOutput);
108
+ resolvedOutput = resolveSingleOutput(config.outputPath, output, turn === 0 ? config.outputSnapshot : resolvedOutput.writtenSnapshot);
109
+ if (resolvedOutput.saveError) submission.error = `Failed to save output file '${config.outputPath}': ${resolvedOutput.saveError}`;
110
+ }
111
+ const continueReview = !submission.error && ledger.status !== "blocked" && turn < acceptance.finalization.maxTurns
112
+ && (turn === 0 || Boolean(submission.reportSubmissionError) || ledger.status === "rejected");
113
+ const nextPrompt = continueReview ? formatAcceptanceFinalizationPrompt({ acceptance, initialOutput, initialLedger,
114
+ turn: turn + 1, maxTurns: acceptance.finalization.maxTurns,
115
+ previousFailure: submission.reportSubmissionError ?? acceptanceFailureMessage(ledger), nativeReport: true, outputSchema: config.publicOutput?.schema }) : undefined;
116
+ const marker: NativeFinalizationEvent = { type: FINALIZATION_EVENT, nonce: config.nonce, turn, lastEntryId: ctx.sessionManager.getLeafId() ?? undefined, messageCount: messages.length, at: Date.now(), submission, acceptance: ledger, resolvedOutput, nextPrompt };
117
+ fs.appendFileSync(path.join(path.dirname(reportRuntime.schemaPath), "boundaries.jsonl"), `${JSON.stringify(marker)}\n`, { mode: 0o600 });
118
+ if (!nextPrompt) return;
119
+ if (turn === 0) {
120
+ // Native registration refreshes the active schema after the public payload is captured.
121
+ registerOutput(reportRuntime);
122
+ pi.setActiveTools([...new Set([...pi.getActiveTools(), "structured_output"])]);
123
+ }
124
+ turn++;
125
+ messageOffset = messages.length;
126
+ return { entries: [...event.entries, { type: "custom_message", customType: "subagent-self-review", content: nextPrompt, display: false }], continue: true };
127
+ });
128
+ }
@@ -0,0 +1,113 @@
1
+ import type { Usage as NativeUsage } from "@earendil-works/pi-ai";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ import type { Usage, UsageContribution } from "../../shared/types.ts";
4
+ import { scanJournal, nativeProjection } from "../../shared/journal-reader.ts";
5
+
6
+ export function snapshotNativeBaseline(file: string | undefined): { ids: Set<string>; entryCount: number; legacy: boolean; sessionId?: string } {
7
+ const ids = new Set<string>();
8
+ let first = true, native = false, entryCount = 0, legacy = false, sessionId: string | undefined;
9
+ if (file) try { scanJournal(file, (path) => !path.length || path.length === 1 && ["id", "type", "version"].includes(String(path[0])), ({ value }) => {
10
+ if (first) {
11
+ first = false; native = value?.type === "session";
12
+ if (native) {
13
+ if (typeof value.id !== "string" || !value.id) throw new SyntaxError("Invalid native session identity");
14
+ sessionId = value.id; legacy = value.version === 1;
15
+ }
16
+ } else if (native) entryCount++;
17
+ if (native && typeof value?.id === "string") ids.add(value.id);
18
+ }, { requireNewline: true }); }
19
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
20
+ return { ids, entryCount, legacy, sessionId };
21
+ }
22
+
23
+ export function snapshotNativeUsage(file: string | undefined): Set<string> { return snapshotNativeBaseline(file).ids; }
24
+
25
+ export function addUsage(usage: Usage, value: NativeUsage, attribution?: Omit<UsageContribution, "usage">): void {
26
+ for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens", "reasoning", "cacheWrite1h"] as const) {
27
+ if (value[key] !== undefined && typeof value[key] !== "number") throw new Error(`Invalid native usage counter: ${key}`);
28
+ }
29
+ for (const [key, counter] of Object.entries(value)) {
30
+ if (typeof counter === "number" && (!Number.isFinite(counter) || counter < 0)) throw new Error(`Invalid native usage counter: ${key}`);
31
+ }
32
+ if (value.cost) for (const counter of Object.values(value.cost)) if (!Number.isFinite(counter) || counter < 0) throw new Error("Invalid native usage cost");
33
+ usage.input += value.input ?? 0;
34
+ usage.output += value.output ?? 0;
35
+ usage.cacheRead += value.cacheRead ?? 0;
36
+ usage.cacheWrite += value.cacheWrite ?? 0;
37
+ usage.cost += value.cost?.total ?? 0;
38
+ if (attribution) (usage.contributions ??= []).push({ ...attribution, usage: value });
39
+ }
40
+
41
+ export function validateNativeUsage(value: NativeUsage): void {
42
+ for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"] as const) {
43
+ if (typeof value[key] !== "number" || !Number.isFinite(value[key]) || value[key] < 0) throw new Error(`Native usage ${key} is unavailable or invalid`);
44
+ }
45
+ for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"] as const) {
46
+ if (!value.cost || typeof value.cost[key] !== "number" || !Number.isFinite(value.cost[key]) || value.cost[key] < 0) throw new Error(`Native usage cost.${key} is unavailable or invalid`);
47
+ }
48
+ }
49
+
50
+ export interface NativeUsageMetadata {
51
+ type: string;
52
+ id: string;
53
+ checkpoint?: boolean;
54
+ provider?: string;
55
+ model?: string;
56
+ usage?: NativeUsage;
57
+ message?: { role: string; timestamp?: number; toolCallId?: string; provider?: string; model?: string; responseModel?: string; usage?: NativeUsage };
58
+ }
59
+
60
+ /** One billing reducer for published journals and settled, once-audited in-memory sessions. */
61
+ export function nativeUsageCollector(sessionId: string, baseline: ReadonlySet<string>, boundaries: Array<string | undefined> = []) {
62
+ const totals: Usage[] = Array.from({ length: Math.max(1, boundaries.length) }, () => ({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0, contributions: [] }));
63
+ let segment = 0;
64
+ const contributions = new Map<string, UsageContribution>(), turns = new Set<string>();
65
+ return { totals, append(entry: NativeUsageMetadata) {
66
+ if (!entry || typeof entry !== "object") throw new SyntaxError("Invalid native entry");
67
+ if (entry.type === "message" && (!entry.message || typeof entry.message.role !== "string")) throw new SyntaxError("Invalid native message");
68
+ if (!baseline.has(entry.id) && entry.checkpoint !== true) {
69
+ let value: NativeUsage | undefined, provider: string | undefined, model: string | undefined;
70
+ if (entry.type === "usage") { value = entry.usage; provider = entry.provider; model = entry.model; }
71
+ else if (entry.type === "compaction" || entry.type === "branch_summary") value = entry.usage;
72
+ else if (entry.type === "message") {
73
+ if (entry.message?.role === "assistant") {
74
+ value = entry.message.usage; provider = entry.message.provider; model = entry.message.responseModel ?? entry.message.model;
75
+ if (!turns.has(entry.id)) { totals[segment]!.turns++; turns.add(entry.id); }
76
+ } else if (entry.message?.role === "toolResult") value = entry.message.usage;
77
+ }
78
+ if (!value && (entry.type === "usage" || entry.type === "message" && entry.message?.role === "assistant")) throw new Error(`Required native usage is unavailable: ${entry.id}`);
79
+ if (value) {
80
+ validateNativeUsage(value);
81
+ if (typeof entry.id !== "string" || !entry.id) throw new Error("Native usage entry identity is unavailable");
82
+ const next = { id: `${sessionId}:${entry.id}`, provider, model, usage: value };
83
+ const previous = contributions.get(next.id);
84
+ if (previous && !isDeepStrictEqual(previous, next)) throw new Error(`Conflicting native usage contribution: ${next.id}`);
85
+ if (!previous) { contributions.set(next.id, next); addUsage(totals[segment]!, value, { id: next.id, provider, model }); }
86
+ }
87
+ }
88
+ if (entry.id === boundaries[segment] && segment < totals.length - 1) segment++;
89
+ } };
90
+ }
91
+
92
+ /** Read native journals without opening a second writer or copying their transcripts. */
93
+ export function readNativeUsage(file: string | undefined, baseline: ReadonlySet<string>, boundaries: Array<string | undefined> = [],
94
+ options: { terminalEntryId?: string; onEntry?: (entry: NativeUsageMetadata) => void; onBoundary?: (boundary: { sessionId: string; lastEntryId?: string }) => void } = {}): Usage[] | undefined {
95
+ if (!file) return;
96
+ let sessionId: string | undefined, first = true, lastEntryId: string | undefined, reached = false;
97
+ let collector: ReturnType<typeof nativeUsageCollector> | undefined;
98
+ const boundaryReached = Symbol("terminal entry");
99
+ try { scanJournal(file, (path, root) => path[0] === "message" && path[1] === "content" ? false : nativeProjection(path, root), ({ value: entry }) => {
100
+ if (first) {
101
+ first = false; sessionId = entry?.type === "session" ? entry.id : undefined;
102
+ if (sessionId) collector = nativeUsageCollector(sessionId, baseline, boundaries);
103
+ }
104
+ if (!collector) return;
105
+ options.onEntry?.(entry as NativeUsageMetadata);
106
+ collector.append(entry as NativeUsageMetadata);
107
+ if (entry.type !== "session") lastEntryId = entry.id;
108
+ if (entry.id === options.terminalEntryId) { reached = true; throw boundaryReached; }
109
+ }, { requireNewline: true }); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return; if (error !== boundaryReached) throw error; }
110
+ if (options.terminalEntryId && !reached) throw new Error("Native accounting terminal entry is missing");
111
+ if (sessionId) options.onBoundary?.({ sessionId, lastEntryId });
112
+ return collector?.totals;
113
+ }