@selesai/code 0.9.11 → 0.9.13

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 (580) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/config.d.ts +4 -6
  3. package/dist/config.js +10 -14
  4. package/dist/core/agent-session.d.ts +0 -1
  5. package/dist/core/agent-session.js +3 -49
  6. package/dist/core/extensions/types.d.ts +0 -2
  7. package/dist/core/settings-manager.d.ts +0 -2
  8. package/dist/core/settings-manager.js +0 -3
  9. package/dist/core/tools/index.d.ts +0 -1
  10. package/dist/core/tools/index.js +0 -1
  11. package/dist/defaults/settings.json +2 -3
  12. package/dist/extensions/model-prompt-injector/index.test.ts +0 -10
  13. package/dist/extensions/model-prompt-injector/index.ts +0 -3
  14. package/dist/extensions/pi-intercom/CHANGELOG.md +53 -16
  15. package/dist/extensions/pi-intercom/README.md +41 -8
  16. package/dist/extensions/pi-intercom/banner.png +0 -0
  17. package/dist/extensions/pi-intercom/broker/broker.ts +498 -215
  18. package/dist/extensions/pi-intercom/broker/client.ts +39 -12
  19. package/dist/extensions/pi-intercom/broker/extension.test.ts +102 -3
  20. package/dist/extensions/pi-intercom/broker/paths.test.ts +2 -2
  21. package/dist/extensions/pi-intercom/broker/protocol.ts +26 -0
  22. package/dist/extensions/pi-intercom/broker/spawn.test.ts +5 -20
  23. package/dist/extensions/pi-intercom/config.test.ts +41 -0
  24. package/dist/extensions/pi-intercom/config.ts +21 -0
  25. package/dist/extensions/pi-intercom/extension-api.ts +40 -0
  26. package/dist/extensions/pi-intercom/index.ts +362 -26
  27. package/dist/extensions/pi-intercom/intercom.integration.test.ts +819 -13
  28. package/dist/extensions/pi-intercom/package.json +1 -1
  29. package/dist/extensions/pi-intercom/project-agent.ts +2 -2
  30. package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +8 -110
  31. package/dist/extensions/pi-intercom/types.ts +32 -5
  32. package/dist/extensions/pi-intercom/ui/inline-message.ts +6 -0
  33. package/dist/extensions/pi-subagents/.github/assets/subagent-fleet-inspector.png +0 -0
  34. package/dist/extensions/pi-subagents/.github/workflows/release.yml +28 -0
  35. package/dist/extensions/pi-subagents/.github/workflows/test.yml +51 -0
  36. package/dist/extensions/pi-subagents/.greptile/config.json +33 -0
  37. package/dist/extensions/pi-subagents/.greptile/files.json +8 -0
  38. package/dist/extensions/pi-subagents/.greptile/rules.md +16 -0
  39. package/dist/extensions/pi-subagents/CHANGELOG.md +309 -87
  40. package/dist/extensions/pi-subagents/README.md +15 -17
  41. package/dist/extensions/pi-subagents/agents/oracle.md +3 -1
  42. package/dist/extensions/pi-subagents/agents/researcher.md +32 -13
  43. package/dist/extensions/pi-subagents/agents/reviewer.md +13 -2
  44. package/dist/extensions/pi-subagents/agents/scout.md +2 -2
  45. package/dist/extensions/pi-subagents/agents/worker.md +2 -1
  46. package/dist/extensions/pi-subagents/async-retention-discovery-worker.mjs +180 -0
  47. package/dist/extensions/pi-subagents/docs/configuration.md +106 -27
  48. package/dist/extensions/pi-subagents/docs/extension-api.md +85 -21
  49. package/dist/extensions/pi-subagents/docs/missions.md +41 -4
  50. package/dist/extensions/pi-subagents/docs/models.md +52 -9
  51. package/dist/extensions/pi-subagents/docs/observability.md +103 -16
  52. package/dist/extensions/pi-subagents/docs/tool-reference.md +60 -45
  53. package/dist/extensions/pi-subagents/docs/watchdog.md +60 -3
  54. package/dist/extensions/pi-subagents/docs/workflows.md +181 -7
  55. package/dist/extensions/pi-subagents/index.ts +10 -1
  56. package/dist/extensions/pi-subagents/install.mjs +4 -4
  57. package/dist/extensions/pi-subagents/package-lock.json +1483 -1470
  58. package/dist/extensions/pi-subagents/package.json +6 -3
  59. package/dist/extensions/pi-subagents/prompts/council.md +60 -0
  60. package/dist/extensions/pi-subagents/prompts/parallel-review.md +5 -1
  61. package/dist/extensions/pi-subagents/prompts/review-loop.md +9 -5
  62. package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +254 -0
  63. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +18 -17
  64. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +31 -9
  65. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +107 -140
  66. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +9 -12
  67. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +39 -0
  68. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +44 -92
  69. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +261 -368
  70. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +6 -2
  71. package/dist/extensions/pi-subagents/src/agents/agents.ts +388 -85
  72. package/dist/extensions/pi-subagents/src/agents/builtin-names.ts +14 -0
  73. package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +418 -0
  74. package/dist/extensions/pi-subagents/src/agents/skills.ts +1 -1
  75. package/dist/extensions/pi-subagents/src/api/agents.ts +7 -0
  76. package/dist/extensions/pi-subagents/src/api/external-job-provider.ts +195 -0
  77. package/dist/extensions/pi-subagents/src/api/preflight.ts +88 -27
  78. package/dist/extensions/pi-subagents/src/api/project-panes.ts +2 -0
  79. package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -0
  80. package/dist/extensions/pi-subagents/src/extension/config.ts +20 -11
  81. package/dist/extensions/pi-subagents/src/extension/doctor.ts +14 -6
  82. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +5 -4
  83. package/dist/extensions/pi-subagents/src/extension/index.ts +270 -187
  84. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +65 -28
  85. package/dist/extensions/pi-subagents/src/extension/rpc.ts +120 -11
  86. package/dist/extensions/pi-subagents/src/extension/schemas.ts +46 -64
  87. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +51 -37
  88. package/dist/extensions/pi-subagents/src/extension/tool-result.ts +19 -0
  89. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +2 -2
  90. package/dist/extensions/pi-subagents/src/inspectors/herdr/client.ts +3 -3
  91. package/dist/extensions/pi-subagents/src/inspectors/herdr/focus.ts +55 -0
  92. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +2 -1
  93. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +230 -46
  94. package/dist/extensions/pi-subagents/src/integrations/herdr-status.ts +26 -4
  95. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +32 -10
  96. package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +6 -1
  97. package/dist/extensions/pi-subagents/src/missions/store.ts +4 -9
  98. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +2 -2
  99. package/dist/extensions/pi-subagents/src/policy/authority.ts +2 -0
  100. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +3 -1
  101. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +31 -8
  102. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +210 -52
  103. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +63 -33
  104. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +71 -17
  105. package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +886 -0
  106. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +14 -5
  107. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +49 -56
  108. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +1 -0
  109. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +3 -33
  110. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +5 -0
  111. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +5 -1
  112. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +22 -12
  113. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +105 -71
  114. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +68 -19
  115. package/dist/extensions/pi-subagents/src/runs/background/index-segment.ts +59 -0
  116. package/dist/extensions/pi-subagents/src/runs/background/inspect-rpc.ts +443 -0
  117. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +94 -10
  118. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +158 -90
  119. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +141 -53
  120. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +8 -5
  121. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +14 -4
  122. package/dist/extensions/pi-subagents/src/runs/background/run-id-query.ts +7 -0
  123. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +13 -14
  124. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +46 -25
  125. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +823 -0
  126. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +9 -3
  127. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +11 -1
  128. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +594 -216
  129. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +17 -13
  130. package/dist/extensions/pi-subagents/src/runs/background/terminal-run-index.ts +138 -0
  131. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +23 -4
  132. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +82 -4
  133. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +5 -3
  134. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +21 -14
  135. package/dist/extensions/pi-subagents/src/runs/foreground/async-stop-action.ts +23 -2
  136. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +153 -58
  137. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -9
  138. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1283 -1657
  139. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +274 -0
  140. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  141. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +59 -10
  142. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +1 -1
  143. package/dist/extensions/pi-subagents/src/runs/shared/chain-outputs.ts +1 -3
  144. package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +36 -0
  145. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +52 -2
  146. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
  147. package/dist/extensions/pi-subagents/src/runs/shared/extension-bindings.ts +78 -0
  148. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +2 -0
  149. package/dist/extensions/pi-subagents/src/runs/shared/external-job-bridge.ts +460 -0
  150. package/dist/extensions/pi-subagents/src/runs/shared/external-job-runner.ts +389 -0
  151. package/dist/extensions/pi-subagents/src/runs/shared/fast-mode-extension.ts +10 -0
  152. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  153. package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +243 -0
  154. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +201 -85
  155. package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +106 -39
  156. package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +145 -0
  157. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +150 -27
  158. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +46 -4
  159. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +19 -15
  160. package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +5 -1
  161. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +86 -2
  162. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +9 -9
  163. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +0 -6
  164. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +18 -4
  165. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +51 -7
  166. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +62 -21
  167. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +1 -3
  168. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +1 -3
  169. package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +1 -23
  170. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +14 -2
  171. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +2 -2
  172. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +22 -2
  173. package/dist/extensions/pi-subagents/src/shared/capacity-resilient-json.ts +102 -0
  174. package/dist/extensions/pi-subagents/src/shared/completion-owner.ts +14 -0
  175. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +49 -1
  176. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +59 -1
  177. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +12 -5
  178. package/dist/extensions/pi-subagents/src/shared/model-info.ts +20 -0
  179. package/dist/extensions/pi-subagents/src/shared/prompt-resources.ts +0 -40
  180. package/dist/extensions/pi-subagents/src/shared/settings.ts +11 -30
  181. package/dist/extensions/pi-subagents/src/shared/thinking-ceiling.ts +52 -0
  182. package/dist/extensions/pi-subagents/src/shared/types.ts +228 -49
  183. package/dist/extensions/pi-subagents/src/shared/utils.ts +11 -3
  184. package/dist/extensions/pi-subagents/src/shared/watch-strategy.ts +10 -0
  185. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +0 -162
  186. package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +2 -5
  187. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +105 -13
  188. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +6 -36
  189. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +829 -0
  190. package/dist/extensions/pi-subagents/src/tui/fleet.ts +1415 -0
  191. package/dist/extensions/pi-subagents/src/tui/render.ts +78 -7
  192. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +1 -1
  193. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +1 -0
  194. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +59 -51
  195. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +8 -2
  196. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +2 -1
  197. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +4 -4
  198. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +471 -86
  199. package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +140 -0
  200. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +2 -2
  201. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/cli.d.ts +3 -0
  202. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/register-bedrock.d.ts +2 -0
  203. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/restore-sandbox-env.d.ts +17 -0
  204. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/args.d.ts +57 -0
  205. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/config-selector.d.ts +16 -0
  206. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/file-processor.d.ts +15 -0
  207. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/initial-message.d.ts +18 -0
  208. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/list-models.d.ts +9 -0
  209. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/project-trust.d.ts +10 -0
  210. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/session-picker.d.ts +10 -0
  211. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/startup-ui.d.ts +20 -0
  212. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli.d.ts +3 -0
  213. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/config.d.ts +96 -0
  214. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session-runtime.d.ts +119 -0
  215. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session-services.d.ts +85 -0
  216. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session.d.ts +620 -0
  217. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/auth-guidance.d.ts +5 -0
  218. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/auth-storage.d.ts +56 -0
  219. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/bash-executor.d.ts +32 -0
  220. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/cache-stats.d.ts +49 -0
  221. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/branch-summarization.d.ts +93 -0
  222. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/compaction.d.ts +128 -0
  223. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/index.d.ts +7 -0
  224. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/utils.d.ts +38 -0
  225. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/defaults.d.ts +3 -0
  226. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/diagnostics.d.ts +15 -0
  227. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/event-bus.d.ts +9 -0
  228. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/exec.d.ts +29 -0
  229. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/experimental.d.ts +2 -0
  230. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/ansi-to-html.d.ts +22 -0
  231. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/index.d.ts +37 -0
  232. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/tool-renderer.d.ts +34 -0
  233. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/index.d.ts +12 -0
  234. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/loader.d.ts +23 -0
  235. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/runner.d.ts +171 -0
  236. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +1261 -0
  237. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/wrapper.d.ts +20 -0
  238. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/footer-data-provider.d.ts +54 -0
  239. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/http-dispatcher.d.ts +22 -0
  240. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/index.d.ts +13 -0
  241. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/keybindings.d.ts +358 -0
  242. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/messages.d.ts +77 -0
  243. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-config.d.ts +512 -0
  244. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-registry.d.ts +43 -0
  245. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-resolver.d.ts +121 -0
  246. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-runtime.d.ts +82 -0
  247. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/models-store.d.ts +17 -0
  248. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/output-guard.d.ts +7 -0
  249. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/package-manager.d.ts +210 -0
  250. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/project-trust.d.ts +15 -0
  251. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/prompt-templates.d.ts +54 -0
  252. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/provider-attribution.d.ts +4 -0
  253. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/provider-composer.d.ts +55 -0
  254. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/radius.d.ts +2 -0
  255. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/remote-catalog-provider.d.ts +5 -0
  256. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/resolve-config-value.d.ts +30 -0
  257. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/resource-loader.d.ts +206 -0
  258. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/runtime-credentials.d.ts +15 -0
  259. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/sdk.d.ts +106 -0
  260. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/session-cwd.d.ts +19 -0
  261. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/session-manager.d.ts +356 -0
  262. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/settings-manager.d.ts +297 -0
  263. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/skills.d.ts +60 -0
  264. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/slash-commands.d.ts +15 -0
  265. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/source-info.d.ts +18 -0
  266. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/system-prompt.d.ts +28 -0
  267. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/telemetry.d.ts +3 -0
  268. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/timings.d.ts +10 -0
  269. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/bash.d.ts +68 -0
  270. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/edit-diff.d.ts +106 -0
  271. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/edit.d.ts +51 -0
  272. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/file-mutation-queue.d.ts +6 -0
  273. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/find.d.ts +35 -0
  274. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/grep.d.ts +37 -0
  275. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/index.d.ts +40 -0
  276. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/ls.d.ts +37 -0
  277. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/output-accumulator.d.ts +52 -0
  278. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/path-utils.d.ts +10 -0
  279. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/read.d.ts +35 -0
  280. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/render-utils.d.ts +24 -0
  281. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/tool-definition-wrapper.d.ts +14 -0
  282. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/truncate.d.ts +70 -0
  283. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/write.d.ts +26 -0
  284. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/trust-manager.d.ts +36 -0
  285. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/usage-totals.d.ts +19 -0
  286. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/index.d.ts +3 -0
  287. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/client.d.ts +61 -0
  288. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/huggingface.d.ts +23 -0
  289. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/index.d.ts +3 -0
  290. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/provider.d.ts +10 -0
  291. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/ui.d.ts +42 -0
  292. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/index.d.ts +35 -0
  293. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/main.d.ts +12 -0
  294. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/migrations.d.ts +33 -0
  295. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/index.d.ts +9 -0
  296. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/armin.d.ts +34 -0
  297. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/assistant-message.d.ts +22 -0
  298. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/bash-execution.d.ts +34 -0
  299. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/bordered-loader.d.ts +16 -0
  300. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/branch-summary-message.d.ts +16 -0
  301. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/compaction-summary-message.d.ts +16 -0
  302. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/config-selector.d.ts +102 -0
  303. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/countdown-timer.d.ts +14 -0
  304. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-editor.d.ts +21 -0
  305. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-entry.d.ts +19 -0
  306. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-message.d.ts +20 -0
  307. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/daxnuts.d.ts +23 -0
  308. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/diff.d.ts +12 -0
  309. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/dynamic-border.d.ts +15 -0
  310. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/earendil-announcement.d.ts +5 -0
  311. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-editor.d.ts +22 -0
  312. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-input.d.ts +23 -0
  313. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-selector.d.ts +26 -0
  314. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/first-time-setup.d.ts +25 -0
  315. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/footer.d.ts +32 -0
  316. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/index.d.ts +34 -0
  317. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/keybinding-hints.d.ts +13 -0
  318. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/login-dialog.d.ts +52 -0
  319. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/model-selector.d.ts +54 -0
  320. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/oauth-selector.d.ts +33 -0
  321. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/scoped-models-selector.d.ts +42 -0
  322. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/session-selector-search.d.ts +23 -0
  323. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/session-selector.d.ts +95 -0
  324. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/settings-selector.d.ts +77 -0
  325. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/show-images-selector.d.ts +10 -0
  326. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/skill-invocation-message.d.ts +17 -0
  327. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/status-indicator.d.ts +28 -0
  328. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/theme-selector.d.ts +11 -0
  329. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/thinking-selector.d.ts +11 -0
  330. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/tool-execution.d.ts +63 -0
  331. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/tree-selector.d.ts +94 -0
  332. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/trust-selector.d.ts +23 -0
  333. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/user-message-selector.d.ts +30 -0
  334. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/user-message.d.ts +14 -0
  335. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/visual-truncate.d.ts +24 -0
  336. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/interactive-mode.d.ts +394 -0
  337. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/model-search.d.ts +12 -0
  338. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/theme/theme-controller.d.ts +29 -0
  339. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/theme/theme.d.ts +120 -0
  340. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/print-mode.d.ts +28 -0
  341. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/jsonl.d.ts +17 -0
  342. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-client.d.ts +246 -0
  343. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-mode.d.ts +20 -0
  344. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-types.d.ts +457 -0
  345. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/package-manager-cli.d.ts +8 -0
  346. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/rpc-entry.d.ts +3 -0
  347. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/ansi.d.ts +2 -0
  348. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/changelog.d.ts +22 -0
  349. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/child-process.d.ts +18 -0
  350. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard-image.d.ts +11 -0
  351. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard-native.d.ts +11 -0
  352. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard.d.ts +4 -0
  353. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/deprecation.d.ts +4 -0
  354. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/exif-orientation.d.ts +5 -0
  355. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/frontmatter.d.ts +8 -0
  356. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/fs-watch.d.ts +5 -0
  357. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/git.d.ts +26 -0
  358. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/html.d.ts +7 -0
  359. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-convert.d.ts +10 -0
  360. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-process.d.ts +18 -0
  361. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize-core.d.ts +30 -0
  362. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize-worker.d.ts +2 -0
  363. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize.d.ts +16 -0
  364. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/json.d.ts +3 -0
  365. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/mime.d.ts +3 -0
  366. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/open-browser.d.ts +9 -0
  367. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/paths.d.ts +31 -0
  368. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/photon.d.ts +21 -0
  369. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/pi-user-agent.d.ts +2 -0
  370. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/shell.d.ts +31 -0
  371. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/sleep.d.ts +5 -0
  372. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/syntax-highlight.d.ts +12 -0
  373. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/tools-manager.d.ts +3 -0
  374. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/version-check.d.ts +15 -0
  375. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/windows-self-update.d.ts +3 -0
  376. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/index.mjs +1 -1
  377. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +0 -54
  378. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1053 -414
  379. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +194 -4
  380. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +59 -4
  381. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +0 -77
  382. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +107 -6
  383. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +211 -897
  384. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +265 -375
  385. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +77 -7
  386. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +1 -1
  387. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +10 -10
  388. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +547 -30
  389. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +2112 -310
  390. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +274 -34
  391. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +1 -1
  392. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +20 -3
  393. package/dist/extensions/pi-subagents/test/support/isolated-temp-root.mjs +9 -0
  394. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +30 -5
  395. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +2 -0
  396. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +4 -2
  397. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +1 -0
  398. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +197 -0
  399. package/dist/extensions/pi-subagents/test/unit/agent-contract.test.ts +14 -0
  400. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
  401. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +204 -27
  402. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +444 -145
  403. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +256 -4
  404. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +9 -7
  405. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +152 -22
  406. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +29 -0
  407. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +205 -23
  408. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +579 -0
  409. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +24 -0
  410. package/dist/extensions/pi-subagents/test/unit/authority-policy.test.ts +1 -0
  411. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +3 -3
  412. package/dist/extensions/pi-subagents/test/unit/capacity-resilient-json.test.ts +155 -0
  413. package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +2 -2
  414. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +7 -0
  415. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +123 -0
  416. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +81 -3
  417. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +109 -7
  418. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +2 -2
  419. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +5 -2
  420. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -2
  421. package/dist/extensions/pi-subagents/test/unit/extension-bindings.test.ts +79 -0
  422. package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +22 -0
  423. package/dist/extensions/pi-subagents/test/unit/external-job-runner.test.ts +961 -0
  424. package/dist/extensions/pi-subagents/test/unit/file-system-retry.test.ts +64 -0
  425. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +1110 -0
  426. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +1664 -0
  427. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +81 -1
  428. package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +138 -11
  429. package/dist/extensions/pi-subagents/test/unit/herdr-status-bridge.test.ts +41 -2
  430. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +210 -79
  431. package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +95 -0
  432. package/dist/extensions/pi-subagents/test/unit/inspect-rpc.test.ts +596 -0
  433. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +10 -1
  434. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +3 -5
  435. package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +167 -0
  436. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +319 -18
  437. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +78 -0
  438. package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +19 -5
  439. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +44 -0
  440. package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +129 -0
  441. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +231 -2
  442. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +28 -1
  443. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +106 -2
  444. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +204 -7
  445. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +59 -4
  446. package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +2 -6
  447. package/dist/extensions/pi-subagents/test/unit/pi-args-permission-system.test.ts +34 -2
  448. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +116 -6
  449. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +12 -3
  450. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +56 -61
  451. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +416 -1
  452. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +9 -2
  453. package/dist/extensions/pi-subagents/test/unit/project-panes-public-api.test.ts +42 -0
  454. package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +21 -30
  455. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +86 -11
  456. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +49 -1
  457. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +233 -5
  458. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +28 -5
  459. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +323 -20
  460. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +66 -10
  461. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +68 -5
  462. package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +174 -0
  463. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +679 -0
  464. package/dist/extensions/pi-subagents/test/unit/scheduled-store-root.test.ts +67 -0
  465. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +55 -44
  466. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +399 -84
  467. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +13 -0
  468. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
  469. package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +15 -10
  470. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +11 -7
  471. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -0
  472. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +40 -1
  473. package/dist/extensions/pi-subagents/test/unit/subagent-action-recovery.test.ts +10 -2
  474. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +38 -3
  475. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +110 -12
  476. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +204 -19
  477. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +25 -0
  478. package/dist/extensions/pi-subagents/test/unit/thinking-ceiling.test.ts +73 -0
  479. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +90 -50
  480. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +1 -2
  481. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +234 -7
  482. package/dist/extensions/pi-subagents/test/unit/watch-strategy.test.ts +24 -0
  483. package/dist/extensions/pi-subagents/test/unit/watchdog-permission-arbiter.test.ts +36 -0
  484. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +37 -9
  485. package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +302 -0
  486. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +167 -7
  487. package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +113 -0
  488. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +6 -6
  489. package/dist/extensions/ponytail/index.js +31 -25
  490. package/dist/extensions/ponytail/ponytail-config.cjs +34 -7
  491. package/dist/extensions/ponytail/ponytail-instructions.cjs +7 -9
  492. package/dist/extensions/ponytail/test/extension.test.js +7 -8
  493. package/dist/extensions/ponytail/test/helpers.test.js +1 -1
  494. package/dist/skills/brandkit/SKILL.md +799 -0
  495. package/dist/skills/design-taste-frontend/SKILL.md +1207 -0
  496. package/dist/skills/design-taste-frontend-v1/SKILL.md +227 -0
  497. package/dist/skills/full-output-enforcement/SKILL.md +50 -0
  498. package/dist/skills/gpt-taste/SKILL.md +75 -0
  499. package/dist/skills/high-end-visual-design/SKILL.md +99 -0
  500. package/dist/skills/image-to-code/SKILL.md +1229 -0
  501. package/dist/skills/imagegen-frontend-mobile/SKILL.md +1466 -0
  502. package/dist/skills/imagegen-frontend-web/SKILL.md +988 -0
  503. package/dist/skills/industrial-brutalist-ui/SKILL.md +93 -0
  504. package/dist/skills/minimalist-ui/SKILL.md +86 -0
  505. package/dist/skills/pi-subagents/SKILL.md +18 -17
  506. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +31 -9
  507. package/dist/skills/pi-subagents/references/execution-controls.md +95 -143
  508. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +9 -12
  509. package/dist/skills/pi-subagents/references/multi-lane-orchestration.md +39 -0
  510. package/dist/skills/pi-subagents/references/prompting-and-roles.md +45 -94
  511. package/dist/skills/ponytail/SKILL.md +12 -8
  512. package/dist/skills/ponytail-audit/SKILL.md +0 -1
  513. package/dist/skills/ponytail-debt/SKILL.md +0 -1
  514. package/dist/skills/ponytail-gain/SKILL.md +0 -1
  515. package/dist/skills/ponytail-help/SKILL.md +4 -3
  516. package/dist/skills/ponytail-review/SKILL.md +0 -1
  517. package/dist/skills/redesign-existing-projects/SKILL.md +179 -0
  518. package/dist/skills/stitch-design-taste/SKILL.md +185 -0
  519. package/dist/skills/unlazy/CHANGELOG.md +100 -0
  520. package/dist/skills/unlazy/CONTRIBUTING.md +66 -0
  521. package/dist/skills/unlazy/LICENSE +21 -0
  522. package/dist/skills/unlazy/README.md +242 -0
  523. package/dist/skills/unlazy/SECURITY.md +70 -0
  524. package/dist/skills/unlazy/SKILL.md +100 -0
  525. package/dist/skills/unlazy/package.json +13 -0
  526. package/dist/skills/unlazy/references/dispatch.md +82 -0
  527. package/dist/skills/unlazy/references/gates.md +132 -0
  528. package/dist/skills/unlazy/references/method.md +49 -0
  529. package/dist/skills/unlazy/references/orchestration.md +99 -0
  530. package/dist/skills/unlazy/references/parallel.md +123 -0
  531. package/dist/skills/unlazy/references/token-economy.md +39 -0
  532. package/dist/skills/unlazy/research/validation-protocol.md +81 -0
  533. package/dist/skills/unlazy/scripts/dispatch-check.mjs +108 -0
  534. package/dist/skills/unlazy/scripts/gate-check.mjs +894 -0
  535. package/dist/skills/unlazy/scripts/gate-lint.mjs +178 -0
  536. package/dist/skills/unlazy/scripts/install-hooks.mjs +206 -0
  537. package/dist/skills/unlazy/scripts/lib/check-supervisor.mjs +46 -0
  538. package/dist/skills/unlazy/scripts/lib/dispatch.mjs +283 -0
  539. package/dist/skills/unlazy/scripts/lib/gates.mjs +629 -0
  540. package/dist/skills/unlazy/scripts/lib/process-tree.mjs +161 -0
  541. package/dist/skills/unlazy/scripts/lib/regex-worker.mjs +9 -0
  542. package/dist/skills/unlazy/scripts/stop-hook.mjs +174 -0
  543. package/dist/skills/unlazy/templates/PLAN.md +65 -0
  544. package/dist/skills/unlazy/templates/gates-leaf.md +51 -0
  545. package/dist/skills/unlazy/templates/gates-node.md +50 -0
  546. package/dist/skills/unlazy/tests/contract-tests.mjs +127 -0
  547. package/dist/skills/unlazy/tests/dispatch-tests.mjs +481 -0
  548. package/dist/skills/unlazy/tests/hardening-tests.mjs +903 -0
  549. package/dist/skills/unlazy/tests/lint-tests.mjs +242 -0
  550. package/dist/skills/unlazy/tests/run-tests.mjs +554 -0
  551. package/dist/skills/unlazy/tests/self-check.mjs +165 -0
  552. package/dist/skills/unlazy/tests/stress-tests.mjs +356 -0
  553. package/dist/skills/web-design-guidelines/SKILL.md +40 -0
  554. package/docs/settings.md +0 -5
  555. package/package.json +1 -1
  556. package/dist/core/tools/schema-prune.d.ts +0 -16
  557. package/dist/core/tools/schema-prune.js +0 -39
  558. package/dist/core/tools/schema-prune.test.d.ts +0 -1
  559. package/dist/core/tools/schema-prune.test.js +0 -111
  560. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +0 -172
  561. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +0 -55
  562. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +0 -61
  563. package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +0 -132
  564. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +0 -181
  565. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +0 -1354
  566. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +0 -1581
  567. package/dist/extensions/pi-subagents/src/shared/env.ts +0 -16
  568. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +0 -71
  569. package/dist/extensions/pi-subagents/src/slash/inline-subagents.ts +0 -136
  570. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +0 -41
  571. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +0 -466
  572. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +0 -1859
  573. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +0 -733
  574. package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +0 -63
  575. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +0 -326
  576. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +0 -103
  577. package/dist/extensions/pi-subagents/test/unit/inline-subagents.test.ts +0 -127
  578. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +0 -73
  579. package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +0 -213
  580. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +0 -65
