@selesai/code 0.5.28 → 0.6.1

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 (341) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +9 -1
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/config-selector.d.ts.map +1 -1
  7. package/dist/cli/config-selector.js +1 -1
  8. package/dist/cli/config-selector.js.map +1 -1
  9. package/dist/cli/credential-print.d.ts +23 -0
  10. package/dist/cli/credential-print.d.ts.map +1 -0
  11. package/dist/cli/credential-print.js +117 -0
  12. package/dist/cli/credential-print.js.map +1 -0
  13. package/dist/cli/startup-ui.d.ts.map +1 -1
  14. package/dist/cli/startup-ui.js +1 -1
  15. package/dist/cli/startup-ui.js.map +1 -1
  16. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  17. package/dist/core/agent-session-runtime.js +3 -0
  18. package/dist/core/agent-session-runtime.js.map +1 -1
  19. package/dist/core/agent-session.d.ts +12 -1
  20. package/dist/core/agent-session.d.ts.map +1 -1
  21. package/dist/core/agent-session.js +20 -14
  22. package/dist/core/agent-session.js.map +1 -1
  23. package/dist/core/compaction/compaction.d.ts.map +1 -1
  24. package/dist/core/compaction/compaction.js +11 -3
  25. package/dist/core/compaction/compaction.js.map +1 -1
  26. package/dist/core/extensions/runner.d.ts +1 -0
  27. package/dist/core/extensions/runner.d.ts.map +1 -1
  28. package/dist/core/extensions/runner.js +11 -0
  29. package/dist/core/extensions/runner.js.map +1 -1
  30. package/dist/core/extensions/types.d.ts +14 -1
  31. package/dist/core/extensions/types.d.ts.map +1 -1
  32. package/dist/core/extensions/types.js.map +1 -1
  33. package/dist/core/footer-data-provider.d.ts +10 -0
  34. package/dist/core/footer-data-provider.d.ts.map +1 -1
  35. package/dist/core/footer-data-provider.js +1 -1
  36. package/dist/core/footer-data-provider.js.map +1 -1
  37. package/dist/core/llama/provider.d.ts.map +1 -1
  38. package/dist/core/llama/provider.js +8 -3
  39. package/dist/core/llama/provider.js.map +1 -1
  40. package/dist/core/model-config.d.ts +30 -0
  41. package/dist/core/model-config.d.ts.map +1 -1
  42. package/dist/core/model-config.js +6 -0
  43. package/dist/core/model-config.js.map +1 -1
  44. package/dist/core/model-registry.d.ts.map +1 -1
  45. package/dist/core/model-registry.js +2 -2
  46. package/dist/core/model-registry.js.map +1 -1
  47. package/dist/core/model-resolver.d.ts +1 -0
  48. package/dist/core/model-resolver.d.ts.map +1 -1
  49. package/dist/core/model-resolver.js +20 -3
  50. package/dist/core/model-resolver.js.map +1 -1
  51. package/dist/core/model-runtime.d.ts +2 -0
  52. package/dist/core/model-runtime.d.ts.map +1 -1
  53. package/dist/core/model-runtime.js +5 -4
  54. package/dist/core/model-runtime.js.map +1 -1
  55. package/dist/core/package-manager.d.ts.map +1 -1
  56. package/dist/core/package-manager.js +13 -6
  57. package/dist/core/package-manager.js.map +1 -1
  58. package/dist/core/remote-catalog-provider.d.ts +1 -1
  59. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  60. package/dist/core/remote-catalog-provider.js +24 -11
  61. package/dist/core/remote-catalog-provider.js.map +1 -1
  62. package/dist/core/resource-loader.d.ts +15 -0
  63. package/dist/core/resource-loader.d.ts.map +1 -1
  64. package/dist/core/resource-loader.js +66 -9
  65. package/dist/core/resource-loader.js.map +1 -1
  66. package/dist/core/settings-manager.d.ts +1 -1
  67. package/dist/core/settings-manager.d.ts.map +1 -1
  68. package/dist/core/settings-manager.js.map +1 -1
  69. package/dist/core/system-prompt.d.ts.map +1 -1
  70. package/dist/core/system-prompt.js +19 -1
  71. package/dist/core/system-prompt.js.map +1 -1
  72. package/dist/core/system-prompt.test.d.ts +2 -0
  73. package/dist/core/system-prompt.test.d.ts.map +1 -0
  74. package/dist/core/system-prompt.test.js +89 -0
  75. package/dist/core/system-prompt.test.js.map +1 -0
  76. package/dist/core/tools/bash.d.ts +2 -0
  77. package/dist/core/tools/bash.d.ts.map +1 -1
  78. package/dist/core/tools/bash.js +34 -5
  79. package/dist/core/tools/bash.js.map +1 -1
  80. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  81. package/dist/core/tools/tool-definition-wrapper.js +3 -1
  82. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  83. package/dist/defaults/models.json +13 -45
  84. package/dist/defaults/settings.json +1 -2
  85. package/dist/extensions/copy-turn.test.ts +131 -0
  86. package/dist/extensions/copy-turn.ts +6 -1
  87. package/dist/extensions/package.json +0 -1
  88. package/dist/extensions/pi-intercom/CHANGELOG.md +249 -0
  89. package/dist/extensions/pi-intercom/README.md +102 -39
  90. package/dist/extensions/pi-intercom/broker/broker.ts +1233 -36
  91. package/dist/extensions/pi-intercom/broker/client.test.ts +83 -0
  92. package/dist/extensions/pi-intercom/broker/client.ts +315 -12
  93. package/dist/extensions/pi-intercom/broker/extension-state.ts +186 -0
  94. package/dist/extensions/pi-intercom/broker/extension.test.ts +387 -0
  95. package/dist/extensions/pi-intercom/broker/framing.test.ts +114 -0
  96. package/dist/extensions/pi-intercom/broker/framing.ts +82 -24
  97. package/dist/extensions/pi-intercom/broker/paths.test.ts +153 -0
  98. package/dist/extensions/pi-intercom/broker/paths.ts +117 -8
  99. package/dist/extensions/pi-intercom/broker/runtime-claim.test.ts +34 -0
  100. package/dist/extensions/pi-intercom/broker/runtime-claim.ts +21 -0
  101. package/dist/extensions/pi-intercom/broker/spawn.test.ts +160 -23
  102. package/dist/extensions/pi-intercom/broker/spawn.ts +113 -27
  103. package/dist/extensions/pi-intercom/config.test.ts +93 -0
  104. package/dist/extensions/pi-intercom/config.ts +55 -6
  105. package/dist/extensions/pi-intercom/cwd.test.ts +40 -0
  106. package/dist/extensions/pi-intercom/cwd.ts +31 -0
  107. package/dist/extensions/pi-intercom/extension-api.ts +44 -0
  108. package/dist/extensions/pi-intercom/format-context.test.ts +31 -0
  109. package/dist/extensions/pi-intercom/format-context.ts +32 -0
  110. package/dist/extensions/pi-intercom/index.ts +742 -145
  111. package/dist/extensions/pi-intercom/intercom.integration.test.ts +2646 -0
  112. package/dist/extensions/pi-intercom/package.json +15 -5
  113. package/dist/extensions/pi-intercom/reply-tracker.test.ts +134 -0
  114. package/dist/extensions/pi-intercom/reply-tracker.ts +31 -13
  115. package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +13 -11
  116. package/dist/extensions/pi-intercom/test/inline-message.test.ts +184 -0
  117. package/dist/extensions/pi-intercom/test/overlay-width.test.ts +66 -0
  118. package/dist/extensions/pi-intercom/types.ts +94 -4
  119. package/dist/extensions/pi-intercom/ui/compose.ts +8 -4
  120. package/dist/extensions/pi-intercom/ui/inline-message.ts +61 -25
  121. package/dist/extensions/pi-intercom/ui/session-list.ts +7 -3
  122. package/dist/extensions/pi-subagents/CHANGELOG.md +88 -0
  123. package/dist/extensions/pi-subagents/LICENSE +21 -0
  124. package/dist/extensions/pi-subagents/README.md +158 -69
  125. package/dist/extensions/pi-subagents/agents/architect.md +5 -4
  126. package/dist/extensions/pi-subagents/agents/builder.md +6 -4
  127. package/dist/extensions/pi-subagents/agents/commentator.md +3 -2
  128. package/dist/extensions/pi-subagents/agents/explorer.md +3 -2
  129. package/dist/extensions/pi-subagents/agents/recapper.md +3 -2
  130. package/dist/extensions/pi-subagents/agents/researcher.md +4 -3
  131. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  132. package/dist/extensions/pi-subagents/package.json +3 -3
  133. package/dist/extensions/pi-subagents/prompts/review-loop.md +1 -1
  134. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +22 -988
  135. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +257 -0
  136. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +431 -0
  137. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +144 -0
  138. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +281 -0
  139. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +125 -37
  140. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +4 -0
  141. package/dist/extensions/pi-subagents/src/agents/agents.ts +118 -9
  142. package/dist/extensions/pi-subagents/src/agents/skills.ts +14 -12
  143. package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +125 -0
  144. package/dist/extensions/pi-subagents/src/api/delegation.ts +3 -0
  145. package/dist/extensions/pi-subagents/src/api/preflight.ts +17 -13
  146. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +17 -1
  147. package/dist/extensions/pi-subagents/src/extension/index.ts +22 -8
  148. package/dist/extensions/pi-subagents/src/extension/rpc.ts +248 -6
  149. package/dist/extensions/pi-subagents/src/extension/schemas.ts +33 -8
  150. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +32 -14
  151. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +9 -4
  152. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +33 -4
  153. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +86 -21
  154. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +24 -13
  155. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -5
  156. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
  157. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +48 -5
  158. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +68 -1
  159. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +56 -5
  160. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +92 -11
  161. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +29 -3
  162. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +388 -29
  163. package/dist/extensions/pi-subagents/src/runs/foreground/async-stop-action.ts +65 -0
  164. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +3 -3
  165. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +214 -60
  166. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +546 -252
  167. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +42 -0
  168. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +553 -154
  169. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +19 -13
  170. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +51 -19
  171. package/dist/extensions/pi-subagents/src/runs/shared/chain-outputs.ts +3 -1
  172. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
  173. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +44 -11
  174. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +8 -0
  175. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +97 -20
  176. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +43 -12
  177. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +52 -4
  178. package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +19 -0
  179. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +45 -9
  180. package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +71 -0
  181. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +63 -9
  182. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +31 -1
  183. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +101 -0
  184. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +22 -1
  185. package/dist/extensions/pi-subagents/src/runs/shared/usage-budget.ts +65 -0
  186. package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +26 -1
  187. package/dist/extensions/pi-subagents/src/shared/settings.ts +17 -1
  188. package/dist/extensions/pi-subagents/src/shared/types.ts +196 -12
  189. package/dist/extensions/pi-subagents/src/shared/utils.ts +102 -5
  190. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +10 -1
  191. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +46 -4
  192. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +5 -3
  193. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +87 -24
  194. package/dist/extensions/pi-subagents/src/tui/fleet.ts +182 -9
  195. package/dist/extensions/pi-subagents/src/tui/render.ts +55 -34
  196. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +14 -7
  197. package/dist/extensions/pi-subagents/src/watchdog/review.ts +5 -4
  198. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +170 -17
  199. package/dist/extensions/pi-subagents/src/watchdog/scope.ts +62 -0
  200. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +41 -1
  201. package/dist/extensions/pi-subagents/src/watchdog/types.ts +10 -0
  202. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +113 -8
  203. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +419 -47
  204. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +84 -0
  205. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +4 -2
  206. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +51 -2
  207. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +62 -22
  208. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +48 -0
  209. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +8 -6
  210. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +231 -19
  211. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +48 -7
  212. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +44 -0
  213. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +199 -17
  214. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +898 -16
  215. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +112 -23
  216. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -5
  217. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +2 -0
  218. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +18 -2
  219. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +3 -3
  220. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +3 -1
  221. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
  222. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +198 -6
  223. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +161 -1
  224. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +47 -2
  225. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +60 -0
  226. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +8 -0
  227. package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +63 -0
  228. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +136 -0
  229. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +1 -1
  230. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +6 -0
  231. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +1 -1
  232. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +48 -48
  233. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +23 -0
  234. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +27 -0
  235. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +28 -2
  236. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +1 -0
  237. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +176 -8
  238. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +164 -5
  239. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +41 -1
  240. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +17 -12
  241. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +18 -2
  242. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +20 -1
  243. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +30 -2
  244. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +39 -2
  245. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +22 -0
  246. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +103 -0
  247. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +18 -0
  248. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +48 -0
  249. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +13 -1
  250. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +29 -4
  251. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +223 -4
  252. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +30 -0
  253. package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +52 -0
  254. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +46 -2
  255. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +91 -1
  256. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
  257. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +1 -1
  258. package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +78 -0
  259. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +41 -0
  260. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +77 -0
  261. package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +213 -0
  262. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +26 -1
  263. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +61 -10
  264. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +1 -0
  265. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +210 -1
  266. package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +33 -0
  267. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +32 -0
  268. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +1 -1
  269. package/dist/main.d.ts.map +1 -1
  270. package/dist/main.js +43 -0
  271. package/dist/main.js.map +1 -1
  272. package/dist/modes/interactive/components/custom-message.d.ts +3 -1
  273. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  274. package/dist/modes/interactive/components/custom-message.js +10 -2
  275. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  276. package/dist/modes/interactive/components/extension-editor.d.ts +1 -2
  277. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  278. package/dist/modes/interactive/components/extension-editor.js +16 -46
  279. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  280. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  281. package/dist/modes/interactive/components/model-selector.js +4 -1
  282. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  283. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  284. package/dist/modes/interactive/components/scoped-models-selector.js +22 -13
  285. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  286. package/dist/modes/interactive/external-editor.d.ts +12 -0
  287. package/dist/modes/interactive/external-editor.d.ts.map +1 -0
  288. package/dist/modes/interactive/external-editor.js +37 -0
  289. package/dist/modes/interactive/external-editor.js.map +1 -0
  290. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  291. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  292. package/dist/modes/interactive/interactive-mode.js +72 -64
  293. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  294. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  295. package/dist/modes/rpc/rpc-mode.js +14 -0
  296. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  297. package/dist/skills/ponytail/SKILL.md +1 -3
  298. package/dist/utils/clipboard.d.ts.map +1 -1
  299. package/dist/utils/clipboard.js +19 -8
  300. package/dist/utils/clipboard.js.map +1 -1
  301. package/dist/utils/version-check.d.ts.map +1 -1
  302. package/dist/utils/version-check.js +1 -1
  303. package/dist/utils/version-check.js.map +1 -1
  304. package/docs/compaction.md +1 -1
  305. package/docs/custom-provider.md +14 -5
  306. package/docs/environment-variables.md +86 -0
  307. package/docs/extensions.md +15 -5
  308. package/docs/index.md +1 -0
  309. package/docs/models.md +9 -2
  310. package/docs/plans/subagent-delegation/phase-0-correctness.md +265 -0
  311. package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +486 -0
  312. package/docs/plans/subagent-delegation/phase-2-context-controls.md +282 -0
  313. package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +362 -0
  314. package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +381 -0
  315. package/docs/providers.md +21 -2
  316. package/docs/rpc.md +23 -6
  317. package/docs/session-format.md +2 -0
  318. package/docs/settings.md +1 -1
  319. package/docs/usage.md +0 -13
  320. package/examples/extensions/custom-compaction.ts +5 -2
  321. package/examples/extensions/custom-provider-anthropic/index.ts +9 -3
  322. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  323. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  324. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  325. package/examples/extensions/gondolin/package-lock.json +2 -2
  326. package/examples/extensions/gondolin/package.json +1 -1
  327. package/examples/extensions/handoff.ts +11 -3
  328. package/examples/extensions/message-renderer.ts +3 -3
  329. package/examples/extensions/sandbox/package-lock.json +2 -2
  330. package/examples/extensions/sandbox/package.json +1 -1
  331. package/examples/extensions/summarize.ts +5 -2
  332. package/examples/extensions/with-deps/package-lock.json +2 -2
  333. package/examples/extensions/with-deps/package.json +1 -1
  334. package/examples/sdk/12-full-control.ts +3 -1
  335. package/package.json +16 -7
  336. package/dist/extensions/caveman/caveman-instructions.cjs +0 -11
  337. package/dist/extensions/caveman/index.js +0 -118
  338. package/dist/extensions/caveman/package.json +0 -8
  339. package/dist/extensions/caveman/test/extension.test.js +0 -203
  340. package/dist/extensions/caveman/test/helpers.test.js +0 -58
  341. package/dist/skills/caveman/SKILL.md +0 -50
