@selesai/code 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/dist/config.d.ts +4 -15
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +5 -45
  4. package/dist/config.js.map +1 -1
  5. package/dist/core/agent-session.d.ts +1 -0
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +8 -0
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/diagnostics.d.ts +1 -0
  10. package/dist/core/diagnostics.d.ts.map +1 -1
  11. package/dist/core/diagnostics.js.map +1 -1
  12. package/dist/core/model-registry.d.ts +4 -1
  13. package/dist/core/model-registry.d.ts.map +1 -1
  14. package/dist/core/model-registry.js +101 -51
  15. package/dist/core/model-registry.js.map +1 -1
  16. package/dist/core/package-manager.d.ts +10 -0
  17. package/dist/core/package-manager.d.ts.map +1 -1
  18. package/dist/core/package-manager.js +93 -8
  19. package/dist/core/package-manager.js.map +1 -1
  20. package/dist/core/package-manager.test.d.ts +2 -0
  21. package/dist/core/package-manager.test.d.ts.map +1 -0
  22. package/dist/core/package-manager.test.js +20 -0
  23. package/dist/core/package-manager.test.js.map +1 -0
  24. package/dist/core/resource-loader.d.ts +3 -0
  25. package/dist/core/resource-loader.d.ts.map +1 -1
  26. package/dist/core/resource-loader.js +10 -0
  27. package/dist/core/resource-loader.js.map +1 -1
  28. package/dist/core/settings-manager.d.ts +1 -0
  29. package/dist/core/settings-manager.d.ts.map +1 -1
  30. package/dist/core/settings-manager.js.map +1 -1
  31. package/dist/defaults/models.json +64 -113
  32. package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
  33. package/dist/extensions/caveman/index.js +116 -0
  34. package/dist/extensions/caveman/package.json +8 -0
  35. package/dist/extensions/caveman/test/extension.test.js +185 -0
  36. package/dist/extensions/caveman/test/helpers.test.js +67 -0
  37. package/dist/extensions/copy-turn.ts +2 -2
  38. package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
  39. package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
  40. package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
  41. package/dist/extensions/pi-intercom/config.ts +2 -2
  42. package/dist/extensions/pi-intercom/index.ts +2 -2
  43. package/dist/extensions/pi-intercom/package.json +2 -2
  44. package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
  45. package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
  46. package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
  47. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +2 -2
  48. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
  49. package/dist/extensions/pi-powerline-footer/index.ts +40 -47
  50. package/dist/extensions/pi-powerline-footer/package.json +2 -2
  51. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +10 -10
  52. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +1 -1
  53. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +2 -2
  54. package/dist/extensions/pi-powerline-footer/theme.ts +6 -6
  55. package/dist/extensions/pi-powerline-footer/tps.ts +1 -1
  56. package/dist/extensions/pi-powerline-footer/types.ts +7 -7
  57. package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
  58. package/dist/extensions/pi-powerline-footer/working-vibes.ts +52 -59
  59. package/dist/extensions/pi-rewind-hook/index.ts +1 -1
  60. package/dist/extensions/pi-rewind-hook/package.json +1 -1
  61. package/dist/extensions/pi-subagents/README.md +224 -52
  62. package/dist/extensions/pi-subagents/agents/architect.md +188 -0
  63. package/dist/extensions/pi-subagents/agents/builder.md +120 -0
  64. package/dist/extensions/pi-subagents/agents/commentator.md +131 -0
  65. package/dist/extensions/pi-subagents/agents/explorer.md +51 -0
  66. package/dist/extensions/pi-subagents/agents/recapper.md +21 -0
  67. package/dist/extensions/pi-subagents/install.mjs +93 -0
  68. package/dist/extensions/pi-subagents/package.json +4 -5
  69. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  70. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  71. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
  72. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
  73. package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
  74. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
  75. package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
  76. package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
  77. package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
  78. package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
  79. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
  80. package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
  81. package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
  82. package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
  83. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
  84. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
  85. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
  86. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
  87. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
  88. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
  89. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
  90. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
  91. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
  92. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
  93. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
  94. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
  95. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
  96. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
  97. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
  98. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
  99. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
  100. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
  101. package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
  102. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
  103. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
  104. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
  105. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
  106. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
  107. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
  108. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
  109. package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
  110. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
  112. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
  113. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
  114. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
  115. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
  116. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
  117. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
  118. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
  119. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
  120. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
  121. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
  122. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
  123. package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
  124. package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
  125. package/dist/extensions/pi-subagents/src/shared/utils.ts +108 -17
  126. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
  127. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
  128. package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
  129. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
  130. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
  131. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
  132. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
  133. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
  134. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
  135. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
  136. package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
  137. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
  138. package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
  139. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
  140. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
  141. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
  142. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
  143. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
  144. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
  145. package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
  146. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
  147. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
  148. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
  149. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
  150. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
  151. package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
  152. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
  153. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
  154. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
  155. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
  156. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
  157. package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
  158. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
  159. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
  160. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
  161. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
  162. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
  163. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
  164. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
  165. package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
  166. package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
  167. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
  168. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
  169. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
  170. package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
  171. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
  172. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
  173. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
  174. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
  175. package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
  176. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
  177. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
  178. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
  179. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
  180. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
  181. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
  182. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
  183. package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
  184. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
  185. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
  186. package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
  187. package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
  188. package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
  189. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
  190. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
  191. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
  192. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
  193. package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
  194. package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
  195. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
  196. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
  197. package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
  198. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
  199. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
  200. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
  201. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
  202. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
  203. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
  204. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
  205. package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
  206. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
  207. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
  208. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
  209. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
  210. package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
  211. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
  212. package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
  213. package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
  214. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
  215. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
  216. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
  217. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
  218. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
  219. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
  220. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
  221. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
  222. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
  223. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
  224. package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
  225. package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
  226. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
  227. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
  228. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
  229. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
  230. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
  231. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
  232. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
  233. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
  234. package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
  235. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
  236. package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
  237. package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
  238. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
  239. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
  240. package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
  241. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
  242. package/dist/extensions/pi-web-agent/package.json +2 -2
  243. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +2 -2
  244. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
  245. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +1 -1
  246. package/dist/extensions/pi-web-agent/src/extension.ts +1 -1
  247. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
  248. package/dist/extensions/question/helpers.ts +3 -3
  249. package/dist/extensions/question/index.ts +2 -2
  250. package/dist/extensions/question/navigation.ts +2 -2
  251. package/dist/extensions/question/package.json +2 -2
  252. package/dist/extensions/question/question-list.ts +2 -2
  253. package/dist/extensions/question/tests/question-list.test.ts +2 -2
  254. package/dist/extensions/question/tui-adapter.ts +2 -2
  255. package/dist/extensions/question/types.ts +2 -2
  256. package/dist/extensions/question/ui-protocol.ts +2 -2
  257. package/dist/extensions/rtk.ts +2 -2
  258. package/dist/extensions/tokenin-onboarding.ts +2 -2
  259. package/dist/extensions/undo.ts +1 -1
  260. package/dist/extensions/web-agent-onboarding.ts +3 -3
  261. package/dist/extensions/workflow/adapter.ts +16 -8
  262. package/dist/extensions/workflow/extension.ts +1 -1
  263. package/dist/extensions/workflow/package.json +1 -1
  264. package/dist/main.d.ts.map +1 -1
  265. package/dist/main.js +4 -0
  266. package/dist/main.js.map +1 -1
  267. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  268. package/dist/modes/interactive/components/assistant-message.js +2 -0
  269. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  270. package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
  271. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  272. package/dist/modes/interactive/components/first-time-setup.js +19 -5
  273. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  274. package/dist/skills/caveman/SKILL.md +49 -0
  275. package/dist/skills/handoff-text/SKILL.md +13 -0
  276. package/dist/utils/thinking-tags.d.ts +7 -0
  277. package/dist/utils/thinking-tags.d.ts.map +1 -0
  278. package/dist/utils/thinking-tags.js +63 -0
  279. package/dist/utils/thinking-tags.js.map +1 -0
  280. package/docs/docs.json +4 -0
  281. package/docs/extensions.md +2 -0
  282. package/docs/settings.md +15 -0
  283. package/docs/shared-host-extensions.md +109 -0
  284. package/package.json +2 -2