@@ -27,6 +27,7 @@ import {
27
27
  tryImport,
28
28
  } from "../support/helpers.ts";
29
29
  import registerSubagentExtension from "../../src/extension/index.ts";
30
+ import { handleSubagentControlNotice } from "../../src/extension/control-notices.ts";
30
31
  import { discoverAgents } from "../../src/agents/agents.ts";
31
32
  import {
32
33
  SUBAGENT_DELEGATION_REQUEST_EVENT,
@@ -36,10 +37,9 @@ import {
36
37
  type SubagentDelegationResponse,
37
38
  type SubagentDelegationStarted,
38
39
  } from "../../src/api/delegation.ts";
39
- import { CHAIN_RUNS_DIR, DIRS, INTERCOM_DETACH_REQUEST_EVENT, INTERCOM_DETACH_RESPONSE_EVENT, type AsyncStatus, type SubagentState } from "../../src/shared/types.ts";
40
+ import { CHAIN_RUNS_DIR, DIRS, INTERCOM_DETACH_REQUEST_EVENT, INTERCOM_DETACH_RESPONSE_EVENT, SUBAGENT_CONTROL_EVENT, TEMP_ARTIFACTS_DIR, type AsyncStatus, type ControlEvent, type SubagentState } from "../../src/shared/types.ts";
40
41
  import { ACTIVE_RUN_INDEX_DIR } from "../../src/runs/background/active-run-index.ts";
41
- import { resolveMissionStoreLocation } from "../../src/missions/store.ts";
42
- import { missionStatePath } from "../../src/missions/workflow-state.ts";
42
+ import { listAsyncRuns } from "../../src/runs/background/async-status.ts";
43
43
  import { CHILD_WATCHDOG_STATUS_EVENT } from "../../src/watchdog/child-status.ts";
44
44
  import { WAIT_TOOL_ENABLED_ENV } from "../../src/runs/background/wait-config.ts";
45
45
  import { TOOL_BUDGET_ENV, TOOL_BUDGET_ZERO_AUTH_ENV } from "../../src/runs/shared/tool-budget.ts";
@@ -47,6 +47,7 @@ import { createRunFanoutBudget, encodeRunFanoutBudgetDescriptor, RUN_FANOUT_BUDG
47
47
  import { MainWatchdogRuntime } from "../../src/watchdog/runtime.ts";
48
48
  import { MAX_CHILD_PENDING_LINE_BYTES, MAX_CHILD_STDERR_BYTES } from "../../src/runs/shared/child-protocol.ts";
49
49
  import {
50
+ SUBAGENT_CHILD_ENV,
50
51
  SUBAGENT_FANOUT_CHILD_ENV,
51
52
  SUBAGENT_STEER_ACK_DIR_ENV,
52
53
  SUBAGENT_STEER_CAPABILITY_ENV,
@@ -58,6 +59,11 @@ import {
58
59
  SUBAGENT_PARENT_RUN_ID_ENV,
59
60
  } from "../../src/runs/shared/pi-args.ts";
60
61
  import { createNestedRoute, nestedRouteEnv, parseNestedEventRecords } from "../../src/runs/shared/nested-events.ts";
62
+ import { resolveMissionStoreLocation } from "../../src/missions/store.ts";
63
+ import { missionStatePath } from "../../src/missions/workflow-state.ts";
64
+ import { discardPreservedWorktrees } from "../../src/runs/shared/parallel-handoff.ts";
65
+ import { resolveAsyncResumeTarget } from "../../src/runs/background/async-resume.ts";
66
+ import { clearExclusions } from "../../src/runs/shared/model-exclusions.ts";
61
67
 
62
68
  interface ModelAttempt {
63
69
  success?: boolean;
@@ -117,6 +123,7 @@ interface RunSyncResult {
117
123
  skillsWarning?: string;
118
124
  attemptedModels?: string[];
119
125
  modelAttempts?: ModelAttempt[];
126
+ contextOverflow?: boolean;
120
127
  usage: { turns: number; input: number; output: number };
121
128
  progress: ProgressSummary;
122
129
  controlEvents?: Array<{ type?: string; message: string; reason?: string; turns?: number; tokens?: number; currentPath?: string; recentFailureSummary?: string }>;
@@ -276,6 +283,10 @@ function escapeRegExp(value: string): string {
276
283
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
277
284
  }
278
285
 
286
+ function pathContainsSegments(filePath: string, ...segments: string[]): boolean {
287
+ return segments.every((segment) => filePath.split(path.sep).includes(segment));
288
+ }
289
+
279
290
  function writePackageSkill(packageRoot: string, skillName: string): void {
280
291
  const skillDir = path.join(packageRoot, "skills", skillName);
281
292
  fs.mkdirSync(skillDir, { recursive: true });
@@ -293,7 +304,9 @@ function writePackageSkill(packageRoot: string, skillName: string): void {
293
304
 
294
305
  describe("single sync execution", { skip: !available ? "pi packages not available" : undefined }, () => {
295
306
  let tempDir: string;
307
+ let agentDir: string;
296
308
  let mockPi: MockPi;
309
+ let previousAgentDir: string | undefined;
297
310
 
298
311
  before(() => {
299
312
  mockPi = createMockPi();
@@ -306,10 +319,18 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
306
319
 
307
320
  beforeEach(() => {
308
321
  tempDir = createTempDir();
322
+ agentDir = createTempDir("pi-subagent-agent-");
323
+ previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
324
+ process.env.SELESAI_CODING_AGENT_DIR = agentDir;
309
325
  mockPi.reset();
326
+ clearExclusions();
310
327
  });
311
328
 
312
329
  afterEach(() => {
330
+ clearExclusions();
331
+ if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
332
+ else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
333
+ removeTempDir(agentDir);
313
334
  removeTempDir(tempDir);
314
335
  });
315
336
 
@@ -345,6 +366,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
345
366
  workflowControllers?: Map<string, AbortController>,
346
367
  handleScheduledRunAction?: Parameters<typeof createSubagentExecutor>[0]["handleScheduledRunAction"],
347
368
  piEvents = createEventBus(),
369
+ discoverAgentsForCwd?: (cwd: string) => typeof agents,
348
370
  ) {
349
371
  return createSubagentExecutor!({
350
372
  pi: { events: piEvents, getSessionName: () => undefined },
@@ -362,7 +384,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
362
384
  tempArtifactsDir: tempDir,
363
385
  getSubagentSessionRoot: () => path.join(tempDir, ".pi-subagents", "sessions"),
364
386
  expandTilde: (value: string) => value,
365
- discoverAgents: () => ({ agents }),
387
+ discoverAgents: (cwd: string) => ({ agents: discoverAgentsForCwd ? discoverAgentsForCwd(cwd) : agents }),
366
388
  allowMutatingManagementActions,
367
389
  ...(handleScheduledRunAction ? { handleScheduledRunAction } : {}),
368
390
  });
@@ -384,20 +406,188 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
384
406
  assert.equal(output, "Hello from mock agent");
385
407
  });
386
408
 
387
- it("rejects action='single' with execution fields", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
409
+ it("runs public structured single-child requests directly", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
410
+ mockPi.onCall({ output: "Structured child completed" });
388
411
  const executor = makeExecutor([makeAgent("echo")]);
389
412
 
390
413
  const result = await executor.executePublic(
391
- "single-alias",
392
- { action: "single", agent: "echo", task: "Run through alias" },
414
+ "structured-single",
415
+ { agent: "echo", task: "Run through workflow", async: false, context: "fresh" },
416
+ new AbortController().signal,
417
+ undefined,
418
+ makeMinimalCtx(tempDir),
419
+ );
420
+
421
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "");
422
+ assert.equal(result.details.mode, "single");
423
+ assert.equal(mockPi.callCount(), 1);
424
+ assert.doesNotMatch(result.content[0]?.text ?? "", /Console:/);
425
+ });
426
+
427
+ it("keeps public structured children alive when tool results backfill without execution_end", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
428
+ mockPi.onCall({
429
+ steps: [
430
+ { jsonl: [{ type: "tool_execution_start", toolCallId: "bash-1", toolName: "bash", args: { command: "echo PROBE_OK" } }] },
431
+ { delay: 25, jsonl: [
432
+ { type: "tool_result_end", message: { role: "toolResult", toolCallId: "bash-1", toolName: "bash", isError: false, content: [{ type: "text", text: "PROBE_OK" }] } },
433
+ events.assistantMessage("PROBE_OK"),
434
+ ] },
435
+ ],
436
+ keepAliveAfterFinalMessageMs: 400,
437
+ });
438
+ const executor = makeExecutor([makeAgent("bash-worker")]);
439
+
440
+ const result = await executor.executePublic(
441
+ "structured-single-tool-backfill",
442
+ { agent: "bash-worker", task: "Run exactly one tool: bash with command echo PROBE_OK.", async: false, toolTimeoutMs: 100, timeoutMs: 5_000 },
443
+ new AbortController().signal,
444
+ undefined,
445
+ makeMinimalCtx(tempDir),
446
+ );
447
+
448
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
449
+ assert.match(result.content[0]?.text ?? "", /PROBE_OK/);
450
+ assert.equal(result.details.results[0]?.timedOut, undefined);
451
+ });
452
+
453
+ it("keeps public structured single-child calls foreground when async is disabled by default", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
454
+ mockPi.onCall({ output: "Structured child used the foreground default" });
455
+ const executor = makeExecutor([makeAgent("echo")], {}, false);
456
+
457
+ const result = await executor.executePublic(
458
+ "structured-single-foreground-default",
459
+ { agent: "echo", task: "Run through workflow" },
460
+ new AbortController().signal,
461
+ undefined,
462
+ makeMinimalCtx(tempDir),
463
+ );
464
+
465
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "");
466
+ assert.match(result.content[0]?.text ?? "", /Structured child used the foreground default/);
467
+ assert.equal(result.details.asyncId, undefined);
468
+ });
469
+
470
+ it("does not override structured single output unless configured by the agent", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
471
+ for (const params of [
472
+ { agent: "echo", task: "Use the task output path", async: false },
473
+ { agent: "echo", task: "Disable file output", output: false, async: false },
474
+ ] as const) {
475
+ mockPi.onCall({ output: "Structured child completed" });
476
+ const result = await makeExecutor([makeAgent("echo")]).executePublic(
477
+ "structured-single-output",
478
+ params,
479
+ new AbortController().signal,
480
+ undefined,
481
+ makeMinimalCtx(tempDir),
482
+ );
483
+
484
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
485
+ assert.doesNotMatch(readCallArgs().join("\n"), /This path is authoritative for this run/);
486
+ }
487
+
488
+ mockPi.onCall({ output: "Agent report" });
489
+ const configuredPath = path.join(tempDir, "agent-report.md");
490
+ const configured = await makeExecutor([makeAgent("echo", { output: configuredPath })]).executePublic(
491
+ "structured-single-agent-output",
492
+ { agent: "echo", task: "Use agent output", async: false },
493
+ new AbortController().signal,
494
+ undefined,
495
+ makeMinimalCtx(tempDir),
496
+ );
497
+
498
+ assert.equal(configured.isError, undefined, configured.content[0]?.text ?? "workflow failed");
499
+ const configuredTask = readCallArgs().join("\n");
500
+ assert.match(configuredTask, new RegExp(escapeRegExp(configuredPath)));
501
+ assert.match(configuredTask, /This path is authoritative for this run/);
502
+ assert.equal(fs.readFileSync(configuredPath, "utf-8"), "Agent report");
503
+ });
504
+
505
+ it("does not inject a workflow child output without an aggregate or explicit output", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
506
+ mockPi.onCall({ output: "Workflow child completed" });
507
+ const result = await makeExecutor([makeAgent("echo")]).execute(
508
+ "workflow-omitted-output",
509
+ { async: false, workflowScript: `return runs.run("main", { agent: "echo", task: "Use the task output path" });` },
510
+ new AbortController().signal,
511
+ undefined,
512
+ makeMinimalCtx(tempDir),
513
+ );
514
+
515
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
516
+ assert.doesNotMatch(readCallArgs().join("\n"), /This path is authoritative for this run/);
517
+ });
518
+
519
+ it("resolves workflow child profile context from its agent default", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
520
+ mockPi.onCall({ output: "Workflow child completed" });
521
+ const result = await makeExecutor([makeAgent("echo", { defaultContext: "fresh" })], { defaultSubagentContext: "fork" }).execute(
522
+ "workflow-profile-context",
523
+ { async: false, workflowScript: `return runs.run("main", { agent: "echo", task: "Use profile context", context: "profile" });` },
524
+ new AbortController().signal,
525
+ undefined,
526
+ makeMinimalCtx(tempDir),
527
+ );
528
+
529
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
530
+ assert.equal(result.details?.results?.[0]?.context, "fresh");
531
+ });
532
+
533
+ it("reports a user-requested foreground detach without supervisor guidance", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
534
+ mockPi.onCall({ steps: [{ delay: 500, jsonl: [events.assistantMessage("completed after user detach")] }] });
535
+ const state: SubagentState = {
536
+ baseCwd: tempDir,
537
+ currentSessionId: null,
538
+ asyncJobs: new Map(),
539
+ foregroundControls: new Map(),
540
+ lastForegroundControlId: null,
541
+ };
542
+ const executor = createSubagentExecutor!({
543
+ pi: { events: createEventBus(), getSessionName: () => undefined },
544
+ state,
545
+ config: {},
546
+ asyncByDefault: false,
547
+ tempArtifactsDir: tempDir,
548
+ getSubagentSessionRoot: () => path.join(tempDir, ".pi-subagents", "sessions"),
549
+ expandTilde: (value: string) => value,
550
+ discoverAgents: () => ({ agents: [makeAgent("echo")] }),
551
+ allowMutatingManagementActions: true,
552
+ });
553
+
554
+ const pending = executor.execute(
555
+ "user-detach-guidance",
556
+ { agent: "echo", task: "Keep working", async: false },
393
557
  new AbortController().signal,
394
558
  undefined,
395
559
  makeMinimalCtx(tempDir),
396
560
  );
561
+ let control = state.lastForegroundControlId ? state.foregroundControls.get(state.lastForegroundControlId) : undefined;
562
+ for (let attempt = 0; attempt < 100 && !control?.detach; attempt++) {
563
+ await new Promise((resolve) => setTimeout(resolve, 10));
564
+ control = state.lastForegroundControlId ? state.foregroundControls.get(state.lastForegroundControlId) : undefined;
565
+ }
566
+ assert.ok(control?.detach, "foreground detach control should become available");
567
+ assert.equal(control.detach(), true);
568
+
569
+ const result = await pending;
570
+ const text = result.content.map((part) => part.type === "text" ? part.text : "").join("\n");
571
+ assert.equal(result.details.results[0]?.detachedReason, "user request");
572
+ assert.match(text, /Detached at user request/);
573
+ assert.match(text, /subagent_wait\(\{ id: "[^"]+", nonBlocking: true \}\)/);
574
+ assert.doesNotMatch(text, /intercom coordination|supervisor request|Wait with subagent_wait/);
575
+ assert.doesNotMatch(text, /subagent_wait\(\{ id: "[^"]+" \}\)/);
576
+
577
+ let terminalChild = state.foregroundRuns?.get(control.runId)?.children[0];
578
+ for (let attempt = 0; attempt < 250 && terminalChild?.status !== "completed"; attempt++) {
579
+ await new Promise((resolve) => setTimeout(resolve, 20));
580
+ terminalChild = state.foregroundRuns?.get(control.runId)?.children[0];
581
+ }
582
+ assert.equal(terminalChild?.status, "completed", "detached child should reach its terminal callback before teardown");
583
+ assert.equal(terminalChild.finalOutput, "completed after user detach");
584
+ });
397
585
 
586
+ it("rejects action='single' with execution fields", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
587
+ const executor = makeExecutor([makeAgent("echo")]);
588
+ const result = await executor.executePublic("single-alias", { action: "single", agent: "echo", task: "work" }, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
398
589
  assert.equal(result.isError, true);
399
- assert.match(result.content[0]?.text ?? "", /is not a management action/);
400
- assert.equal(mockPi.callCount(), 0);
590
+ assert.match(result.content[0]?.text ?? "", /action='single' is not supported/);
401
591
  });
402
592
 
403
593
  it("rejects internal fan-out fields from public workflows", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
@@ -413,6 +603,53 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
413
603
  assert.equal(mockPi.callCount(), 0);
414
604
  });
415
605
 
606
+ it("runs isolation none outside Git and keeps worktree isolation strict", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
607
+ assert.equal(fs.existsSync(path.join(tempDir, ".git")), false);
608
+ mockPi.onCall({ output: "shared cwd" });
609
+ const executor = makeExecutor([makeAgent("echo")]);
610
+ const script = `return runs.run("main", { agent: "echo", task: "work" })`;
611
+
612
+ const shared = await executor.executePublic(
613
+ "isolation-none",
614
+ { async: false, isolation: "none", workflowScript: script },
615
+ new AbortController().signal,
616
+ undefined,
617
+ makeMinimalCtx(tempDir),
618
+ );
619
+ assert.equal(shared.isError, undefined, shared.content[0]?.text ?? "shared workflow failed");
620
+
621
+ const isolated = await executor.executePublic(
622
+ "isolation-worktree",
623
+ { async: false, isolation: "worktree", workflowScript: script },
624
+ new AbortController().signal,
625
+ undefined,
626
+ makeMinimalCtx(tempDir),
627
+ );
628
+ assert.equal(isolated.isError, true);
629
+ assert.match(isolated.content[0]?.text ?? "", /worktree isolation requires a git repository/i);
630
+ assert.equal(mockPi.callCount(), 1);
631
+ });
632
+
633
+ it("allows schedule.create to carry the required workflowScript target", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
634
+ let forwarded;
635
+ const executor = makeExecutor([makeAgent("echo")], {}, false, undefined, true, new Map(), undefined, async (params) => {
636
+ forwarded = params;
637
+ return { content: [{ type: "text", text: "created" }], details: { mode: "management", results: [] } };
638
+ });
639
+
640
+ const result = await executor.execute(
641
+ "schedule-create",
642
+ { action: "schedule.create", id: "nightly", every: "1h", workflowScript: "return runs.run('main', { agent: 'echo' })" },
643
+ new AbortController().signal,
644
+ undefined,
645
+ makeMinimalCtx(tempDir),
646
+ );
647
+
648
+ assert.equal(result.isError, undefined);
649
+ assert.equal(result.content[0]?.text, "created");
650
+ assert.equal(forwarded?.workflowScript, "return runs.run('main', { agent: 'echo' })");
651
+ });
652
+
416
653
  it("starts workflow scripts asynchronously with a portable internal run id", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
417
654
  mockPi.onCall({ echoEnv: [SUBAGENT_STEER_INBOX_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_ACK_DIR_ENV] });
418
655
  const asyncJobs: SubagentState["asyncJobs"] = new Map();
@@ -420,6 +657,8 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
420
657
  const workflowCwd = path.join(tempDir, "workflow-cwd");
421
658
  fs.mkdirSync(workflowCwd);
422
659
  const toolCallId = "call_demo|fc_demo";
660
+ const context = makeMinimalCtx(tempDir);
661
+ context.sessionManager.getSessionFile = () => path.join(tempDir, "parent-session.jsonl");
423
662
 
424
663
  const result = await executor.execute(
425
664
  toolCallId,
@@ -430,7 +669,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
430
669
  },
431
670
  new AbortController().signal,
432
671
  undefined,
433
- makeMinimalCtx(tempDir),
672
+ context,
434
673
  );
435
674
 
436
675
  assert.equal(result.isError, undefined);
@@ -444,11 +683,12 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
444
683
  assert.equal(path.basename(result.details.asyncDir!), workflowRunId);
445
684
  assert.equal(asyncJobs.has(workflowRunId), true);
446
685
  assert.equal(asyncJobs.get(workflowRunId)?.cwd, workflowCwd);
686
+ assert.equal(asyncJobs.get(workflowRunId)?.sessionRoot, path.join(tempDir, ".pi-subagents", "sessions"));
447
687
  assert.equal(asyncJobs.has(toolCallId), false);
448
688
  assert.equal(fs.existsSync(path.join(DIRS.async, toolCallId)), false);
449
689
  assert.match(result.content[0]?.text ?? "", /Async workflow/);
450
690
  const statusPath = path.join(result.details.asyncDir!, "status.json");
451
- let status: { runId?: string; toolCallId?: string; cwd?: string; state?: string; steps?: Array<{ agent?: string; label?: string; workflowKey?: string; parentWorkflowRunId?: string }>; workflow?: { value?: unknown; emits?: unknown[]; trace?: Array<{ key?: string; agent?: string; state?: string }> } } = {};
691
+ let status: { runId?: string; toolCallId?: string; cwd?: string; sessionRoot?: string; state?: string; steps?: Array<{ agent?: string; label?: string; workflowKey?: string; parentWorkflowRunId?: string }>; workflow?: { value?: unknown; emits?: unknown[]; trace?: Array<{ key?: string; agent?: string; state?: string }> } } = {};
452
692
  for (let attempt = 0; attempt < 100; attempt++) {
453
693
  status = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
454
694
  if (status.state === "complete" || status.state === "failed") break;
@@ -458,6 +698,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
458
698
  assert.equal(status.runId, workflowRunId);
459
699
  assert.equal(status.toolCallId, toolCallId);
460
700
  assert.equal(status.cwd, workflowCwd);
701
+ assert.equal(status.sessionRoot, path.join(tempDir, ".pi-subagents", "sessions"));
461
702
  assert.equal(status.steps?.length, 1);
462
703
  assert.deepEqual(status.steps?.map(({ agent, label, workflowKey }) => ({ agent, label, workflowKey })), [
463
704
  { agent: "echo", label: "work", workflowKey: "work" },
@@ -479,7 +720,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
479
720
  { key: "work", state: "completed" },
480
721
  ]);
481
722
  const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
482
- const persistedResult = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { id?: string; runId?: string; toolCallId?: string; agent?: string; cwd?: string; summary?: string; workflow?: { value?: unknown }; results?: Array<{ agent?: string; workflowKey?: string; runId?: string; output?: string }> };
723
+ const persistedResult = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { id?: string; runId?: string; toolCallId?: string; agent?: string; cwd?: string; summary?: string; workflow?: { value?: unknown; receipt?: unknown }; workflowReceipt?: { path?: string; receipt?: { workflowRunId?: string; entries?: Record<string, { key?: string; agent?: string; latestRunId?: string; resumability?: { state?: string }; continuation?: { runIds?: string[] } }> } }; results?: Array<{ agent?: string; workflowKey?: string; runId?: string; output?: string }> };
483
724
  assert.equal(persistedResult.id, workflowRunId);
484
725
  assert.equal(persistedResult.runId, workflowRunId);
485
726
  assert.equal(persistedResult.toolCallId, toolCallId);
@@ -495,6 +736,16 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
495
736
  assert.equal(fs.existsSync(path.join(result.details.asyncDir!, "control", "workflow-foreground", persistedResult.results?.[0]?.runId ?? "missing")), false);
496
737
  assert.match(persistedResult.summary ?? "", /Return: \{\n "answer": 42\n\}/);
497
738
  assert.deepEqual(persistedResult.workflow?.value, { answer: 42 });
739
+ assert.equal(persistedResult.workflow?.receipt, undefined, "status/result workflow projection must stay receipt-free");
740
+ assert.equal(persistedResult.workflowReceipt?.path, path.join(result.details.asyncDir!, "workflow-receipt.json"));
741
+ assert.equal(persistedResult.workflowReceipt?.receipt?.workflowRunId, workflowRunId);
742
+ assert.equal(persistedResult.workflowReceipt?.receipt?.entries?.work?.key, "work");
743
+ assert.equal(persistedResult.workflowReceipt?.receipt?.entries?.work?.agent, "echo");
744
+ assert.equal(persistedResult.workflowReceipt?.receipt?.entries?.work?.latestRunId, persistedResult.results?.[0]?.runId);
745
+ assert.equal(fs.existsSync(path.join(DIRS.async, persistedResult.results?.[0]?.runId ?? "missing")), false);
746
+ assert.deepEqual(persistedResult.workflowReceipt?.receipt?.entries?.work?.continuation?.runIds, [persistedResult.results?.[0]?.runId]);
747
+ assert.equal(persistedResult.workflowReceipt?.receipt?.entries?.work?.resumability?.state, "resumable");
748
+ assert.deepEqual(JSON.parse(fs.readFileSync(persistedResult.workflowReceipt!.path!, "utf-8")), persistedResult.workflowReceipt?.receipt);
498
749
  assert.equal(fs.existsSync(path.join(DIRS.results, `${toolCallId}.json`)), false);
499
750
  fs.rmSync(result.details.asyncDir!, { recursive: true, force: true });
500
751
  fs.rmSync(resultPath, { force: true });
@@ -535,153 +786,439 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
535
786
  assert.equal(fs.readFileSync(markerPath, "utf-8"), "started");
536
787
  assert.equal(mockPi.callCount(), 0);
537
788
 
538
- fs.rmSync(started.details.asyncDir!, { recursive: true, force: true });
789
+ fs.rmSync(started.details.asyncDir!, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
539
790
  fs.rmSync(workflowResultPath, { force: true });
540
791
  });
541
792
 
542
- it("rejects explicit model overrides for external CLI agents", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
793
+ it("awaits omitted external CLI workflow children through their async result", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
794
+ const markerPath = path.join(tempDir, "external-awaited-started");
543
795
  const executor = makeExecutor([
544
796
  makeAgent("external", {
545
- runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('unreachable')"] },
797
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", `require("node:fs").writeFileSync(${JSON.stringify(markerPath)}, "started"); process.stdout.write("awaited external result")`] },
798
+ model: "mock/default-model",
799
+ modelSource: { type: "subagents.defaultModel", scope: "user", path: "/settings.json", model: "mock/default-model" },
546
800
  }),
547
801
  ]);
548
802
  const result = await executor.execute(
549
- "external-explicit-model",
550
- { agent: "external", task: "Run external", async: true, model: "mock/override" },
803
+ "external-awaited-workflow",
804
+ { workflowScript: `return await runs.run("external", { agent: "external", task: "Run external" });`, async: false },
551
805
  new AbortController().signal,
552
806
  undefined,
553
- makeMinimalCtx(tempDir),
807
+ { ...makeMinimalCtx(tempDir), model: { provider: "mock", id: "parent-model" } },
554
808
  );
555
809
 
556
- assert.equal(result.isError, true);
557
- assert.match(result.content[0]?.text ?? "", /does not support: model override/);
810
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
811
+ assert.equal(result.details.mode, "workflow");
812
+ assert.match(result.content[0]?.text ?? "", /awaited external result/);
813
+ assert.equal(fs.readFileSync(markerPath, "utf-8"), "started");
558
814
  assert.equal(mockPi.callCount(), 0);
559
815
  });
560
816
 
561
- it("rejects external CLI agent models that differ from inherited subagents.defaultModel", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
817
+ it("starts omitted external CLI single-child calls in async mode", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
818
+ const markerPath = path.join(tempDir, "external-single-omitted-async-started");
562
819
  const executor = makeExecutor([
563
820
  makeAgent("external", {
564
- runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('unreachable')"] },
565
- model: "mock/override-model",
821
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", `require("node:fs").writeFileSync(${JSON.stringify(markerPath)}, "started"); process.stdout.write("single async result")`] },
822
+ model: "mock/default-model",
566
823
  modelSource: { type: "subagents.defaultModel", scope: "user", path: "/settings.json", model: "mock/default-model" },
567
824
  }),
568
825
  ]);
569
826
  const result = await executor.execute(
570
- "external-agent-override-model",
571
- { agent: "external", task: "Run external", async: true },
827
+ "external-single-omitted-async",
828
+ { agent: "external", task: "Run external" },
572
829
  new AbortController().signal,
573
830
  undefined,
574
- makeMinimalCtx(tempDir),
831
+ { ...makeMinimalCtx(tempDir), model: { provider: "mock", id: "parent-model" } },
575
832
  );
576
833
 
577
- assert.equal(result.isError, true);
578
- assert.match(result.content[0]?.text ?? "", /does not support: model override/);
834
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "launch failed");
835
+ assert.ok(result.details.asyncId);
836
+ assert.match(result.content[0]?.text ?? "", /Async: external/);
837
+ for (let attempt = 0; attempt < 100 && !fs.existsSync(markerPath); attempt++) {
838
+ await new Promise((resolve) => setTimeout(resolve, 20));
839
+ }
840
+ assert.equal(fs.readFileSync(markerPath, "utf-8"), "started");
579
841
  assert.equal(mockPi.callCount(), 0);
842
+
843
+ const resultPath = path.join(DIRS.results, `${result.details.asyncId}.json`);
844
+ let runResult: { state?: string } = {};
845
+ for (let attempt = 0; attempt < 100; attempt++) {
846
+ if (fs.existsSync(resultPath)) runResult = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
847
+ if (runResult.state === "complete" || runResult.state === "failed") break;
848
+ await new Promise((resolve) => setTimeout(resolve, 20));
849
+ }
850
+ assert.equal(runResult.state, "complete");
851
+
852
+ fs.rmSync(result.details.asyncDir!, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
853
+ fs.rmSync(resultPath, { force: true });
580
854
  });
581
855
 
582
- it("rejects external CLI agent models that equal inherited subagents.defaultModel without provenance", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
856
+ it("lets explicit fast false opt out external CLI agents from inherited fast mode", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
857
+ const markerPath = path.join(tempDir, "external-fast-false-started");
583
858
  const executor = makeExecutor([
584
859
  makeAgent("external", {
585
- runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('unreachable')"] },
586
- model: "mock/default-model",
860
+ fast: true,
861
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", `require("node:fs").writeFileSync(${JSON.stringify(markerPath)}, "started"); process.stdout.write("external fast false result")`] },
587
862
  }),
588
863
  ]);
589
- const result = await executor.execute(
590
- "external-agent-same-value-override-model",
864
+
865
+ const rejected = await executor.execute(
866
+ "external-fast-inherited",
591
867
  { agent: "external", task: "Run external", async: true },
592
868
  new AbortController().signal,
593
869
  undefined,
594
870
  makeMinimalCtx(tempDir),
595
871
  );
596
-
597
- assert.equal(result.isError, true);
598
- assert.match(result.content[0]?.text ?? "", /does not support: model override/);
599
- assert.equal(mockPi.callCount(), 0);
600
- });
601
-
602
- it("projects live child activity into async workflow status", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
603
- mockPi.onCall({
604
- steps: [
605
- { jsonl: [events.toolStart("read", { path: "src/example.ts" })] },
606
- { delay: 2_500, jsonl: [events.toolEnd("read"), events.toolResult("read", "contents")] },
607
- { jsonl: [events.assistantMessage("Done")] },
608
- ],
609
- });
610
- const asyncJobs: SubagentState["asyncJobs"] = new Map();
611
- const executor = makeExecutor([makeAgent("echo")], {
612
- control: {
613
- enabled: true,
614
- needsAttentionAfterMs: 100,
615
- activeNoticeAfterMs: 100,
616
- activeNoticeAfterTurns: 999_999,
617
- activeNoticeAfterTokens: 999_999,
618
- notifyOn: ["active_long_running", "needs_attention"],
619
- notifyChannels: ["event"],
620
- },
621
- }, false, undefined, true, asyncJobs);
872
+ assert.equal(rejected.isError, true);
873
+ assert.match(rejected.content[0]?.text ?? "", /does not support fast mode/);
874
+ assert.equal(fs.existsSync(markerPath), false);
622
875
 
623
876
  const result = await executor.execute(
624
- "workflow-live-activity",
625
- { workflowScript: `return runs.run("main", { agent: "echo", task: "Inspect the file" });` },
877
+ "external-fast-false",
878
+ { agent: "external", task: "Run external", async: true, fast: false },
626
879
  new AbortController().signal,
627
880
  undefined,
628
881
  makeMinimalCtx(tempDir),
629
882
  );
630
- const { asyncId: workflowRunId, asyncDir } = result.details;
631
- assert.ok(workflowRunId);
632
- assert.ok(asyncDir);
633
- const statusPath = path.join(asyncDir, "status.json");
634
- const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
635
- const activeMarkerPath = path.join(DIRS.async, ACTIVE_RUN_INDEX_DIR, workflowRunId);
636
- assert.equal(fs.existsSync(activeMarkerPath), true);
637
- let liveStatus: AsyncStatus | undefined;
638
- const activityDeadline = Date.now() + 5_000;
639
- while (Date.now() < activityDeadline && !fs.existsSync(resultPath)) {
640
- const candidate = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
641
- if (candidate.activityState === "needs_attention" && candidate.steps?.[0]?.currentTool === "read") {
642
- liveStatus = candidate;
643
- break;
644
- }
645
- await new Promise((resolve) => setTimeout(resolve, 50));
646
- }
647
883
 
648
- assert.ok(liveStatus, "expected workflow status to expose live child activity");
649
- assert.equal(liveStatus.activityState, "needs_attention");
650
- assert.equal(typeof liveStatus.lastActivityAt, "number");
651
- assert.equal(liveStatus.currentTool, "read");
652
- assert.match(liveStatus.currentPath ?? "", /src[/\\]example\.ts$/);
653
- assert.equal(liveStatus.toolCount, 1);
654
- assert.equal(liveStatus.steps?.[0]?.status, "running");
655
- assert.equal(liveStatus.steps?.[0]?.agent, "echo");
656
- assert.match(liveStatus.steps?.[0]?.sessionFile ?? "", /session\.jsonl$/);
657
- assert.equal(fs.existsSync(liveStatus.steps?.[0]?.sessionFile ?? ""), true);
658
- assert.equal(liveStatus.steps?.[0]?.activityState, "needs_attention");
659
- assert.equal(typeof liveStatus.steps?.[0]?.lastActivityAt, "number");
660
- assert.equal(liveStatus.steps?.[0]?.toolCount, 1);
661
- assert.equal(asyncJobs.get(workflowRunId)?.activityState, "needs_attention");
662
- assert.equal(asyncJobs.get(workflowRunId)?.steps?.[0]?.currentTool, "read");
884
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "launch failed");
885
+ assert.ok(result.details.asyncId);
886
+ for (let attempt = 0; attempt < 100 && !fs.existsSync(markerPath); attempt++) {
887
+ await new Promise((resolve) => setTimeout(resolve, 20));
888
+ }
889
+ assert.equal(fs.readFileSync(markerPath, "utf-8"), "started");
890
+ assert.equal(mockPi.callCount(), 0);
663
891
 
664
- const completionDeadline = Date.now() + 5_000;
665
- while (!fs.existsSync(resultPath)) {
666
- if (Date.now() > completionDeadline) assert.fail("Timed out waiting for async workflow completion");
667
- await new Promise((resolve) => setTimeout(resolve, 50));
892
+ const resultPath = path.join(DIRS.results, `${result.details.asyncId}.json`);
893
+ let runResult: { state?: string } = {};
894
+ for (let attempt = 0; attempt < 100; attempt++) {
895
+ if (fs.existsSync(resultPath)) runResult = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
896
+ if (runResult.state === "complete" || runResult.state === "failed") break;
897
+ await new Promise((resolve) => setTimeout(resolve, 20));
668
898
  }
669
- assert.equal(fs.existsSync(activeMarkerPath), false);
670
- fs.rmSync(asyncDir, { recursive: true, force: true });
899
+ assert.equal(runResult.state, "complete");
900
+
901
+ fs.rmSync(result.details.asyncDir!, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
671
902
  fs.rmSync(resultPath, { force: true });
672
903
  });
673
904
 
674
- it("rejects an invalid async workflow usage budget before creating run state", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
675
- const asyncJobs: SubagentState["asyncJobs"] = new Map();
676
- const executor = makeExecutor([makeAgent("echo")], {}, false, undefined, true, asyncJobs);
677
- const runId = `scripted-workflow-invalid-budget-${Date.now()}`;
678
-
905
+ it("runs external CLI agents with fallback models without registry validation", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
906
+ const markerPath = path.join(tempDir, "external-fallback-started");
907
+ const executor = makeExecutor([
908
+ makeAgent("external", {
909
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", `require("node:fs").writeFileSync(${JSON.stringify(markerPath)}, "started")`] },
910
+ fallbackModels: ["mock/fallback"],
911
+ }),
912
+ ]);
679
913
  const result = await executor.execute(
680
- runId,
681
- { workflowScript: `return "unreachable";`, usageBudget: { tokens: { hard: 0 } } },
914
+ "external-fallback-model",
915
+ { agent: "external", task: "Run external", async: true },
682
916
  new AbortController().signal,
683
917
  undefined,
684
- makeMinimalCtx(tempDir),
918
+ {
919
+ ...makeMinimalCtx(tempDir),
920
+ modelRegistry: { getAvailable: () => [{ provider: "other", id: "known" }] },
921
+ },
922
+ );
923
+
924
+ assert.equal(result.isError, undefined);
925
+ assert.doesNotMatch(result.content[0]?.text ?? "", /Unknown subagent model/);
926
+ for (let attempt = 0; attempt < 100 && !fs.existsSync(markerPath); attempt++) {
927
+ await new Promise((resolve) => setTimeout(resolve, 20));
928
+ }
929
+ assert.equal(fs.readFileSync(markerPath, "utf-8"), "started");
930
+ assert.equal(mockPi.callCount(), 0);
931
+
932
+ assert.ok(result.details.asyncId);
933
+ const resultPath = path.join(DIRS.results, `${result.details.asyncId}.json`);
934
+ let runResult: { state?: string } = {};
935
+ for (let attempt = 0; attempt < 100; attempt++) {
936
+ if (fs.existsSync(resultPath)) runResult = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
937
+ if (runResult.state === "complete" || runResult.state === "failed") break;
938
+ await new Promise((resolve) => setTimeout(resolve, 20));
939
+ }
940
+ assert.equal(runResult.state, "complete");
941
+
942
+ fs.rmSync(result.details.asyncDir!, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
943
+ fs.rmSync(resultPath, { force: true });
944
+ });
945
+
946
+ it("rejects external CLI fork context before fallback model validation", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
947
+ const markerPath = path.join(tempDir, "external-fork-started");
948
+ const parentSessionFile = path.join(mockPi.dir, "external-fork-parent.jsonl");
949
+ fs.writeFileSync(parentSessionFile, `${JSON.stringify({ type: "session", version: 3, id: "parent", cwd: tempDir })}\n`, "utf-8");
950
+ const ctx = makeMinimalCtx(tempDir);
951
+ Object.assign(ctx.sessionManager, {
952
+ getSessionFile: () => parentSessionFile,
953
+ getLeafId: () => "parent-leaf",
954
+ openSession: () => ({
955
+ createBranchedSession: () => parentSessionFile,
956
+ }),
957
+ });
958
+ const executor = makeExecutor([
959
+ makeAgent("external", {
960
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", `require("node:fs").writeFileSync(${JSON.stringify(markerPath)}, "started")`] },
961
+ defaultContext: "fork",
962
+ fallbackModels: ["mock/fallback"],
963
+ }),
964
+ ]);
965
+ const result = await executor.execute(
966
+ "external-fork-fallback",
967
+ { agent: "external", task: "Run external", async: true },
968
+ new AbortController().signal,
969
+ undefined,
970
+ {
971
+ ...ctx,
972
+ modelRegistry: { getAvailable: () => [{ provider: "other", id: "known" }] },
973
+ },
974
+ );
975
+
976
+ assert.equal(result.isError, true);
977
+ assert.match(result.content[0]?.text ?? "", /does not support: fork context/);
978
+ assert.doesNotMatch(result.content[0]?.text ?? "", /Unknown subagent model/);
979
+ assert.equal(mockPi.callCount(), 0);
980
+ assert.equal(fs.existsSync(markerPath), false);
981
+ });
982
+
983
+ it("rejects explicit model overrides for external CLI agents", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
984
+ const executor = makeExecutor([
985
+ makeAgent("external", {
986
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('unreachable')"] },
987
+ }),
988
+ ]);
989
+ const result = await executor.execute(
990
+ "external-explicit-model",
991
+ { agent: "external", task: "Run external", async: true, model: "mock/override" },
992
+ new AbortController().signal,
993
+ undefined,
994
+ {
995
+ ...makeMinimalCtx(tempDir),
996
+ modelRegistry: { getAvailable: () => [{ provider: "other", id: "known" }] },
997
+ },
998
+ );
999
+
1000
+ assert.equal(result.isError, true);
1001
+ assert.match(result.content[0]?.text ?? "", /does not support: model override/);
1002
+ assert.doesNotMatch(result.content[0]?.text ?? "", /Unknown subagent model/);
1003
+ assert.equal(mockPi.callCount(), 0);
1004
+ });
1005
+
1006
+ it("rejects external CLI agent models that differ from inherited subagents.defaultModel", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1007
+ const executor = makeExecutor([
1008
+ makeAgent("external", {
1009
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('unreachable')"] },
1010
+ model: "mock/override-model",
1011
+ modelSource: { type: "subagents.defaultModel", scope: "user", path: "/settings.json", model: "mock/default-model" },
1012
+ }),
1013
+ ]);
1014
+ const result = await executor.execute(
1015
+ "external-agent-override-model",
1016
+ { agent: "external", task: "Run external", async: true },
1017
+ new AbortController().signal,
1018
+ undefined,
1019
+ makeMinimalCtx(tempDir),
1020
+ );
1021
+
1022
+ assert.equal(result.isError, true);
1023
+ assert.match(result.content[0]?.text ?? "", /does not support: model override/);
1024
+ assert.equal(mockPi.callCount(), 0);
1025
+ });
1026
+
1027
+ it("rejects external CLI agent models that equal inherited subagents.defaultModel without provenance", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1028
+ const executor = makeExecutor([
1029
+ makeAgent("external", {
1030
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('unreachable')"] },
1031
+ model: "mock/default-model",
1032
+ }),
1033
+ ]);
1034
+ const result = await executor.execute(
1035
+ "external-agent-same-value-override-model",
1036
+ { agent: "external", task: "Run external", async: true },
1037
+ new AbortController().signal,
1038
+ undefined,
1039
+ makeMinimalCtx(tempDir),
1040
+ );
1041
+
1042
+ assert.equal(result.isError, true);
1043
+ assert.match(result.content[0]?.text ?? "", /does not support: model override/);
1044
+ assert.equal(mockPi.callCount(), 0);
1045
+ });
1046
+
1047
+ it("projects live child activity into async workflow status", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1048
+ mockPi.onCall({
1049
+ steps: [
1050
+ { jsonl: [events.toolStart("read", { path: "src/example.ts" })] },
1051
+ { delay: 2_500, jsonl: [events.toolEnd("read"), events.toolResult("read", "contents")] },
1052
+ { jsonl: [events.assistantMessage("Done")] },
1053
+ ],
1054
+ });
1055
+ const asyncJobs: SubagentState["asyncJobs"] = new Map();
1056
+ const executor = makeExecutor([makeAgent("echo")], {
1057
+ control: {
1058
+ enabled: true,
1059
+ needsAttentionAfterMs: 100,
1060
+ activeNoticeAfterMs: 100,
1061
+ activeNoticeAfterTurns: 999_999,
1062
+ activeNoticeAfterTokens: 999_999,
1063
+ notifyOn: ["active_long_running", "needs_attention"],
1064
+ notifyChannels: ["event"],
1065
+ },
1066
+ }, false, undefined, true, asyncJobs);
1067
+
1068
+ const result = await executor.execute(
1069
+ "workflow-live-activity",
1070
+ { workflowScript: `return runs.run("main", { agent: "echo", task: "Inspect the file" });` },
1071
+ new AbortController().signal,
1072
+ undefined,
1073
+ makeMinimalCtx(tempDir),
1074
+ );
1075
+ const { asyncId: workflowRunId, asyncDir } = result.details;
1076
+ assert.ok(workflowRunId);
1077
+ assert.ok(asyncDir);
1078
+ const statusPath = path.join(asyncDir, "status.json");
1079
+ const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
1080
+ const activeMarkerPath = path.join(DIRS.async, ACTIVE_RUN_INDEX_DIR, workflowRunId);
1081
+ assert.equal(fs.existsSync(activeMarkerPath), true);
1082
+ let liveStatus: AsyncStatus | undefined;
1083
+ const activityDeadline = Date.now() + 5_000;
1084
+ while (Date.now() < activityDeadline && !fs.existsSync(resultPath)) {
1085
+ const candidate = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
1086
+ if (candidate.activityState === "needs_attention" && candidate.steps?.[0]?.currentTool === "read") {
1087
+ liveStatus = candidate;
1088
+ break;
1089
+ }
1090
+ await new Promise((resolve) => setTimeout(resolve, 50));
1091
+ }
1092
+
1093
+ assert.ok(liveStatus, "expected workflow status to expose live child activity");
1094
+ assert.equal(liveStatus.activityState, "needs_attention");
1095
+ assert.equal(typeof liveStatus.lastActivityAt, "number");
1096
+ assert.equal(liveStatus.currentTool, "read");
1097
+ assert.match(liveStatus.currentPath ?? "", /src[/\\]example\.ts$/);
1098
+ assert.equal(liveStatus.toolCount, 1);
1099
+ assert.equal(liveStatus.steps?.[0]?.status, "running");
1100
+ assert.equal(liveStatus.steps?.[0]?.agent, "echo");
1101
+ assert.match(liveStatus.steps?.[0]?.sessionFile ?? "", /session\.jsonl$/);
1102
+ assert.equal(fs.existsSync(liveStatus.steps?.[0]?.sessionFile ?? ""), true);
1103
+ assert.equal(liveStatus.steps?.[0]?.activityState, "needs_attention");
1104
+ assert.equal(typeof liveStatus.steps?.[0]?.lastActivityAt, "number");
1105
+ assert.equal(liveStatus.steps?.[0]?.toolCount, 1);
1106
+ assert.equal(asyncJobs.get(workflowRunId)?.activityState, "needs_attention");
1107
+ assert.equal(asyncJobs.get(workflowRunId)?.steps?.[0]?.currentTool, "read");
1108
+
1109
+ const completionDeadline = Date.now() + 5_000;
1110
+ while (!fs.existsSync(resultPath)) {
1111
+ if (Date.now() > completionDeadline) assert.fail("Timed out waiting for async workflow completion");
1112
+ await new Promise((resolve) => setTimeout(resolve, 50));
1113
+ }
1114
+ assert.equal(fs.existsSync(activeMarkerPath), false);
1115
+ fs.rmSync(asyncDir, { recursive: true, force: true });
1116
+ fs.rmSync(resultPath, { force: true });
1117
+ });
1118
+
1119
+ it("notifies the parent when an async workflow child needs attention", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1120
+ mockPi.onCall({
1121
+ steps: [
1122
+ { jsonl: [events.toolStart("read", { path: "src/example.ts" }), events.toolEnd("read"), events.toolResult("read", "contents")] },
1123
+ { delay: 2_500, jsonl: [events.assistantMessage("Done")] },
1124
+ ],
1125
+ });
1126
+ const asyncJobs: SubagentState["asyncJobs"] = new Map();
1127
+ const piEvents = createEventBus();
1128
+ const controlPayloads: Array<{ event?: ControlEvent; source?: string }> = [];
1129
+ piEvents.on(SUBAGENT_CONTROL_EVENT, (payload) => {
1130
+ controlPayloads.push(payload as { event?: ControlEvent; source?: string });
1131
+ });
1132
+ const executor = makeExecutor([makeAgent("echo")], {
1133
+ control: {
1134
+ enabled: true,
1135
+ needsAttentionAfterMs: 100,
1136
+ activeNoticeAfterMs: 999_999,
1137
+ activeNoticeAfterTurns: 999_999,
1138
+ activeNoticeAfterTokens: 999_999,
1139
+ notifyOn: ["needs_attention"],
1140
+ notifyChannels: ["event"],
1141
+ },
1142
+ }, false, undefined, true, asyncJobs, undefined, undefined, piEvents);
1143
+
1144
+ const result = await executor.execute(
1145
+ "workflow-child-attention-notice",
1146
+ { workflowScript: `return runs.run("stalled-review", { agent: "echo", task: "Inspect the file" });` },
1147
+ new AbortController().signal,
1148
+ undefined,
1149
+ makeMinimalCtx(tempDir),
1150
+ );
1151
+ const { asyncId: workflowRunId, asyncDir } = result.details;
1152
+ assert.ok(workflowRunId);
1153
+ assert.ok(asyncDir);
1154
+ const statusPath = path.join(asyncDir, "status.json");
1155
+ const eventsPath = path.join(asyncDir, "events.jsonl");
1156
+ const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
1157
+ let liveStatus: AsyncStatus | undefined;
1158
+ const activityDeadline = Date.now() + 5_000;
1159
+ while (Date.now() < activityDeadline && !fs.existsSync(resultPath)) {
1160
+ if (fs.existsSync(statusPath)) {
1161
+ const candidate = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
1162
+ if (candidate.activityState === "needs_attention" && !candidate.steps?.[0]?.currentTool) {
1163
+ liveStatus = candidate;
1164
+ break;
1165
+ }
1166
+ }
1167
+ await new Promise((resolve) => setTimeout(resolve, 50));
1168
+ }
1169
+
1170
+ assert.ok(liveStatus, "expected workflow status to expose idle child attention");
1171
+ assert.equal(liveStatus.activityState, "needs_attention");
1172
+ assert.equal(liveStatus.steps?.[0]?.activityState, "needs_attention");
1173
+ assert.equal(liveStatus.steps?.[0]?.workflowKey, "stalled-review");
1174
+
1175
+ const attentionPayload = controlPayloads.find((payload) => payload.event?.type === "needs_attention");
1176
+ assert.ok(attentionPayload, "expected a live parent control event");
1177
+ assert.equal(attentionPayload.source, "async");
1178
+ assert.equal(attentionPayload.event?.workflowKey, "stalled-review");
1179
+ assert.equal(attentionPayload.event?.reason, "idle");
1180
+ const sent: Array<{ options?: { triggerTurn?: boolean } }> = [];
1181
+ handleSubagentControlNotice({
1182
+ pi: { sendMessage(_message, options) { sent.push({ options: options as { triggerTurn?: boolean } }); } },
1183
+ state: { asyncJobs } as SubagentState,
1184
+ visibleControlNotices: new Set(),
1185
+ details: { event: attentionPayload.event!, source: "async" },
1186
+ });
1187
+ assert.equal(sent.length, 1);
1188
+ assert.deepEqual(sent[0]?.options, { triggerTurn: true });
1189
+
1190
+ assert.equal(fs.existsSync(eventsPath), true);
1191
+ const controlRecords = fs.readFileSync(eventsPath, "utf-8")
1192
+ .split("\n")
1193
+ .filter((line) => line.trim())
1194
+ .map((line) => JSON.parse(line) as { type?: string; event?: ControlEvent; runId?: string })
1195
+ .filter((record) => record.type === "subagent.control");
1196
+ const persisted = controlRecords.find((record) => record.event?.type === "needs_attention");
1197
+ assert.ok(persisted, "expected a persisted workflow control event");
1198
+ assert.equal(persisted.runId, workflowRunId);
1199
+ assert.equal(persisted.event?.workflowKey, "stalled-review");
1200
+ assert.equal(controlRecords.filter((record) => record.event?.type === "needs_attention").length, 1);
1201
+
1202
+ const completionDeadline = Date.now() + 5_000;
1203
+ while (!fs.existsSync(resultPath)) {
1204
+ if (Date.now() > completionDeadline) assert.fail("Timed out waiting for async workflow completion");
1205
+ await new Promise((resolve) => setTimeout(resolve, 50));
1206
+ }
1207
+ fs.rmSync(asyncDir, { recursive: true, force: true });
1208
+ fs.rmSync(resultPath, { force: true });
1209
+ });
1210
+
1211
+ it("rejects an invalid async workflow usage budget before creating run state", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1212
+ const asyncJobs: SubagentState["asyncJobs"] = new Map();
1213
+ const executor = makeExecutor([makeAgent("echo")], {}, false, undefined, true, asyncJobs);
1214
+ const runId = `scripted-workflow-invalid-budget-${Date.now()}`;
1215
+
1216
+ const result = await executor.execute(
1217
+ runId,
1218
+ { workflowScript: `return "unreachable";`, usageBudget: { tokens: { hard: 0 } } },
1219
+ new AbortController().signal,
1220
+ undefined,
1221
+ makeMinimalCtx(tempDir),
685
1222
  );
686
1223
 
687
1224
  assert.equal(result.isError, true);
@@ -787,7 +1324,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
787
1324
  fs.writeFileSync(path.join(tempDir, "base.txt"), "base\n", "utf-8");
788
1325
  execFileSync("git", ["add", "base.txt"], { cwd: tempDir });
789
1326
  execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
790
- mockPi.onCall({ output: "async child done" });
1327
+ mockPi.onCall({ output: "async child done", writeFiles: [{ path: "feature.txt", content: "feature\n" }] });
791
1328
  const executor = makeExecutor([makeAgent("echo")]);
792
1329
  const toolCallId = `scripted-workflow-parent-${Date.now()}`;
793
1330
  const started = await executor.execute(
@@ -815,16 +1352,26 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
815
1352
  assert.match(workflowStepSessionFile, /session\.jsonl$/);
816
1353
  const childDir = path.join(DIRS.async, childRunId);
817
1354
  const childStatusPath = path.join(childDir, "status.json");
818
- let childStatus: { state?: string; mode?: string; parentWorkflowRunId?: string; workflowKey?: string; parallelHandoff?: { path?: string } } = {};
1355
+ let childStatus: { state?: string; mode?: string; parentWorkflowRunId?: string; workflowKey?: string; parallelHandoff?: { path?: string; changedPatches?: number } } = {};
819
1356
  for (let attempt = 0; attempt < 200; attempt++) {
820
1357
  if (fs.existsSync(childStatusPath)) childStatus = JSON.parse(fs.readFileSync(childStatusPath, "utf-8"));
821
1358
  if (["complete", "failed", "stopped"].includes(childStatus.state ?? "")) break;
822
1359
  await new Promise((resolve) => setTimeout(resolve, 20));
823
1360
  }
824
- assert.equal(childStatus.mode, "parallel");
1361
+ assert.equal(childStatus.mode, "single");
825
1362
  assert.equal(childStatus.parentWorkflowRunId, workflowRunId);
826
1363
  assert.equal(childStatus.workflowKey, "background");
827
1364
  assert.equal(typeof childStatus.parallelHandoff?.path, "string");
1365
+ assert.equal(childStatus.parallelHandoff?.changedPatches, 1);
1366
+ assert.equal(fs.existsSync(path.join(tempDir, "feature.txt")), false);
1367
+ const handoff = JSON.parse(fs.readFileSync(childStatus.parallelHandoff!.path!, "utf-8")) as { groups?: Array<{ children?: Array<{ patch?: { changed?: boolean; filesChanged?: number } }>; cleanup?: { state?: string; tasks?: Array<{ path?: string; preserved?: boolean; worktreeRemoved?: boolean; reason?: string }> } }> };
1368
+ assert.equal(handoff.groups?.[0]?.children?.[0]?.patch?.changed, true);
1369
+ assert.equal(handoff.groups?.[0]?.children?.[0]?.patch?.filesChanged, 1);
1370
+ assert.equal(handoff.groups?.[0]?.cleanup?.state, "partial");
1371
+ assert.equal(handoff.groups?.[0]?.cleanup?.tasks?.[0]?.preserved, true);
1372
+ assert.equal(handoff.groups?.[0]?.cleanup?.tasks?.[0]?.worktreeRemoved, false);
1373
+ assert.equal(handoff.groups?.[0]?.cleanup?.tasks?.[0]?.reason, "retained child resume requires managed worktree cwd");
1374
+ assert.equal(fs.existsSync(handoff.groups?.[0]?.cleanup?.tasks?.[0]?.path ?? ""), true);
828
1375
  const childResultPath = path.join(DIRS.results, `${childRunId}.json`);
829
1376
  for (let attempt = 0; attempt < 200 && !fs.existsSync(childResultPath); attempt++) {
830
1377
  await new Promise((resolve) => setTimeout(resolve, 20));
@@ -833,6 +1380,12 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
833
1380
  assert.equal(childResult.parentWorkflowRunId, workflowRunId);
834
1381
  assert.equal(childResult.workflowKey, "background");
835
1382
  assert.equal(fs.existsSync(workflowStepSessionFile), true);
1383
+ const retainedCwd = handoff.groups?.[0]?.cleanup?.tasks?.[0]?.path;
1384
+ assert.ok(retainedCwd);
1385
+ const resumeTarget = resolveAsyncResumeTarget({ id: childRunId }, { asyncDirRoot: DIRS.async, resultsDir: DIRS.results });
1386
+ assert.equal(resumeTarget.recoveryDescriptor?.sourceRunId, childRunId);
1387
+ assert.equal(path.resolve(resumeTarget.cwd ?? ""), path.resolve(retainedCwd));
1388
+ discardPreservedWorktrees(childStatus.parallelHandoff!.path!, { kind: "confirmed" });
836
1389
  fs.rmSync(started.details.asyncDir!, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
837
1390
  fs.rmSync(workflowResultPath, { force: true });
838
1391
  fs.rmSync(childDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
@@ -854,26 +1407,168 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
854
1407
  assert.match(result.content[0]?.text ?? "", /Stop requested for async workflow workflow-stop/);
855
1408
  });
856
1409
 
857
- it("reports completed async workflows as not running when stopped after completion", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
858
- const runId = `workflow-stop-complete-${Date.now()}`;
859
- const asyncDir = path.join(DIRS.async, runId);
860
- fs.mkdirSync(asyncDir, { recursive: true });
861
- fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
862
- runId,
863
- sessionId: "session-123",
864
- mode: "workflow",
865
- state: "complete",
866
- startedAt: Date.now(),
867
- lastUpdate: Date.now(),
868
- cwd: tempDir,
869
- pid: process.pid,
870
- }), "utf-8");
871
- const asyncJobs: SubagentState["asyncJobs"] = new Map([[runId, {
872
- asyncId: runId,
873
- asyncDir,
874
- cwd: tempDir,
875
- status: "complete",
876
- mode: "workflow",
1410
+ it("persists parent-stopped workflow children as stopped instead of failed", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1411
+ mockPi.onCall({ delay: 5_000, output: "too late" });
1412
+ const workflowControllers = new Map<string, AbortController>();
1413
+ const executor = makeExecutor([makeAgent("echo")], {}, false, undefined, true, new Map(), workflowControllers);
1414
+ const started = await executor.execute(
1415
+ `workflow-stop-child-${Date.now()}`,
1416
+ { workflowScript: `return await runs.run("review", { agent: "echo", task: "Wait" });` },
1417
+ new AbortController().signal,
1418
+ undefined,
1419
+ makeMinimalCtx(tempDir),
1420
+ );
1421
+ assert.ok(started.details.asyncId);
1422
+ const workflowRunId = started.details.asyncId;
1423
+ const statusPath = path.join(started.details.asyncDir!, "status.json");
1424
+ for (let attempt = 0; attempt < 100; attempt++) {
1425
+ const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
1426
+ if (status.steps?.some((step) => step.workflowKey === "review" && step.status === "running")) break;
1427
+ await new Promise((resolve) => setTimeout(resolve, 20));
1428
+ }
1429
+ let childCall: string | undefined;
1430
+ for (let attempt = 0; attempt < 100 && !childCall; attempt++) {
1431
+ childCall = fs.readdirSync(mockPi.dir).find((name) => /^call-\d+-\d+-/.test(name));
1432
+ if (!childCall) await new Promise((resolve) => setTimeout(resolve, 20));
1433
+ }
1434
+ const childPid = Number(childCall?.match(/^call-\d+-(\d+)-/)?.[1]);
1435
+ assert.equal(Number.isInteger(childPid) && childPid > 0, true);
1436
+
1437
+ const stopped = await executor.execute(
1438
+ "stop-workflow-child",
1439
+ { action: "stop", id: workflowRunId },
1440
+ new AbortController().signal,
1441
+ undefined,
1442
+ makeMinimalCtx(tempDir),
1443
+ );
1444
+ assert.equal(stopped.isError, undefined);
1445
+
1446
+ let status: AsyncStatus = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
1447
+ for (let attempt = 0; attempt < 100 && status.state !== "stopped"; attempt++) {
1448
+ await new Promise((resolve) => setTimeout(resolve, 20));
1449
+ status = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
1450
+ }
1451
+ assert.equal(status.state, "stopped");
1452
+ assert.equal(status.error, "Workflow stopped by user.");
1453
+ assert.equal(status.steps?.[0]?.status, "stopped");
1454
+ assert.equal(status.steps?.[0]?.stopped, true);
1455
+ assert.equal(status.steps?.[0]?.error, "Workflow stopped by user.");
1456
+ assert.equal(status.workflow?.trace.some((entry) => entry.key === "review" && entry.state === "stopped"), true);
1457
+ assert.equal(status.workflow?.trace.some((entry) => entry.key === "review" && entry.state === "failed"), false);
1458
+
1459
+ let childSettled = false;
1460
+ for (let attempt = 0; attempt < 100; attempt++) {
1461
+ try {
1462
+ process.kill(childPid, 0);
1463
+ } catch (error) {
1464
+ if ((error as NodeJS.ErrnoException).code === "ESRCH") {
1465
+ childSettled = true;
1466
+ break;
1467
+ }
1468
+ throw error;
1469
+ }
1470
+ await new Promise((resolve) => setTimeout(resolve, 20));
1471
+ }
1472
+ assert.equal(childSettled, true, "child process must settle after the workflow stop");
1473
+ await new Promise((resolve) => setTimeout(resolve, 50));
1474
+ status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
1475
+ assert.equal(status.steps?.[0]?.status, "stopped");
1476
+ assert.equal(status.steps?.[0]?.stopped, true);
1477
+ assert.equal(status.steps?.[0]?.error, "Workflow stopped by user.");
1478
+
1479
+ fs.rmSync(started.details.asyncDir!, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
1480
+ fs.rmSync(path.join(DIRS.results, `${workflowRunId}.json`), { force: true });
1481
+ });
1482
+
1483
+ it("stops one live async workflow child without stopping the parent or sibling", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1484
+ mockPi.onCall({ matchArgIncludes: "Slow child", delay: 5_000, output: "slow late" });
1485
+ mockPi.onCall({ matchArgIncludes: "Fast child", delay: 250, output: "fast done" });
1486
+ const asyncJobs: SubagentState["asyncJobs"] = new Map();
1487
+ const executor = makeExecutor([makeAgent("echo")], {}, false, undefined, true, asyncJobs);
1488
+ const started = await executor.execute(
1489
+ `workflow-child-stop-${Date.now()}`,
1490
+ {
1491
+ workflowScript: `
1492
+ const results = await runs.all([
1493
+ { key: "slow", agent: "echo", task: "Slow child" },
1494
+ { key: "fast", agent: "echo", task: "Fast child" }
1495
+ ]);
1496
+ return results.map((result) => result.key + ":" + (result.stopped ? "stopped" : result.ok ? "ok" : "failed")).join(",");
1497
+ `,
1498
+ },
1499
+ new AbortController().signal,
1500
+ undefined,
1501
+ makeMinimalCtx(tempDir),
1502
+ );
1503
+ assert.equal(started.isError, undefined);
1504
+ assert.ok(started.details.asyncDir);
1505
+ const workflowRunId = started.details.asyncId!;
1506
+ const statusPath = path.join(started.details.asyncDir, "status.json");
1507
+ const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
1508
+ let status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
1509
+ for (let attempt = 0; attempt < 150 && !status.steps?.some((step) => step.workflowKey === "slow" && step.status === "running"); attempt++) {
1510
+ await new Promise((resolve) => setTimeout(resolve, 20));
1511
+ status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
1512
+ }
1513
+ const listed = listAsyncRuns(DIRS.async, { sessionId: "session-123", runId: workflowRunId, exactRunId: true })
1514
+ .find((run) => run.id === workflowRunId);
1515
+ assert.equal(listed?.steps?.find((step) => step.workflowKey === "slow")?.childId, "slow");
1516
+
1517
+ const stop = await executor.execute(
1518
+ "stop-workflow-child-only",
1519
+ { action: "stop", id: workflowRunId, childId: "slow" },
1520
+ new AbortController().signal,
1521
+ undefined,
1522
+ makeMinimalCtx(tempDir),
1523
+ );
1524
+ assert.equal(stop.isError, undefined, stop.content[0]?.text ?? "");
1525
+ assert.match(stop.content[0]?.text ?? "", /Stop requested for child slow/);
1526
+
1527
+ for (let attempt = 0; attempt < 150 && status.state !== "complete"; attempt++) {
1528
+ await new Promise((resolve) => setTimeout(resolve, 20));
1529
+ status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
1530
+ }
1531
+ assert.equal(status.state, "complete", status.error);
1532
+ assert.equal(status.stopped, undefined);
1533
+ assert.equal(status.steps?.find((step) => step.workflowKey === "slow")?.status, "stopped");
1534
+ assert.equal(status.steps?.find((step) => step.workflowKey === "slow")?.stopped, true);
1535
+ assert.equal(status.steps?.find((step) => step.workflowKey === "fast")?.status, "completed");
1536
+ assert.equal(status.steps?.find((step) => step.workflowKey === "fast")?.stopped, undefined);
1537
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { state?: string; stopped?: boolean; results?: Array<{ workflowKey?: string; success?: boolean; stopped?: boolean }> };
1538
+ assert.equal(payload.state, "complete");
1539
+ assert.equal(payload.stopped, undefined);
1540
+ assert.equal(payload.results?.find((entry) => entry.workflowKey === "slow")?.stopped, true);
1541
+ assert.equal(payload.results?.find((entry) => entry.workflowKey === "fast")?.success, true);
1542
+ const childStatusEvents = fs.readFileSync(path.join(started.details.asyncDir, "events.jsonl"), "utf-8")
1543
+ .trim()
1544
+ .split("\n")
1545
+ .map((line) => JSON.parse(line) as { type?: string; childId?: string; status?: string });
1546
+ assert.ok(childStatusEvents.some((event) => event.type === "subagent.child-status" && event.childId === "slow" && event.status === "stopping"));
1547
+ assert.ok(childStatusEvents.some((event) => event.type === "subagent.child-status" && event.childId === "slow" && event.status === "stopped"));
1548
+ fs.rmSync(started.details.asyncDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
1549
+ fs.rmSync(resultPath, { force: true });
1550
+ });
1551
+
1552
+ it("reports completed async workflows as not running when stopped after completion", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1553
+ const runId = `workflow-stop-complete-${Date.now()}`;
1554
+ const asyncDir = path.join(DIRS.async, runId);
1555
+ fs.mkdirSync(asyncDir, { recursive: true });
1556
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
1557
+ runId,
1558
+ sessionId: "session-123",
1559
+ mode: "workflow",
1560
+ state: "complete",
1561
+ startedAt: Date.now(),
1562
+ lastUpdate: Date.now(),
1563
+ cwd: tempDir,
1564
+ pid: process.pid,
1565
+ }), "utf-8");
1566
+ const asyncJobs: SubagentState["asyncJobs"] = new Map([[runId, {
1567
+ asyncId: runId,
1568
+ asyncDir,
1569
+ cwd: tempDir,
1570
+ status: "complete",
1571
+ mode: "workflow",
877
1572
  agents: [],
878
1573
  steps: [],
879
1574
  startedAt: Date.now(),
@@ -939,10 +1634,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
939
1634
  const mission = JSON.parse(fs.readFileSync(path.join(agentDir, "missions", "projects", missionFiles[0]!), "utf-8")) as { objective?: string };
940
1635
  assert.equal(mission.objective, utils.PROMPT_REDACTED);
941
1636
  assert.deepEqual(result.details.workflow?.trace.filter((entry) => entry.state === "completed").map((entry) => entry.key), ["scan", "review"]);
942
- // Selesai fork: artifactDir defaults to "project" (.pi-subagents), so the
943
- // tree keeps untracked runtime state; only that dir may appear.
944
- const porcelain = execFileSync("git", ["status", "--porcelain"], { cwd: projectDir, encoding: "utf-8" });
945
- assert.match(porcelain, /^\?\? \.pi-subagents\/\n$/);
1637
+ assert.equal(execFileSync("git", ["status", "--porcelain"], { cwd: projectDir, encoding: "utf-8" }), "");
946
1638
  } finally {
947
1639
  if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
948
1640
  else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
@@ -954,7 +1646,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
954
1646
  mockPi.onCall({ output: "reviewed auth" });
955
1647
  const blockedIndex = path.join(tempDir, "blocked-mission-index");
956
1648
  fs.writeFileSync(blockedIndex, "not a directory", "utf-8");
957
- const executor = makeExecutor([makeAgent("echo")], { missions: { directory: ".pi/subagents/missions", globalIndexDir: blockedIndex } });
1649
+ const executor = makeExecutor([makeAgent("echo")], { missions: { directory: ".pi-subagents/missions", globalIndexDir: blockedIndex } });
958
1650
 
959
1651
  const result = await executor.execute(
960
1652
  "scripted-workflow-mission-warning",
@@ -975,7 +1667,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
975
1667
  assert.equal(result.details.missionId, undefined);
976
1668
  assert.match(result.details.missionWarning ?? "", /Mission tracking unavailable/);
977
1669
  assert.equal(result.details.results.length, 2);
978
- const missionDir = path.join(tempDir, ".pi/subagents", "missions");
1670
+ const missionDir = path.join(tempDir, ".pi-subagents", "missions");
979
1671
  const missionFiles = fs.existsSync(missionDir) ? fs.readdirSync(missionDir).filter((entry) => entry.endsWith(".json")) : [];
980
1672
  assert.equal(missionFiles.length, 1);
981
1673
  });
@@ -1041,7 +1733,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1041
1733
  execFileSync("git", ["add", "base.txt"], { cwd: tempDir });
1042
1734
  execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
1043
1735
  mockPi.onCall({ output: "isolated feature", writeFiles: [{ path: "feature.txt", content: "feature\n" }] });
1044
- const executor = makeExecutor([makeAgent("worker")]);
1736
+ const executor = makeExecutor([makeAgent("worker", { completionGuard: false })]);
1045
1737
 
1046
1738
  const result = await executor.execute(
1047
1739
  "direct-worktree",
@@ -1051,7 +1743,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1051
1743
  makeMinimalCtx(tempDir),
1052
1744
  );
1053
1745
 
1054
- assert.equal(result.isError, undefined);
1746
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "managed worktree child failed");
1055
1747
  assert.equal(mockPi.callCount(), 1);
1056
1748
  assert.equal(fs.existsSync(path.join(tempDir, "feature.txt")), false);
1057
1749
  const handoffPath = (result.content[0]?.text ?? "").match(/([^\s]+\/handoffs\/[^\s]+\.json)/)?.[1];
@@ -1092,38 +1784,664 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1092
1784
  const executor = makeExecutor([makeAgent("worker", { defaultContext: "fork" })]);
1093
1785
 
1094
1786
  const result = await executor.execute(
1095
- "forked-worktree-workflow",
1096
- { async: false, workflowScript: `return runs.run("isolated", { agent: "worker", task: "Work in isolation", worktree: true });` },
1787
+ "forked-worktree-workflow",
1788
+ { async: false, workflowScript: `return runs.run("isolated", { agent: "worker", task: "Work in isolation", worktree: true });` },
1789
+ new AbortController().signal,
1790
+ undefined,
1791
+ ctx,
1792
+ );
1793
+
1794
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
1795
+ const workflowValue = result.details.workflow?.value as { artifactPaths?: string[] } | undefined;
1796
+ const handoffPath = workflowValue?.artifactPaths?.find((candidate) => candidate.endsWith(".json") && candidate.includes("handoffs"));
1797
+ assert.ok(handoffPath, JSON.stringify(workflowValue));
1798
+ const handoff = JSON.parse(fs.readFileSync(handoffPath, "utf-8")) as {
1799
+ groups: Array<{ cleanup: { tasks: Array<{ path: string }> } }>;
1800
+ };
1801
+ const managedWorktreeCwd = handoff.groups[0]?.cleanup.tasks[0]?.path;
1802
+ assert.ok(managedWorktreeCwd);
1803
+ const callCwd = readCall().cwd;
1804
+ assert.ok(callCwd);
1805
+ assert.notEqual(path.resolve(callCwd), path.resolve(tempDir));
1806
+ assert.equal(path.basename(callCwd), path.basename(managedWorktreeCwd));
1807
+ const sessionHeader = JSON.parse(fs.readFileSync(childSessionFile, "utf-8").split("\n", 1)[0]!) as { cwd?: string };
1808
+ assert.ok(sessionHeader.cwd);
1809
+ assert.equal(path.basename(sessionHeader.cwd), path.basename(callCwd));
1810
+ });
1811
+
1812
+ it("rejects workflowScript implementation children under a read-only capability ceiling before spawn", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1813
+ mockPi.onCall({ output: "completed without edits" });
1814
+ const executor = makeExecutor([makeAgent("worker")]);
1815
+
1816
+ const result = await executor.execute(
1817
+ "workflow-readonly-implementation-contract",
1818
+ {
1819
+ async: false,
1820
+ workflowScript: `return await runs.run("impl", { agent: "worker", task: "Implement the requested source fix" });`,
1821
+ capabilityCeiling: { version: 1, allowedTools: ["read", "grep", "find", "ls", "contact_supervisor"], denyExtensions: true, sources: ["test"] },
1822
+ },
1823
+ new AbortController().signal,
1824
+ undefined,
1825
+ makeMinimalCtx(tempDir),
1826
+ );
1827
+
1828
+ assert.equal(result.isError, true);
1829
+ assert.match(result.content[0]?.text ?? "", /no mutation-capable tools/);
1830
+ assert.doesNotMatch(result.content[0]?.text ?? "", /completed without making edits/);
1831
+ assert.equal(mockPi.callCount(), 0);
1832
+ });
1833
+
1834
+ it("stringifies workflow child results without object placeholders", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1835
+ mockPi.onCall({ output: "first report", matchArgIncludes: "Review" });
1836
+ mockPi.onCall({ output: "second report", matchArgIncludes: "Monitor" });
1837
+ const executor = makeExecutor([makeAgent("echo")]);
1838
+
1839
+ const result = await executor.execute(
1840
+ "scripted-workflow-stringified-child-results",
1841
+ {
1842
+ async: false,
1843
+ workflowScript: `
1844
+ const [review, monitor] = await runs.all([
1845
+ { key: "review", agent: "echo", task: "Review" },
1846
+ { key: "monitor", agent: "echo", task: "Monitor" }
1847
+ ]);
1848
+ return "## Lane 1\\n" + review + "\\n\\n---\\n\\n## Lane 2\\n" + monitor;
1849
+ `,
1850
+ },
1851
+ new AbortController().signal,
1852
+ undefined,
1853
+ makeMinimalCtx(tempDir),
1854
+ );
1855
+
1856
+ const text = result.content[0]?.text ?? "";
1857
+ assert.equal(result.isError, undefined, text || "workflow failed");
1858
+ assert.doesNotMatch(text, /\[object Object\]/);
1859
+ assert.match(text, /## Lane 1\nfirst report/);
1860
+ assert.match(text, /## Lane 2\nsecond report/);
1861
+ assert.equal(result.details.workflow?.value, "## Lane 1\nfirst report\n\n---\n\n## Lane 2\nsecond report");
1862
+ });
1863
+
1864
+ it("stringifies awaited workflow child results without object placeholders", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1865
+ mockPi.onCall({ output: "single report", matchArgIncludes: "Review" });
1866
+ const executor = makeExecutor([makeAgent("echo")]);
1867
+
1868
+ const result = await executor.execute(
1869
+ "scripted-workflow-stringified-single-child-result",
1870
+ {
1871
+ async: false,
1872
+ workflowScript: `
1873
+ const review = await runs.run("review", { agent: "echo", task: "Review" });
1874
+ return "## Lane\\n" + review;
1875
+ `,
1876
+ },
1877
+ new AbortController().signal,
1878
+ undefined,
1879
+ makeMinimalCtx(tempDir),
1880
+ );
1881
+
1882
+ const text = result.content[0]?.text ?? "";
1883
+ assert.equal(result.isError, undefined, text || "workflow failed");
1884
+ assert.doesNotMatch(text, /\[object Object\]/);
1885
+ assert.match(text, /## Lane\nsingle report/);
1886
+ assert.equal(result.details.workflow?.value, "## Lane\nsingle report");
1887
+ });
1888
+
1889
+ it("derives workflow child output paths from the workflow output", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1890
+ mockPi.onCall({ output: "first report", matchArgIncludes: "Review" });
1891
+ mockPi.onCall({ output: "second report", matchArgIncludes: "Monitor" });
1892
+ const executor = makeExecutor([makeAgent("echo")]);
1893
+ const workflowOutput = path.join(tempDir, "workflow-report.md");
1894
+
1895
+ const result = await executor.execute(
1896
+ "scripted-workflow-child-output-defaults",
1897
+ {
1898
+ async: false,
1899
+ output: workflowOutput,
1900
+ workflowScript: `
1901
+ const children = await runs.all([
1902
+ { key: "review", agent: "echo", task: "Review" },
1903
+ { key: "monitor", agent: "echo", task: "Monitor" }
1904
+ ]);
1905
+ return children.map(({ key, artifactPaths }) => ({ key, artifactPaths }));
1906
+ `,
1907
+ },
1908
+ new AbortController().signal,
1909
+ undefined,
1910
+ makeMinimalCtx(tempDir),
1911
+ );
1912
+
1913
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
1914
+ assert.match(fs.readFileSync(workflowOutput, "utf-8"), /Workflow completed\./);
1915
+ const value = result.details.workflow?.value as Array<{ key: string; artifactPaths: string[] }>;
1916
+ const childOutputs = value.map((child) => child.artifactPaths.find((candidate) => candidate.endsWith(".md")) ?? "").sort();
1917
+ assert.deepEqual(childOutputs, [
1918
+ path.join(tempDir, "workflow-report.monitor.md"),
1919
+ path.join(tempDir, "workflow-report.review.md"),
1920
+ ]);
1921
+ assert.equal(fs.readFileSync(path.join(tempDir, "workflow-report.review.md"), "utf-8"), "first report");
1922
+ assert.equal(fs.readFileSync(path.join(tempDir, "workflow-report.monitor.md"), "utf-8"), "second report");
1923
+ });
1924
+
1925
+ it("uses child-cwd agent output defaults for omitted workflow child output", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1926
+ mockPi.onCall({ output: "app report" });
1927
+ const appDir = path.join(tempDir, "packages", "app");
1928
+ fs.mkdirSync(appDir, { recursive: true });
1929
+ const rootAgents = [makeAgent("echo", { output: "root-report.md" })];
1930
+ const appAgents = [makeAgent("echo", { output: "app-report.md" })];
1931
+ const executor = makeExecutor(rootAgents, {}, false, undefined, true, new Map(), undefined, undefined, createEventBus(), (cwd) => path.resolve(cwd) === path.resolve(appDir) ? appAgents : rootAgents);
1932
+
1933
+ const result = await executor.execute(
1934
+ "scripted-workflow-child-cwd-omitted-output-default",
1935
+ {
1936
+ async: false,
1937
+ workflowScript: `return await runs.run("app", { agent: "echo", task: "Review app", cwd: "packages/app" });`,
1938
+ },
1939
+ new AbortController().signal,
1940
+ undefined,
1941
+ makeMinimalCtx(tempDir),
1942
+ );
1943
+
1944
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
1945
+ assert.equal(fs.existsSync(path.join(appDir, "app-report.md")), false);
1946
+ assert.ok(result.details.results[0]?.savedOutputPath && pathContainsSegments(result.details.results[0].savedOutputPath, "artifacts", "outputs", "scripted-workflow-child-cwd-omitted-output-default"));
1947
+ assert.equal(path.basename(result.details.results[0]?.savedOutputPath ?? ""), "app-report.md");
1948
+ assert.equal(fs.readFileSync(result.details.results[0]?.savedOutputPath ?? "", "utf-8"), "app report");
1949
+ assert.equal(fs.existsSync(path.join(tempDir, "root-report.md")), false);
1950
+ });
1951
+
1952
+ it("uses child-cwd agent output defaults for workflow output true", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1953
+ mockPi.onCall({ output: "app report" });
1954
+ const appDir = path.join(tempDir, "packages", "app");
1955
+ fs.mkdirSync(appDir, { recursive: true });
1956
+ const rootAgents = [makeAgent("echo", { output: "root-report.md" })];
1957
+ const appAgents = [makeAgent("echo", { output: "app-report.md" })];
1958
+ const executor = makeExecutor(rootAgents, {}, false, undefined, true, new Map(), undefined, undefined, createEventBus(), (cwd) => path.resolve(cwd) === path.resolve(appDir) ? appAgents : rootAgents);
1959
+
1960
+ const result = await executor.execute(
1961
+ "scripted-workflow-child-cwd-output-default",
1962
+ {
1963
+ async: false,
1964
+ workflowScript: `return await runs.run("app", { agent: "echo", task: "Review app", cwd: "packages/app", output: true });`,
1965
+ },
1966
+ new AbortController().signal,
1967
+ undefined,
1968
+ makeMinimalCtx(tempDir),
1969
+ );
1970
+
1971
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
1972
+ assert.equal(fs.existsSync(path.join(appDir, "app-report.md")), false);
1973
+ assert.ok(result.details.results[0]?.savedOutputPath && pathContainsSegments(result.details.results[0].savedOutputPath, "artifacts", "outputs", "scripted-workflow-child-cwd-output-default"));
1974
+ assert.equal(path.basename(result.details.results[0]?.savedOutputPath ?? ""), "app-report.md");
1975
+ assert.equal(fs.readFileSync(result.details.results[0]?.savedOutputPath ?? "", "utf-8"), "app report");
1976
+ assert.equal(fs.existsSync(path.join(tempDir, "root-report.md")), false);
1977
+ });
1978
+
1979
+ it("reports workflow aggregate output write failures without throwing", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1980
+ const outputDir = path.join(tempDir, "aggregate-dir");
1981
+ fs.mkdirSync(outputDir);
1982
+ const result = await makeExecutor([makeAgent("echo")]).execute(
1983
+ "scripted-workflow-aggregate-output-write-error",
1984
+ {
1985
+ async: false,
1986
+ output: outputDir,
1987
+ workflowScript: `return "ok";`,
1988
+ },
1989
+ new AbortController().signal,
1990
+ undefined,
1991
+ makeMinimalCtx(tempDir),
1992
+ );
1993
+
1994
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
1995
+ assert.match(result.content[0]?.text ?? "", /Workflow completed\./);
1996
+ assert.match(result.content[0]?.text ?? "", /Output file error:/);
1997
+ assert.match(result.content[0]?.text ?? "", new RegExp(escapeRegExp(outputDir)));
1998
+ });
1999
+
2000
+ it("routes workflow relative outputs to the run output artifact directory", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2001
+ mockPi.onCall({ output: "child report" });
2002
+ const executor = makeExecutor([makeAgent("echo")]);
2003
+
2004
+ const result = await executor.execute(
2005
+ "scripted-workflow-relative-output-base",
2006
+ {
2007
+ async: false,
2008
+ output: "workflow-summary.md",
2009
+ workflowScript: `return await runs.run("review", { agent: "echo", task: "Review", output: "plans/review.md" });`,
2010
+ },
2011
+ new AbortController().signal,
2012
+ undefined,
2013
+ makeMinimalCtx(tempDir),
2014
+ );
2015
+
2016
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
2017
+ assert.equal(fs.existsSync(path.join(tempDir, "workflow-summary.md")), false);
2018
+ assert.equal(fs.existsSync(path.join(tempDir, "plans", "review.md")), false);
2019
+ const workflowOutputPath = path.join(TEMP_ARTIFACTS_DIR, "outputs", "scripted-workflow-relative-output-base", "workflow-summary.md");
2020
+ assert.match(fs.readFileSync(workflowOutputPath, "utf-8"), /Workflow completed\./);
2021
+ assert.ok(result.details.results[0]?.savedOutputPath && pathContainsSegments(result.details.results[0].savedOutputPath, "artifacts", "outputs", "scripted-workflow-relative-output-base", "plans"));
2022
+ assert.equal(path.basename(result.details.results[0]?.savedOutputPath ?? ""), "review.md");
2023
+ assert.equal(fs.readFileSync(result.details.results[0]?.savedOutputPath ?? "", "utf-8"), "child report");
2024
+ });
2025
+
2026
+ it("rejects workflow child output collisions before launch", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2027
+ const executor = makeExecutor([makeAgent("echo")]);
2028
+ const sharedOutput = path.join(tempDir, "shared.md");
2029
+
2030
+ const duplicate = await executor.execute(
2031
+ "scripted-workflow-duplicate-child-output",
2032
+ {
2033
+ async: false,
2034
+ workflowScript: `return await runs.all([
2035
+ { key: "review", agent: "echo", task: "Review", output: ${JSON.stringify(sharedOutput)} },
2036
+ { key: "monitor", agent: "echo", task: "Monitor", output: ${JSON.stringify(sharedOutput)} }
2037
+ ]);`,
2038
+ },
2039
+ new AbortController().signal,
2040
+ undefined,
2041
+ makeMinimalCtx(tempDir),
2042
+ );
2043
+
2044
+ assert.equal(duplicate.isError, undefined, duplicate.content[0]?.text ?? "workflow failed");
2045
+ const duplicateChildren = duplicate.details.workflow?.value as Array<{ ok: boolean; error?: string }>;
2046
+ assert.deepEqual(duplicateChildren.map(({ ok }) => ok), [false, false]);
2047
+ for (const child of duplicateChildren) {
2048
+ assert.match(child.error ?? "", /Workflow children 'review' and 'monitor' resolve output to the same path/);
2049
+ assert.match(child.error ?? "", new RegExp(escapeRegExp(sharedOutput)));
2050
+ }
2051
+ assert.equal(mockPi.callCount(), 0);
2052
+
2053
+ const relativeDuplicateOutput = "relative-shared.md";
2054
+ const relativeDuplicate = await executor.execute(
2055
+ "scripted-workflow-relative-duplicate-child-output",
2056
+ {
2057
+ async: false,
2058
+ workflowScript: `return await runs.all([
2059
+ { key: "review", agent: "echo", task: "Review", output: ${JSON.stringify(relativeDuplicateOutput)} },
2060
+ { key: "monitor", agent: "echo", task: "Monitor", output: ${JSON.stringify(relativeDuplicateOutput)} }
2061
+ ]);`,
2062
+ },
2063
+ new AbortController().signal,
2064
+ undefined,
2065
+ makeMinimalCtx(tempDir),
2066
+ );
2067
+
2068
+ assert.equal(relativeDuplicate.isError, undefined, relativeDuplicate.content[0]?.text ?? "workflow failed");
2069
+ const relativeDuplicateChildren = relativeDuplicate.details.workflow?.value as Array<{ ok: boolean; error?: string }>;
2070
+ assert.deepEqual(relativeDuplicateChildren.map(({ ok }) => ok), [false, false]);
2071
+ for (const child of relativeDuplicateChildren) {
2072
+ assert.match(child.error ?? "", /Workflow children 'review' and 'monitor' resolve output to the same path/);
2073
+ assert.match(child.error ?? "", new RegExp(`${escapeRegExp(TEMP_ARTIFACTS_DIR)}.*outputs.*${escapeRegExp(relativeDuplicateOutput)}`));
2074
+ }
2075
+ assert.equal(mockPi.callCount(), 0);
2076
+ });
2077
+
2078
+ it("isolates colliding inherited agent-default outputs for parallel workflow children", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2079
+ mockPi.onCall({ output: "review report", matchArgIncludes: "Review" });
2080
+ mockPi.onCall({ output: "monitor report", matchArgIncludes: "Monitor" });
2081
+ const result = await makeExecutor([makeAgent("echo", { output: "context.md" })]).execute(
2082
+ "scripted-workflow-parallel-inherited-output-collision",
2083
+ {
2084
+ async: false,
2085
+ workflowScript: `return await runs.all([
2086
+ { key: "review", agent: "echo", task: "Review" },
2087
+ { key: "monitor", agent: "echo", task: "Monitor" }
2088
+ ]);`,
2089
+ },
2090
+ new AbortController().signal,
2091
+ undefined,
2092
+ makeMinimalCtx(tempDir),
2093
+ );
2094
+
2095
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
2096
+ const children = result.details.workflow?.value as Array<{ ok: boolean }>;
2097
+ assert.deepEqual(children.map(({ ok }) => ok), [true, true]);
2098
+ const outputPaths = result.details.results.map(({ savedOutputPath }) => savedOutputPath ?? "").sort();
2099
+ assert.equal(outputPaths.length, 2);
2100
+ assert.notEqual(outputPaths[0], outputPaths[1]);
2101
+ assert.ok(pathContainsSegments(outputPaths[0]!, "artifacts", "outputs"));
2102
+ assert.ok(pathContainsSegments(outputPaths[1]!, "artifacts", "outputs"));
2103
+ assert.match(path.basename(outputPaths[0]!), /^(monitor|review)\.md$/);
2104
+ assert.match(path.basename(outputPaths[1]!), /^(monitor|review)\.md$/);
2105
+ assert.deepEqual(outputPaths.map((outputPath) => fs.readFileSync(outputPath, "utf-8")).sort(), ["monitor report", "review report"]);
2106
+ assert.equal(fs.existsSync(path.join(tempDir, "context.md")), false);
2107
+ });
2108
+
2109
+ it("isolates inherited outputs that collide with a resumed child output", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2110
+ const retainedRunId = `retained-output-${Date.now()}`;
2111
+ const retainedAsyncDir = path.join(DIRS.async, retainedRunId);
2112
+ const retainedSessionFile = path.join(tempDir, "retained-session.jsonl");
2113
+ const retainedOutputPath = path.join(tempDir, "context.md");
2114
+ const runFanoutBudget = createRunFanoutBudget(retainedRunId, 10);
2115
+ fs.mkdirSync(retainedAsyncDir, { recursive: true });
2116
+ fs.writeFileSync(retainedSessionFile, "{}\n", "utf-8");
2117
+ fs.writeFileSync(path.join(retainedAsyncDir, "status.json"), JSON.stringify({
2118
+ runId: retainedRunId,
2119
+ sessionId: "session-123",
2120
+ state: "failed",
2121
+ cwd: tempDir,
2122
+ sessionFile: retainedSessionFile,
2123
+ steps: [
2124
+ { agent: "echo", status: "failed", sessionFile: retainedSessionFile },
2125
+ { agent: "echo", status: "failed", sessionFile: retainedSessionFile },
2126
+ ],
2127
+ }), "utf-8");
2128
+ fs.writeFileSync(path.join(retainedAsyncDir, "recovery-descriptor.json"), JSON.stringify({
2129
+ version: 1,
2130
+ runFanoutBudget,
2131
+ sourceRunId: retainedRunId,
2132
+ agent: "echo",
2133
+ cwd: tempDir,
2134
+ systemPromptMode: "append",
2135
+ inheritProjectContext: true,
2136
+ inheritSkills: true,
2137
+ outputPath: retainedOutputPath,
2138
+ outputMode: "inline",
2139
+ maxSubagentDepth: 1,
2140
+ share: false,
2141
+ }), "utf-8");
2142
+ mockPi.onCall({ output: "resumed report", matchArgIncludes: "Resume" });
2143
+ mockPi.onCall({ output: "review report", matchArgIncludes: "Review" });
2144
+
2145
+ try {
2146
+ const result = await makeExecutor([makeAgent("echo", { output: "context.md" })]).execute(
2147
+ "scripted-workflow-resumed-inherited-output-collision",
2148
+ {
2149
+ async: false,
2150
+ workflowScript: `return await runs.all([
2151
+ { key: "resume", resume: ${JSON.stringify(retainedRunId)}, index: 1, task: "Resume" },
2152
+ { key: "review", agent: "echo", task: "Review" }
2153
+ ]);`,
2154
+ },
2155
+ new AbortController().signal,
2156
+ undefined,
2157
+ makeMinimalCtx(tempDir),
2158
+ );
2159
+
2160
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
2161
+ const children = result.details.workflow?.value as Array<{ ok: boolean }>;
2162
+ assert.deepEqual(children.map(({ ok }) => ok), [true, true]);
2163
+ assert.equal(fs.readFileSync(retainedOutputPath, "utf-8"), "resumed report");
2164
+ const outputPaths = result.details.results.map(({ savedOutputPath }) => savedOutputPath ?? "");
2165
+ const inheritedOutputPaths = outputPaths.filter((outputPath) => outputPath && outputPath !== retainedOutputPath).sort();
2166
+ assert.deepEqual(inheritedOutputPaths.map((outputPath) => path.basename(outputPath)), ["context.md"]);
2167
+ assert.ok(inheritedOutputPaths.every((outputPath) => pathContainsSegments(outputPath, "artifacts", "outputs")));
2168
+ } finally {
2169
+ fs.rmSync(retainedAsyncDir, { recursive: true, force: true });
2170
+ fs.rmSync(runFanoutBudget.directory, { recursive: true, force: true });
2171
+ }
2172
+ });
2173
+
2174
+ it("reroutes a later inherited agent-default output collision", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2175
+ mockPi.onCall({ output: "first report", matchArgIncludes: "First" });
2176
+ mockPi.onCall({ output: "second report", matchArgIncludes: "Second" });
2177
+ const result = await makeExecutor([makeAgent("echo", { output: "context.md" })]).execute(
2178
+ "scripted-workflow-sequential-inherited-output-collision",
2179
+ {
2180
+ async: false,
2181
+ workflowScript: `
2182
+ const first = await runs.run("first", { agent: "echo", task: "First" });
2183
+ const second = await runs.run("second", { agent: "echo", task: "Second" });
2184
+ return [first, second];
2185
+ `,
2186
+ },
2187
+ new AbortController().signal,
2188
+ undefined,
2189
+ makeMinimalCtx(tempDir),
2190
+ );
2191
+
2192
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
2193
+ const children = result.details.workflow?.value as Array<{ ok: boolean }>;
2194
+ assert.deepEqual(children.map(({ ok }) => ok), [true, true]);
2195
+ assert.equal(fs.existsSync(path.join(tempDir, "context.md")), false);
2196
+ const outputPaths = result.details.results.map(({ savedOutputPath }) => savedOutputPath ?? "");
2197
+ assert.ok(pathContainsSegments(outputPaths[0]!, "artifacts", "outputs"));
2198
+ assert.equal(path.basename(outputPaths[0]!), "context.md");
2199
+ assert.equal(fs.readFileSync(outputPaths[0]!, "utf-8"), "first report");
2200
+ assert.ok(pathContainsSegments(outputPaths[1]!, "artifacts", "outputs"));
2201
+ assert.equal(path.basename(outputPaths[1]!), "second.md");
2202
+ assert.equal(fs.readFileSync(outputPaths[1]!, "utf-8"), "second report");
2203
+ });
2204
+
2205
+ it("preserves a rejected file-only child report when its path matches workflow output", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2206
+ const usefulReport = "# Review findings\n\nThe implementation loses the final report.";
2207
+ const sharedOutput = path.join(tempDir, "review.md");
2208
+ mockPi.onCall({ stdoutRaw: `${JSON.stringify(events.assistantMessage(usefulReport))}\n` });
2209
+ const executor = makeExecutor([makeAgent("reviewer", { tools: ["read"], completionGuard: false })]);
2210
+
2211
+ const result = await executor.execute(
2212
+ "scripted-workflow-file-only-acceptance-collision",
2213
+ {
2214
+ async: false,
2215
+ output: sharedOutput,
2216
+ workflowScript: `
2217
+ const child = await runs.run("review", {
2218
+ agent: "reviewer",
2219
+ task: "Write a structured review report.",
2220
+ output: ${JSON.stringify(sharedOutput)},
2221
+ outputMode: "file-only",
2222
+ acceptance: { level: "checked", criteria: ["Return the structured review report"] }
2223
+ });
2224
+ if (!child.ok) throw new Error(child.error);
2225
+ return child;
2226
+ `,
2227
+ },
2228
+ new AbortController().signal,
2229
+ undefined,
2230
+ makeMinimalCtx(tempDir),
2231
+ );
2232
+
2233
+ assert.equal(result.isError, true);
2234
+ assert.match(result.content[0]?.text ?? "", new RegExp(escapeRegExp(sharedOutput)));
2235
+ assert.equal(result.details.results[0]?.acceptance?.status, "rejected");
2236
+ assert.match(result.details.results[0]?.acceptance?.runtimeChecks[0]?.message ?? "", /Structured acceptance report not found/);
2237
+ assert.equal(result.details.results[0]?.savedOutputPath, sharedOutput);
2238
+ assert.deepEqual(fs.readFileSync(sharedOutput), Buffer.from(usefulReport));
2239
+ });
2240
+
2241
+ it("replaces stale workflow output when a child claims its path but writes no report", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2242
+ const sharedOutput = path.join(tempDir, "failed-review.md");
2243
+ fs.writeFileSync(sharedOutput, "stale workflow output", "utf-8");
2244
+ mockPi.onCall({ exitCode: 1, stderr: "review child failed before writing output" });
2245
+ const executor = makeExecutor([makeAgent("reviewer", { completionGuard: false })]);
2246
+
2247
+ const result = await executor.execute(
2248
+ "scripted-workflow-missing-child-output-collision",
2249
+ {
2250
+ async: false,
2251
+ output: sharedOutput,
2252
+ workflowScript: `
2253
+ const child = await runs.run("review", {
2254
+ agent: "reviewer",
2255
+ task: "Write a review report.",
2256
+ output: ${JSON.stringify(sharedOutput)},
2257
+ outputMode: "file-only"
2258
+ });
2259
+ if (!child.ok) throw new Error(child.error);
2260
+ return child;
2261
+ `,
2262
+ },
2263
+ new AbortController().signal,
2264
+ undefined,
2265
+ makeMinimalCtx(tempDir),
2266
+ );
2267
+
2268
+ assert.equal(result.isError, true);
2269
+ assert.match(result.content[0]?.text ?? "", /review child failed before writing output/);
2270
+ const workflowOutput = fs.readFileSync(sharedOutput, "utf-8");
2271
+ assert.match(workflowOutput, /Workflow failed:.*review child failed before writing output/s);
2272
+ assert.doesNotMatch(workflowOutput, /stale workflow output/);
2273
+ });
2274
+
2275
+ it("rejects sequential workflow child output collisions before launch", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2276
+ mockPi.onCall({ output: "first report" });
2277
+ const executor = makeExecutor([makeAgent("echo")]);
2278
+ const sharedOutput = path.join(tempDir, "sequential-shared.md");
2279
+
2280
+ const result = await executor.execute(
2281
+ "scripted-workflow-sequential-output-collision",
2282
+ {
2283
+ async: false,
2284
+ workflowScript: `
2285
+ const first = await runs.run("review", { agent: "echo", task: "Review", output: ${JSON.stringify(sharedOutput)} });
2286
+ const second = await runs.run("monitor", { agent: "echo", task: "Monitor", output: ${JSON.stringify(sharedOutput)} })
2287
+ .catch((error) => ({ ok: false, error: error.message }));
2288
+ return [first, second];
2289
+ `,
2290
+ },
2291
+ new AbortController().signal,
2292
+ undefined,
2293
+ makeMinimalCtx(tempDir),
2294
+ );
2295
+
2296
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
2297
+ const children = result.details.workflow?.value as Array<{ ok: boolean; error?: string }>;
2298
+ assert.deepEqual(children.map(({ ok }) => ok), [true, false]);
2299
+ assert.match(children[1]?.error ?? "", /Workflow children 'review' and 'monitor' resolve output to the same path/);
2300
+ assert.match(children[1]?.error ?? "", new RegExp(escapeRegExp(sharedOutput)));
2301
+ assert.equal(mockPi.callCount(), 1);
2302
+ });
2303
+
2304
+ it("checks workflow child output collisions against configured output base", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2305
+ const configuredBase = path.join(tempDir, "configured-outputs");
2306
+ const workflowOutput = "shared.md";
2307
+
2308
+ const resolvedSharedOutput = path.join(configuredBase, workflowOutput);
2309
+ const agentDefaultResult = await makeExecutor([makeAgent("echo", { output: workflowOutput })], { singleRunOutputBaseDir: configuredBase }).execute(
2310
+ "scripted-workflow-configured-agent-default-output-collision",
2311
+ {
2312
+ async: false,
2313
+ workflowScript: `return await runs.all([
2314
+ { key: "review", agent: "echo", task: "Review", output: true },
2315
+ { key: "monitor", agent: "echo", task: "Monitor", output: true }
2316
+ ]);`,
2317
+ },
2318
+ new AbortController().signal,
2319
+ undefined,
2320
+ makeMinimalCtx(tempDir),
2321
+ );
2322
+
2323
+ assert.equal(agentDefaultResult.isError, undefined, agentDefaultResult.content[0]?.text ?? "workflow failed");
2324
+ const agentDefaultChildren = agentDefaultResult.details.workflow?.value as Array<{ ok: boolean; error?: string }>;
2325
+ assert.deepEqual(agentDefaultChildren.map(({ ok }) => ok), [false, false]);
2326
+ for (const child of agentDefaultChildren) {
2327
+ assert.match(child.error ?? "", /Workflow children 'review' and 'monitor' resolve output to the same path/);
2328
+ assert.match(child.error ?? "", new RegExp(escapeRegExp(resolvedSharedOutput)));
2329
+ }
2330
+ assert.equal(mockPi.callCount(), 0);
2331
+ });
2332
+
2333
+ it("lets runs.all siblings settle when one child fails", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2334
+ mockPi.onCall({ exitCode: 1, stderr: "first child failed" });
2335
+ mockPi.onCall({ output: "second child completed" });
2336
+ const executor = makeExecutor([makeAgent("echo")]);
2337
+
2338
+ const result = await executor.execute(
2339
+ "scripted-workflow-settlement",
2340
+ {
2341
+ async: false,
2342
+ workflowScript: `
2343
+ const children = await runs.all([
2344
+ { key: "first", agent: "echo", task: "First task" },
2345
+ { key: "second", agent: "echo", task: "Second task" }
2346
+ ]);
2347
+ return children.map(({ key, ok, error }) => error === undefined ? { key, ok } : { key, ok, error });
2348
+ `,
2349
+ },
2350
+ new AbortController().signal,
2351
+ undefined,
2352
+ makeMinimalCtx(tempDir),
2353
+ );
2354
+
2355
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
2356
+ assert.equal(mockPi.callCount(), 2);
2357
+ const value = result.details.workflow?.value as Array<{ key: string; ok: boolean; error?: string }>;
2358
+ assert.deepEqual(value.map(({ key }) => key), ["first", "second"]);
2359
+ assert.deepEqual(value.map(({ ok }) => ok).sort(), [false, true]);
2360
+ const failed = value.find(({ ok }) => !ok);
2361
+ const succeeded = value.find(({ ok }) => ok);
2362
+ assert.match(failed?.error ?? "", /first child failed/);
2363
+ assert.equal(succeeded?.error, undefined);
2364
+ assert.deepEqual(result.details.workflow?.trace.filter((entry) => entry.state !== "started").map(({ state }) => state).sort(), ["completed", "failed"]);
2365
+ });
2366
+
2367
+ it("reports keyed runs.all result access after siblings settle", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2368
+ mockPi.onCall({ output: "first child completed", matchArgIncludes: "First task" });
2369
+ mockPi.onCall({ output: "second child completed", matchArgIncludes: "Second task" });
2370
+ const executor = makeExecutor([makeAgent("echo")]);
2371
+
2372
+ const result = await executor.execute(
2373
+ "scripted-workflow-runs-all-keyed-result-access",
2374
+ {
2375
+ async: false,
2376
+ workflowScript: `
2377
+ const children = await runs.all([
2378
+ { key: "first", agent: "echo", task: "First task" },
2379
+ { key: "second", agent: "echo", task: "Second task" }
2380
+ ]);
2381
+ return children.first.output;
2382
+ `,
2383
+ },
2384
+ new AbortController().signal,
2385
+ undefined,
2386
+ makeMinimalCtx(tempDir),
2387
+ );
2388
+
2389
+ assert.equal(result.isError, true);
2390
+ assert.equal(mockPi.callCount(), 2);
2391
+ assert.match(result.content[0]?.text ?? "", /runs\.all resolves to an ordered array, not a key map/);
2392
+ assert.match(result.content[0]?.text ?? "", /Use results\[0\], array destructuring, or results\.map/);
2393
+ assert.deepEqual(result.details.workflow?.trace.filter((entry) => entry.state === "completed").map(({ key }) => key).sort(), ["first", "second"]);
2394
+ });
2395
+
2396
+ it("keeps array access working when runs.all child keys collide with array properties", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2397
+ mockPi.onCall({ output: "length child completed", matchArgIncludes: "Length task" });
2398
+ mockPi.onCall({ output: "map child completed", matchArgIncludes: "Map task" });
2399
+ const executor = makeExecutor([makeAgent("echo")]);
2400
+
2401
+ const result = await executor.execute(
2402
+ "scripted-workflow-runs-all-colliding-key-access",
2403
+ {
2404
+ async: false,
2405
+ workflowScript: `
2406
+ const children = await runs.all([
2407
+ { key: "length", agent: "echo", task: "Length task" },
2408
+ { key: "map", agent: "echo", task: "Map task" }
2409
+ ]);
2410
+ const [, second] = children;
2411
+ return {
2412
+ length: children.length,
2413
+ first: children[0].output,
2414
+ second: second.output,
2415
+ outputs: children.map((child) => child.output),
2416
+ children
2417
+ };
2418
+ `,
2419
+ },
1097
2420
  new AbortController().signal,
1098
2421
  undefined,
1099
- ctx,
2422
+ makeMinimalCtx(tempDir),
1100
2423
  );
1101
2424
 
1102
2425
  assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
1103
- const workflowValue = result.details.workflow?.value as { artifactPaths?: string[] } | undefined;
1104
- const handoffPath = workflowValue?.artifactPaths?.find((candidate) => candidate.endsWith(".json") && candidate.includes("handoffs"));
1105
- assert.ok(handoffPath, JSON.stringify(workflowValue));
1106
- const handoff = JSON.parse(fs.readFileSync(handoffPath, "utf-8")) as {
1107
- groups: Array<{ cleanup: { tasks: Array<{ path: string }> } }>;
1108
- };
1109
- const managedWorktreeCwd = handoff.groups[0]?.cleanup.tasks[0]?.path;
1110
- assert.ok(managedWorktreeCwd);
1111
- const callCwd = readCall().cwd;
1112
- assert.ok(callCwd);
1113
- assert.notEqual(path.resolve(callCwd), path.resolve(tempDir));
1114
- assert.equal(path.basename(callCwd), path.basename(managedWorktreeCwd));
1115
- const sessionHeader = JSON.parse(fs.readFileSync(childSessionFile, "utf-8").split("\n", 1)[0]!) as { cwd?: string };
1116
- assert.ok(sessionHeader.cwd);
1117
- assert.equal(path.basename(sessionHeader.cwd), path.basename(callCwd));
2426
+ assert.equal(mockPi.callCount(), 2);
2427
+ const value = result.details.workflow?.value as { length?: number; first?: string; second?: string; outputs?: string[]; children?: Array<{ key?: string; ok?: boolean; output?: string }> } | undefined;
2428
+ assert.equal(value?.length, 2);
2429
+ assert.equal(value?.first, "length child completed");
2430
+ assert.equal(value?.second, "map child completed");
2431
+ assert.deepEqual(value?.outputs, ["length child completed", "map child completed"]);
2432
+ assert.deepEqual(value?.children?.map(({ key, ok, output }) => ({ key, ok, output })), [
2433
+ { key: "length", ok: true, output: "length child completed" },
2434
+ { key: "map", ok: true, output: "map child completed" },
2435
+ ]);
1118
2436
  });