@@ -15,7 +15,7 @@ import * as os from "node:os";
15
15
  import * as path from "node:path";
16
16
  import { createEventBus, createMockPi, createTempDir, events, makeAgent, makeMinimalCtx, removeTempDir, tryImport } from "../support/helpers.ts";
17
17
  import type { MockPi } from "../support/helpers.ts";
18
- import { deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest } from "../../src/runs/background/control-channel.ts";
18
+ import { deliverCheckpointDecisionRequest, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest } from "../../src/runs/background/control-channel.ts";
19
19
  import { waitForSubagents } from "../../src/runs/background/subagent-wait.ts";
20
20
  import { writeAtomicJson } from "../../src/shared/atomic-json.ts";
21
21
  import { CHILD_WATCHDOG_STATUS_EVENT } from "../../src/watchdog/child-status.ts";
@@ -26,10 +26,35 @@ import { resolveSubagentLaunchContract } from "../../src/api/preflight.ts";
26
26
  import { discoverAgents } from "../../src/agents/agents.ts";
27
27
  import { runSync } from "../../src/runs/foreground/execution.ts";
28
28
 
29
+ interface LaunchResolvedExtensions {
30
+ version?: number;
31
+ source?: string;
32
+ disableAmbientExtensions?: boolean;
33
+ runtime?: string[];
34
+ configured?: string[];
35
+ effective?: string[];
36
+ }
37
+
38
+ interface RuntimeAcknowledgedExtensions {
39
+ version?: number;
40
+ source?: string;
41
+ ids?: string[];
42
+ omitted?: number;
43
+ }
44
+
45
+ interface UsageBudgetState {
46
+ version?: number;
47
+ source?: string;
48
+ exhausted?: boolean;
49
+ reason?: string;
50
+ tokens?: { used?: number; hard?: number; exhausted?: boolean };
51
+ costUsd?: { used?: number; hard?: number; exhausted?: boolean };
52
+ }
53
+
29
54
  interface AsyncExecutionResult {
30
55
  content: Array<{ text?: string }>;
31
56
  isError?: boolean;
32
- details: { asyncId?: string; asyncDir?: string; launchContractDigest?: string };
57
+ details: { asyncId?: string; asyncDir?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; usageBudget?: UsageBudgetState; checkpoint?: { name?: string; status?: string } };
33
58
  }
