@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,52 @@
1
+ import * as path from "node:path";
2
+
3
+ const MAX_NESTED_ID_LENGTH = 128;
4
+ export const MAX_NESTED_PATH_ENTRIES = 4;
5
+
6
+ export type NestedPathEntry = { runId: string; stepIndex?: number; agent?: string };
7
+
8
+ export function isSafeNestedPathId(value: unknown): value is string {
9
+ return typeof value === "string"
10
+ && value.length > 0
11
+ && value.length <= MAX_NESTED_ID_LENGTH
12
+ && !path.isAbsolute(value)
13
+ && !value.includes("/")
14
+ && !value.includes("\\")
15
+ && !value.includes("..");
16
+ }
17
+
18
+ function stepIndex(value: unknown): number | undefined {
19
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
20
+ }
21
+
22
+ function nonEmptyString(value: unknown, max: number): string | undefined {
23
+ return typeof value === "string" && value.length > 0 ? value.slice(0, max) : undefined;
24
+ }
25
+
26
+ export function sanitizeNestedPath(value: unknown): NestedPathEntry[] {
27
+ if (!Array.isArray(value)) return [];
28
+ return value.map((part) => {
29
+ if (!part || typeof part !== "object") return undefined;
30
+ const record = part as Record<string, unknown>;
31
+ if (!isSafeNestedPathId(record.runId)) return undefined;
32
+ return {
33
+ runId: record.runId,
34
+ ...(stepIndex(record.stepIndex) !== undefined ? { stepIndex: stepIndex(record.stepIndex) } : {}),
35
+ ...(nonEmptyString(record.agent, 128) ? { agent: nonEmptyString(record.agent, 128) } : {}),
36
+ };
37
+ }).filter((part): part is NestedPathEntry => Boolean(part)).slice(0, MAX_NESTED_PATH_ENTRIES);
38
+ }
39
+
40
+ export function parseNestedPathEnv(value: string | undefined): NestedPathEntry[] {
41
+ if (!value) return [];
42
+ try {
43
+ return sanitizeNestedPath(JSON.parse(value) as unknown);
44
+ } catch {
45
+ return [];
46
+ }
47
+ }
48
+
49
+ export function encodeNestedPathEnv(value: NestedPathEntry[]): string {
50
+ const sanitized = sanitizeNestedPath(value);
51
+ return sanitized.length ? JSON.stringify(sanitized) : "";
52
+ }
@@ -0,0 +1,119 @@
1
+ import { formatDuration, formatTokens, shortenPath } from "../../shared/formatters.ts";
2
+ import { formatActivityLabel, formatRunAction } from "../../shared/status-format.ts";
3
+ import type { ActivityState, NestedRunSummary } from "../../shared/types.ts";
4
+
5
+ export interface NestedRunCounts {
6
+ total: number;
7
+ running: number;
8
+ paused: number;
9
+ complete: number;
10
+ failed: number;
11
+ blocked: number;
12
+ queued: number;
13
+ }
14
+
15
+ export function countNestedRuns(children: NestedRunSummary[] | undefined): NestedRunCounts {
16
+ const counts: NestedRunCounts = { total: 0, running: 0, paused: 0, complete: 0, failed: 0, blocked: 0, queued: 0 };
17
+ for (const child of children ?? []) {
18
+ counts.total++;
19
+ counts[child.state]++;
20
+ const nested = countNestedRuns([...(child.children ?? []), ...(child.steps?.flatMap((step) => step.children ?? []) ?? [])]);
21
+ counts.total += nested.total;
22
+ counts.running += nested.running;
23
+ counts.paused += nested.paused;
24
+ counts.complete += nested.complete;
25
+ counts.failed += nested.failed;
26
+ counts.blocked += nested.blocked;
27
+ counts.queued += nested.queued;
28
+ }
29
+ return counts;
30
+ }
31
+
32
+ export function formatNestedAggregate(children: NestedRunSummary[] | undefined): string | undefined {
33
+ const counts = countNestedRuns(children);
34
+ if (counts.total === 0) return undefined;
35
+ const parts = [
36
+ counts.running > 0 ? `${counts.running} running` : "",
37
+ counts.paused > 0 ? `${counts.paused} paused` : "",
38
+ counts.failed > 0 ? `${counts.failed} failed` : "",
39
+ counts.blocked > 0 ? `${counts.blocked} need human action` : "",
40
+ counts.complete > 0 ? `${counts.complete} complete` : "",
41
+ counts.queued > 0 ? `${counts.queued} queued` : "",
42
+ ].filter(Boolean);
43
+ return `+${counts.total} nested run${counts.total === 1 ? "" : "s"}${parts.length ? ` (${parts.join(", ")})` : ""}`;
44
+ }
45
+
46
+ function nestedRunLabel(run: NestedRunSummary): string {
47
+ if (run.agent) return run.agent;
48
+ if (run.agents?.length) return run.agents.length === 1 ? run.agents[0]! : `${run.agents.slice(0, 2).join(", ")}${run.agents.length > 2 ? ` +${run.agents.length - 2}` : ""}`;
49
+ return run.id;
50
+ }
51
+
52
+ function formatNestedActivity(input: {
53
+ activityState?: ActivityState;
54
+ lastActivityAt?: number;
55
+ currentTool?: string;
56
+ currentToolStartedAt?: number;
57
+ currentPath?: string;
58
+ turnCount?: number;
59
+ toolCount?: number;
60
+ totalTokens?: NestedRunSummary["totalTokens"];
61
+ }): string | undefined {
62
+ const facts: string[] = [];
63
+ if (input.currentTool && input.currentToolStartedAt !== undefined) facts.push(`tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`);
64
+ else if (input.currentTool) facts.push(`tool ${input.currentTool}`);
65
+ if (input.currentPath) facts.push(shortenPath(input.currentPath));
66
+ if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
67
+ if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
68
+ if (input.totalTokens) facts.push(`${formatTokens(input.totalTokens.total)} tok`);
69
+ const activity = formatActivityLabel(input.lastActivityAt, input.activityState as ActivityState | undefined);
70
+ return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : undefined;
71
+ }
72
+
73
+ function formatNestedRunLines(children: NestedRunSummary[] | undefined, options: { indent: string; maxDepth: number; maxLines: number; commandHints?: boolean; childSafe?: boolean }): string[] {
74
+ const lines: string[] = [];
75
+ const append = (items: NestedRunSummary[] | undefined, depth: number, indent: string): void => {
76
+ if (!items?.length || lines.length >= options.maxLines) return;
77
+ if (depth > options.maxDepth) {
78
+ const aggregate = formatNestedAggregate(items);
79
+ if (aggregate && lines.length < options.maxLines) lines.push(`${indent}↳ ${aggregate}`);
80
+ return;
81
+ }
82
+ for (let index = 0; index < items.length; index++) {
83
+ const child = items[index]!;
84
+ if (lines.length >= options.maxLines) {
85
+ const aggregate = formatNestedAggregate(items.slice(index));
86
+ if (aggregate) lines[lines.length - 1] = `${indent}↳ ${aggregate}`;
87
+ return;
88
+ }
89
+ const activity = child.state === "running" ? formatNestedActivity(child) : undefined;
90
+ const error = child.error ? ` | error: ${child.error}` : "";
91
+ lines.push(`${indent}↳ ${nestedRunLabel(child)} [${child.id}] ${child.state}${activity ? ` | ${activity}` : ""}${error}`);
92
+ if (options.commandHints && lines.length < options.maxLines) lines.push(`${indent} Status: ${formatRunAction("status", child.id, {}, options.childSafe)}`);
93
+ if (depth === options.maxDepth) {
94
+ const aggregate = formatNestedAggregate([...(child.steps?.flatMap((step) => step.children ?? []) ?? []), ...(child.children ?? [])]);
95
+ if (aggregate && lines.length < options.maxLines) lines.push(`${indent} ↳ ${aggregate}`);
96
+ continue;
97
+ }
98
+ for (const [stepIndex, step] of (child.steps ?? []).entries()) {
99
+ if (lines.length >= options.maxLines) return;
100
+ const stepActivity = step.status === "running" ? formatNestedActivity(step) : undefined;
101
+ lines.push(`${indent} ${stepIndex + 1}. ${step.agent} ${step.status}${stepActivity ? ` | ${stepActivity}` : ""}${step.error ? ` | error: ${step.error}` : ""}`);
102
+ append(step.children, depth + 1, `${indent} `);
103
+ }
104
+ append(child.children, depth + 1, `${indent} `);
105
+ }
106
+ };
107
+ append(children, 0, options.indent);
108
+ return lines;
109
+ }
110
+
111
+ export function formatNestedRunStatusLines(children: NestedRunSummary[] | undefined, options: { indent?: string; maxDepth?: number; maxLines?: number; commandHints?: boolean; childSafe?: boolean } = {}): string[] {
112
+ return formatNestedRunLines(children, {
113
+ indent: options.indent ?? " ",
114
+ maxDepth: options.maxDepth ?? 2,
115
+ maxLines: options.maxLines ?? 40,
116
+ commandHints: options.commandHints ?? false,
117
+ childSafe: options.childSafe,
118
+ });
119
+ }
@@ -0,0 +1,153 @@
1
+ import type { DynamicCollectSpec, DynamicExpandSpec } from "../../shared/settings.ts";
2
+ import type { JsonSchemaObject, ResolvedAcceptanceConfig, SavedLaunchConfig } from "../../shared/types.ts";
3
+
4
+ export interface RunnerSubagentStep {
5
+ agent: string;
6
+ task: string;
7
+ phase?: string;
8
+ label?: string;
9
+ outputName?: string;
10
+ structured?: boolean;
11
+ cwd?: string;
12
+ model?: string;
13
+ thinking?: string;
14
+ modelCandidates?: string[];
15
+ tools?: string[];
16
+ allowSubagents?: boolean;
17
+ extensions?: string[];
18
+ mcpDirectTools?: string[];
19
+ completionGuard?: boolean;
20
+ systemPrompt?: string | null;
21
+ systemPromptMode?: "append" | "replace";
22
+ inheritProjectContext: boolean;
23
+ inheritSkills: boolean;
24
+ skills?: string[];
25
+ outputPath?: string;
26
+ output?: string | false;
27
+ outputMode?: "inline" | "file-only";
28
+ outputPathFromAgentDefault?: boolean;
29
+ sessionFile?: string;
30
+ maxSubagentDepth?: number;
31
+ maxExecutionTimeMs?: number;
32
+ maxTokens?: number;
33
+ structuredOutput?: {
34
+ schema: JsonSchemaObject;
35
+ schemaPath: string;
36
+ outputPath: string;
37
+ };
38
+ structuredOutputSchema?: JsonSchemaObject;
39
+ effectiveAcceptance?: ResolvedAcceptanceConfig;
40
+ launch?: SavedLaunchConfig;
41
+ }
42
+
43
+ export interface ParallelStepGroup {
44
+ parallel: RunnerSubagentStep[];
45
+ cwd?: string;
46
+ concurrency?: number;
47
+ failFast?: boolean;
48
+ worktree?: boolean;
49
+ }
50
+
51
+ export interface DynamicRunnerGroup {
52
+ expand: DynamicExpandSpec;
53
+ parallel: RunnerSubagentStep;
54
+ collect: DynamicCollectSpec;
55
+ concurrency?: number;
56
+ failFast?: boolean;
57
+ phase?: string;
58
+ label?: string;
59
+ sessionFiles?: Array<string | undefined>;
60
+ }
61
+
62
+ export type RunnerStep = RunnerSubagentStep | ParallelStepGroup | DynamicRunnerGroup;
63
+
64
+ export function isParallelGroup(step: RunnerStep): step is ParallelStepGroup {
65
+ return "parallel" in step && Array.isArray(step.parallel);
66
+ }
67
+
68
+ export function isDynamicRunnerGroup(step: RunnerStep): step is DynamicRunnerGroup {
69
+ return "expand" in step && "collect" in step && "parallel" in step && !Array.isArray((step as { parallel?: unknown }).parallel);
70
+ }
71
+
72
+ export function flattenSteps(steps: RunnerStep[]): RunnerSubagentStep[] {
73
+ const flat: RunnerSubagentStep[] = [];
74
+ for (const step of steps) {
75
+ if (isParallelGroup(step)) {
76
+ for (const task of step.parallel) flat.push(task);
77
+ } else if (isDynamicRunnerGroup(step)) {
78
+ continue;
79
+ } else {
80
+ flat.push(step);
81
+ }
82
+ }
83
+ return flat;
84
+ }
85
+
86
+ export async function mapConcurrent<T, R>(
87
+ items: T[],
88
+ limit: number,
89
+ fn: (item: T, i: number) => Promise<R>,
90
+ ): Promise<R[]> {
91
+ const safeLimit = Math.max(1, Math.floor(limit) || 1);
92
+ const results: R[] = new Array(items.length);
93
+ let next = 0;
94
+
95
+ async function worker(_workerIndex: number): Promise<void> {
96
+ while (next < items.length) {
97
+ const i = next++;
98
+ results[i] = await fn(items[i], i);
99
+ }
100
+ }
101
+
102
+ await Promise.all(
103
+ Array.from({ length: Math.min(safeLimit, items.length) }, (_, wi) => worker(wi)),
104
+ );
105
+ return results;
106
+ }
107
+
108
+ export interface ParallelTaskResult {
109
+ agent: string;
110
+ taskIndex?: number;
111
+ output: string;
112
+ exitCode: number | null;
113
+ error?: string;
114
+ model?: string;
115
+ attemptedModels?: string[];
116
+ outputTargetPath?: string;
117
+ outputTargetExists?: boolean;
118
+ }
119
+
120
+ export function aggregateParallelOutputs(
121
+ results: ParallelTaskResult[],
122
+ headerFormat: (index: number, agent: string) => string = (i, agent) =>
123
+ `=== Parallel Task ${i + 1} (${agent}) ===`,
124
+ ): string {
125
+ return results
126
+ .map((r, i) => {
127
+ const header = headerFormat(r.taskIndex ?? i, r.agent);
128
+ const hasOutput = Boolean(r.output?.trim());
129
+ const status =
130
+ r.exitCode === -1
131
+ ? `SKIPPED${r.error ? `: ${r.error}` : ""}`
132
+ : r.exitCode !== 0 && r.exitCode !== null
133
+ ? `FAILED (exit code ${r.exitCode})${r.error ? `: ${r.error}` : ""}`
134
+ : r.error
135
+ ? `WARNING: ${r.error}`
136
+ : !hasOutput && r.outputTargetPath && r.outputTargetExists === false
137
+ ? `EMPTY OUTPUT (expected output file missing: ${r.outputTargetPath})`
138
+ : !hasOutput && !r.outputTargetPath
139
+ ? "EMPTY OUTPUT (no textual response returned)"
140
+ : "";
141
+ const body = status ? (hasOutput ? `${status}\n${r.output}` : status) : r.output;
142
+ return `${header}\n${body}`;
143
+ })
144
+ .join("\n\n");
145
+ }
146
+
147
+ export const FAIL_FAST_REASON = "subagent-fail-fast";
148
+
149
+ export function isFailFastAbort(signal: AbortSignal | undefined): boolean {
150
+ return signal?.aborted === true && signal.reason === FAIL_FAST_REASON;
151
+ }
152
+
153
+ export const MAX_PARALLEL_CONCURRENCY = 4;
@@ -0,0 +1,59 @@
1
+ import { createHash, type Hash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
4
+ import { journalStamp, scanJournal, type JournalPolicy, type Projection } from "../../shared/journal-reader.ts";
5
+
6
+ function hashRange(fd: number, start: number, end: number, hash: Hash): void {
7
+ const bytes = Buffer.allocUnsafe(64 * 1024);
8
+ while (start < end) {
9
+ const count = fs.readSync(fd, bytes, 0, Math.min(bytes.length, end - start), start);
10
+ if (!count) throw new Error("Parent journal truncated during inspection");
11
+ hash.update(bytes.subarray(0, count));
12
+ start += count;
13
+ }
14
+ }
15
+
16
+ const receiptProjection: Projection = (path) => {
17
+ if (!path.length || ["type", "id", "timestamp", "customType"].includes(String(path[0]))) return true;
18
+ if (path[0] === "details") return path.length === 1 || ["completion", "subagentCompletion", "result"].includes(String(path[1]))
19
+ && (path[1] !== "result" || path.length <= 3 || path[3] === "wait");
20
+ if (path[0] === "message") return path.length === 1 || ["role", "toolName", "usage"].includes(String(path[1]))
21
+ || path[1] === "details" && (path.length === 2 || ["wait", "parentUsage"].includes(String(path[2])));
22
+ return false;
23
+ };
24
+
25
+ /** Cache only LF-published receipt fields, never transcripts or accepted-but-unsaved messages. */
26
+ export function createParentReceiptReader(policy: JournalPolicy) {
27
+ let cache: { file: string; identity: string; end: number; digest: string; records: Map<string, SessionEntry> } | undefined;
28
+ return {
29
+ clear() { cache = undefined; },
30
+ read(file: string | undefined): ReadonlyMap<string, SessionEntry> {
31
+ if (!file || !fs.existsSync(file)) { cache = undefined; return new Map(); }
32
+ const fd = fs.openSync(file, "r");
33
+ try {
34
+ const stat = fs.fstatSync(fd, { bigint: true }), stamp = journalStamp(stat), identity = `${stat.dev}:${stat.ino}`;
35
+ let hash = createHash("sha256");
36
+ let append = cache?.file === file && cache.identity === identity && Number(stat.size) >= cache.end;
37
+ if (append) {
38
+ // Coarse timestamps can hide same-size edits; even a stat hit needs verified bytes.
39
+ hashRange(fd, 0, cache!.end, hash);
40
+ append = hash.copy().digest("hex") === cache!.digest;
41
+ }
42
+ if (append && Number(stat.size) === cache!.end) {
43
+ if (journalStamp(fs.fstatSync(fd, { bigint: true })) !== stamp) throw new Error("Parent journal changed during inspection");
44
+ return cache!.records;
45
+ }
46
+ if (!append) hash = createHash("sha256");
47
+ const start = append ? cache!.end : 0, records = append ? new Map(cache!.records) : new Map<string, SessionEntry>();
48
+ const end = scanJournal(fd, receiptProjection, ({ value }) => {
49
+ if (value.type === "custom_message" || value.type === "message" && value.message?.role === "toolResult")
50
+ records.set(value.id, value as SessionEntry);
51
+ }, { policy, requireNewline: true, start, end: Number(stat.size) });
52
+ hashRange(fd, start, end, hash);
53
+ if (journalStamp(fs.fstatSync(fd, { bigint: true })) !== stamp) throw new Error("Parent journal changed during inspection");
54
+ cache = { file, identity, end, digest: hash.digest("hex"), records };
55
+ return records;
56
+ } finally { fs.closeSync(fd); }
57
+ },
58
+ };
59
+ }
@@ -0,0 +1,121 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import type { Usage as NativeUsage } from "@earendil-works/pi-ai";
3
+ import type { ExtensionAPI, ExtensionContext, MessageEndEventResult, SessionEntry } from "@earendil-works/pi-coding-agent";
4
+ import type { Details, OwnedRunView, SubagentExecutionResult, UsageContribution } from "../../shared/types.ts";
5
+ import { SessionEntryCursor } from "../../shared/session-entries.ts";
6
+ import { createParentReceiptReader } from "./parent-receipts.ts";
7
+ import { validateNativeUsage } from "./native-usage.ts";
8
+
9
+ const PREFIX = "subagent:";
10
+ const UNATTRIBUTED = "unattributed";
11
+
12
+ export function finalizedChildUsage(children: OwnedRunView["children"], index?: number): UsageContribution[] {
13
+ return children.flatMap((child) => (index === undefined || child.index === index) && child.state !== "live" && child.state !== "unknown" && !child.result?.detached && child.result?.accounting?.state !== "incomplete"
14
+ ? child.result?.usage.contributions ?? [] : []);
15
+ }
16
+
17
+ function sumUsage(contributions: readonly UsageContribution[]): NativeUsage {
18
+ const total: NativeUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0,
19
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
20
+ for (const { usage } of contributions) {
21
+ for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"] as const) total[key] += usage[key];
22
+ for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"] as const) total.cost[key] += usage.cost[key];
23
+ // These are subsets of output/cacheWrite, never additional total tokens.
24
+ for (const key of ["reasoning", "cacheWrite1h"] as const) if (usage[key] !== undefined) total[key] = (total[key] ?? 0) + usage[key];
25
+ }
26
+ return total;
27
+ }
28
+
29
+ function sameUsage(a: NativeUsage, b: NativeUsage): boolean {
30
+ return isDeepStrictEqual({ ...a, reasoning: a.reasoning, cacheWrite1h: a.cacheWrite1h }, { ...b, reasoning: b.reasoning, cacheWrite1h: b.cacheWrite1h });
31
+ }
32
+
33
+ function checkContribution(previous: UsageContribution | undefined, next: UsageContribution): void {
34
+ if (previous && ((previous.provider ?? UNATTRIBUTED) !== (next.provider ?? UNATTRIBUTED)
35
+ || (previous.model ?? UNATTRIBUTED) !== (next.model ?? UNATTRIBUTED) || !sameUsage(previous.usage, next.usage))) {
36
+ throw new Error(`Conflicting subagent usage contribution: ${next.id}`);
37
+ }
38
+ }
39
+
40
+ function uniqueContributions(contributions: readonly UsageContribution[]): UsageContribution[] {
41
+ const unique = new Map<string, UsageContribution>();
42
+ for (const contribution of contributions) {
43
+ if (!contribution.id.trim()) throw new Error("Subagent usage requires a stable native contribution ID");
44
+ validateNativeUsage(contribution.usage);
45
+ checkContribution(unique.get(contribution.id), contribution);
46
+ unique.set(contribution.id, contribution);
47
+ }
48
+ return [...unique.values()];
49
+ }
50
+
51
+ function createReceiptIndex(toolNames: readonly string[]) {
52
+ const cursor = new SessionEntryCursor();
53
+ const received = new Map<string, UsageContribution>();
54
+ return (manager: ExtensionContext["sessionManager"], saved: ReadonlyMap<string, SessionEntry>) => {
55
+ const changes = cursor.read(manager);
56
+ if (changes.reset) received.clear();
57
+ for (const metadata of changes.entries) {
58
+ if (metadata.type === "usage") {
59
+ const id = (metadata as typeof metadata & { contributionId?: string }).contributionId;
60
+ if (id?.startsWith(PREFIX)) received.set(id.slice(PREFIX.length), { id: id.slice(PREFIX.length), provider: metadata.provider, model: metadata.model, usage: metadata.usage });
61
+ continue;
62
+ }
63
+ if (metadata.type !== "message" || metadata.message.role !== "toolResult" || !toolNames.includes(metadata.message.toolName)) continue;
64
+ const entry = saved.get(metadata.id) ?? (metadata.message.details !== undefined ? metadata : manager.getEntry(metadata.id) ?? metadata);
65
+ if (entry.type !== "message" || entry.message.role !== "toolResult") continue;
66
+ const contributions = (entry.message.details as Details | undefined)?.parentUsage?.contributions;
67
+ // Details/custom notifications alone are never evidence that native accounting ran.
68
+ if (Array.isArray(contributions) && entry.message.usage && sameUsage(entry.message.usage, sumUsage(contributions))) {
69
+ for (const contribution of contributions) received.set(contribution.id, contribution);
70
+ }
71
+ }
72
+ return received;
73
+ };
74
+ }
75
+
76
+ function unrecorded(contributions: readonly UsageContribution[], received: Map<string, UsageContribution>): UsageContribution[] {
77
+ return uniqueContributions(contributions).filter((contribution) => {
78
+ const previous = received.get(contribution.id);
79
+ checkContribution(previous, contribution);
80
+ return !previous;
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Register once per extension instance. Attach only to finalized wait/execution
86
+ * results, never inspection or streaming updates. Legacy tool aliases and saved
87
+ * native usage entries still deduplicate old-session accounting.
88
+ */
89
+ export function registerParentUsage(pi: ExtensionAPI, toolNames: readonly string[]) {
90
+ const toolReceipts = createParentReceiptReader("inspect");
91
+ const receipts = createReceiptIndex(toolNames);
92
+ pi.on("message_end", (event, ctx): MessageEndEventResult | undefined => {
93
+ const message = event.message;
94
+ if (message.role !== "toolResult" || !toolNames.includes(message.toolName)) return;
95
+ const details = message.details as Details | undefined;
96
+ if (!details?.parentUsage) return;
97
+ const pending = unrecorded(details.parentUsage.contributions, receipts(ctx.sessionManager, toolReceipts.read(ctx.sessionManager.getSessionFile())));
98
+ const { usage: _usage, ...rest } = message;
99
+ const { parentUsage: _parentUsage, ...restDetails } = details;
100
+ // Public replacement hook: native emits/persists final tool messages serially,
101
+ // even for concurrent tools. No in-memory reservation can outlive an aborted result.
102
+ return { message: { ...rest, details: JSON.parse(JSON.stringify({ ...restDetails, ...(pending.length ? { parentUsage: { contributions: pending } } : {}) })),
103
+ ...(pending.length ? { usage: sumUsage(pending) } : {}) } };
104
+ });
105
+
106
+ return {
107
+ isRecorded(contributions: readonly UsageContribution[], ctx: ExtensionContext, saved: ReadonlyMap<string, SessionEntry>): boolean {
108
+ return unrecorded(contributions, receipts(ctx.sessionManager, saved)).length === 0;
109
+ },
110
+ attach(result: SubagentExecutionResult, contributions: readonly UsageContribution[], ctx: ExtensionContext): SubagentExecutionResult {
111
+ const { usage: _usage, ...rest } = result;
112
+ const { parentUsage: _parentUsage, ...details } = result.details;
113
+ if (!contributions.length) return { ...rest, details };
114
+ // Required identity/conflict rejection precedes any optional host I/O.
115
+ const saved = toolReceipts.read(ctx.sessionManager.getSessionFile());
116
+ const pending = unrecorded(contributions, receipts(ctx.sessionManager, saved));
117
+ // Intent only. Top-level usage is added at final message_end, immediately before native persistence.
118
+ return { ...rest, details: { ...details, ...(pending.length ? { parentUsage: { contributions: pending } } : {}) } };
119
+ },
120
+ };
121
+ }