1119
2437
 
1120
- it("lets runs.all siblings settle when one child fails", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1121
- mockPi.onCall({ exitCode: 1, stderr: "first child failed" });
1122
- mockPi.onCall({ output: "second child completed" });
2438
+ it("emits runs.all results as plain arrays", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2439
+ mockPi.onCall({ output: "first child completed", matchArgIncludes: "First task" });
2440
+ mockPi.onCall({ output: "second child completed", matchArgIncludes: "Second task" });
1123
2441
  const executor = makeExecutor([makeAgent("echo")]);
1124
2442
 
1125
2443
  const result = await executor.execute(
1126
- "scripted-workflow-settlement",
2444
+ "scripted-workflow-runs-all-emitted-array",
1127
2445
  {
1128
2446
  async: false,
1129
2447
  workflowScript: `
@@ -1131,7 +2449,8 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1131
2449
  { key: "first", agent: "echo", task: "First task" },
1132
2450
  { key: "second", agent: "echo", task: "Second task" }
1133
2451
  ]);
1134
- return children.map(({ key, ok, error }) => error === undefined ? { key, ok } : { key, ok, error });
2452
+ emit(children);
2453
+ return children.map((child) => child.output);
1135
2454
  `,
1136
2455
  },
1137
2456
  new AbortController().signal,
@@ -1141,14 +2460,11 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1141
2460
 
1142
2461
  assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
1143
2462
  assert.equal(mockPi.callCount(), 2);
1144
- const value = result.details.workflow?.value as Array<{ key: string; ok: boolean; error?: string }>;
1145
- assert.deepEqual(value.map(({ key }) => key), ["first", "second"]);
1146
- assert.deepEqual(value.map(({ ok }) => ok).sort(), [false, true]);
1147
- const failed = value.find(({ ok }) => !ok);
1148
- const succeeded = value.find(({ ok }) => ok);
1149
- assert.match(failed?.error ?? "", /first child failed/);
1150
- assert.equal(succeeded?.error, undefined);
1151
- assert.deepEqual(result.details.workflow?.trace.filter((entry) => entry.state !== "started").map(({ state }) => state).sort(), ["completed", "failed"]);
2463
+ assert.deepEqual((result.details.workflow?.emits[0] as Array<{ key: string; output: string }>).map(({ key, output }) => ({ key, output })), [
2464
+ { key: "first", output: "first child completed" },
2465
+ { key: "second", output: "second child completed" },
2466
+ ]);
2467
+ assert.deepEqual(result.details.workflow?.value, ["first child completed", "second child completed"]);
1152
2468
  });
1153
2469
 
1154
2470
  it("rejects an over-limit runs.all batch before launching any workflow child", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
@@ -1314,85 +2630,392 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1314
2630
  assert.match(result.content[0]?.text ?? "", /handoffs/);
1315
2631
  });