34
59
 
35
60
  interface AsyncResultPayload {
@@ -40,6 +65,8 @@ interface AsyncResultPayload {
40
65
  sessionId?: string;
41
66
  mode?: string;
42
67
  launchContractDigest?: string;
68
+ launchResolvedExtensions?: LaunchResolvedExtensions;
69
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions;
43
70
  summary?: string;
44
71
  error?: string;
45
72
  timeoutMs?: number;
@@ -51,7 +78,9 @@ interface AsyncResultPayload {
51
78
  wrapUpRequested?: boolean;
52
79
  totalTokens?: { input: number; output: number; total: number };
53
80
  totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
54
- results: Array<{ agent?: string; launchContractDigest?: string; output?: string; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string }; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>;
81
+ usageBudget?: UsageBudgetState;
82
+ checkpoint?: { name?: string; status?: string };
83
+ results: Array<{ agent?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; output?: string; outputPath?: string; outputState?: "present" | "absent" | "unknown"; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string }; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>;
55
84
  outputs?: Record<string, { text?: string; structured?: unknown }>;
56
85
  workflowGraph?: { nodes?: Array<{ kind?: string; label?: string; phase?: string; status?: string; acceptanceStatus?: string; error?: string; outputName?: string; structured?: boolean; children?: Array<{ label?: string; outputName?: string; itemKey?: string; status?: string; acceptanceStatus?: string; error?: string }> }> };
57
86
  parallelHandoff?: { version?: number; path?: string; groupCount?: number; childCount?: number; changedPatches?: number; cleanupState?: string };
@@ -68,6 +97,8 @@ interface AsyncStatusPayload {
68
97
  currentPath?: string;
69
98
  state?: string;
70
99
  launchContractDigest?: string;
100
+ launchResolvedExtensions?: LaunchResolvedExtensions;
101
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions;
71
102
  error?: string;
72
103
  timeoutMs?: number;
73
104
  deadlineAt?: number;
@@ -78,6 +109,8 @@ interface AsyncStatusPayload {
78
109
  wrapUpRequested?: boolean;
79
110
  totalTokens?: { total: number };
80
111
  totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
112
+ usageBudget?: UsageBudgetState;
113
+ checkpoint?: { name?: string; status?: string; message?: string };
81
114
  parallelGroups?: Array<{ start: number; count: number; stepIndex: number }>;
82
115
  parallelHandoff?: { version?: number; path?: string; groupCount?: number; childCount?: number; changedPatches?: number; cleanupState?: string };
83
116
  capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] };
@@ -100,6 +133,8 @@ interface AsyncStatusPayload {
100
133
  totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
101
134
  agentContract?: { version: 1 };
102
135
  launchContractDigest?: string;
136
+ launchResolvedExtensions?: LaunchResolvedExtensions;
137
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions;
103
138
  execution?: { status?: string; success?: boolean; exitCode?: number };
104
139
  effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean } };
105
140
  acceptance?: { status?: string };
@@ -292,6 +327,19 @@ async function waitForAsyncResultFile(id: string, timeoutMs = 15_000): Promise<s
292
327
  return resultPath;
293
328
  }
294
329
 
330
+ async function waitForAsyncState(id: string, predicate: (status: AsyncStatusPayload) => boolean, timeoutMs = 10_000): Promise<AsyncStatusPayload> {
331
+ const statusPath = path.join(ASYNC_DIR, id, "status.json");
332
+ const deadline = Date.now() + timeoutMs;
333
+ while (Date.now() <= deadline) {
334
+ if (fs.existsSync(statusPath)) {
335
+ const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload;
336
+ if (predicate(status)) return status;
337
+ }
338
+ await new Promise((resolve) => setTimeout(resolve, 50));
339
+ }
340
+ assert.fail(`Timed out waiting for async status: ${statusPath}`);
341
+ }
342
+
295
343
  async function waitForDeferredTurnBudget(id: string, timeoutMs = 10_000): Promise<AsyncStatusPayload> {
296
344
  const statusPath = path.join(ASYNC_DIR, id, "status.json");
297
345
  const deadline = Date.now() + timeoutMs;
@@ -405,6 +453,18 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
405
453
  return JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
406
454
  }
407
455
 
456
+ /**
457
+ * Reference-first async child assertion: the result payload carries the
458
+ * saved-output reference; the full text is read from the durable output path.
459
+ */
460
+ function assertAsyncChildOutput(payload: AsyncResultPayload, index: number, expected: string): void {
461
+ const child = payload.results[index];
462
+ assert.ok(child, `expected async child ${index}`);
463
+ assert.match(child.output ?? "", /Output saved to: /);
464
+ assert.ok(child.outputPath, `expected a durable output path for child ${index}`);
465
+ assert.equal(fs.readFileSync(child.outputPath, "utf-8"), expected);
466
+ }
467
+
408
468
  function launchProtocolTest(id: string): void {
409
469
  executeAsyncSingle(id, {
410
470
  agent: "worker",
@@ -432,7 +492,27 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
432
492
  launchProtocolTest(id);
433
493
  const payload = await readAsyncPayload(id);
434
494
  assert.equal(payload.success, true);
435
- assert.equal(payload.results[0]?.output, "你好 from fragmented async JSON");
495
+ assertAsyncChildOutput(payload, 0, "你好 from fragmented async JSON");
496
+ });
497
+
498
+ it("persists absent output provenance when async lifecycle text is synthetic", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
499
+ mockPi.onCall({ jsonl: [mockAssistantMessage("", "tool_use")], stderr: "mock child failure", exitCode: 1 });
500
+ const id = `async-output-absent-${Date.now().toString(36)}`;
501
+ launchProtocolTest(id);
502
+ const payload = await readAsyncPayload(id);
503
+ assert.equal(payload.success, false);
504
+ assert.equal(payload.results[0]?.outputState, "absent");
505
+ assert.match(payload.results[0]?.error ?? "", /mock child failure/);
506
+ });
507
+
508
+ it("persists present output provenance when async failure follows partial output", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
509
+ mockPi.onCall({ output: "usable partial answer", stderr: "mock post-output failure", exitCode: 1 });
510
+ const id = `async-output-present-${Date.now().toString(36)}`;
511
+ launchProtocolTest(id);
512
+ const payload = await readAsyncPayload(id);
513
+ assert.equal(payload.success, false);
514
+ assert.equal(payload.results[0]?.outputState, "present");
515
+ assertAsyncChildOutput(payload, 0, "usable partial answer");
436
516
  });