@@ -0,0 +1,1033 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { after, afterEach, before, beforeEach, describe, it } from "node:test";
6
+ import { ASYNC_DIR, INTERCOM_DETACH_REQUEST_EVENT, RESULTS_DIR, SUBAGENT_ASYNC_STARTED_EVENT } from "../../src/shared/types.ts";
7
+ import type { MockPi } from "../support/helpers.ts";
8
+ import {
9
+ createMockPi,
10
+ createTempDir,
11
+ events,
12
+ makeAgent,
13
+ makeMinimalCtx,
14
+ removeTempDir,
15
+ tryImport,
16
+ } from "../support/helpers.ts";
17
+
18
+ interface ExecutorResult {
19
+ content: Array<{ text?: string }>;
20
+ isError?: boolean;
21
+ details?: {
22
+ mode?: string;
23
+ runId?: string;
24
+ results?: Array<{ agent?: string; finalOutput?: string }>;
25
+ asyncId?: string;
26
+ };
27
+ }
28
+
29
+ interface ExecutorModule {
30
+ createSubagentExecutor?: (...args: unknown[]) => {
31
+ execute: (
32
+ id: string,
33
+ params: Record<string, unknown>,
34
+ signal: AbortSignal,
35
+ onUpdate: ((result: unknown) => void) | undefined,
36
+ ctx: unknown,
37
+ ) => Promise<ExecutorResult>;
38
+ };
39
+ }
40
+
41
+ const executorMod = await tryImport<ExecutorModule>("./src/runs/foreground/subagent-executor.ts");
42
+ const available = !!executorMod?.createSubagentExecutor;
43
+ const createSubagentExecutor = executorMod?.createSubagentExecutor;
44
+
45
+ function createRecordingEventBus(options: { acknowledgeResults?: boolean } = {}) {
46
+ const listeners = new Map<string, Set<(payload: unknown) => void>>();
47
+ const emitted: Array<{ channel: string; payload: unknown }> = [];
48
+ const bus = {
49
+ emitted,
50
+ on(channel: string, handler: (payload: unknown) => void) {
51
+ const channelListeners = listeners.get(channel) ?? new Set();
52
+ channelListeners.add(handler);
53
+ listeners.set(channel, channelListeners);
54
+ return () => {
55
+ channelListeners.delete(handler);
56
+ if (channelListeners.size === 0) listeners.delete(channel);
57
+ };
58
+ },
59
+ emit(channel: string, payload: unknown) {
60
+ emitted.push({ channel, payload });
61
+ for (const handler of listeners.get(channel) ?? []) {
62
+ handler(payload);
63
+ }
64
+ if (options.acknowledgeResults && channel === "subagent:result-intercom") {
65
+ const requestId = payload && typeof payload === "object" ? (payload as { requestId?: unknown }).requestId : undefined;
66
+ if (typeof requestId === "string") {
67
+ setImmediate(() => bus.emit("subagent:result-intercom-delivery", { requestId, delivered: true }));
68
+ }
69
+ }
70
+ },
71
+ };
72
+ return bus;
73
+ }
74
+
75
+ describe("intercom result delivery cutover", { skip: !available ? "executor not importable" : undefined }, () => {
76
+ let tempDir: string;
77
+ let homeDir: string;
78
+ let mockPi: MockPi;
79
+ let originalHome: string | undefined;
80
+ let originalUserProfile: string | undefined;
81
+
82
+ before(() => {
83
+ originalHome = process.env.HOME;
84
+ originalUserProfile = process.env.USERPROFILE;
85
+ homeDir = createTempDir("pi-subagent-intercom-home-");
86
+ process.env.HOME = homeDir;
87
+ process.env.USERPROFILE = homeDir;
88
+ mockPi = createMockPi();
89
+ mockPi.install();
90
+ fs.mkdirSync(path.join(os.homedir(), ".selesai", "agent", "extensions", "pi-intercom"), { recursive: true });
91
+ fs.mkdirSync(path.join(os.homedir(), ".selesai", "agent", "intercom"), { recursive: true });
92
+ fs.writeFileSync(path.join(os.homedir(), ".selesai", "agent", "intercom", "config.json"), JSON.stringify({ enabled: true }), "utf-8");
93
+ });
94
+
95
+ after(() => {
96
+ mockPi.uninstall();
97
+ if (originalHome === undefined) delete process.env.HOME;
98
+ else process.env.HOME = originalHome;
99
+ if (originalUserProfile === undefined) delete process.env.USERPROFILE;
100
+ else process.env.USERPROFILE = originalUserProfile;
101
+ removeTempDir(homeDir);
102
+ });
103
+
104
+ beforeEach(() => {
105
+ tempDir = createTempDir("pi-subagent-intercom-result-");
106
+ mockPi.reset();
107
+ });
108
+
109
+ afterEach(() => {
110
+ removeTempDir(tempDir);
111
+ });
112
+
113
+ async function readMockCallArgs(index: number): Promise<string[]> {
114
+ const deadline = Date.now() + 10_000;
115
+ let callFile: string | undefined;
116
+ while (!callFile) {
117
+ callFile = fs.readdirSync(mockPi.dir)
118
+ .filter((name) => name.startsWith("call-") && name.endsWith(".json"))
119
+ .sort()[index];
120
+ if (callFile || Date.now() > deadline) break;
121
+ await new Promise((resolve) => setTimeout(resolve, 50));
122
+ }
123
+ assert.ok(callFile, `expected mock pi call at index ${index}`);
124
+ return JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")).args as string[];
125
+ }
126
+
127
+ async function waitForFile(filePath: string, timeoutMs = 10_000): Promise<void> {
128
+ const deadline = Date.now() + timeoutMs;
129
+ while (!fs.existsSync(filePath)) {
130
+ if (Date.now() > deadline) assert.fail(`Timed out waiting for file: ${filePath}`);
131
+ await new Promise((resolve) => setTimeout(resolve, 50));
132
+ }
133
+ }
134
+
135
+ function makeExecutor(options: { bridgeMode?: "always" | "off"; agents?: ReturnType<typeof makeAgent>[]; acknowledgeResults?: boolean; kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean } = {}) {
136
+ const events = createRecordingEventBus({ acknowledgeResults: options.acknowledgeResults ?? true });
137
+ const state = {
138
+ baseCwd: tempDir,
139
+ currentSessionId: null,
140
+ asyncJobs: new Map(),
141
+ foregroundRuns: new Map(),
142
+ foregroundControls: new Map(),
143
+ lastForegroundControlId: null,
144
+ cleanupTimers: new Map(),
145
+ lastUiContext: null,
146
+ poller: null,
147
+ completionSeen: new Map(),
148
+ watcher: null,
149
+ watcherRestartTimer: null,
150
+ resultFileCoalescer: {
151
+ schedule: () => false,
152
+ clear: () => {},
153
+ },
154
+ };
155
+ const executor = createSubagentExecutor!({
156
+ pi: {
157
+ events,
158
+ getSessionName: () => "orchestrator",
159
+ setSessionName: () => {},
160
+ },
161
+ state,
162
+ config: {
163
+ intercomBridge: { mode: options.bridgeMode ?? "always" },
164
+ },
165
+ asyncByDefault: false,
166
+ tempArtifactsDir: tempDir,
167
+ getSubagentSessionRoot: () => tempDir,
168
+ expandTilde: (value: string) => value,
169
+ discoverAgents: () => ({ agents: options.agents ?? [makeAgent("worker")] }),
170
+ kill: options.kill,
171
+ });
172
+ return { executor, events, state };
173
+ }
174
+
175
+ it("single foreground runs emit one grouped event and return a compact receipt", async () => {
176
+ mockPi.onCall({ output: "Full child output from worker" });
177
+ const { executor, events } = makeExecutor();
178
+
179
+ const result = await executor.execute(
180
+ "single-intercom",
181
+ { agent: "worker", task: "Implement feature" },
182
+ new AbortController().signal,
183
+ undefined,
184
+ makeMinimalCtx(tempDir),
185
+ );
186
+
187
+ const intercomEvents = events.emitted.filter((entry) => entry.channel === "subagent:result-intercom");
188
+ assert.equal(intercomEvents.length, 1);
189
+ const payload = intercomEvents[0]!.payload as { children?: Array<{ agent?: string; intercomTarget?: string }>; message?: string; mode?: string };
190
+ assert.equal(payload.mode, "single");
191
+ assert.equal(payload.children?.length, 1);
192
+ assert.equal(payload.children?.[0]?.agent, "worker");
193
+ assert.match(payload.children?.[0]?.intercomTarget ?? "", /^subagent-worker-[a-f0-9]+-1$/);
194
+ assert.match(String(payload.message ?? ""), /Intercom targets below identify child sessions used while they were running/);
195
+ assert.match(String(payload.message ?? ""), /Run intercom target: subagent-worker-[a-f0-9]+-1/);
196
+ assert.match(result.content[0]?.text ?? "", /Delivered single subagent result via intercom\./);
197
+ assert.doesNotMatch(result.content[0]?.text ?? "", /Full child output from worker/);
198
+ assert.equal(result.details?.results?.[0]?.finalOutput, undefined);
199
+ assert.match(String(payload.message ?? ""), /Full child output from worker/);
200
+ });
201
+
202
+ it("falls back to legacy foreground output when the bridge is inactive", async () => {
203
+ mockPi.onCall({ output: "Legacy foreground output" });
204
+ const { executor, events } = makeExecutor({ bridgeMode: "off" });
205
+
206
+ const result = await executor.execute(
207
+ "single-no-intercom",
208
+ { agent: "worker", task: "Summarize feature" },
209
+ new AbortController().signal,
210
+ undefined,
211
+ makeMinimalCtx(tempDir),
212
+ );
213
+
214
+ assert.equal(events.emitted.some((entry) => entry.channel === "subagent:result-intercom"), false);
215
+ assert.match(result.content[0]?.text ?? "", /Legacy foreground output/);
216
+ });
217
+
218
+ it("falls back to legacy foreground output when grouped delivery is not acknowledged", async () => {
219
+ mockPi.onCall({ output: "Unacknowledged foreground output" });
220
+ const { executor, events } = makeExecutor({ acknowledgeResults: false });
221
+
222
+ const result = await executor.execute(
223
+ "single-no-ack",
224
+ { agent: "worker", task: "Summarize feature" },
225
+ new AbortController().signal,
226
+ undefined,
227
+ makeMinimalCtx(tempDir),
228
+ );
229
+
230
+ assert.equal(events.emitted.some((entry) => entry.channel === "subagent:result-intercom"), true);
231
+ assert.match(result.content[0]?.text ?? "", /Unacknowledged foreground output/);
232
+ });
233
+
234
+ it("top-level parallel runs emit one grouped event containing all children", async () => {
235
+ mockPi.onCall({ output: "Parallel child output" });
236
+ const { executor, events } = makeExecutor({ agents: [makeAgent("a"), makeAgent("b")] });
237
+
238
+ const result = await executor.execute(
239
+ "parallel-intercom",
240
+ { tasks: [{ agent: "a", task: "task-a" }, { agent: "b", task: "task-b" }] },
241
+ new AbortController().signal,
242
+ undefined,
243
+ makeMinimalCtx(tempDir),
244
+ );
245
+
246
+ const intercomEvents = events.emitted.filter((entry) => entry.channel === "subagent:result-intercom");
247
+ assert.equal(intercomEvents.length, 1);
248
+ const payload = intercomEvents[0]!.payload as { children?: Array<{ agent?: string; intercomTarget?: string }>; message?: string; mode?: string };
249
+ assert.equal(payload.mode, "parallel");
250
+ assert.deepEqual((payload.children ?? []).map((child) => child.agent).sort(), ["a", "b"]);
251
+ assert.equal((payload.children ?? []).every((child) => /^subagent-[ab]-[a-f0-9]+-[12]$/.test(child.intercomTarget ?? "")), true);
252
+ assert.match(String(payload.message ?? ""), /Intercom targets below identify child sessions used while they were running/);
253
+ assert.match(String(payload.message ?? ""), /Run intercom target: subagent-a-[a-f0-9]+-1/);
254
+ assert.match(String(payload.message ?? ""), /1\. a — completed/);
255
+ assert.match(String(payload.message ?? ""), /2\. b — completed/);
256
+ assert.match(result.content[0]?.text ?? "", /Delivered parallel subagent results via intercom\./);
257
+ assert.equal(result.details?.results?.every((entry) => entry.finalOutput === undefined), true);
258
+ });
259
+
260
+ it("chain runs emit one grouped event containing all executed children", async () => {
261
+ mockPi.onCall({ output: "Chain child output" });
262
+ const { executor, events } = makeExecutor({ agents: [makeAgent("a"), makeAgent("b"), makeAgent("c")] });
263
+
264
+ const result = await executor.execute(
265
+ "chain-intercom",
266
+ {
267
+ chain: [
268
+ { agent: "a", task: "step-a" },
269
+ { parallel: [{ agent: "b", task: "step-b" }, { agent: "c", task: "step-c" }] },
270
+ ],
271
+ },
272
+ new AbortController().signal,
273
+ undefined,
274
+ makeMinimalCtx(tempDir),
275
+ );
276
+
277
+ const intercomEvents = events.emitted.filter((entry) => entry.channel === "subagent:result-intercom");
278
+ assert.equal(intercomEvents.length, 1);
279
+ const payload = intercomEvents[0]!.payload as { children?: Array<{ agent?: string; intercomTarget?: string }>; message?: string; mode?: string };
280
+ assert.equal(payload.mode, "chain");
281
+ assert.deepEqual((payload.children ?? []).map((child) => child.agent).sort(), ["a", "b", "c"]);
282
+ assert.equal((payload.children ?? []).every((child) => /^subagent-[abc]-[a-f0-9]+-[123]$/.test(child.intercomTarget ?? "")), true);
283
+ assert.match(String(payload.message ?? ""), /1\. a — completed/);
284
+ assert.match(String(payload.message ?? ""), /2\. b — completed/);
285
+ assert.match(String(payload.message ?? ""), /3\. c — completed/);
286
+ assert.match(result.content[0]?.text ?? "", /Delivered chain subagent results via intercom\./);
287
+ assert.equal(result.details?.results?.every((entry) => entry.finalOutput === undefined), true);
288
+ });
289
+
290
+ it("detached chain runs do not emit grouped completion receipts", async () => {
291
+ mockPi.onCall({
292
+ steps: [
293
+ { jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
294
+ { delay: 1000, jsonl: [events.assistantMessage("after reply")] },
295
+ ],
296
+ });
297
+ const { executor, events: bus } = makeExecutor({ agents: [makeAgent("a", { systemPrompt: "Intercom orchestration channel:" }), makeAgent("b")] });
298
+ let detachEmitted = false;
299
+
300
+ const result = await executor.execute(
301
+ "chain-detached-intercom",
302
+ {
303
+ chain: [
304
+ { agent: "a", task: "ask supervisor" },
305
+ { agent: "b", task: "must not run" },
306
+ ],
307
+ },
308
+ new AbortController().signal,
309
+ (update: { details?: { progress?: Array<{ currentTool?: string }> } }) => {
310
+ if (detachEmitted) return;
311
+ if (!update.details?.progress?.some((entry) => entry.currentTool === "contact_supervisor")) return;
312
+ detachEmitted = true;
313
+ bus.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "chain-detached" });
314
+ },
315
+ makeMinimalCtx(tempDir),
316
+ );
317
+
318
+ assert.equal(detachEmitted, true);
319
+ assert.match(result.content[0]?.text ?? "", /Chain detached for intercom coordination/);
320
+ assert.doesNotMatch(result.content[0]?.text ?? "", /resume/);
321
+ assert.equal(bus.emitted.some((entry) => entry.channel === "subagent:result-intercom"), false);
322
+ assert.equal(mockPi.callCount(), 1);
323
+ });
324
+
325
+ it("resume action sends a follow-up to a live async child when the target is registered", async () => {
326
+ const runId = `resume-live-${Date.now()}`;
327
+ const asyncDir = path.join(ASYNC_DIR, runId);
328
+ const kills: Array<{ pid: number; signal?: NodeJS.Signals | 0 }> = [];
329
+ try {
330
+ fs.mkdirSync(asyncDir, { recursive: true });
331
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
332
+ runId,
333
+ mode: "single",
334
+ state: "running",
335
+ pid: process.pid,
336
+ startedAt: 100,
337
+ lastUpdate: Date.now(),
338
+ steps: [{ agent: "worker", status: "running" }],
339
+ }, null, 2), "utf-8");
340
+ const { executor, events } = makeExecutor({
341
+ kill: (pid, signal) => {
342
+ kills.push({ pid, signal });
343
+ return true;
344
+ },
345
+ });
346
+
347
+ const result = await executor.execute(
348
+ "resume-live",
349
+ { action: "resume", id: runId, message: "Can you clarify the last change?" },
350
+ new AbortController().signal,
351
+ undefined,
352
+ makeMinimalCtx(tempDir),
353
+ );
354
+
355
+ assert.equal(result.isError, undefined);
356
+ assert.match(result.content[0]?.text ?? "", /Interrupted live async child, then delivered follow-up/);
357
+ assert.deepEqual(kills, [
358
+ { pid: process.pid, signal: 0 },
359
+ { pid: process.pid, signal: process.platform === "win32" ? "SIGBREAK" : "SIGUSR2" },
360
+ ]);
361
+ const payload = events.emitted.find((entry) => entry.channel === "subagent:result-intercom")?.payload as { to?: string; message?: string } | undefined;
362
+ assert.equal(payload?.to, `subagent-worker-${runId}-1`);
363
+ assert.match(payload?.message ?? "", /Can you clarify the last change\?/);
364
+ } finally {
365
+ fs.rmSync(asyncDir, { recursive: true, force: true });
366
+ }
367
+ });
368
+
369
+ it("resume action can attach a live async child as the first step of a new chain", async () => {
370
+ const sourceRunId = `resume-chain-root-${Date.now()}`;
371
+ const sourceAsyncDir = path.join(ASYNC_DIR, sourceRunId);
372
+ const sourceResultPath = path.join(RESULTS_DIR, `${sourceRunId}.json`);
373
+ const sourceSession = path.join(tempDir, "source-child.jsonl");
374
+ try {
375
+ fs.mkdirSync(sourceAsyncDir, { recursive: true });
376
+ fs.mkdirSync(RESULTS_DIR, { recursive: true });
377
+ fs.writeFileSync(sourceSession, "", "utf-8");
378
+ fs.writeFileSync(path.join(sourceAsyncDir, "status.json"), JSON.stringify({
379
+ runId: sourceRunId,
380
+ mode: "single",
381
+ state: "running",
382
+ pid: process.pid,
383
+ startedAt: 100,
384
+ lastUpdate: 100,
385
+ cwd: tempDir,
386
+ steps: [{ agent: "worker", status: "running", sessionFile: sourceSession }],
387
+ }, null, 2), "utf-8");
388
+ fs.writeFileSync(sourceResultPath, JSON.stringify({
389
+ id: sourceRunId,
390
+ agent: "worker",
391
+ mode: "single",
392
+ success: true,
393
+ state: "complete",
394
+ summary: "root output",
395
+ results: [{ agent: "worker", output: "root output", success: true, sessionFile: sourceSession }],
396
+ }, null, 2), "utf-8");
397
+ const { executor, events } = makeExecutor({ agents: [makeAgent("worker"), makeAgent("reviewer")] });
398
+
399
+ const result = await executor.execute(
400
+ "resume-chain-root",
401
+ {
402
+ action: "resume",
403
+ id: sourceRunId,
404
+ chain: [{ agent: "reviewer", task: "Review this root result: {previous}" }],
405
+ },
406
+ new AbortController().signal,
407
+ undefined,
408
+ makeMinimalCtx(tempDir),
409
+ );
410
+
411
+ assert.equal(result.isError, undefined);
412
+ assert.match(result.content[0]?.text ?? "", /Attached async subagent/);
413
+ const startedEvent = events.emitted.find((entry) => entry.channel === SUBAGENT_ASYNC_STARTED_EVENT)?.payload as { agent?: string; agents?: string[]; chain?: string[]; chainStepCount?: number } | undefined;
414
+ assert.equal(startedEvent?.agent, "worker");
415
+ assert.deepEqual(startedEvent?.agents, ["worker", "reviewer"]);
416
+ assert.deepEqual(startedEvent?.chain, ["worker", "reviewer"]);
417
+ assert.equal(startedEvent?.chainStepCount, 2);
418
+ const attachedId = result.details?.asyncId;
419
+ assert.ok(attachedId, "expected attached chain async id");
420
+ assert.match(result.details?.asyncDir ?? "", new RegExp(`${attachedId}$`));
421
+ const statusPath = path.join(result.details!.asyncDir!, "status.json");
422
+ await waitForFile(statusPath);
423
+ const attachedStatus = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as { mode?: string; chainStepCount?: number; steps?: Array<{ agent?: string; label?: string; status?: string }> };
424
+ assert.equal(attachedStatus.mode, "chain");
425
+ assert.equal(attachedStatus.chainStepCount, 2);
426
+ assert.deepEqual(attachedStatus.steps?.map((step) => step.agent), ["worker", "reviewer"]);
427
+ assert.match(attachedStatus.steps?.[0]?.label ?? "", /Attached resume-chain-root-/);
428
+ await waitForFile(path.join(RESULTS_DIR, `${attachedId}.json`));
429
+ } finally {
430
+ fs.rmSync(sourceAsyncDir, { recursive: true, force: true });
431
+ fs.rmSync(sourceResultPath, { force: true });
432
+ }
433
+ });
434
+
435
+ it("resume action can attach a completed async result without reviving from a session", async () => {
436
+ const sourceRunId = `resume-chain-complete-root-${Date.now()}`;
437
+ const sourceAsyncDir = path.join(ASYNC_DIR, sourceRunId);
438
+ const sourceResultPath = path.join(RESULTS_DIR, `${sourceRunId}.json`);
439
+ try {
440
+ fs.mkdirSync(sourceAsyncDir, { recursive: true });
441
+ fs.mkdirSync(RESULTS_DIR, { recursive: true });
442
+ fs.writeFileSync(path.join(sourceAsyncDir, "status.json"), JSON.stringify({
443
+ runId: sourceRunId,
444
+ mode: "single",
445
+ state: "complete",
446
+ startedAt: 100,
447
+ lastUpdate: 200,
448
+ cwd: tempDir,
449
+ steps: [{ agent: "worker", status: "complete" }],
450
+ }, null, 2), "utf-8");
451
+ fs.writeFileSync(sourceResultPath, JSON.stringify({
452
+ id: sourceRunId,
453
+ agent: "worker",
454
+ mode: "single",
455
+ success: true,
456
+ state: "complete",
457
+ summary: "completed root output",
458
+ results: [{ agent: "worker", output: "completed root output", success: true }],
459
+ }, null, 2), "utf-8");
460
+ const { executor } = makeExecutor({ agents: [makeAgent("worker"), makeAgent("reviewer")] });
461
+
462
+ const reviveOnly = await executor.execute(
463
+ "resume-chain-complete-root-revive-only",
464
+ { action: "resume", id: sourceRunId, message: "Follow up" },
465
+ new AbortController().signal,
466
+ undefined,
467
+ makeMinimalCtx(tempDir),
468
+ );
469
+ assert.equal(reviveOnly.isError, true);
470
+ assert.match(reviveOnly.content[0]?.text ?? "", /does not have a persisted session file/);
471
+
472
+ const attached = await executor.execute(
473
+ "resume-chain-complete-root",
474
+ {
475
+ action: "resume",
476
+ id: sourceRunId,
477
+ chain: [{ agent: "reviewer", task: "Review this completed root result: {previous}" }],
478
+ },
479
+ new AbortController().signal,
480
+ undefined,
481
+ makeMinimalCtx(tempDir),
482
+ );
483
+
484
+ assert.equal(attached.isError, undefined);
485
+ assert.match(attached.content[0]?.text ?? "", /Attached async subagent/);
486
+ assert.ok(attached.details?.asyncId, "expected attached chain async id");
487
+ await waitForFile(path.join(RESULTS_DIR, `${attached.details.asyncId}.json`));
488
+ } finally {
489
+ fs.rmSync(sourceAsyncDir, { recursive: true, force: true });
490
+ fs.rmSync(sourceResultPath, { force: true });
491
+ }
492
+ });
493
+
494
+ it("resume action revives completed multi-child async runs by index", async () => {
495
+ mockPi.onCall({ output: "revived async child b" });
496
+ const runId = `resume-revive-multi-${Date.now()}`;
497
+ const asyncDir = path.join(ASYNC_DIR, runId);
498
+ const firstSession = path.join(tempDir, "child-a.jsonl");
499
+ const secondSession = path.join(tempDir, "child-b.jsonl");
500
+ try {
501
+ fs.mkdirSync(asyncDir, { recursive: true });
502
+ fs.writeFileSync(firstSession, "", "utf-8");
503
+ fs.writeFileSync(secondSession, "", "utf-8");
504
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
505
+ runId,
506
+ mode: "parallel",
507
+ state: "complete",
508
+ startedAt: 100,
509
+ lastUpdate: 200,
510
+ cwd: tempDir,
511
+ steps: [
512
+ { agent: "a", status: "complete", sessionFile: firstSession },
513
+ { agent: "b", status: "complete", sessionFile: secondSession },
514
+ ],
515
+ }, null, 2), "utf-8");
516
+ const { executor } = makeExecutor({ agents: [makeAgent("a"), makeAgent("b")] });
517
+
518
+ const result = await executor.execute(
519
+ "resume-revive-multi",
520
+ { action: "resume", id: runId, index: 1, message: "What did b find?" },
521
+ new AbortController().signal,
522
+ undefined,
523
+ makeMinimalCtx(tempDir),
524
+ );
525
+
526
+ assert.equal(result.isError, undefined);
527
+ assert.match(result.content[0]?.text ?? "", /Revived async subagent from/);
528
+ assert.match(result.content[0]?.text ?? "", /Agent: b/);
529
+ assert.match(result.content[0]?.text ?? "", new RegExp(secondSession.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
530
+ const args = await readMockCallArgs(0);
531
+ assert.equal(args[args.indexOf("--session") + 1], secondSession);
532
+ } finally {
533
+ fs.rmSync(asyncDir, { recursive: true, force: true });
534
+ }
535
+ });
536
+
537
+ it("resume action revives completed async runs with no-poll handoff guidance", async () => {
538
+ mockPi.onCall({ output: "revived answer" });
539
+ const runId = `resume-revive-${Date.now()}`;
540
+ const asyncDir = path.join(ASYNC_DIR, runId);
541
+ const sessionFile = path.join(tempDir, "child-session.jsonl");
542
+ try {
543
+ fs.mkdirSync(asyncDir, { recursive: true });
544
+ fs.writeFileSync(sessionFile, "", "utf-8");
545
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
546
+ runId,
547
+ mode: "single",
548
+ state: "complete",
549
+ startedAt: 100,
550
+ lastUpdate: 200,
551
+ cwd: tempDir,
552
+ sessionFile,
553
+ steps: [{ agent: "worker", status: "complete" }],
554
+ }, null, 2), "utf-8");
555
+ const { executor } = makeExecutor();
556
+
557
+ const result = await executor.execute(
558
+ "resume-revive",
559
+ { action: "resume", id: runId, message: "What changed?" },
560
+ new AbortController().signal,
561
+ undefined,
562
+ makeMinimalCtx(tempDir),
563
+ );
564
+
565
+ assert.equal(result.isError, undefined);
566
+ assert.match(result.content[0]?.text ?? "", /Revived async subagent from/);
567
+ assert.match(result.content[0]?.text ?? "", /Do not run sleep timers or polling loops/);
568
+ assert.match(result.content[0]?.text ?? "", /call wait\(\)/);
569
+ assert.match(result.content[0]?.text ?? "", /Status if needed: subagent\(\{ action: "status"/);
570
+ assert.doesNotMatch(result.content[0]?.text ?? "", /Follow:/);
571
+ const revivedId = result.details?.asyncId;
572
+ assert.ok(revivedId, "expected revived async id");
573
+ const resultPath = path.join(RESULTS_DIR, `${revivedId}.json`);
574
+ const deadline = Date.now() + 10_000;
575
+ while (!fs.existsSync(resultPath)) {
576
+ if (Date.now() > deadline) assert.fail(`Timed out waiting for revived result file: ${resultPath}`);
577
+ await new Promise((resolve) => setTimeout(resolve, 50));
578
+ }
579
+ } finally {
580
+ fs.rmSync(asyncDir, { recursive: true, force: true });
581
+ }
582
+ });
583
+
584
+ it("resume action revives a completed foreground child by index", async () => {
585
+ mockPi.onCall({ output: "first child done" });
586
+ mockPi.onCall({ output: "second child done" });
587
+ mockPi.onCall({ output: "revived foreground answer" });
588
+ const { executor } = makeExecutor({ bridgeMode: "off", agents: [makeAgent("a"), makeAgent("b")] });
589
+
590
+ const original = await executor.execute(
591
+ "foreground-resume-original",
592
+ { tasks: [{ agent: "a", task: "task-a" }, { agent: "b", task: "task-b" }] },
593
+ new AbortController().signal,
594
+ undefined,
595
+ makeMinimalCtx(tempDir),
596
+ );
597
+ const runId = original.details?.runId;
598
+ assert.ok(runId, "expected foreground run id");
599
+
600
+ const revived = await executor.execute(
601
+ "foreground-resume",
602
+ { action: "resume", id: runId, index: 1, message: "Follow up with b" },
603
+ new AbortController().signal,
604
+ undefined,
605
+ makeMinimalCtx(tempDir),
606
+ );
607
+
608
+ assert.equal(revived.isError, undefined);
609
+ assert.match(revived.content[0]?.text ?? "", /Revived foreground subagent from/);
610
+ assert.match(revived.content[0]?.text ?? "", /Agent: b/);
611
+ const reviveArgs = await readMockCallArgs(2);
612
+ const selectedSession = original.details?.results?.[1]?.sessionFile;
613
+ assert.ok(selectedSession, "expected selected child session file");
614
+ assert.equal(reviveArgs[reviveArgs.indexOf("--session") + 1], selectedSession);
615
+ const revivedId = revived.details?.asyncId;
616
+ assert.ok(revivedId, "expected revived async id");
617
+ const resultPath = path.join(RESULTS_DIR, `${revivedId}.json`);
618
+ const deadline = Date.now() + 10_000;
619
+ while (!fs.existsSync(resultPath)) {
620
+ if (Date.now() > deadline) assert.fail(`Timed out waiting for revived result file: ${resultPath}`);
621
+ await new Promise((resolve) => setTimeout(resolve, 50));
622
+ }
623
+ });
624
+
625
+ it("status recovers remembered detached foreground output after child exit", async () => {
626
+ mockPi.onCall({
627
+ steps: [
628
+ { jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
629
+ { delay: 50, jsonl: [events.assistantMessage("final recovered answer")] },
630
+ ],
631
+ });
632
+ const { executor, events: bus } = makeExecutor({ agents: [makeAgent("a", { systemPrompt: "Intercom orchestration channel:" })] });
633
+ let detachEmitted = false;
634
+ const original = await executor.execute(
635
+ "foreground-detached-status-original",
636
+ { agent: "a", task: "ask supervisor" },
637
+ new AbortController().signal,
638
+ (update: { details?: { progress?: Array<{ currentTool?: string }> } }) => {
639
+ if (detachEmitted) return;
640
+ if (!update.details?.progress?.some((entry) => entry.currentTool === "contact_supervisor")) return;
641
+ detachEmitted = true;
642
+ bus.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "single-detached-status" });
643
+ },
644
+ makeMinimalCtx(tempDir),
645
+ );
646
+ assert.equal(detachEmitted, true);
647
+ const runId = original.details?.runId;
648
+ assert.ok(runId, "expected foreground run id");
649
+ assert.match(original.content[0]?.text ?? "", /Detached for intercom coordination/);
650
+
651
+ const deadline = Date.now() + 5000;
652
+ let statusText = "";
653
+ while (Date.now() < deadline) {
654
+ const status = await executor.execute(
655
+ "foreground-detached-status",
656
+ { action: "status", id: runId },
657
+ new AbortController().signal,
658
+ undefined,
659
+ makeMinimalCtx(tempDir),
660
+ );
661
+ statusText = status.content[0]?.text ?? "";
662
+ if (/final recovered answer/.test(statusText)) break;
663
+ await new Promise((resolve) => setTimeout(resolve, 25));
664
+ }
665
+
666
+ assert.doesNotMatch(statusText, /Async run not found/);
667
+ assert.match(statusText, /State: remembered foreground/);
668
+ assert.match(statusText, /a completed/);
669
+ assert.match(statusText, /final recovered answer/);
670
+
671
+ const transcript = await executor.execute(
672
+ "foreground-detached-transcript",
673
+ { action: "status", id: runId, view: "transcript" },
674
+ new AbortController().signal,
675
+ undefined,
676
+ makeMinimalCtx(tempDir),
677
+ );
678
+ const transcriptText = transcript.content[0]?.text ?? "";
679
+ assert.doesNotMatch(transcriptText, /Async run not found/);
680
+ assert.match(transcriptText, /final recovered answer/);
681
+ });
682
+
683
+ it("status recovers remembered detached chain output after child exit", async () => {
684
+ mockPi.onCall({
685
+ steps: [
686
+ { jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
687
+ { delay: 50, jsonl: [events.assistantMessage("chain recovered answer")] },
688
+ ],
689
+ });
690
+ const { executor, events: bus } = makeExecutor({ agents: [makeAgent("a", { systemPrompt: "Intercom orchestration channel:" }), makeAgent("b")] });
691
+ let detachEmitted = false;
692
+ const original = await executor.execute(
693
+ "foreground-detached-chain-status-original",
694
+ { chain: [{ agent: "a", task: "ask supervisor" }, { agent: "b", task: "must not run" }] },
695
+ new AbortController().signal,
696
+ (update: { details?: { progress?: Array<{ currentTool?: string }> } }) => {
697
+ if (detachEmitted) return;
698
+ if (!update.details?.progress?.some((entry) => entry.currentTool === "contact_supervisor")) return;
699
+ detachEmitted = true;
700
+ bus.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "chain-detached-status" });
701
+ },
702
+ makeMinimalCtx(tempDir),
703
+ );
704
+ assert.equal(detachEmitted, true);
705
+ const runId = original.details?.runId;
706
+ assert.ok(runId, "expected foreground run id");
707
+ assert.match(original.content[0]?.text ?? "", /Chain detached for intercom coordination/);
708
+ assert.equal(mockPi.callCount(), 1);
709
+
710
+ const deadline = Date.now() + 5000;
711
+ let statusText = "";
712
+ while (Date.now() < deadline) {
713
+ const status = await executor.execute(
714
+ "foreground-detached-chain-status",
715
+ { action: "status", id: runId },
716
+ new AbortController().signal,
717
+ undefined,
718
+ makeMinimalCtx(tempDir),
719
+ );
720
+ statusText = status.content[0]?.text ?? "";
721
+ if (/chain recovered answer/.test(statusText)) break;
722
+ await new Promise((resolve) => setTimeout(resolve, 25));
723
+ }
724
+
725
+ assert.doesNotMatch(statusText, /Async run not found/);
726
+ assert.match(statusText, /State: remembered foreground/);
727
+ assert.match(statusText, /a completed/);
728
+ assert.match(statusText, /chain recovered answer/);
729
+
730
+ const transcript = await executor.execute(
731
+ "foreground-detached-chain-transcript",
732
+ { action: "status", id: runId, index: 0, view: "transcript" },
733
+ new AbortController().signal,
734
+ undefined,
735
+ makeMinimalCtx(tempDir),
736
+ );
737
+ assert.match(transcript.content[0]?.text ?? "", /chain recovered answer/);
738
+ });
739
+
740
+ it("status recovers a later detached serial chain child under its original index", async () => {
741
+ mockPi.onCall({ output: "first step done" });
742
+ mockPi.onCall({
743
+ steps: [
744
+ { jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
745
+ { delay: 200, jsonl: [events.assistantMessage("second recovered answer")] },
746
+ ],
747
+ });
748
+ const { executor, events: bus } = makeExecutor({ agents: [makeAgent("a"), makeAgent("b", { systemPrompt: "Intercom orchestration channel:" }), makeAgent("c")] });
749
+ let detachEmitted = false;
750
+ const original = await executor.execute(
751
+ "foreground-later-detached-chain-status-original",
752
+ { chain: [{ agent: "a", task: "first" }, { agent: "b", task: "ask supervisor" }, { agent: "c", task: "must not run" }] },
753
+ new AbortController().signal,
754
+ (update: { details?: { progress?: Array<{ currentTool?: string }> } }) => {
755
+ if (detachEmitted) return;
756
+ if (!update.details?.progress?.some((entry) => entry.currentTool === "contact_supervisor")) return;
757
+ detachEmitted = true;
758
+ bus.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "later-chain-detached-status" });
759
+ },
760
+ makeMinimalCtx(tempDir),
761
+ );
762
+ assert.equal(detachEmitted, true);
763
+ const runId = original.details?.runId;
764
+ assert.ok(runId, "expected foreground run id");
765
+ assert.match(original.content[0]?.text ?? "", /Chain detached for intercom coordination/);
766
+ assert.equal(mockPi.callCount(), 2);
767
+
768
+ const deadline = Date.now() + 5000;
769
+ let statusText = "";
770
+ while (Date.now() < deadline) {
771
+ const status = await executor.execute(
772
+ "foreground-later-detached-chain-status",
773
+ { action: "status", id: runId },
774
+ new AbortController().signal,
775
+ undefined,
776
+ makeMinimalCtx(tempDir),
777
+ );
778
+ statusText = status.content[0]?.text ?? "";
779
+ if (/second recovered answer/.test(statusText)) break;
780
+ await new Promise((resolve) => setTimeout(resolve, 25));
781
+ }
782
+
783
+ assert.doesNotMatch(statusText, /Async run not found/);
784
+ assert.match(statusText, /State: remembered foreground/);
785
+ assert.match(statusText, /a completed/);
786
+ assert.match(statusText, /b completed/);
787
+ assert.match(statusText, /second recovered answer/);
788
+
789
+ const transcript = await executor.execute(
790
+ "foreground-later-detached-chain-transcript",
791
+ { action: "status", id: runId, index: 1, view: "transcript" },
792
+ new AbortController().signal,
793
+ undefined,
794
+ makeMinimalCtx(tempDir),
795
+ );
796
+ assert.match(transcript.content[0]?.text ?? "", /second recovered answer/);
797
+ });
798
+
799
+ it("resume action rejects detached foreground children that may still be live", async () => {
800
+ mockPi.onCall({
801
+ steps: [
802
+ { jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
803
+ { delay: 1000, jsonl: [events.assistantMessage("after reply")] },
804
+ ],
805
+ });
806
+ const { executor, events: bus } = makeExecutor({ agents: [makeAgent("a", { systemPrompt: "Intercom orchestration channel:" })] });
807
+ let detachEmitted = false;
808
+ const original = await executor.execute(
809
+ "foreground-detached-original",
810
+ { agent: "a", task: "ask supervisor" },
811
+ new AbortController().signal,
812
+ (update: { details?: { progress?: Array<{ currentTool?: string }> } }) => {
813
+ if (detachEmitted) return;
814
+ if (!update.details?.progress?.some((entry) => entry.currentTool === "contact_supervisor")) return;
815
+ detachEmitted = true;
816
+ bus.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "single-detached" });
817
+ },
818
+ makeMinimalCtx(tempDir),
819
+ );
820
+ assert.equal(detachEmitted, true);
821
+ const runId = original.details?.runId;
822
+ assert.ok(runId, "expected foreground run id");
823
+
824
+ const resumed = await executor.execute(
825
+ "foreground-detached-resume",
826
+ { action: "resume", id: runId, message: "Follow up" },
827
+ new AbortController().signal,
828
+ undefined,
829
+ makeMinimalCtx(tempDir),
830
+ );
831
+
832
+ assert.equal(resumed.isError, true);
833
+ assert.match(resumed.content[0]?.text ?? "", /detached for intercom coordination/);
834
+ assert.match(resumed.content[0]?.text ?? "", /Reply to the supervisor request first/);
835
+ assert.doesNotMatch(resumed.content[0]?.text ?? "", /revive only/);
836
+ });
837
+
838
+ it("resume action keeps exact foreground validation errors over async prefix matches", async () => {
839
+ const base = `exact-invalid-${Date.now()}`;
840
+ const asyncSession = path.join(tempDir, "async-exact-prefix.jsonl");
841
+ fs.writeFileSync(asyncSession, "", "utf-8");
842
+ const asyncDir = path.join(ASYNC_DIR, `${base}-async`);
843
+ try {
844
+ fs.mkdirSync(asyncDir, { recursive: true });
845
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
846
+ runId: `${base}-async`,
847
+ mode: "single",
848
+ state: "complete",
849
+ startedAt: 100,
850
+ lastUpdate: 200,
851
+ cwd: tempDir,
852
+ steps: [{ agent: "a", status: "complete", sessionFile: asyncSession }],
853
+ }, null, 2), "utf-8");
854
+ const { executor, state } = makeExecutor({ bridgeMode: "off", agents: [makeAgent("a")] });
855
+ state.foregroundRuns.set(base, {
856
+ runId: base,
857
+ mode: "single",
858
+ cwd: tempDir,
859
+ updatedAt: Date.now(),
860
+ children: [{ agent: "a", index: 0, status: "completed" }],
861
+ });
862
+
863
+ const result = await executor.execute(
864
+ "resume-exact-invalid-foreground",
865
+ { action: "resume", id: base, message: "Follow up" },
866
+ new AbortController().signal,
867
+ undefined,
868
+ makeMinimalCtx(tempDir),
869
+ );
870
+
871
+ assert.equal(result.isError, true);
872
+ assert.match(result.content[0]?.text ?? "", /Foreground run '.+' child 0 does not have a persisted session file/);
873
+ assert.equal(mockPi.callCount(), 0);
874
+ } finally {
875
+ fs.rmSync(asyncDir, { recursive: true, force: true });
876
+ }
877
+ });
878
+
879
+ it("resume action keeps exact async validation errors over foreground prefix matches", async () => {
880
+ const base = `exact-invalid-async-${Date.now()}`;
881
+ const foregroundSession = path.join(tempDir, "foreground-exact-prefix.jsonl");
882
+ fs.writeFileSync(foregroundSession, "", "utf-8");
883
+ const asyncDir = path.join(ASYNC_DIR, base);
884
+ try {
885
+ fs.mkdirSync(asyncDir, { recursive: true });
886
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
887
+ runId: base,
888
+ mode: "single",
889
+ state: "complete",
890
+ startedAt: 100,
891
+ lastUpdate: 200,
892
+ cwd: tempDir,
893
+ steps: [{ agent: "a", status: "complete" }],
894
+ }, null, 2), "utf-8");
895
+ const { executor, state } = makeExecutor({ bridgeMode: "off", agents: [makeAgent("a")] });
896
+ state.foregroundRuns.set(`${base}-foreground`, {
897
+ runId: `${base}-foreground`,
898
+ mode: "single",
899
+ cwd: tempDir,
900
+ updatedAt: Date.now(),
901
+ children: [{ agent: "a", index: 0, status: "completed", sessionFile: foregroundSession }],
902
+ });
903
+
904
+ const result = await executor.execute(
905
+ "resume-exact-invalid-async",
906
+ { action: "resume", id: base, message: "Follow up" },
907
+ new AbortController().signal,
908
+ undefined,
909
+ makeMinimalCtx(tempDir),
910
+ );
911
+
912
+ assert.equal(result.isError, true);
913
+ assert.match(result.content[0]?.text ?? "", /Async run '.+' child 0 does not have a persisted session file/);
914
+ assert.equal(mockPi.callCount(), 0);
915
+ } finally {
916
+ fs.rmSync(asyncDir, { recursive: true, force: true });
917
+ }
918
+ });
919
+
920
+ it("resume action reports async ambiguity even when foreground has one prefix match", async () => {
921
+ const base = `namespace-ambiguous-${Date.now()}`;
922
+ const foregroundSession = path.join(tempDir, "foreground-prefix.jsonl");
923
+ const firstAsyncSession = path.join(tempDir, "async-a.jsonl");
924
+ const secondAsyncSession = path.join(tempDir, "async-b.jsonl");
925
+ fs.writeFileSync(foregroundSession, "", "utf-8");
926
+ fs.writeFileSync(firstAsyncSession, "", "utf-8");
927
+ fs.writeFileSync(secondAsyncSession, "", "utf-8");
928
+ const firstAsyncDir = path.join(ASYNC_DIR, `${base}-async-a`);
929
+ const secondAsyncDir = path.join(ASYNC_DIR, `${base}-async-b`);
930
+ try {
931
+ for (const [asyncDir, runId, sessionFile] of [[firstAsyncDir, `${base}-async-a`, firstAsyncSession], [secondAsyncDir, `${base}-async-b`, secondAsyncSession]] as const) {
932
+ fs.mkdirSync(asyncDir, { recursive: true });
933
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
934
+ runId,
935
+ mode: "single",
936
+ state: "complete",
937
+ startedAt: 100,
938
+ lastUpdate: 200,
939
+ cwd: tempDir,
940
+ steps: [{ agent: "a", status: "complete", sessionFile }],
941
+ }, null, 2), "utf-8");
942
+ }
943
+ const { executor, state } = makeExecutor({ bridgeMode: "off", agents: [makeAgent("a")] });
944
+ state.foregroundRuns.set(`${base}-foreground`, {
945
+ runId: `${base}-foreground`,
946
+ mode: "single",
947
+ cwd: tempDir,
948
+ updatedAt: Date.now(),
949
+ children: [{ agent: "a", index: 0, status: "completed", sessionFile: foregroundSession }],
950
+ });
951
+
952
+ const result = await executor.execute(
953
+ "ambiguous-async-prefix-resume",
954
+ { action: "resume", id: base, message: "Follow up" },
955
+ new AbortController().signal,
956
+ undefined,
957
+ makeMinimalCtx(tempDir),
958
+ );
959
+
960
+ assert.equal(result.isError, true);
961
+ assert.match(result.content[0]?.text ?? "", /Ambiguous subagent run id prefix/);
962
+ } finally {
963
+ fs.rmSync(firstAsyncDir, { recursive: true, force: true });
964
+ fs.rmSync(secondAsyncDir, { recursive: true, force: true });
965
+ }
966
+ });
967
+
968
+ it("resume action reports ambiguous ids across remembered foreground and async runs", async () => {
969
+ const base = `ambiguous-${Date.now()}`;
970
+ const foregroundSession = path.join(tempDir, "foreground.jsonl");
971
+ const asyncSession = path.join(tempDir, "async.jsonl");
972
+ const asyncId = `${base}-async`;
973
+ const foregroundId = `${base}-foreground`;
974
+ const asyncDir = path.join(ASYNC_DIR, asyncId);
975
+ fs.writeFileSync(foregroundSession, "", "utf-8");
976
+ fs.writeFileSync(asyncSession, "", "utf-8");
977
+ try {
978
+ fs.mkdirSync(asyncDir, { recursive: true });
979
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
980
+ runId: asyncId,
981
+ mode: "single",
982
+ state: "complete",
983
+ startedAt: 100,
984
+ lastUpdate: 200,
985
+ cwd: tempDir,
986
+ steps: [{ agent: "a", status: "complete", sessionFile: asyncSession }],
987
+ }, null, 2), "utf-8");
988
+ const { executor, state } = makeExecutor({ bridgeMode: "off", agents: [makeAgent("a")] });
989
+ state.foregroundRuns.set(foregroundId, {
990
+ runId: foregroundId,
991
+ mode: "single",
992
+ cwd: tempDir,
993
+ updatedAt: Date.now(),
994
+ children: [{ agent: "a", index: 0, status: "completed", sessionFile: foregroundSession }],
995
+ });
996
+
997
+ const result = await executor.execute(
998
+ "ambiguous-resume",
999
+ { action: "resume", id: base, message: "Follow up" },
1000
+ new AbortController().signal,
1001
+ undefined,
1002
+ makeMinimalCtx(tempDir),
1003
+ );
1004
+
1005
+ assert.equal(result.isError, true);
1006
+ assert.match(result.content[0]?.text ?? "", /ambiguous between foreground run/);
1007
+ } finally {
1008
+ fs.rmSync(asyncDir, { recursive: true, force: true });
1009
+ }
1010
+ });
1011
+
1012
+ it("mixed foreground outcomes produce failed grouped status and receipt counts", async () => {
1013
+ mockPi.onCall({ matchArgIncludes: "task-a", output: "Parallel child success", exitCode: 0 });
1014
+ mockPi.onCall({ matchArgIncludes: "task-b", output: "Parallel child failure", stderr: "Parallel child failure", exitCode: 1 });
1015
+ const { executor, events } = makeExecutor({ agents: [makeAgent("a"), makeAgent("b")] });
1016
+
1017
+ const result = await executor.execute(
1018
+ "parallel-mixed-intercom",
1019
+ { tasks: [{ agent: "a", task: "task-a" }, { agent: "b", task: "task-b" }] },
1020
+ new AbortController().signal,
1021
+ undefined,
1022
+ makeMinimalCtx(tempDir),
1023
+ );
1024
+
1025
+ const intercomEvents = events.emitted.filter((entry) => entry.channel === "subagent:result-intercom");
1026
+ assert.equal(intercomEvents.length, 1);
1027
+ const payload = intercomEvents[0]!.payload as { status?: string; summary?: string; message?: string };
1028
+ assert.equal(payload.status, "failed");
1029
+ assert.match(String(payload.summary ?? ""), /1 completed, 1 failed/);
1030
+ assert.match(String(payload.message ?? ""), /Status: failed/);
1031
+ assert.match(result.content[0]?.text ?? "", /Children: 1 completed, 1 failed/);
1032
+ });
1033
+ });