1316
2632
 
1317
- it("preserves a workflow worktree when its child detaches for supervisor coordination", { skip: !createSubagentExecutor || process.platform === "win32" ? "executor unavailable or worktree paths differ on Windows" : undefined }, async () => {
1318
- execFileSync("git", ["init"], { cwd: tempDir, stdio: "ignore" });
1319
- execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDir });
1320
- execFileSync("git", ["config", "user.name", "Test User"], { cwd: tempDir });
1321
- fs.writeFileSync(path.join(tempDir, "base.txt"), "base\n", "utf-8");
1322
- execFileSync("git", ["add", "base.txt"], { cwd: tempDir });
1323
- execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
2633
+ it("preserves a workflow worktree when its child detaches for supervisor coordination", { skip: !createSubagentExecutor ? "executor unavailable" : undefined }, async () => {
2634
+ execFileSync("git", ["init"], { cwd: tempDir, stdio: "ignore" });
2635
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDir });
2636
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd: tempDir });
2637
+ fs.writeFileSync(path.join(tempDir, "base.txt"), "base\n", "utf-8");
2638
+ execFileSync("git", ["add", "base.txt"], { cwd: tempDir });
2639
+ execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
2640
+ mockPi.onCall({
2641
+ steps: [
2642
+ { jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
2643
+ { delay: 500, jsonl: [events.assistantMessage("done after coordination")] },
2644
+ ],
2645
+ });
2646
+ const piEvents = createEventBus();
2647
+ const executor = makeExecutor(
2648
+ [makeAgent("worker", { systemPrompt: "Intercom orchestration channel:" })],
2649
+ {},
2650
+ false,
2651
+ undefined,
2652
+ true,
2653
+ new Map(),
2654
+ undefined,
2655
+ undefined,
2656
+ piEvents,
2657
+ );
2658
+ let detachAccepted = false;
2659
+ piEvents.on(INTERCOM_DETACH_RESPONSE_EVENT, (payload) => {
2660
+ if ((payload as { requestId?: unknown }).requestId === "workflow-worktree-detach") {
2661
+ detachAccepted ||= (payload as { accepted?: unknown }).accepted === true;
2662
+ }
2663
+ });
2664
+ const detachTimer = setInterval(() => {
2665
+ if (!detachAccepted) piEvents.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "workflow-worktree-detach" });
2666
+ }, 10);
2667
+ detachTimer.unref();
2668
+
2669
+ const result = await executor.execute(
2670
+ "scripted-workflow-detached-worktree",
2671
+ {
2672
+ async: false,
2673
+ workflowScript: `
2674
+ const children = await runs.all([
2675
+ { key: "detaches", agent: "worker", task: "Ask then continue", worktree: true }
2676
+ ]);
2677
+ return children.map(({ key, ok, artifactPaths }) => ({ key, ok, artifactPaths }));
2678
+ `,
2679
+ },
2680
+ new AbortController().signal,
2681
+ undefined,
2682
+ makeMinimalCtx(tempDir),
2683
+ );
2684
+ clearInterval(detachTimer);
2685
+
2686
+ assert.equal(detachAccepted, true);
2687
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
2688
+ assert.match(result.content[0]?.text ?? "", /run detaches: detached/);
2689
+ const workflowValue = result.details.workflow?.value as Array<{ ok: boolean; artifactPaths: string[] }>;
2690
+ assert.equal(workflowValue[0]?.ok, false);
2691
+ const handoffPath = workflowValue[0]?.artifactPaths.find((candidate) => candidate.endsWith(".json"));
2692
+ assert.ok(handoffPath, result.content[0]?.text ?? "missing pending handoff");
2693
+ const handoff = JSON.parse(fs.readFileSync(handoffPath, "utf-8")) as {
2694
+ groups: Array<{
2695
+ cleanup: { state: string; tasks: Array<{ path: string; branch: string; preserved: boolean; worktreeRemoved: boolean; branchRemoved: boolean }> };
2696
+ }>;
2697
+ };
2698
+ const cleanup = handoff.groups[0]?.cleanup;
2699
+ assert.equal(cleanup?.state, "partial");
2700
+ assert.equal(cleanup?.tasks[0]?.preserved, true);
2701
+ assert.equal(cleanup?.tasks[0]?.worktreeRemoved, false);
2702
+ assert.equal(cleanup?.tasks[0]?.branchRemoved, false);
2703
+ const worktreePath = cleanup?.tasks[0]?.path;
2704
+ const branch = cleanup?.tasks[0]?.branch;
2705
+ assert.ok(worktreePath);
2706
+ assert.ok(branch);
2707
+ assert.equal(fs.existsSync(worktreePath), true, "live detached worktree must remain present");
2708
+
2709
+ await new Promise((resolve) => setTimeout(resolve, 750));
2710
+ execFileSync("git", ["worktree", "remove", "--force", worktreePath], { cwd: tempDir });
2711
+ execFileSync("git", ["branch", "-D", branch], { cwd: tempDir, stdio: "ignore" });
2712
+ });
2713
+
2714
+ it("pauses an async workflow when its child detaches for supervisor coordination", { skip: !createSubagentExecutor ? "executor unavailable" : undefined }, async () => {
2715
+ execFileSync("git", ["init"], { cwd: tempDir, stdio: "ignore" });
2716
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDir });
2717
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd: tempDir });
2718
+ fs.writeFileSync(path.join(tempDir, "base.txt"), "base\n", "utf-8");
2719
+ execFileSync("git", ["add", "base.txt"], { cwd: tempDir });
2720
+ execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
2721
+ mockPi.onCall({
2722
+ steps: [
2723
+ { jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
2724
+ { delay: 500, jsonl: [events.assistantMessage("done after coordination")] },
2725
+ ],
2726
+ });
2727
+ const piEvents = createEventBus();
2728
+ const asyncJobs: SubagentState["asyncJobs"] = new Map();
2729
+ const executor = makeExecutor(
2730
+ [makeAgent("worker", { systemPrompt: "Intercom orchestration channel:" })],
2731
+ {},
2732
+ false,
2733
+ undefined,
2734
+ true,
2735
+ asyncJobs,
2736
+ undefined,
2737
+ undefined,
2738
+ piEvents,
2739
+ );
2740
+ let detachAccepted = false;
2741
+ piEvents.on(INTERCOM_DETACH_RESPONSE_EVENT, (payload) => {
2742
+ if ((payload as { requestId?: unknown }).requestId === "async-workflow-detach") {
2743
+ detachAccepted ||= (payload as { accepted?: unknown }).accepted === true;
2744
+ }
2745
+ });
2746
+ const detachTimer = setInterval(() => {
2747
+ if (!detachAccepted) piEvents.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "async-workflow-detach" });
2748
+ }, 10);
2749
+ detachTimer.unref();
2750
+
2751
+ const started = await executor.execute(
2752
+ "async-scripted-workflow-detached-worktree",
2753
+ {
2754
+ workflowScript: `
2755
+ const child = await runs.run("detaches", { agent: "worker", task: "Ask then continue", worktree: true });
2756
+ return child.output;
2757
+ `,
2758
+ },
2759
+ new AbortController().signal,
2760
+ undefined,
2761
+ makeMinimalCtx(tempDir),
2762
+ );
2763
+ assert.equal(started.isError, undefined);
2764
+ assert.ok(started.details.asyncId);
2765
+ assert.ok(started.details.asyncDir);
2766
+ const workflowRunId = started.details.asyncId;
2767
+ const statusPath = path.join(started.details.asyncDir, "status.json");
2768
+ const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
2769
+ const activeMarkerPath = path.join(DIRS.async, ACTIVE_RUN_INDEX_DIR, workflowRunId);
2770
+
2771
+ let status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
2772
+ for (let attempt = 0; attempt < 150 && status.state !== "paused" && status.state !== "failed"; attempt++) {
2773
+ await new Promise((resolve) => setTimeout(resolve, 20));
2774
+ status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
2775
+ }
2776
+ clearInterval(detachTimer);
2777
+
2778
+ assert.equal(detachAccepted, true);
2779
+ assert.equal(status.state, "paused", status.error);
2780
+ assert.equal(status.activityState, "needs_attention");
2781
+ assert.match(status.error ?? "", /detached for intercom coordination/i);
2782
+ assert.equal(status.workflow?.trace.some((entry) => entry.key === "detaches" && entry.state === "detached"), true);
2783
+ assert.equal(status.workflow?.trace.some((entry) => entry.key === "detaches" && entry.state === "failed"), false);
2784
+ assert.equal(status.steps?.[0]?.status, "paused");
2785
+ assert.equal(status.steps?.[0]?.activityState, "needs_attention");
2786
+ assert.equal(asyncJobs.get(workflowRunId)?.status, "paused");
2787
+ assert.equal(asyncJobs.get(workflowRunId)?.activityState, "needs_attention");
2788
+ assert.equal(fs.existsSync(activeMarkerPath), false);
2789
+
2790
+ let persistedResult = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as {
2791
+ state?: string;
2792
+ activityState?: string;
2793
+ error?: string;
2794
+ summary?: string;
2795
+ workflow?: { trace?: Array<{ key?: string; state?: string }> };
2796
+ results?: Array<{ detached?: boolean; artifactPaths?: { outputPath?: string } }>;
2797
+ };
2798
+ assert.equal(persistedResult.state, "paused");
2799
+ assert.equal(persistedResult.activityState, "needs_attention");
2800
+ assert.match(persistedResult.error ?? "", /Reply to the supervisor request first/);
2801
+ assert.match(persistedResult.summary ?? "", /subagent_wait/);
2802
+ assert.equal(persistedResult.workflow?.trace?.some((entry) => entry.key === "detaches" && entry.state === "detached"), true);
2803
+ assert.equal(persistedResult.workflow?.trace?.some((entry) => entry.key === "detaches" && entry.state === "failed"), false);
2804
+ assert.equal(persistedResult.results?.[0]?.detached, true);
2805
+ const handoffPath = persistedResult.results?.[0]?.artifactPaths?.outputPath;
2806
+ assert.ok(handoffPath, "missing preserved worktree handoff path");
2807
+ const handoff = JSON.parse(fs.readFileSync(handoffPath, "utf-8")) as {
2808
+ groups: Array<{
2809
+ cleanup: { state: string; tasks: Array<{ path: string; branch: string; preserved: boolean; worktreeRemoved: boolean; branchRemoved: boolean }> };
2810
+ }>;
2811
+ };
2812
+ const cleanup = handoff.groups[0]?.cleanup;
2813
+ assert.equal(cleanup?.state, "partial");
2814
+ assert.equal(cleanup?.tasks[0]?.preserved, true);
2815
+ assert.equal(cleanup?.tasks[0]?.worktreeRemoved, false);
2816
+ assert.equal(cleanup?.tasks[0]?.branchRemoved, false);
2817
+ const worktreePath = cleanup?.tasks[0]?.path;
2818
+ const branch = cleanup?.tasks[0]?.branch;
2819
+ assert.ok(worktreePath);
2820
+ assert.ok(branch);
2821
+ assert.equal(fs.existsSync(worktreePath), true, "live detached worktree must remain present");
2822
+
2823
+ let reconciled: AsyncStatus | undefined;
2824
+ for (let attempt = 0; attempt < 150; attempt++) {
2825
+ reconciled = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
2826
+ if (reconciled.state === "complete" || reconciled.state === "failed") break;
2827
+ await new Promise((resolve) => setTimeout(resolve, 20));
2828
+ }
2829
+ assert.equal(reconciled?.state, "failed", reconciled?.error);
2830
+ assert.match(reconciled?.error ?? "", /unsupported-continuation/);
2831
+ assert.equal(reconciled?.activityState, undefined);
2832
+ assert.equal(reconciled?.steps?.[0]?.status, "completed");
2833
+ assert.equal(reconciled?.steps?.[0]?.activityState, undefined);
2834
+ assert.equal(asyncJobs.get(workflowRunId)?.status, "failed");
2835
+
2836
+ persistedResult = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
2837
+ assert.equal(persistedResult.state, "failed");
2838
+ assert.equal(persistedResult.activityState, undefined);
2839
+ assert.match(persistedResult.error ?? "", /unsupported-continuation/);
2840
+ execFileSync("git", ["worktree", "remove", "--force", worktreePath], { cwd: tempDir });
2841
+ execFileSync("git", ["branch", "-D", branch], { cwd: tempDir, stdio: "ignore" });
2842
+ fs.rmSync(started.details.asyncDir, { recursive: true, force: true });
2843
+ fs.rmSync(resultPath, { force: true });
2844
+ });
2845
+
2846
+ it("keeps async workflows failed when a detached child is mixed with a real failure", { skip: !createSubagentExecutor ? "executor unavailable" : undefined }, async () => {
2847
+ mockPi.onCall({
2848
+ matchArgIncludes: "Ask then continue",
2849
+ steps: [
2850
+ { jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
2851
+ { delay: 500, jsonl: [events.assistantMessage("done after coordination")] },
2852
+ ],
2853
+ });
2854
+ mockPi.onCall({ matchArgIncludes: "Fail for real", exitCode: 1, stderr: "real child failure" });
2855
+ const piEvents = createEventBus();
2856
+ const asyncJobs: SubagentState["asyncJobs"] = new Map();
2857
+ const executor = makeExecutor(
2858
+ [makeAgent("worker", { systemPrompt: "Intercom orchestration channel:" })],
2859
+ {},
2860
+ false,
2861
+ undefined,
2862
+ true,
2863
+ asyncJobs,
2864
+ undefined,
2865
+ undefined,
2866
+ piEvents,
2867
+ );
2868
+ let detachAccepted = false;
2869
+ piEvents.on(INTERCOM_DETACH_RESPONSE_EVENT, (payload) => {
2870
+ if ((payload as { requestId?: unknown }).requestId === "async-workflow-detach-with-failure") {
2871
+ detachAccepted ||= (payload as { accepted?: unknown }).accepted === true;
2872
+ }
2873
+ });
2874
+ const detachTimer = setInterval(() => {
2875
+ if (!detachAccepted) piEvents.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "async-workflow-detach-with-failure" });
2876
+ }, 10);
2877
+ detachTimer.unref();
2878
+
2879
+ const started = await executor.execute(
2880
+ "async-scripted-workflow-detached-and-failed",
2881
+ {
2882
+ workflowScript: `
2883
+ await runs.all([
2884
+ { key: "detaches", agent: "worker", task: "Ask then continue" },
2885
+ { key: "fails", agent: "worker", task: "Fail for real" }
2886
+ ]);
2887
+ throw new Error("manual hard failure");
2888
+ `,
2889
+ },
2890
+ new AbortController().signal,
2891
+ undefined,
2892
+ makeMinimalCtx(tempDir),
2893
+ );
2894
+ assert.equal(started.isError, undefined);
2895
+ assert.ok(started.details.asyncId);
2896
+ assert.ok(started.details.asyncDir);
2897
+ const workflowRunId = started.details.asyncId;
2898
+ const statusPath = path.join(started.details.asyncDir, "status.json");
2899
+ const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
2900
+
2901
+ let status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
2902
+ for (let attempt = 0; attempt < 150 && status.state !== "failed" && status.state !== "paused"; attempt++) {
2903
+ await new Promise((resolve) => setTimeout(resolve, 20));
2904
+ status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
2905
+ }
2906
+ clearInterval(detachTimer);
2907
+
2908
+ assert.equal(detachAccepted, true);
2909
+ assert.equal(status.state, "failed");
2910
+ assert.equal(status.activityState, undefined);
2911
+ assert.match(status.error ?? "", /manual hard failure/);
2912
+ assert.equal(status.workflow?.trace.some((entry) => entry.key === "detaches" && entry.state === "detached"), true);
2913
+ assert.equal(status.workflow?.trace.some((entry) => entry.key === "fails" && entry.state === "failed"), true);
2914
+ assert.equal(status.steps?.find((step) => step.workflowKey === "detaches")?.status, "paused");
2915
+ assert.equal(status.steps?.find((step) => step.workflowKey === "detaches")?.activityState, "needs_attention");
2916
+ assert.equal(status.steps?.find((step) => step.workflowKey === "fails")?.status, "failed");
2917
+ assert.equal(asyncJobs.get(workflowRunId)?.status, "failed");
2918
+ assert.equal(asyncJobs.get(workflowRunId)?.activityState, undefined);
2919
+
2920
+ let persistedResult = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as {
2921
+ state?: string;
2922
+ activityState?: string;
2923
+ error?: string;
2924
+ workflow?: { trace?: Array<{ key?: string; state?: string }> };
2925
+ results?: Array<{ workflowKey?: string; detached?: boolean; success?: boolean }>;
2926
+ };
2927
+ assert.equal(persistedResult.state, "failed");
2928
+ assert.equal(persistedResult.activityState, undefined);
2929
+ assert.match(persistedResult.error ?? "", /manual hard failure/);
2930
+ assert.equal(persistedResult.workflow?.trace?.some((entry) => entry.key === "detaches" && entry.state === "detached"), true);
2931
+ assert.equal(persistedResult.workflow?.trace?.some((entry) => entry.key === "fails" && entry.state === "failed"), true);
2932
+ assert.equal(persistedResult.results?.find((entry) => entry.workflowKey === "detaches")?.detached, true);
2933
+ assert.equal(persistedResult.results?.find((entry) => entry.workflowKey === "fails")?.success, false);
2934
+
2935
+ await new Promise((resolve) => setTimeout(resolve, 750));
2936
+ persistedResult = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
2937
+ assert.equal(persistedResult.state, "failed", "real workflow failure must not be overwritten by detached child completion");
2938
+ fs.rmSync(started.details.asyncDir, { recursive: true, force: true });
2939
+ fs.rmSync(resultPath, { force: true });
2940
+ });
2941
+
2942
+ it("fails closed after a detached child finishes while a sibling is aborted", { skip: !createSubagentExecutor ? "executor unavailable" : undefined }, async () => {
1324
2943
  mockPi.onCall({
2944
+ matchArgIncludes: "Ask then continue",
1325
2945
  steps: [
1326
2946
  { jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
1327
2947
  { delay: 500, jsonl: [events.assistantMessage("done after coordination")] },
1328
2948
  ],
1329
2949
  });
2950
+ mockPi.onCall({ matchArgIncludes: "Stay running", delay: 10_000 });
1330
2951
  const piEvents = createEventBus();
2952
+ const asyncJobs: SubagentState["asyncJobs"] = new Map();
1331
2953
  const executor = makeExecutor(
1332
2954
  [makeAgent("worker", { systemPrompt: "Intercom orchestration channel:" })],
1333
2955
  {},
1334
2956
  false,
1335
2957
  undefined,
1336
2958
  true,
1337
- new Map(),
2959
+ asyncJobs,
1338
2960
  undefined,
1339
2961
  undefined,
1340
2962
  piEvents,
1341
2963
  );
1342
2964
  let detachAccepted = false;
1343
2965
  piEvents.on(INTERCOM_DETACH_RESPONSE_EVENT, (payload) => {
1344
- if ((payload as { requestId?: unknown }).requestId === "workflow-worktree-detach") {
2966
+ if ((payload as { requestId?: unknown }).requestId === "async-workflow-detach-with-sibling") {
1345
2967
  detachAccepted ||= (payload as { accepted?: unknown }).accepted === true;
1346
2968
  }
1347
2969
  });
1348
2970
  const detachTimer = setInterval(() => {
1349
- if (!detachAccepted) piEvents.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "workflow-worktree-detach" });
2971
+ if (!detachAccepted) piEvents.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "async-workflow-detach-with-sibling" });
1350
2972
  }, 10);