437
517
 
438
518
  it("matches preflight launch digest in equivalent foreground and async execution", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
@@ -444,11 +524,14 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
444
524
  fs.writeFileSync(agentPath, `---\nname: ${agentName}\ndescription: Contract comparison worker\n---\n`, "utf-8");
445
525
  const discovered = discoverAgents(tempDir).agents.find((agent) => agent.name === agentName);
446
526
  assert.ok(discovered, "expected temporary agent definition to be discovered");
447
- const preflight = await resolveSubagentLaunchContract({ agent: agentName, cwd: tempDir, task, turnBudget, runId: "contract-preflight" });
527
+ // Stable explicit output keeps the preflight launch contract equivalent to
528
+ // both execution paths (generated per-run paths would differ by design).
529
+ const contractOutputPath = path.join(tempDir, "contract-output.md");
530
+ const preflight = await resolveSubagentLaunchContract({ agent: agentName, cwd: tempDir, task, turnBudget, runId: "contract-preflight", output: contractOutputPath });
448
531
  assert.equal(preflight.ok, true);
449
532
 
450
533
  mockPi.onCall({ output: "foreground contract comparison" });
451
- const foreground = await runSync(tempDir, [discovered], agentName, task, { runId: "contract-foreground", acceptance: false, turnBudget });
534
+ const foreground = await runSync(tempDir, [discovered], agentName, task, { runId: "contract-foreground", acceptance: false, turnBudget, outputPath: contractOutputPath });
452
535
  assert.equal(foreground.exitCode, 0);
453
536
  assert.equal(foreground.launchContractDigest, preflight.contract.launchContractDigest);
454
537
 
@@ -457,6 +540,8 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
457
540
  const launch = executeAsyncSingle(asyncId, {
458
541
  agent: agentName,
459
542
  task,
543
+ output: contractOutputPath,
544
+ outputMode: "inline",
460
545
  agentConfig: discovered,
461
546
  ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
462
547
  artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
@@ -473,12 +558,16 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
473
558
  });
474
559
 
475
560
  it("persists the actual launch digest in async status and result metadata", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
476
- mockPi.onCall({ output: "digest-bound async done" });
561
+ mockPi.onCall({
562
+ output: "digest-bound async done",
563
+ runtimeAcknowledgedExtensions: { version: 1, source: "child-runtime", ids: ["ext.async"], omitted: 0 },
564
+ });
477
565
  const id = `async-launch-digest-${Date.now().toString(36)}`;
566
+ const privateExtension = path.join(tempDir, "extensions", "private-extension.ts");
478
567
  const launch = executeAsyncSingle(id, {
479
568
  agent: "worker",
480
569
  task: "Exercise launch digest reporting",
481
- agentConfig: makeAgent("worker", { completionGuard: false }),
570
+ agentConfig: makeAgent("worker", { completionGuard: false, extensions: [privateExtension] }),
482
571
  ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
483
572
  artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
484
573
  shareEnabled: false,
@@ -493,6 +582,140 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
493
582
  assert.equal(payload.results[0]?.launchContractDigest, launch.details.launchContractDigest);
494
583
  assert.equal(status.launchContractDigest, launch.details.launchContractDigest);
495
584
  assert.equal(status.steps?.[0]?.launchContractDigest, launch.details.launchContractDigest);
585
+ assert.equal(launch.details.launchResolvedExtensions?.source, "launch-resolved");
586
+ assert.equal(launch.details.launchResolvedExtensions?.disableAmbientExtensions, true);
587
+ assert.deepEqual(payload.launchResolvedExtensions, launch.details.launchResolvedExtensions);
588
+ assert.deepEqual(payload.results[0]?.launchResolvedExtensions, launch.details.launchResolvedExtensions);
589
+ assert.deepEqual(status.launchResolvedExtensions, launch.details.launchResolvedExtensions);
590
+ assert.deepEqual(status.steps?.[0]?.launchResolvedExtensions, launch.details.launchResolvedExtensions);
591
+ const runtimeAck = { version: 1, source: "child-runtime", ids: ["ext.async"], omitted: 0 };
592
+ assert.deepEqual(payload.runtimeAcknowledgedExtensions, runtimeAck);
593
+ assert.deepEqual(payload.results[0]?.runtimeAcknowledgedExtensions, runtimeAck);
594
+ assert.deepEqual(status.runtimeAcknowledgedExtensions, runtimeAck);
595
+ assert.deepEqual(status.steps?.[0]?.runtimeAcknowledgedExtensions, runtimeAck);
596
+ assert.ok(!JSON.stringify(launch.details.launchResolvedExtensions).includes(tempDir), "projection should not expose raw extension paths");
597
+ });
598
+
599
+ it("async chain checkpoints pause before the next child until approved", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
600
+ mockPi.onCall({ output: "analysis done" });
601
+ mockPi.onCall({ output: "implementation done" });
602
+ const id = `async-checkpoint-approve-${Date.now().toString(36)}`;
603
+ const launch = executeAsyncChain(id, {
604
+ chain: [
605
+ { agent: "analyst", task: "Analyze" },
606
+ { checkpoint: "review", message: "Approve implementation?" },
607
+ { agent: "reporter", task: "Summarize {previous}" },
608
+ ],
609
+ agents: [makeAgent("analyst"), makeAgent("reporter")],
610
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
611
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
612
+ shareEnabled: false,
613
+ maxSubagentDepth: 2,
614
+ });
615
+
616
+ const paused = await waitForAsyncState(id, (status) => status.state === "paused" && status.checkpoint?.name === "review");
617
+ assert.equal(launch.isError, undefined);
618
+ assert.equal(paused.steps?.[1]?.status, "paused");
619
+ assert.equal(mockPi.callCount(), 1);
620
+
621
+ deliverCheckpointDecisionRequest({ asyncDir: path.join(ASYNC_DIR, id), decision: "approved" });
622
+ const payload = await readAsyncPayload(id);
623
+ assert.equal(payload.success, true);
624
+ assert.equal(payload.state, "complete");
625
+ assert.equal(payload.checkpoint?.status, "approved");
626
+ assert.equal(payload.results.length, 2);
627
+ assert.equal(mockPi.callCount(), 2);
628
+ });
629
+
630
+ it("foreground checkpoint decisions use current-session control actions", { skip: !createSubagentExecutor ? "executor not available" : undefined }, async () => {
631
+ mockPi.onCall({ output: "analysis done" });
632
+ const executor = makeAsyncExecutor([makeAgent("analyst"), makeAgent("worker")]);
633
+ const ctx = makeMinimalCtx(tempDir);
634
+ ctx.sessionManager.getSessionId = () => "session-foreground-checkpoint";
635
+ const id = `foreground-checkpoint-${Date.now().toString(36)}`;
636
+
637
+ const paused = await executor.execute(id, {
638
+ chain: [
639
+ { agent: "analyst", task: "Analyze" },
640
+ { checkpoint: "review", message: "Approve implementation?" },
641
+ { agent: "worker", task: "Implement" },
642
+ ],
643
+ }, new AbortController().signal, undefined, ctx) as AsyncExecutionResult;
644
+
645
+ assert.equal(paused.isError, undefined);
646
+ assert.equal(paused.details.checkpoint?.status, "pending");
647
+ assert.equal(mockPi.callCount(), 1);
648
+ const runId = (paused.details as AsyncExecutionResult["details"] & { runId?: string }).runId;
649
+ assert.ok(runId);
650
+
651
+ const approved = await executor.execute(`${id}-approve`, { action: "approve-checkpoint", id: runId }, new AbortController().signal, undefined, ctx) as AsyncExecutionResult;
652
+ assert.equal(approved.isError, undefined);
653
+ assert.equal(approved.details.checkpoint?.name, "review");
654
+ assert.equal(approved.details.checkpoint?.status, "approved");
655
+ assert.equal(mockPi.callCount(), 1);
656
+ });
657
+
658
+ it("async chain checkpoint rejection is an explicit terminal state", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
659
+ mockPi.onCall({ output: "analysis done" });
660
+ mockPi.onCall({ output: "should not run" });
661
+ const id = `async-checkpoint-reject-${Date.now().toString(36)}`;
662
+ executeAsyncChain(id, {
663
+ chain: [
664
+ { agent: "analyst", task: "Analyze" },
665
+ { checkpoint: "review" },
666
+ { agent: "worker", task: "Implement" },
667
+ ],
668
+ agents: [makeAgent("analyst"), makeAgent("worker")],
669
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
670
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
671
+ shareEnabled: false,
672
+ maxSubagentDepth: 2,
673
+ });
674
+
675
+ await waitForAsyncState(id, (status) => status.state === "paused" && status.checkpoint?.name === "review");
676
+ deliverCheckpointDecisionRequest({ asyncDir: path.join(ASYNC_DIR, id), decision: "rejected" });
677
+ const payload = await readAsyncPayload(id);
678
+ assert.equal(payload.success, false);
679
+ assert.equal(payload.state, "rejected");
680
+ assert.equal(payload.checkpoint?.status, "rejected");
681
+ assert.equal(mockPi.callCount(), 1);
682
+ });
683
+
684
+ it("background parallel groups report usage budget state and block queued children", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
685
+ mockPi.onCall({ output: "first async result" });
686
+ const id = `async-usage-budget-${Date.now().toString(36)}`;
687
+ const launch = executeAsyncChain(id, {
688
+ chain: [{
689
+ parallel: [
690
+ { agent: "first", task: "First task" },
691
+ { agent: "second", task: "Second task" },
692
+ ],
693
+ concurrency: 1,
694
+ }],
695
+ resultMode: "parallel",
696
+ usageBudget: { tokens: { hard: 10 } },
697
+ agents: [makeAgent("first"), makeAgent("second")],
698
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
699
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
700
+ shareEnabled: false,
701
+ maxSubagentDepth: 2,
702
+ });
703
+
704
+ assert.equal(launch.details.usageBudget?.exhausted, false);
705
+ const payload = await readAsyncPayload(id);
706
+ const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
707
+ assert.equal(payload.success, false);
708
+ assert.equal(payload.state, "failed");
709
+ assert.match(payload.error ?? payload.summary ?? "", /Usage budget exhausted/);
710
+ assert.equal(payload.results.length, 2);
711
+ assert.equal(payload.results[1]?.skipped, true);
712
+ assert.match(payload.results[1]?.error ?? "", /Usage budget exhausted/);
713
+ assert.equal(mockPi.callCount(), 1);
714
+ assert.equal(payload.usageBudget?.exhausted, true);
715
+ assert.equal(payload.usageBudget?.reason, "tokens");
716
+ assert.equal(status.usageBudget?.exhausted, true);
717
+ assert.equal(status.steps?.[0]?.status, "complete");
718
+ assert.equal(status.steps?.[1]?.status, "failed");
496
719
  });