1351
2973
  detachTimer.unref();
1352
2974
 
1353
- const result = await executor.execute(
1354
- "scripted-workflow-detached-worktree",
2975
+ const started = await executor.execute(
2976
+ "async-scripted-workflow-detached-with-sibling",
1355
2977
  {
1356
- async: false,
1357
2978
  workflowScript: `
1358
- const children = await runs.all([
1359
- { key: "detaches", agent: "worker", task: "Ask then continue", worktree: true }
2979
+ await Promise.all([
2980
+ runs.run("detaches", { agent: "worker", task: "Ask then continue" }),
2981
+ runs.run("slow", { agent: "worker", task: "Stay running" })
1360
2982
  ]);
1361
- return children.map(({ key, ok, artifactPaths }) => ({ key, ok, artifactPaths }));
2983
+ return "should not reach";
1362
2984
  `,
1363
2985
  },
1364
2986
  new AbortController().signal,
1365
2987
  undefined,
1366
2988
  makeMinimalCtx(tempDir),
1367
2989
  );
1368
- clearInterval(detachTimer);
2990
+ assert.equal(started.isError, undefined);
2991
+ assert.ok(started.details.asyncDir);
2992
+ const workflowRunId = started.details.asyncId;
2993
+ const statusPath = path.join(started.details.asyncDir, "status.json");
1369
2994
 
2995
+ let status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
2996
+ for (let attempt = 0; attempt < 150 && status.state !== "paused" && status.state !== "failed" && status.state !== "complete"; attempt++) {
2997
+ await new Promise((resolve) => setTimeout(resolve, 20));
2998
+ status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
2999
+ }
3000
+ clearInterval(detachTimer);
1370
3001
  assert.equal(detachAccepted, true);
1371
- assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
1372
- assert.match(result.content[0]?.text ?? "", /run detaches: detached/);
1373
- const workflowValue = result.details.workflow?.value as Array<{ ok: boolean; artifactPaths: string[] }>;
1374
- assert.equal(workflowValue[0]?.ok, false);
1375
- const handoffPath = workflowValue[0]?.artifactPaths.find((candidate) => candidate.endsWith(".json"));
1376
- assert.ok(handoffPath, result.content[0]?.text ?? "missing pending handoff");
1377
- const handoff = JSON.parse(fs.readFileSync(handoffPath, "utf-8")) as {
1378
- groups: Array<{
1379
- cleanup: { state: string; tasks: Array<{ path: string; branch: string; preserved: boolean; worktreeRemoved: boolean; branchRemoved: boolean }> };
1380
- }>;
1381
- };
1382
- const cleanup = handoff.groups[0]?.cleanup;
1383
- assert.equal(cleanup?.state, "partial");
1384
- assert.equal(cleanup?.tasks[0]?.preserved, true);
1385
- assert.equal(cleanup?.tasks[0]?.worktreeRemoved, false);
1386
- assert.equal(cleanup?.tasks[0]?.branchRemoved, false);
1387
- const worktreePath = cleanup?.tasks[0]?.path;
1388
- const branch = cleanup?.tasks[0]?.branch;
1389
- assert.ok(worktreePath);
1390
- assert.ok(branch);
1391
- assert.equal(fs.existsSync(worktreePath), true, "live detached worktree must remain present");
3002
+ assert.equal(status.state, "paused", status.error);
3003
+ assert.equal(status.steps?.find((step) => step.workflowKey === "detaches")?.status, "paused");
3004
+ assert.equal(status.steps?.find((step) => step.workflowKey === "slow")?.status, "stopped");
1392
3005
 
1393
- await new Promise((resolve) => setTimeout(resolve, 750));
1394
- execFileSync("git", ["worktree", "remove", "--force", worktreePath], { cwd: tempDir });
1395
- execFileSync("git", ["branch", "-D", branch], { cwd: tempDir, stdio: "ignore" });
3006
+ let reconciled: AsyncStatus | undefined;
3007
+ for (let attempt = 0; attempt < 150; attempt++) {
3008
+ reconciled = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
3009
+ if (reconciled.state === "complete" || reconciled.state === "failed") break;
3010
+ await new Promise((resolve) => setTimeout(resolve, 20));
3011
+ }
3012
+ assert.equal(reconciled?.state, "failed", reconciled?.error);
3013
+ assert.match(reconciled?.error ?? "", /unsupported-continuation/);
3014
+ assert.equal(reconciled?.steps?.find((step) => step.workflowKey === "detaches")?.status, "completed");
3015
+ assert.equal(reconciled?.steps?.find((step) => step.workflowKey === "slow")?.status, "stopped");
3016
+ assert.equal(asyncJobs.get(workflowRunId)?.status, "failed");
3017
+ fs.rmSync(started.details.asyncDir, { recursive: true, force: true });
3018
+ fs.rmSync(path.join(DIRS.results, `${workflowRunId}.json`), { force: true });
1396
3019
  });