497
720
 
498
721
  it("background fails with protocol_output_limit for an oversized stdout line", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
@@ -515,7 +738,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
515
738
 
516
739
  const launch = await executor.execute(
517
740
  "async-session-artifact-dir",
518
- { agent: "worker", task: "Write async session artifacts", async: true, runId: "async-session-artifacts", acceptance: false },
741
+ { agent: "worker", task: "Write async session artifacts", async: true, runId: "async-session-artifacts", acceptance: false, artifacts: true },
519
742
  new AbortController().signal,
520
743
  undefined,
521
744
  ctx,
@@ -623,7 +846,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
623
846
  launchProtocolTest(id);
624
847
  const payload = await readAsyncPayload(id);
625
848
  assert.equal(payload.success, true);
626
- assert.equal(payload.results[0]?.output, "settled async response");
849
+ assertAsyncChildOutput(payload, 0, "settled async response");
627
850
  assert.ok(Date.now() - startedAt >= 1200, "background runner must not terminate during the retry delay");
628
851
  });
629
852
 
@@ -635,7 +858,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
635
858
  const payload = await readAsyncPayload(id);
636
859
  assert.equal(payload.success, true);
637
860
  assert.equal(payload.results[0]?.error, undefined);
638
- assert.equal(payload.results[0]?.output, "settled async without a terminal assistant stop");
861
+ assertAsyncChildOutput(payload, 0, "settled async without a terminal assistant stop");
639
862
  assert.ok(Date.now() - startedAt < 4000, "agent_settled should trigger bounded child cleanup");
640
863
  });
641
864
 
@@ -666,7 +889,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
666
889
  assert.match(call.args.at(-1) ?? "", /\{outputs\.name\}/);
667
890
  const payload = await readAsyncPayload(id);
668
891
  assert.equal(payload.success, true);
669
- assert.equal(payload.results[0]?.output, "OK");
892
+ assertAsyncChildOutput(payload, 0, "OK");
670
893
  });
671
894
 
672
895
  it("spawns the async runner with node when process.execPath is not node", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
@@ -697,7 +920,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
697
920
  const resultPath = await waitForAsyncResultFile(id, 30_000);
698
921
  const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
699
922
  assert.equal(payload.success, true);
700
- assert.equal(payload.results[0]?.output, "non-node exec async done");
923
+ assertAsyncChildOutput(payload, 0, "non-node exec async done");
701
924
  } finally {
702
925
  process.execPath = originalExecPath;
703
926
  }
@@ -731,7 +954,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
731
954
  const resultPath = await waitForAsyncResultFile(id, 10_000);
732
955
  const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
733
956
  assert.equal(payload.success, true);
734
- assert.equal(payload.results[0]?.output, "stale node exec async done");
957
+ assertAsyncChildOutput(payload, 0, "stale node exec async done");
735
958
  } finally {
736
959
  process.execPath = originalExecPath;
737
960
  }
@@ -972,8 +1195,11 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
972
1195
  assert.equal(payload.turnBudget?.turnCount, 2);
973
1196
  assert.equal(payload.results[0]?.wrapUpRequested, true);
974
1197
  assert.equal(payload.results[0]?.turnBudget?.turnCount, 2);
975
- assert.match(payload.results[0]?.output ?? "", /Turn budget wrap-up was requested after 1 assistant turn/);
976
- assert.match(payload.results[0]?.output ?? "", /final wrapped output/);
1198
+ // Reference-first delivery: the saved-output reference replaces inline prose;
1199
+ // the wrap-up note and raw output stay visible through status/result fields
1200
+ // and the persisted result file.
1201
+ assert.match(payload.results[0]?.output ?? "", /Output saved to: /);
1202
+ assertAsyncChildOutput(payload, 0, "final wrapped output");
977
1203
  assert.equal(status.wrapUpRequested, true);
978
1204
  assert.equal(status.turnBudgetExceeded, undefined);
979
1205
  assert.equal(status.steps?.[0]?.wrapUpRequested, true);
@@ -1012,8 +1238,9 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1012
1238
  assert.equal(payload.turnBudget?.turnCount, 3);
1013
1239
  assert.equal(payload.turnBudget?.exceededAtTurn, 3);
1014
1240
  assert.equal(payload.results[0]?.turnBudgetExceeded, true);
1015
- assert.match(payload.results[0]?.output ?? "", /Partial output before turn-budget abort:/);
1016
- assert.match(payload.results[0]?.output ?? "", /safe assistant boundary after tool work/);
1241
+ assert.match(payload.error ?? "", /Subagent exceeded turn budget|turn budget/i);
1242
+ assert.match(payload.results[0]?.output ?? "", /Output saved to: /);
1243
+ assertAsyncChildOutput(payload, 0, "safe assistant boundary after tool work");
1017
1244
  assert.equal(status.state, "failed");
1018
1245
  assert.equal(status.turnBudgetExceeded, true);
1019
1246
  assert.equal(status.steps?.[0]?.turnBudgetExceeded, true);
@@ -1066,7 +1293,8 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1066
1293
  assert.equal(payload.turnBudget?.outcome, "exceeded");
1067
1294
  assert.equal(payload.turnBudget?.turnCount, 2);
1068
1295
  assert.equal(payload.results[0]?.turnBudgetExceeded, true);
1069
- assert.match(payload.results[0]?.output ?? "", /safe assistant boundary reached/);
1296
+ assert.match(payload.results[0]?.output ?? "", /Output saved to: /);
1297
+ assertAsyncChildOutput(payload, 0, "safe assistant boundary reached");
1070
1298
  assert.equal(status.state, "failed");
1071
1299
  assert.equal(status.turnBudgetExceeded, true);
1072
1300
  assert.equal(status.steps?.[0]?.turnBudget?.outcome, "exceeded");
@@ -1349,15 +1577,16 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1349
1577
  tempArtifactsDir: tempDir,
1350
1578
  getSubagentSessionRoot: () => tempDir,
1351
1579
  expandTilde: (p: string) => p,
1352
- discoverAgents: () => ({ agents: [makeAgent("worker", { defaultProgress: true })] }),
1580
+ discoverAgents: () => ({ agents: [makeAgent("builder", { defaultProgress: true })] }),
1353
1581
  });
1354
1582
 
1355
1583
  const result = await executor.execute(
1356
1584
  "async-parallel-fields",
1357
1585
  {
1358
- tasks: [{ agent: "worker", task: "Do async work", output: "async-top-output.md", reads: ["input.md"] }],
1586
+ tasks: [{ agent: "builder", task: "Do async work", output: "async-top-output.md", reads: ["input.md"] }],
1359
1587
  async: true,
1360
1588
  clarify: false,
1589
+ artifacts: true,
1361
1590
  },
1362
1591
  new AbortController().signal,
1363
1592
  undefined,
@@ -1416,7 +1645,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1416
1645
  ];
1417
1646
  const launch = await executor.execute(
1418
1647
  `async-inherited-output-${outputOverride === true ? "true" : "omitted"}`,
1419
- { tasks, async: true, clarify: false },
1648
+ { tasks, async: true, clarify: false, artifacts: true },
1420
1649
  new AbortController().signal,
1421
1650
  undefined,
1422
1651
  makeMinimalCtx(tempDir),
@@ -1425,8 +1654,8 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1425
1654
  assert.equal(launch.isError, undefined);
1426
1655
  const payload = await readAsyncPayload(launch.details?.asyncId as string);
1427
1656
  assert.equal(payload.success, true);
1428
- assert.equal(payload.results[0]?.output?.split("\n\nOutput saved to:")[0], "first async report");
1429
- assert.equal(payload.results[1]?.output?.split("\n\nOutput saved to:")[0], "second async report");
1657
+ assertAsyncChildOutput(payload, 0, "first async report");
1658
+ assertAsyncChildOutput(payload, 1, "second async report");
1430
1659
  const outputDir = path.join(tempDir, ".pi-subagents", "artifacts", "outputs", launch.details?.asyncId as string);
1431
1660
  const authoritativePaths = [
1432
1661
  path.join(outputDir, "parallel-0", "0-worker", "context.md"),
@@ -1488,6 +1717,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1488
1717
  ],
1489
1718
  async: true,
1490
1719
  clarify: false,
1720
+ artifacts: true,
1491
1721
  },
1492
1722
  new AbortController().signal,
1493
1723
  undefined,
@@ -1726,13 +1956,13 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1726
1956
  const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
1727
1957
  const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
1728
1958
  assert.equal(payload.success, true);
1729
- assert.deepEqual(payload.results.map((entry) => entry.output), [
1730
- "Scout A async findings",
1731
- "Scout B async findings",
1732
- "Async funnel synthesis",
1733
- "Async reviewer A done",
1734
- "Async reviewer B done",
1735
- ]);
1959
+ // Reference-first chain delivery: every child result carries the saved-output
1960
+ // reference; full text is read from each durable output path.
1961
+ assertAsyncChildOutput(payload, 0, "Scout A async findings");
1962
+ assertAsyncChildOutput(payload, 1, "Scout B async findings");
1963
+ assertAsyncChildOutput(payload, 2, "Async funnel synthesis");
1964
+ assertAsyncChildOutput(payload, 3, "Async reviewer A done");
1965
+ assertAsyncChildOutput(payload, 4, "Async reviewer B done");
1736
1966
  assert.deepEqual(status.steps?.map((step) => step.status), ["complete", "complete", "complete", "complete", "complete"]);
1737
1967
  assert.deepEqual(status.parallelGroups, [
1738
1968
  { start: 0, count: 2, stepIndex: 0 },
@@ -1740,11 +1970,14 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1740
1970
  ]);
1741
1971
  const funnelTask = readMockPiArgsMatching(mockPi, "Synthesize:").at(-1) ?? "";
1742
1972
  assert.match(funnelTask, /=== Parallel Task 1 \(scout-a\) ===/);
1743
- assert.match(funnelTask, /Scout A async findings/);
1744
1973
  assert.match(funnelTask, /=== Parallel Task 2 \(scout-b\) ===/);
1745
- assert.match(funnelTask, /Scout B async findings/);
1746
- assert.match(readMockPiArgsMatching(mockPi, "Review funnel A:").at(-1) ?? "", /Review funnel A:\nAsync funnel synthesis/);
1747
- assert.match(readMockPiArgsMatching(mockPi, "Review funnel B:").at(-1) ?? "", /Review funnel B:\nAsync funnel synthesis/);
1974
+ // Chain handoff stays reference-first: the funnel consumes the saved-output
1975
+ // references and reads the named paths instead of re-inlined child prose.
1976
+ assert.match(funnelTask, /Output saved to: /);
1977
+ assert.doesNotMatch(funnelTask, /Scout A async findings/);
1978
+ assert.doesNotMatch(funnelTask, /Scout B async findings/);
1979
+ assert.match(readMockPiArgsMatching(mockPi, "Review funnel A:").at(-1) ?? "", /Review funnel A:\nOutput saved to: /);
1980
+ assert.match(readMockPiArgsMatching(mockPi, "Review funnel B:").at(-1) ?? "", /Review funnel B:\nOutput saved to: /);
1748
1981
  assert.equal(payload.workflowGraph?.nodes?.[0]?.kind, "parallel-group");
1749
1982
  assert.equal(payload.workflowGraph?.nodes?.[0]?.status, "completed");
1750
1983
  assert.equal(payload.workflowGraph?.nodes?.[1]?.kind, "step");
@@ -1862,6 +2095,39 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1862
2095
  assert.equal(payload.workflowGraph?.nodes?.[2]?.flatIndex, 3);