1397
3020
 
1398
3021
  it("inherits workflow-level worktree isolation and allows a child opt-out", { skip: !createSubagentExecutor || process.platform === "win32" ? "executor unavailable or worktree paths differ on Windows" : undefined }, async () => {
@@ -1663,6 +3286,45 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1663
3286
  assert.match(result.content[0]?.text ?? "", /Valid:/);
1664
3287
  });
1665
3288
 
3289
+ it("routes watchdog.configure through the management action path", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
3290
+ const gpt = { provider: "openai-codex", id: "gpt-5.5", reasoning: true };
3291
+ const opus = { provider: "anthropic", id: "claude-opus-4-8", reasoning: true };
3292
+ const models = [gpt, opus];
3293
+ const watchdog = new MainWatchdogRuntime({ cwd: tempDir });
3294
+ const executor = createSubagentExecutor!({
3295
+ pi: { events: createEventBus(), getSessionName: () => undefined },
3296
+ state: { baseCwd: tempDir, currentSessionId: null, asyncJobs: new Map(), foregroundControls: new Map(), lastForegroundControlId: null },
3297
+ config: {},
3298
+ asyncByDefault: false,
3299
+ watchdog,
3300
+ tempArtifactsDir: tempDir,
3301
+ getSubagentSessionRoot: () => tempDir,
3302
+ expandTilde: (value: string) => value,
3303
+ discoverAgents: () => ({ agents: [makeAgent("echo")] }),
3304
+ });
3305
+ const ctx = {
3306
+ ...makeMinimalCtx(tempDir),
3307
+ model: gpt,
3308
+ modelRegistry: {
3309
+ getAvailable: () => models,
3310
+ find: (provider: string, id: string) => models.find((model) => model.provider === provider && model.id === id),
3311
+ hasConfiguredAuth: (model: unknown) => Boolean(model),
3312
+ },
3313
+ };
3314
+
3315
+ const result = await executor.execute(
3316
+ "watchdog-configure",
3317
+ { action: "watchdog.configure", model: "recommended" },
3318
+ new AbortController().signal,
3319
+ undefined,
3320
+ ctx,
3321
+ );
3322
+
3323
+ assert.equal(result.isError, undefined);
3324
+ assert.match(result.content[0]?.text ?? "", /session model configured: anthropic\/claude-opus-4-8:high/);
3325
+ assert.equal(watchdog.getSnapshot(tempDir).config.main.model, "anthropic/claude-opus-4-8");
3326
+ });
3327
+
1666
3328
  it("rejects duplicate concurrent subagent execution calls", async () => {
1667
3329
  mockPi.onCall({ output: "first call completed", delay: 100 });
1668
3330
  const executor = makeExecutor([makeAgent("echo")]);
@@ -1794,7 +3456,14 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1794
3456
  } satisfies SubagentDelegationRequest;
1795
3457
 
1796
3458
  try {
1797
- registerSubagentExtension(fakePi as never);
3459
+ const previousChildEnv = process.env[SUBAGENT_CHILD_ENV];
3460
+ delete process.env[SUBAGENT_CHILD_ENV];
3461
+ try {
3462
+ registerSubagentExtension(fakePi as never);
3463
+ } finally {
3464
+ if (previousChildEnv === undefined) delete process.env[SUBAGENT_CHILD_ENV];
3465
+ else process.env[SUBAGENT_CHILD_ENV] = previousChildEnv;
3466
+ }
1798
3467
  for (const handler of runtimeHandlers.get("session_start") ?? []) {
1799
3468
  await handler({ reason: "startup" }, ctx);
1800
3469
  }
@@ -1913,7 +3582,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1913
3582
  const claims = fs.readdirSync(path.join(descriptor.directory, "claims"));
1914
3583
  assert.equal(claims.length, 1);
1915
3584
  const claim = JSON.parse(fs.readFileSync(path.join(descriptor.directory, "claims", claims[0]!), "utf-8")) as { path: string };
1916
- assert.match(claim.path, /^tasks\[0\]\/[a-f0-9]{8}\/single$/);
3585
+ assert.match(claim.path, /^tasks\[0\]\/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\/single$/);
1917
3586
  } finally {
1918
3587
  if (previous === undefined) delete process.env[RUN_FANOUT_BUDGET_ENV];
1919
3588
  else process.env[RUN_FANOUT_BUDGET_ENV] = previous;
@@ -2134,23 +3803,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2134
3803
  assert.equal(mockPi.callCount(), 1);
2135
3804
  });
2136
3805
 
2137
- it("allows intentional parallel tasks inside one subagent execution call", async () => {
2138
- mockPi.onCall({ output: "first parallel result" });
2139
- mockPi.onCall({ output: "second parallel result" });
2140
- const executor = makeExecutor([makeAgent("echo"), makeAgent("second")]);
2141
-
2142
- const result = await executor.execute(
2143
- "parallel",
2144
- { tasks: [{ agent: "echo", task: "First task" }, { agent: "second", task: "Second task" }] },
2145
- new AbortController().signal,
2146
- undefined,
2147
- makeMinimalCtx(tempDir),
2148
- );
2149
3806
 
2150
- assert.equal(result.isError, undefined);
2151
- assert.equal(mockPi.callCount(), 2);
2152
- assert.deepEqual(result.details?.totalCost, { inputTokens: 200, outputTokens: 100, costUsd: 0.002 });
2153
- });
2154
3807
 
2155
3808
  it("reports total cost for foreground single runs", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2156
3809
  mockPi.onCall({ output: "single result" });
@@ -2253,57 +3906,18 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2253
3906
  }
2254
3907
  });
2255
3908
 
2256
- it("blocks later foreground chain children when hard reported usage is exhausted", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2257
- mockPi.onCall({ output: "first result" });
2258
- const executor = makeExecutor([makeAgent("echo"), makeAgent("second")]);
2259
-
2260
- const result = await executor.execute(
2261
- "foreground-usage-budget",
2262
- {
2263
- chain: [
2264
- { agent: "echo", task: "First task" },
2265
- { agent: "second", task: "Second task" },
2266
- ],
2267
- usageBudget: { tokens: { hard: 10 } },
2268
- },
2269
- new AbortController().signal,
2270
- undefined,
2271
- makeMinimalCtx(tempDir),
2272
- );
2273
-
2274
- assert.equal(result.isError, true);
2275
- assert.match(result.content[0]?.text ?? "", /Usage budget exhausted/);
2276
- assert.equal(mockPi.callCount(), 1);
2277
- assert.equal(result.details?.usageBudget?.exhausted, true);
2278
- assert.equal(result.details?.usageBudget?.reason, "tokens");
2279
- });
2280
-
2281
- it("blocks queued foreground parallel children when hard reported usage is exhausted", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2282
- mockPi.onCall({ output: "first result" });
2283
- const executor = makeExecutor([makeAgent("echo"), makeAgent("second")]);
3909
+ it("rejects implementation runs without mutation-capable tools before spawn", async () => {
3910
+ mockPi.onCall({ output: "should not spawn" });
3911
+ const agents = [makeAgent("worker", { tools: ["read", "grep", "find", "ls", "contact_supervisor"] })];
2284
3912
 
2285
- const result = await executor.execute(
2286
- "foreground-parallel-usage-budget",
2287
- {
2288
- tasks: [
2289
- { agent: "echo", task: "First task" },
2290
- { agent: "second", task: "Second task" },
2291
- ],
2292
- concurrency: 1,
2293
- usageBudget: { tokens: { hard: 10 } },
2294
- },
2295
- new AbortController().signal,
2296
- undefined,
2297
- makeMinimalCtx(tempDir),
2298
- );
3913
+ const result = await runSync(tempDir, agents, "worker", "Implement the approved file changes", {
3914
+ runId: "readonly-contract-run",
3915
+ });
2299
3916
 
2300
- assert.equal(result.isError, undefined);
2301
- assert.equal(mockPi.callCount(), 1);
2302
- assert.equal(result.details?.results.length, 2);
2303
- assert.equal(result.details?.results[1]?.skipped, true);
2304
- assert.match(result.details?.results[1]?.error ?? "", /Usage budget exhausted/);
2305
- assert.equal(result.details?.usageBudget?.exhausted, true);
2306
- });
3917
+ assert.equal(result.exitCode, 1);
3918
+ assert.match(result.error ?? "", /no mutation-capable tools/);
3919
+ assert.equal(mockPi.callCount(), 0);
3920
+ });
2307
3921
 