1863
2096
  });
1864
2097
 
2098
+ it("async dynamic fanout blocks queued children when hard reported usage is exhausted", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
2099
+ mockPi.onCall({ output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }, { path: "src/b.ts" }] } });
2100
+ mockPi.onCall({ matchArgIncludes: "Review src/a.ts", output: "review-a", structuredOutput: { ok: "a" } });
2101
+ const id = `async-dynamic-usage-budget-${Date.now().toString(36)}`;
2102
+ executeAsyncChain(id, {
2103
+ chain: [
2104
+ { agent: "producer", task: "Produce targets", as: "targets", outputSchema: { type: "object" } },
2105
+ {
2106
+ expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 },
2107
+ parallel: { agent: "reviewer", task: "Review {target.path}", outputSchema: { type: "object" } },
2108
+ collect: { as: "reviews" },
2109
+ concurrency: 1,
2110
+ },
2111
+ ],
2112
+ usageBudget: { tokens: { hard: 200 } },
2113
+ agents: [makeAgent("producer"), makeAgent("reviewer")],
2114
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-dynamic-budget" },
2115
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
2116
+ shareEnabled: false,
2117
+ maxSubagentDepth: 2,
2118
+ });
2119
+
2120
+ const payload = await readAsyncPayload(id);
2121
+ const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
2122
+ assert.equal(mockPi.callCount(), 2);
2123
+ assert.equal(payload.success, false);
2124
+ assert.equal(payload.usageBudget?.exhausted, true);
2125
+ assert.equal(status.steps?.[1]?.status, "complete");
2126
+ assert.equal(status.steps?.[2]?.status, "failed");
2127
+ assert.match(status.steps?.[2]?.error ?? "", /Usage budget exhausted/);
2128
+ assert.equal(payload.results.find((result) => result.agent === "reviewer" && result.skipped)?.skipped, true);
2129
+ });
2130
+
1865
2131
  it("rejects a shared explicit output before dynamic fanout children start", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1866
2132
  mockPi.onCall({ matchArgIncludes: "Produce targets", output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }, { path: "src/b.ts" }] } });
1867
2133
  const id = `async-dynamic-explicit-output-${Date.now().toString(36)}`;
@@ -2102,7 +2368,10 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
2102
2368
  const expectedConsumerTarget = `subagent-consumer-${id}-4`;
2103
2369
  assert.equal(payload.success, true);
2104
2370
  assert.equal(payload.results[3]?.intercomTarget, expectedConsumerTarget);
2105
- assert.deepEqual(JSON.parse(payload.results[3]?.output ?? "{}"), { SELESAI_SUBAGENT_INTERCOM_SESSION_NAME: expectedConsumerTarget });
2371
+ const consumerChild = payload.results[3];
2372
+ assert.ok(consumerChild?.outputPath, "expected a durable output path for the consumer child");
2373
+ assert.match(consumerChild.output ?? "", /Output saved to: /);
2374
+ assert.deepEqual(JSON.parse(fs.readFileSync(consumerChild.outputPath, "utf-8")), { SELESAI_SUBAGENT_INTERCOM_SESSION_NAME: expectedConsumerTarget });
2106
2375
  });
2107
2376
 
2108
2377
  it("async dynamic pre-spawn failures persist failed graph status and error", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
@@ -2189,6 +2458,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
2189
2458
  async: true,
2190
2459
  clarify: false,
2191
2460
  worktree: true,
2461
+ artifacts: true,
2192
2462
  },
2193
2463
  new AbortController().signal,
2194
2464
  undefined,
@@ -2360,6 +2630,43 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
2360
2630
  assert.equal(mockPi.callCount(), 2);
2361
2631
  });
2362
2632
 
2633
+ it("background runs retry a zero-activity startup exit on the same model", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
2634
+ mockPi.onCall({ exitCode: 1 });
2635
+ mockPi.onCall({ output: "Recovered asynchronously after startup race" });
2636
+ const id = `async-startup-retry-${Date.now().toString(36)}`;
2637
+ executeAsyncSingle(id, {
2638
+ agent: "worker",
2639
+ task: "Do work",
2640
+ agentConfig: makeAgent("worker", {
2641
+ model: "openai/gpt-5-mini:high",
2642
+ fallbackModels: ["anthropic/claude-sonnet-4:low"],
2643
+ }),
2644
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
2645
+ availableModels: [
2646
+ { provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" },
2647
+ { provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" },
2648
+ ],
2649
+ artifactConfig: {
2650
+ enabled: false,
2651
+ includeInput: false,
2652
+ includeOutput: false,
2653
+ includeJsonl: false,
2654
+ includeMetadata: false,
2655
+ cleanupDays: 7,
2656
+ },
2657
+ shareEnabled: false,
2658
+ maxSubagentDepth: 2,
2659
+ });
2660
+
2661
+ const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8")) as AsyncResultPayload;
2662
+ assert.equal(payload.success, true);
2663
+ assert.equal(payload.results[0]?.model, "openai/gpt-5-mini:high");
2664
+ assert.deepEqual(payload.results[0]?.attemptedModels, ["openai/gpt-5-mini:high"]);
2665
+ assert.deepEqual(payload.results[0]?.modelAttempts?.map((attempt) => attempt.success), [false, true]);
2666
+ assertAsyncChildOutput(payload, 0, "Recovered asynchronously after startup race");
2667
+ assert.equal(mockPi.callCount(), 2);
2668
+ });
2669
+
2363
2670
  it("background runs retry the fallback model when the provider stream ends without finish_reason", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
2364
2671
  mockPi.onCall({
2365
2672
  jsonl: [{
@@ -2403,7 +2710,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
2403
2710
  const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8"));
2404
2711
  assert.equal(payload.success, true);
2405
2712
  assert.deepEqual(payload.results[0].attemptedModels, ["openai/gpt-5-mini:high", "anthropic/claude-sonnet-4:low"]);
2406
- assert.match(payload.results[0].output ?? "", /Recovered after stream failure/);
2713
+ assertAsyncChildOutput(payload, 0, "Recovered after stream failure");
2407
2714
  assert.equal(mockPi.callCount(), 2);
2408
2715
  });
2409
2716
 
@@ -2544,7 +2851,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
2544
2851
  const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
2545
2852
  assert.equal(payload.success, true);
2546
2853
  assert.equal(payload.results[0]?.model, "anthropic/claude-sonnet-4");
2547
- assert.match(payload.results[0]?.output ?? "", /Recovered asynchronously from empty output/);
2854
+ assertAsyncChildOutput(payload, 0, "Recovered asynchronously from empty output");
2548
2855
  assert.match(payload.results[0]?.modelAttempts?.[0]?.error ?? "", /no output/i);
2549
2856
  assert.deepEqual(payload.results[0]?.modelAttempts?.map((attempt) => attempt.success), [false, true]);
2550
2857
  assert.equal(mockPi.callCount(), 2);
@@ -2684,7 +2991,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
2684
2991
  assert.equal(payload.exitCode, 0);
2685
2992
  assert.equal(payload.results[0]?.success, true);
2686
2993
  assert.equal(payload.results[0]?.error, undefined);
2687
- assert.equal(payload.results[0]?.output, "Recovered asynchronously");
2994
+ assertAsyncChildOutput(payload, 0, "Recovered asynchronously");
2688
2995
  const statusPayload = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatusPayload;
2689
2996
  assert.equal(statusPayload.state, "complete");
2690
2997
  assert.equal(statusPayload.steps?.[0]?.status, "complete");
@@ -3088,7 +3395,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3088
3395
  assert.equal(payload.success, true);
3089
3396
  assert.equal(payload.exitCode, 0);
3090
3397
  assert.equal(payload.results[0].success, true);
3091
- assert.equal(payload.results[0].output, "cold start test after patch");
3398
+ assertAsyncChildOutput(payload, 0, "cold start test after patch");
3092
3399
 
3093
3400
  const eventsPath = path.join(ASYNC_DIR, id, "events.jsonl");
3094
3401
  const eventsText = fs.readFileSync(eventsPath, "utf-8");
@@ -3143,6 +3450,71 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3143
3450
  assert.deepEqual(payload.results[0].attemptedModels, ["github-copilot/gpt-5-mini"]);
3144
3451
  });
3145
3452
 
3453
+ it("async executor keeps the last parent session model after continuation drops ctx.model", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
3454
+ mockPi.onCall({ output: "Done asynchronously" });
3455
+ const state = {
3456
+ baseCwd: tempDir,
3457
+ currentSessionId: null,
3458
+ asyncJobs: new Map(),
3459
+ foregroundControls: new Map(),
3460
+ lastForegroundControlId: null,
3461
+ };
3462
+ const executor = createSubagentExecutor!({
3463
+ pi: { events: createEventBus(), getSessionName: () => undefined },
3464
+ state,
3465
+ config: {},
3466
+ asyncByDefault: false,
3467
+ tempArtifactsDir: tempDir,
3468
+ getSubagentSessionRoot: () => path.join(tempDir, "sessions"),
3469
+ expandTilde: (p: string) => p,
3470
+ discoverAgents: () => ({ agents: [makeAgent("worker")] }),
3471
+ });
3472
+ const initialCtx = makeMinimalCtx(tempDir);
3473
+ initialCtx.sessionManager.getSessionId = () => "session-continued";
3474
+ initialCtx.model = { provider: "deepseek", id: "deepseek-v4-flash" };
3475
+ await executor.execute("prime-parent-model", { action: "list" }, new AbortController().signal, undefined, initialCtx);
3476
+
3477
+ const continuedCtx = makeMinimalCtx(tempDir);
3478
+ continuedCtx.sessionManager.getSessionId = () => "session-continued";
3479
+ const launch = await executor.execute(
3480
+ "continued-async-child",
3481
+ { agent: "worker", task: "Do work", async: true, acceptance: false },
3482
+ new AbortController().signal,
3483
+ undefined,
3484
+ continuedCtx,
3485
+ ) as AsyncExecutionResult;
3486
+ assert.equal(launch.isError, undefined);
3487
+ assert.ok(launch.details.asyncId);
3488
+
3489
+ const payload = await readAsyncPayload(launch.details.asyncId);
3490
+ assert.equal(payload.success, true);
3491
+ assert.equal(payload.results[0]?.model, "deepseek/deepseek-v4-flash");
3492
+ const args = readMockPiArgs(mockPi, 0);
3493
+ assert.equal(args[args.indexOf("--model") + 1], "deepseek/deepseek-v4-flash");
3494
+ });
3495
+
3496
+ it("foreground chains keep the last parent session model after continuation drops ctx.model", { skip: !createSubagentExecutor ? "executor not available" : undefined }, async () => {
3497
+ mockPi.onCall({ output: "Done foreground" });
3498
+ const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })]);
3499
+ const initialCtx = makeMinimalCtx(tempDir);
3500
+ initialCtx.sessionManager.getSessionId = () => "session-continued";
3501
+ initialCtx.model = { provider: "deepseek", id: "deepseek-v4-flash" };
3502
+ await executor.execute("prime-parent-model", { action: "list" }, new AbortController().signal, undefined, initialCtx);
3503
+
3504
+ const continuedCtx = makeMinimalCtx(tempDir);
3505
+ continuedCtx.sessionManager.getSessionId = () => "session-continued";
3506
+ const result = await executor.execute(
3507
+ "continued-foreground-chain",
3508
+ { chain: [{ agent: "worker", task: "Do work" }], acceptance: false },
3509
+ new AbortController().signal,
3510
+ undefined,
3511
+ continuedCtx,
3512
+ );
3513
+ assert.equal(result.isError, undefined);
3514
+ const args = readMockPiArgs(mockPi, 0);
3515
+ assert.equal(args[args.indexOf("--model") + 1], "deepseek/deepseek-v4-flash");
3516
+ });
3517
+
3146
3518
  it("background single runs inherit the parent session model when no model is set", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3147
3519
  mockPi.onCall({ output: "Done asynchronously" });
3148
3520
 
@@ -3729,7 +4101,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3729
4101
  const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
3730
4102
  assert.ok(elapsed < 6000, `unconfigured watchdog status should not delay async final drain, took ${elapsed}ms`);
3731
4103
  assert.equal(payload.success, true);
3732
- assert.equal(payload.results[0]?.output, "async-done-without-watchdog-config");
4104
+ assertAsyncChildOutput(payload, 0, "async-done-without-watchdog-config");
3733
4105
  assert.equal((payload.results[0] as { watchdog?: unknown }).watchdog, undefined);
3734
4106
  });
3735
4107
  });
@@ -3764,7 +4136,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3764
4136
  assert.ok(elapsed >= 1200, `watchdog settlement should delay async final drain, took ${elapsed}ms`);
3765
4137
  assert.ok(elapsed < 9000, `settled watchdog should still allow async cleanup, took ${elapsed}ms`);
3766
4138
  assert.equal(payload.success, true);
3767
- assert.equal(payload.results[0]?.output, "async-done-before-watchdog");
4139
+ assertAsyncChildOutput(payload, 0, "async-done-before-watchdog");
3768
4140
  assert.equal((payload.results[0] as { watchdog?: { phase?: string } }).watchdog?.phase, "idle");
3769
4141
  });
3770
4142
  });
@@ -3795,7 +4167,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3795
4167
  const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
3796
4168
  assert.ok(elapsed < 6000, `watchdog tail fallback should not hang async final drain, took ${elapsed}ms`);
3797
4169
  assert.equal(payload.success, true);
3798
- assert.equal(payload.results[0]?.output, "async-done-before-watchdog-timeout");
4170
+ assertAsyncChildOutput(payload, 0, "async-done-before-watchdog-timeout");
3799
4171
  const watchdog = (payload.results[0] as { watchdog?: { phase?: string; timedOut?: boolean } }).watchdog;
3800
4172
  assert.equal(watchdog?.phase, "stale");
3801
4173
  assert.equal(watchdog?.timedOut, true);
@@ -3846,7 +4218,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3846
4218
  assert.equal(payload.success, true);
3847
4219
  assert.equal(payload.exitCode, 0);
3848
4220
  assert.equal(payload.results[0].success, true);
3849
- assert.equal(payload.results[0].output, "async-done-before-drain");
4221
+ assertAsyncChildOutput(payload, 0, "async-done-before-drain");
3850
4222
  });
3851
4223
 
3852
4224
  it("background forced drain after empty terminal assistant output is cleanup success", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
@@ -3882,7 +4254,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3882
4254
  assert.equal(payload.success, true);
3883
4255
  assert.equal(payload.exitCode, 0);
3884
4256
  assert.equal(payload.results[0].success, true);
3885
- assert.equal(payload.results[0].output, "");
4257
+ assert.match(payload.results[0].output ?? "", /Output saved to: /);
3886
4258
  });
3887
4259
 
3888
4260
  it("background final-drain cleanup preserves explicit assistant errors", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
@@ -4255,7 +4627,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
4255
4627
  const resultPath = await waitForAsyncResultFile(id, 10_000);
4256
4628
  const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
4257
4629
  assert.equal(payload.success, true);
4258
- assert.equal(payload.results[0]?.output, "Done after noisy stream");
4630
+ assertAsyncChildOutput(payload, 0, "Done after noisy stream");
4259
4631
 
4260
4632
  const eventsText = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf-8");
4261
4633
  assert.doesNotMatch(eventsText, /"type":"message_update"/);
@@ -4334,7 +4706,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
4334
4706
 
4335
4707
  const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
4336
4708
  assert.equal(payload.success, true);
4337
- assert.equal(payload.results[0].output, "Done streaming");
4709
+ assertAsyncChildOutput(payload, 0, "Done streaming");
4338
4710
 
4339
4711
  const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8"));
4340
4712
  assert.deepEqual(status.steps[0].recentTools.map((tool: { tool: string; args: string }) => ({ tool: tool.tool, args: tool.args })), [{ tool: "bash", args: "ls" }]);