2308
3922
  it("fails implementation runs that complete without mutation attempts", async () => {
2309
3923
  mockPi.onCall({ output: "Validation:\nlet rawFilename = params.filename.trim();" });
@@ -2406,6 +4020,133 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2406
4020
  if (child?.artifactPaths?.outputPath) assert.match(fs.readFileSync(child.artifactPaths.outputPath, "utf-8"), /"note": "captured"/);
2407
4021
  });
2408
4022
 
4023
+ it("workflow children with outputSchema can satisfy inherited checked acceptance", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
4024
+ const acceptanceReport = {
4025
+ criteriaSatisfied: [{ id: "proof", status: "satisfied", evidence: "structured output returned ok true" }],
4026
+ changedFiles: ["none"],
4027
+ testsAddedOrUpdated: ["none"],
4028
+ commandsRun: [{ command: "not run", result: "not-run", summary: "mock structured-output child" }],
4029
+ validationOutput: ["mock output validated"],
4030
+ residualRisks: ["none"],
4031
+ noStagedFiles: true,
4032
+ diffSummary: "no file changes",
4033
+ };
4034
+ mockPi.onCall({
4035
+ stdoutRaw: [
4036
+ { type: "tool_execution_start", toolName: "structured_output", args: { value: { ok: true }, acceptanceReport } },
4037
+ { type: "tool_result_end", message: { role: "toolResult", toolName: "structured_output", content: [{ type: "text", text: "Structured output captured." }] } },
4038
+ { type: "tool_execution_end", toolName: "structured_output" },
4039
+ ].map((entry) => JSON.stringify(entry)).join("\n") + "\n",
4040
+ structuredOutputCapture: { ok: true },
4041
+ structuredOutputAcceptanceReport: acceptanceReport,
4042
+ });
4043
+ const executor = makeExecutor([makeAgent("echo")]);
4044
+
4045
+ const result = await executor.execute(
4046
+ "workflow-schema-acceptance-sidecar",
4047
+ {
4048
+ async: false,
4049
+ acceptance: { level: "checked", criteria: [{ id: "proof", must: "Return required proof" }] },
4050
+ workflowScript: `
4051
+ const child = await runs.run("schema", {
4052
+ agent: "echo",
4053
+ task: "Return structured data",
4054
+ outputSchema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" } } }
4055
+ });
4056
+ if (!child.ok) throw new Error(child.error);
4057
+ return child;
4058
+ `,
4059
+ },
4060
+ new AbortController().signal,
4061
+ undefined,
4062
+ makeMinimalCtx(tempDir),
4063
+ );
4064
+
4065
+ const child = result.details.results[0];
4066
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
4067
+ assert.deepEqual(child?.structuredOutput, { ok: true });
4068
+ assert.equal(child?.acceptance?.status, "checked");
4069
+ assert.equal(child?.acceptance?.runtimeChecks.some((check) => check.status === "failed"), false);
4070
+ });
4071
+
4072
+ it("rejects workflow outputSchema children that omit checked acceptance sidecars", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
4073
+ mockPi.onCall({
4074
+ stdoutRaw: [
4075
+ { type: "tool_execution_start", toolName: "structured_output", args: { value: { ok: true } } },
4076
+ { type: "tool_result_end", message: { role: "toolResult", toolName: "structured_output", content: [{ type: "text", text: "Structured output captured." }] } },
4077
+ { type: "tool_execution_end", toolName: "structured_output" },
4078
+ ].map((entry) => JSON.stringify(entry)).join("\n") + "\n",
4079
+ structuredOutputCapture: { ok: true },
4080
+ });
4081
+ const executor = makeExecutor([makeAgent("echo")]);
4082
+
4083
+ const result = await executor.execute(
4084
+ "workflow-schema-acceptance-missing-sidecar",
4085
+ {
4086
+ async: false,
4087
+ acceptance: { level: "checked", criteria: [{ id: "proof", must: "Return required proof" }] },
4088
+ workflowScript: `
4089
+ const child = await runs.run("schema", {
4090
+ agent: "echo",
4091
+ task: "Return structured data",
4092
+ outputSchema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" } } }
4093
+ });
4094
+ if (!child.ok) throw new Error(child.error);
4095
+ return child;
4096
+ `,
4097
+ },
4098
+ new AbortController().signal,
4099
+ undefined,
4100
+ makeMinimalCtx(tempDir),
4101
+ );
4102
+
4103
+ const child = result.details.results[0];
4104
+ assert.equal(result.isError, true);
4105
+ assert.equal(child?.exitCode, 1);
4106
+ assert.equal(child?.structuredOutput?.ok, true);
4107
+ assert.equal(child?.acceptance?.status, "rejected");
4108
+ assert.match(result.content[0]?.text ?? "", /acceptance/i);
4109
+ });
4110
+
4111
+ it("surfaces corrupt outputSchema acceptance sidecar read errors", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
4112
+ mockPi.onCall({
4113
+ stdoutRaw: [
4114
+ { type: "tool_execution_start", toolName: "structured_output", args: { value: { ok: true } } },
4115
+ { type: "tool_result_end", message: { role: "toolResult", toolName: "structured_output", content: [{ type: "text", text: "Structured output captured." }] } },
4116
+ { type: "tool_execution_end", toolName: "structured_output" },
4117
+ ].map((entry) => JSON.stringify(entry)).join("\n") + "\n",
4118
+ structuredOutputCapture: { ok: true },
4119
+ structuredOutputAcceptanceReportRaw: "{not-json",
4120
+ });
4121
+ const executor = makeExecutor([makeAgent("echo")]);
4122
+
4123
+ const result = await executor.execute(
4124
+ "workflow-schema-acceptance-corrupt-sidecar",
4125
+ {
4126
+ async: false,
4127
+ acceptance: { level: "checked", criteria: [{ id: "proof", must: "Return required proof" }] },
4128
+ workflowScript: `
4129
+ const child = await runs.run("schema", {
4130
+ agent: "echo",
4131
+ task: "Return structured data",
4132
+ outputSchema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" } } }
4133
+ });
4134
+ if (!child.ok) throw new Error(child.error);
4135
+ return child;
4136
+ `,
4137
+ },
4138
+ new AbortController().signal,
4139
+ undefined,
4140
+ makeMinimalCtx(tempDir),
4141
+ );
4142
+
4143
+ const child = result.details.results[0];
4144
+ assert.equal(result.isError, true);
4145
+ assert.equal(child?.structuredOutput?.ok, true);
4146
+ assert.match(child?.error ?? "", /Failed to read structured output acceptance report/);
4147
+ assert.doesNotMatch(child?.error ?? "", /Structured acceptance report not found/);
4148
+ });
4149
+
2409
4150
  it("accepts recovered tool errors before valid structured output but rejects later errors", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2410
4151
  const recoveredError = { type: "tool_result_end", message: { role: "toolResult", toolName: "read", isError: true, content: [{ type: "text", text: "EISDIR" }] } };
2411
4152
  const structuredEvents = [
@@ -2642,7 +4383,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2642
4383
 
2643
4384
  const result = await runSync(tempDir, agents, "echo", "Investigate behavior", {
2644
4385
  runId: "run-active",
2645
- controlConfig: { enabled: true, activeNoticeAfterTurns: 2, activeNoticeAfterMs: 999_999, activeNoticeAfterTokens: 999_999, notifyOn: ["active_long_running", "active_long_running"] },
4386
+ controlConfig: { enabled: true, activeNoticeAfterTurns: 2, activeNoticeAfterMs: 999_999, activeNoticeAfterTokens: 999_999, notifyOn: ["active_long_running", "needs_attention"] },
2646
4387
  onControlEvent: (event: NonNullable<RunSyncResult["controlEvents"]>[number]) => controlEvents.push(event),
2647
4388
  });
2648
4389
 
@@ -2699,7 +4440,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2699
4440
 
2700
4441
  const result = await runSync(tempDir, agents, "echo", "Investigate behavior", {
2701
4442
  runId: "run-control-disabled",
2702
- controlConfig: { enabled: false, activeNoticeAfterTurns: 1, activeNoticeAfterMs: 1, activeNoticeAfterTokens: 1, notifyOn: ["active_long_running", "active_long_running"] },
4443
+ controlConfig: { enabled: false, activeNoticeAfterTurns: 1, activeNoticeAfterMs: 1, activeNoticeAfterTokens: 1, notifyOn: ["active_long_running", "needs_attention"] },
2703
4444
  onControlEvent: (event: NonNullable<RunSyncResult["controlEvents"]>[number]) => controlEvents.push(event),
2704
4445
  });
2705
4446
 
@@ -2783,6 +4524,26 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2783
4524
  assert.equal(mockPi.callCount(), 1);
2784
4525
  });
2785
4526
 
4527
+ it("stops model fallback and flags contextOverflow when the input exceeds the context window", async () => {
4528
+ mockPi.onCall({ output: "", stderr: "This model's maximum context length is 8192 tokens", exitCode: 1 });
4529
+ mockPi.onCall({ output: "must not run" });
4530
+ const agents = [makeAgent("worker", {
4531
+ model: "openai/gpt-5-mini",
4532
+ fallbackModels: ["anthropic/claude-sonnet-4"],
4533
+ })];
4534
+
4535
+ const result = await runSync(tempDir, agents, "worker", "Summarize a huge file", {
4536
+ runId: "context-overflow-stops-fallback",
4537
+ acceptance: false,
4538
+ });
4539
+
4540
+ assert.equal(result.exitCode, 1);
4541
+ assert.equal(result.contextOverflow, true);
4542
+ assert.equal(result.modelAttempts?.length, 1);
4543
+ assert.equal(mockPi.callCount(), 1);
4544
+ assert.ok(result.error?.includes("context"), "error should mention context overflow");
4545
+ });
4546
+
2786
4547
  it("does not retry non-SIGKILL signaled child exits", { skip: process.platform === "win32" ? "POSIX child signal reporting is unavailable on Windows" : false }, async () => {
2787
4548
  mockPi.onCall({ signal: "SIGTERM" });
2788
4549
  mockPi.onCall({ output: "must not run" });
@@ -2875,6 +4636,32 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2875
4636
  assert.equal(result.model, "anthropic/claude-sonnet-4");
2876
4637
  });
2877
4638
 
4639
+ it("fails when a configured provider-qualified model starts on a different child model", async () => {
4640
+ mockPi.onCall({ jsonl: [events.assistantMessage("wrong provider", "openai-codex/gpt-5.6-sol")] });
4641
+ const agents = [makeAgent("echo", { model: "opencode-go/ox-alpha-free:max" })];
4642
+
4643
+ const result = await runSync(tempDir, agents, "echo", "Task", {
4644
+ runId: "foreground-model-verification-mismatch",
4645
+ acceptance: false,
4646
+ availableModels: [
4647
+ { provider: "opencode-go", id: "ox-alpha-free", fullId: "opencode-go/ox-alpha-free" },
4648
+ { provider: "openai-codex", id: "gpt-5.6-sol", fullId: "openai-codex/gpt-5.6-sol" },
4649
+ ],
4650
+ });
4651
+
4652
+ assert.equal(result.exitCode, 1);
4653
+ assert.equal(result.model, "opencode-go/ox-alpha-free:max");
4654
+ assert.deepEqual(result.attemptedModels, ["opencode-go/ox-alpha-free:max"]);
4655
+ assert.match(result.error ?? "", /model_verification_failed/);
4656
+ assert.match(result.error ?? "", /Expected 'opencode-go\/ox-alpha-free:max'/);
4657
+ assert.match(result.error ?? "", /observed 'openai-codex\/gpt-5\.6-sol'/);
4658
+ assert.equal(result.modelAttempts?.[0]?.success, false);
4659
+ assert.match(result.modelAttempts?.[0]?.error ?? "", /model_verification_failed/);
4660
+ const args = readAllCallArgs()[0]!;
4661
+ assert.equal(args[args.indexOf("--model") + 1], "opencode-go/ox-alpha-free:max");
4662
+ assert.equal(mockPi.callCount(), 1);
4663
+ });
4664
+
2878
4665
  it("model override from options takes precedence", async () => {
2879
4666
  mockPi.onCall({ output: "Done" });
2880
4667
  const agents = [makeAgent("echo", { model: "anthropic/claude-sonnet-4" })];
@@ -2887,6 +4674,18 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2887
4674
  assert.equal(result.model, "openai/gpt-4o");
2888
4675
  });
2889
4676
 
4677
+ it("rejects an unresolved agent model before spawning Pi", async () => {
4678
+ const agents = [makeAgent("echo", { model: "fast" })];
4679
+
4680
+ await assert.rejects(
4681
+ runSync(tempDir, agents, "echo", "Task", {
4682
+ availableModels: [{ provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" }],
4683
+ }),
4684
+ /Unknown subagent model 'fast'/,
4685
+ );
4686
+ assert.equal(mockPi.callCount(), 0);
4687
+ });
4688
+
2890
4689
  it("prefers the parent session provider for ambiguous bare model ids", async () => {
2891
4690
  mockPi.onCall({ output: "Done" });
2892
4691
  const agents = [makeAgent("echo", { model: "gpt-5-mini" })];
@@ -3168,9 +4967,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
3168
4967
 
3169
4968
  assert.equal(result.exitCode, 1);
3170
4969
  assert.match(result.error ?? "", /429 quota exceeded/);
3171
- // No fallback model configured: the last candidate is retried on the same
3172
- // model up to MODEL_RETRY_DELAYS_MS.length times before giving up.
3173
- assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false, false, false, false]);
4970
+ assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false]);
3174
4971
  });
3175
4972
 
3176
4973
  it("treats recovered child tool errors as successful foreground runs", async () => {
@@ -3278,9 +5075,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
3278
5075
  });
3279
5076
 
3280
5077
  assert.equal(result.exitCode, 1);
3281
- // Primary fails, fallback fails, then the last (fallback) candidate is
3282
- // retried on the same model up to MODEL_RETRY_DELAYS_MS.length times.
3283
- assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false, false, false, false, false]);
5078
+ assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false, false]);
3284
5079
  assert.match(result.error ?? "", /429 quota exceeded/);
3285
5080
  });
3286
5081
 
@@ -3425,14 +5220,14 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
3425
5220
 
3426
5221
  const result = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
3427
5222
  runId: "delayed-tool-attention",
3428
- controlConfig: { enabled: true, needsAttentionAfterMs: 200, activeNoticeAfterMs: 999_999, notifyOn: ["active_long_running"] },
5223
+ controlConfig: { enabled: true, needsAttentionAfterMs: 200, activeNoticeAfterMs: 999_999, notifyOn: ["needs_attention"] },
3429
5224
  onUpdate: (update: { details?: { progress?: ProgressSummary[] } }) => updates.push(update),
3430
5225
  onControlEvent: (event: NonNullable<RunSyncResult["controlEvents"]>[number]) => controlEvents.push(event),
3431
5226
  });
3432
5227
 
3433
5228
  assert.equal(result.exitCode, 0);
3434
5229
  assert.equal(result.progress.activityState, undefined);
3435
- assert.equal(controlEvents.some((event) => event.type === "active_long_running"), false);
5230
+ assert.equal(controlEvents.some((event) => event.type === "needs_attention"), false);
3436
5231
  assert.equal(updates.some((update) => update.details?.progress?.some((progress) => progress.currentTool === "bash")), true);
3437
5232
  });
3438
5233
 
@@ -3603,29 +5398,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
3603
5398
  assert.equal(fs.existsSync(path.join(tempDir, ".pi-subagents", "artifacts")), false);
3604
5399
  });
3605
5400
 
3606
- for (const artifactDir of ["session", "temp"] as const) {
3607
- it(`keeps foreground chain scratch files out of the project for artifactDir=${artifactDir}`, { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
3608
- mockPi.onCall({ output: `${artifactDir} chain result` });
3609
- const sessionFile = path.join(tempDir, "sessions", "parent-session", "session.jsonl");
3610
- const ctx = makeMinimalCtx(tempDir);
3611
- ctx.sessionManager.getSessionFile = () => sessionFile;
3612
- const executor = makeExecutor([makeAgent("echo")], { artifactDir });
3613
-
3614
- const result = await executor.execute(
3615
- `${artifactDir}-chain-artifact-dir`,
3616
- { chain: [{ agent: "echo", task: "Run without project-local scratch files in {chain_dir}" }] },
3617
- new AbortController().signal,
3618
- undefined,
3619
- ctx,
3620
- );
3621
5401
 
3622
- const taskArg = readCallArgs().at(-1) ?? "";
3623
- assert.equal(result.isError, undefined);
3624
- assert.ok(taskArg.includes(`${CHAIN_RUNS_DIR}${path.sep}`), taskArg);
3625
- assert.equal(fs.existsSync(path.join(tempDir, ".pi-subagents", "chain-runs")), false);
3626
- assert.equal(fs.existsSync(path.join(tempDir, ".pi-subagents", "artifacts")), false);
3627
- });
3628
- }
3629
5402
 
3630
5403
  it("writes a failure stub to foreground output artifacts when no output was produced", async () => {
3631
5404
  mockPi.onCall({ output: "", stderr: "model unavailable", exitCode: 1 });
@@ -3745,7 +5518,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
3745
5518
 
3746
5519
  const taskArg = readCallArgs().at(-1) ?? "";
3747
5520
  assert.equal(result.isError, undefined);
3748
- assert.match(taskArg, new RegExp(`Write your findings to exactly this path: ${escapeRegExp(path.join(tempDir, ".pi-subagents", "artifacts", "outputs"))}.*context\\.md`));
5521
+ assert.match(taskArg, new RegExp(`Write your findings to exactly this path: ${escapeRegExp(path.join(TEMP_ARTIFACTS_DIR, "outputs"))}.*context\\.md`));
3749
5522
  assert.equal(fs.existsSync(path.join(tempDir, "context.md")), false);
3750
5523
  });
3751
5524
 
@@ -4368,6 +6141,21 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
4368
6141
  assert.equal(result.modelAttempts?.length, 1);
4369
6142
  });
4370
6143
 
6144
+ it("records blocked mutation effects when foreground implementation tools are missing", async () => {
6145
+ mockPi.onCall({ output: "I cannot edit because fixture_search is missing", missingTools: ["fixture_search"] });
6146
+ const agents = [makeAgent("worker", { tools: ["read", "fixture_search"] })];
6147
+
6148
+ const result = await runSync(tempDir, agents, "worker", "Implement the requested source fix", { runId: "missing-implementation-tool" });
6149
+
6150
+ assert.equal(result.exitCode, 1);
6151
+ assert.match(result.error ?? "", /requested unavailable child tools: fixture_search/);
6152
+ assert.doesNotMatch(result.error ?? "", /completed without making edits/);
6153
+ assert.equal(result.effects?.fileMutation?.status, "blocked");
6154
+ assert.equal(result.effects?.fileMutation?.expected, true);
6155
+ assert.equal(result.effects?.fileMutation?.attempted, false);
6156
+ assert.match(result.effects?.fileMutation?.message ?? "", /requested unavailable child tools: fixture_search/);
6157
+ });
6158
+
4371
6159
  it("passes custom tool extensions through even when explicit extensions are allowlisted", { skip: process.platform === "win32" ? "extension path resolution intermittent on Windows CI" : undefined }, async () => {
4372
6160
  mockPi.onCall({ output: "Done" });
4373
6161
  const agents = [makeAgent("echo", {
@@ -5591,6 +7379,20 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
5591
7379
  assert.equal(firstDetachResponse, true);
5592
7380
  });
5593
7381
 
7382
+ it("returns actionable guidance for ambient extension registration conflicts", async () => {
7383
+ mockPi.onCall({
7384
+ exitCode: 1,
7385
+ stderr: 'Error: Failed to load extension "/tmp/pi-mcp-adapter-clone/index.ts": Tool "mcpScript" conflicts with /tmp/pi-mcp-adapter/index.ts',
7386
+ });
7387
+ const agents = makeAgentConfigs(["echo"]);
7388
+
7389
+ const result = await runSync(tempDir, agents, "echo", "Task", {});
7390
+
7391
+ assert.equal(result.exitCode, 1);
7392
+ assert.match(result.error ?? "", /loaded conflicting ambient Pi extensions/);
7393
+ assert.match(result.error ?? "", /"echo":\{"extensions":\[\]\}/);
7394
+ });
7395
+
5594
7396
  it("handles stderr without exit code as info (not error)", async () => {
5595
7397
  mockPi.onCall({ output: "Success", stderr: "Warning: something", exitCode: 0 });
5596
7398
  const agents = makeAgentConfigs(["echo"]);