macro-agent 0.1.0 → 0.1.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 (337) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/.sudocode/specs.jsonl +4 -0
  3. package/CLAUDE.md +16 -14
  4. package/README.md +11 -29
  5. package/dist/acp/macro-agent.d.ts +15 -0
  6. package/dist/acp/macro-agent.d.ts.map +1 -1
  7. package/dist/acp/macro-agent.js +131 -35
  8. package/dist/acp/macro-agent.js.map +1 -1
  9. package/dist/acp/types.d.ts +32 -1
  10. package/dist/acp/types.d.ts.map +1 -1
  11. package/dist/acp/types.js.map +1 -1
  12. package/dist/agent/agent-manager.d.ts +65 -1
  13. package/dist/agent/agent-manager.d.ts.map +1 -1
  14. package/dist/agent/agent-manager.js +464 -183
  15. package/dist/agent/agent-manager.js.map +1 -1
  16. package/dist/agent/types.d.ts +1 -1
  17. package/dist/agent/types.d.ts.map +1 -1
  18. package/dist/api/server.d.ts +3 -0
  19. package/dist/api/server.d.ts.map +1 -1
  20. package/dist/api/server.js +37 -6
  21. package/dist/api/server.js.map +1 -1
  22. package/dist/auth/index.d.ts +2 -0
  23. package/dist/auth/index.d.ts.map +1 -0
  24. package/dist/auth/index.js +2 -0
  25. package/dist/auth/index.js.map +1 -0
  26. package/dist/auth/token.d.ts +41 -0
  27. package/dist/auth/token.d.ts.map +1 -0
  28. package/dist/auth/token.js +73 -0
  29. package/dist/auth/token.js.map +1 -0
  30. package/dist/cli/acp.d.ts +2 -23
  31. package/dist/cli/acp.d.ts.map +1 -1
  32. package/dist/cli/acp.js +127 -61
  33. package/dist/cli/acp.js.map +1 -1
  34. package/dist/cli/index.js +147 -15
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/cli/mcp.d.ts +6 -0
  37. package/dist/cli/mcp.d.ts.map +1 -1
  38. package/dist/cli/mcp.js +268 -181
  39. package/dist/cli/mcp.js.map +1 -1
  40. package/dist/cli/parse-args.d.ts +20 -0
  41. package/dist/cli/parse-args.d.ts.map +1 -0
  42. package/dist/cli/parse-args.js +43 -0
  43. package/dist/cli/parse-args.js.map +1 -0
  44. package/dist/cli/stable-instance-id.d.ts +8 -0
  45. package/dist/cli/stable-instance-id.d.ts.map +1 -0
  46. package/dist/cli/stable-instance-id.js +14 -0
  47. package/dist/cli/stable-instance-id.js.map +1 -0
  48. package/dist/config/project-config.d.ts +74 -7
  49. package/dist/config/project-config.d.ts.map +1 -1
  50. package/dist/config/project-config.js +123 -20
  51. package/dist/config/project-config.js.map +1 -1
  52. package/dist/map/adapter/acp-over-map.d.ts +17 -0
  53. package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
  54. package/dist/map/adapter/acp-over-map.js +384 -23
  55. package/dist/map/adapter/acp-over-map.js.map +1 -1
  56. package/dist/map/adapter/connection-manager.d.ts.map +1 -1
  57. package/dist/map/adapter/connection-manager.js +3 -0
  58. package/dist/map/adapter/connection-manager.js.map +1 -1
  59. package/dist/map/adapter/event-log.d.ts +87 -0
  60. package/dist/map/adapter/event-log.d.ts.map +1 -0
  61. package/dist/map/adapter/event-log.js +122 -0
  62. package/dist/map/adapter/event-log.js.map +1 -0
  63. package/dist/map/adapter/event-translator.js +6 -6
  64. package/dist/map/adapter/event-translator.js.map +1 -1
  65. package/dist/map/adapter/extensions/agent-lifecycle.d.ts +82 -0
  66. package/dist/map/adapter/extensions/agent-lifecycle.d.ts.map +1 -0
  67. package/dist/map/adapter/extensions/agent-lifecycle.js +164 -0
  68. package/dist/map/adapter/extensions/agent-lifecycle.js.map +1 -0
  69. package/dist/map/adapter/extensions/index.d.ts +10 -1
  70. package/dist/map/adapter/extensions/index.d.ts.map +1 -1
  71. package/dist/map/adapter/extensions/index.js +34 -0
  72. package/dist/map/adapter/extensions/index.js.map +1 -1
  73. package/dist/map/adapter/extensions/mcp-bridge.d.ts +57 -0
  74. package/dist/map/adapter/extensions/mcp-bridge.d.ts.map +1 -0
  75. package/dist/map/adapter/extensions/mcp-bridge.js +745 -0
  76. package/dist/map/adapter/extensions/mcp-bridge.js.map +1 -0
  77. package/dist/map/adapter/extensions/rename.d.ts +29 -0
  78. package/dist/map/adapter/extensions/rename.d.ts.map +1 -0
  79. package/dist/map/adapter/extensions/rename.js +49 -0
  80. package/dist/map/adapter/extensions/rename.js.map +1 -0
  81. package/dist/map/adapter/extensions/task.d.ts.map +1 -1
  82. package/dist/map/adapter/extensions/task.js +10 -0
  83. package/dist/map/adapter/extensions/task.js.map +1 -1
  84. package/dist/map/adapter/extensions/update-metadata.d.ts +29 -0
  85. package/dist/map/adapter/extensions/update-metadata.d.ts.map +1 -0
  86. package/dist/map/adapter/extensions/update-metadata.js +67 -0
  87. package/dist/map/adapter/extensions/update-metadata.js.map +1 -0
  88. package/dist/map/adapter/index.d.ts +2 -1
  89. package/dist/map/adapter/index.d.ts.map +1 -1
  90. package/dist/map/adapter/index.js +8 -2
  91. package/dist/map/adapter/index.js.map +1 -1
  92. package/dist/map/adapter/interface.d.ts +2 -0
  93. package/dist/map/adapter/interface.d.ts.map +1 -1
  94. package/dist/map/adapter/map-adapter.d.ts +3 -0
  95. package/dist/map/adapter/map-adapter.d.ts.map +1 -1
  96. package/dist/map/adapter/map-adapter.js +258 -35
  97. package/dist/map/adapter/map-adapter.js.map +1 -1
  98. package/dist/map/adapter/subscription-manager.d.ts.map +1 -1
  99. package/dist/map/adapter/subscription-manager.js +5 -1
  100. package/dist/map/adapter/subscription-manager.js.map +1 -1
  101. package/dist/map/adapter/types.d.ts +2 -0
  102. package/dist/map/adapter/types.d.ts.map +1 -1
  103. package/dist/mcp/map-client.d.ts +39 -0
  104. package/dist/mcp/map-client.d.ts.map +1 -0
  105. package/dist/mcp/map-client.js +129 -0
  106. package/dist/mcp/map-client.js.map +1 -0
  107. package/dist/mcp/mcp-server.d.ts +14 -0
  108. package/dist/mcp/mcp-server.d.ts.map +1 -1
  109. package/dist/mcp/mcp-server.js +113 -85
  110. package/dist/mcp/mcp-server.js.map +1 -1
  111. package/dist/mcp/types.d.ts +9 -1
  112. package/dist/mcp/types.d.ts.map +1 -1
  113. package/dist/mcp/types.js.map +1 -1
  114. package/dist/metrics/metrics.js +1 -1
  115. package/dist/metrics/metrics.js.map +1 -1
  116. package/dist/roles/capabilities.d.ts +3 -1
  117. package/dist/roles/capabilities.d.ts.map +1 -1
  118. package/dist/roles/capabilities.js +17 -7
  119. package/dist/roles/capabilities.js.map +1 -1
  120. package/dist/roles/config-loader.d.ts +6 -6
  121. package/dist/roles/config-loader.d.ts.map +1 -1
  122. package/dist/roles/config-loader.js +6 -6
  123. package/dist/roles/config-loader.js.map +1 -1
  124. package/dist/roles/registry.d.ts +2 -2
  125. package/dist/roles/registry.js +2 -2
  126. package/dist/server/combined-server.d.ts +20 -0
  127. package/dist/server/combined-server.d.ts.map +1 -1
  128. package/dist/server/combined-server.js +107 -8
  129. package/dist/server/combined-server.js.map +1 -1
  130. package/dist/store/event-store.d.ts +2 -1
  131. package/dist/store/event-store.d.ts.map +1 -1
  132. package/dist/store/event-store.js +69 -20
  133. package/dist/store/event-store.js.map +1 -1
  134. package/dist/store/types/agents.d.ts +18 -0
  135. package/dist/store/types/agents.d.ts.map +1 -1
  136. package/dist/store/types/events.d.ts +1 -1
  137. package/dist/store/types/events.d.ts.map +1 -1
  138. package/dist/task/backend/index.d.ts +47 -29
  139. package/dist/task/backend/index.d.ts.map +1 -1
  140. package/dist/task/backend/index.js +109 -71
  141. package/dist/task/backend/index.js.map +1 -1
  142. package/dist/task/backend/memory.d.ts +1 -0
  143. package/dist/task/backend/memory.d.ts.map +1 -1
  144. package/dist/task/backend/memory.js +3 -0
  145. package/dist/task/backend/memory.js.map +1 -1
  146. package/dist/task/backend/opentasks/backend.d.ts +140 -0
  147. package/dist/task/backend/opentasks/backend.d.ts.map +1 -0
  148. package/dist/task/backend/opentasks/backend.js +1023 -0
  149. package/dist/task/backend/opentasks/backend.js.map +1 -0
  150. package/dist/task/backend/opentasks/client.d.ts +337 -0
  151. package/dist/task/backend/opentasks/client.d.ts.map +1 -0
  152. package/dist/task/backend/opentasks/client.js +225 -0
  153. package/dist/task/backend/opentasks/client.js.map +1 -0
  154. package/dist/task/backend/opentasks/daemon-manager.d.ts +89 -0
  155. package/dist/task/backend/opentasks/daemon-manager.d.ts.map +1 -0
  156. package/dist/task/backend/opentasks/daemon-manager.js +195 -0
  157. package/dist/task/backend/opentasks/daemon-manager.js.map +1 -0
  158. package/dist/task/backend/opentasks/index.d.ts +21 -0
  159. package/dist/task/backend/opentasks/index.d.ts.map +1 -0
  160. package/dist/task/backend/opentasks/index.js +21 -0
  161. package/dist/task/backend/opentasks/index.js.map +1 -0
  162. package/dist/task/backend/opentasks/mapping.d.ts +48 -0
  163. package/dist/task/backend/opentasks/mapping.d.ts.map +1 -0
  164. package/dist/task/backend/opentasks/mapping.js +77 -0
  165. package/dist/task/backend/opentasks/mapping.js.map +1 -0
  166. package/dist/task/backend/types.d.ts +33 -53
  167. package/dist/task/backend/types.d.ts.map +1 -1
  168. package/dist/task/backend/types.js +7 -11
  169. package/dist/task/backend/types.js.map +1 -1
  170. package/dist/task/backend/unified-tool-provider.d.ts +57 -0
  171. package/dist/task/backend/unified-tool-provider.d.ts.map +1 -0
  172. package/dist/task/backend/unified-tool-provider.js +623 -0
  173. package/dist/task/backend/unified-tool-provider.js.map +1 -0
  174. package/dist/teams/team-loader.d.ts +2 -2
  175. package/dist/teams/team-loader.js +3 -3
  176. package/dist/teams/team-loader.js.map +1 -1
  177. package/dist/teams/team-runtime.d.ts.map +1 -1
  178. package/dist/teams/team-runtime.js +2 -0
  179. package/dist/teams/team-runtime.js.map +1 -1
  180. package/docs/architecture.md +7 -6
  181. package/docs/configuration.md +26 -62
  182. package/docs/implementation-details.md +5 -5
  183. package/docs/implementation-summary.md +17 -17
  184. package/docs/plan-self-driving-support.md +4 -4
  185. package/docs/spec-self-driving-support.md +10 -10
  186. package/docs/team-templates.md +2 -2
  187. package/docs/teams.md +3 -3
  188. package/docs/troubleshooting.md +10 -11
  189. package/package.json +6 -4
  190. package/src/__tests__/e2e/agent-spawn-visibility.e2e.test.ts +761 -0
  191. package/src/__tests__/e2e/full-agent-conflict-resolution.e2e.test.ts +2 -2
  192. package/src/__tests__/e2e/mcp-thin-client-bridge.e2e.test.ts +304 -0
  193. package/src/__tests__/e2e/mcp-tools-available.e2e.test.ts +324 -0
  194. package/src/__tests__/e2e/multi-agent.e2e.test.ts +5 -5
  195. package/src/__tests__/e2e/spawn-session-streaming.e2e.test.ts +563 -0
  196. package/src/acp/__tests__/integration.test.ts +56 -31
  197. package/src/acp/__tests__/macro-agent.test.ts +16 -7
  198. package/src/acp/macro-agent.ts +170 -36
  199. package/src/acp/types.ts +46 -1
  200. package/src/agent/__tests__/agent-manager.test.ts +228 -2
  201. package/src/agent/agent-manager.ts +714 -261
  202. package/src/agent/types.ts +3 -1
  203. package/src/api/server.ts +41 -7
  204. package/src/auth/__tests__/token.test.ts +100 -0
  205. package/src/auth/index.ts +1 -0
  206. package/src/auth/token.ts +82 -0
  207. package/src/cli/__tests__/acp.test.ts +1 -1
  208. package/src/cli/__tests__/stable-instance-id.test.ts +1 -1
  209. package/src/cli/acp.ts +130 -72
  210. package/src/cli/index.ts +120 -14
  211. package/src/cli/mcp.ts +311 -207
  212. package/src/cli/parse-args.ts +54 -0
  213. package/src/cli/stable-instance-id.ts +14 -0
  214. package/src/config/project-config.ts +190 -27
  215. package/src/lifecycle/__tests__/cascade-termination.test.ts +1 -1
  216. package/src/map/adapter/__tests__/acp-over-map-cancel.test.ts +22 -4
  217. package/src/map/adapter/__tests__/acp-over-map-getmodels.test.ts +355 -0
  218. package/src/map/adapter/__tests__/acp-over-map-history.test.ts +263 -0
  219. package/src/map/adapter/__tests__/acp-over-map-persistence.e2e.test.ts +1 -1
  220. package/src/map/adapter/__tests__/event-broadcast.test.ts +420 -0
  221. package/src/map/adapter/__tests__/event-log.test.ts +527 -0
  222. package/src/map/adapter/__tests__/event-translator.test.ts +3 -3
  223. package/src/map/adapter/__tests__/extensions.test.ts +408 -0
  224. package/src/map/adapter/__tests__/map-adapter.test.ts +99 -0
  225. package/src/map/adapter/__tests__/mcp-bridge.test.ts +1187 -0
  226. package/src/map/adapter/__tests__/multi-client-broadcast.test.ts +711 -0
  227. package/src/map/adapter/__tests__/websocket-integration.test.ts +218 -0
  228. package/src/map/adapter/acp-over-map.ts +678 -66
  229. package/src/map/adapter/connection-manager.ts +3 -0
  230. package/src/map/adapter/event-log.ts +208 -0
  231. package/src/map/adapter/event-translator.ts +6 -6
  232. package/src/map/adapter/extensions/agent-lifecycle.ts +267 -0
  233. package/src/map/adapter/extensions/index.ts +60 -0
  234. package/src/map/adapter/extensions/mcp-bridge.ts +995 -0
  235. package/src/map/adapter/extensions/task.ts +11 -0
  236. package/src/map/adapter/extensions/update-metadata.ts +126 -0
  237. package/src/map/adapter/index.ts +28 -0
  238. package/src/map/adapter/interface.ts +2 -0
  239. package/src/map/adapter/map-adapter.ts +312 -47
  240. package/src/map/adapter/subscription-manager.ts +5 -1
  241. package/src/map/adapter/types.ts +2 -0
  242. package/src/mcp/__tests__/map-client.test.ts +386 -0
  243. package/src/mcp/__tests__/mcp-server-thin-client.test.ts +368 -0
  244. package/src/mcp/__tests__/mcp-server.test.ts +100 -1
  245. package/src/mcp/map-client.ts +177 -0
  246. package/src/mcp/mcp-server.ts +191 -100
  247. package/src/mcp/types.ts +6 -1
  248. package/src/metrics/metrics.ts +1 -1
  249. package/src/monitor/__tests__/stale-agent-flow.integration.test.ts +1 -1
  250. package/src/roles/__tests__/config-loader.test.ts +7 -7
  251. package/src/roles/capabilities.ts +17 -7
  252. package/src/roles/config-loader.ts +6 -6
  253. package/src/roles/registry.ts +2 -2
  254. package/src/server/__tests__/combined-server.test.ts +94 -21
  255. package/src/server/combined-server.ts +189 -33
  256. package/src/steering/__tests__/steering-integration.test.ts +1 -1
  257. package/src/store/__tests__/event-store.test.ts +196 -1
  258. package/src/store/__tests__/instance.test.ts +3 -3
  259. package/src/store/event-store.ts +80 -21
  260. package/src/store/types/agents.ts +15 -0
  261. package/src/store/types/events.ts +1 -1
  262. package/src/task/backend/__tests__/create-task-backend.test.ts +225 -0
  263. package/src/task/backend/__tests__/e2e/unified-tool-provider-opentasks.e2e.test.ts +524 -0
  264. package/src/task/backend/__tests__/unified-tool-provider.test.ts +579 -0
  265. package/src/task/backend/index.ts +156 -106
  266. package/src/task/backend/memory.ts +4 -0
  267. package/src/task/backend/opentasks/__tests__/backend.test.ts +968 -0
  268. package/src/task/backend/opentasks/__tests__/daemon-manager.test.ts +406 -0
  269. package/src/task/backend/opentasks/__tests__/mapping.test.ts +84 -0
  270. package/src/task/backend/opentasks/__tests__/opentasks-backend.e2e.test.ts +1338 -0
  271. package/src/task/backend/opentasks/backend.ts +1323 -0
  272. package/src/task/backend/opentasks/client.ts +652 -0
  273. package/src/task/backend/opentasks/daemon-manager.ts +253 -0
  274. package/src/task/backend/opentasks/index.ts +69 -0
  275. package/src/task/backend/opentasks/mapping.ts +94 -0
  276. package/src/task/backend/types.ts +42 -66
  277. package/src/task/backend/unified-tool-provider.ts +779 -0
  278. package/src/teams/__tests__/cross-subsystem.integration.test.ts +1 -1
  279. package/src/teams/team-loader.ts +3 -3
  280. package/src/teams/team-runtime.ts +2 -0
  281. package/test_fixtures/README.md +2 -3
  282. package/test_fixtures/fixtures/index.ts +0 -3
  283. package/test_fixtures/fixtures/projects/project-with-specs.ts +7 -149
  284. package/test_fixtures/fixtures/repos/index.ts +1 -3
  285. package/test_fixtures/fixtures/repos/temp-repo-factory.ts +0 -116
  286. package/test_fixtures/fixtures/repos/types.ts +0 -11
  287. package/test_fixtures/harness/__tests__/fixtures.test.ts +10 -102
  288. package/test_fixtures/harness/__tests__/temp-repo-and-simulator.test.ts +0 -33
  289. package/test_fixtures/harness/simulator/agent-simulator.ts +4 -4
  290. package/vitest.config.ts +1 -1
  291. package/vitest.e2e.config.ts +1 -1
  292. package/vitest.setup.ts +1 -30
  293. package/.macro-agent/teams/self-driving/prompts/grinder.md +0 -27
  294. package/.macro-agent/teams/self-driving/prompts/judge.md +0 -27
  295. package/.macro-agent/teams/self-driving/prompts/planner.md +0 -33
  296. package/.macro-agent/teams/self-driving/roles/grinder.yaml +0 -17
  297. package/.macro-agent/teams/self-driving/roles/judge.yaml +0 -24
  298. package/.macro-agent/teams/self-driving/roles/planner.yaml +0 -18
  299. package/.macro-agent/teams/self-driving/team.yaml +0 -103
  300. package/.macro-agent/teams/structured/prompts/developer.md +0 -26
  301. package/.macro-agent/teams/structured/prompts/lead.md +0 -25
  302. package/.macro-agent/teams/structured/prompts/reviewer.md +0 -24
  303. package/.macro-agent/teams/structured/roles/developer.yaml +0 -12
  304. package/.macro-agent/teams/structured/roles/lead.yaml +0 -11
  305. package/.macro-agent/teams/structured/roles/reviewer.yaml +0 -19
  306. package/.macro-agent/teams/structured/team.yaml +0 -89
  307. package/docs/sudocode-integration.md +0 -383
  308. package/src/task/backend/__tests__/backend-parity.test.ts +0 -451
  309. package/src/task/backend/__tests__/tool-provider-edge-cases.test.ts +0 -430
  310. package/src/task/backend/__tests__/tool-provider.test.ts +0 -983
  311. package/src/task/backend/sudocode/__tests__/backend-edge-cases.test.ts +0 -575
  312. package/src/task/backend/sudocode/__tests__/backend.test.ts +0 -1194
  313. package/src/task/backend/sudocode/__tests__/client-integration.test.ts +0 -418
  314. package/src/task/backend/sudocode/__tests__/client.test.ts +0 -345
  315. package/src/task/backend/sudocode/__tests__/e2e/backend.e2e.test.ts +0 -753
  316. package/src/task/backend/sudocode/__tests__/e2e/server-client.e2e.test.ts +0 -680
  317. package/src/task/backend/sudocode/__tests__/e2e-workflow.test.ts +0 -666
  318. package/src/task/backend/sudocode/__tests__/integration/standalone-client.integration.test.ts +0 -396
  319. package/src/task/backend/sudocode/__tests__/integration/sudocode-cli.integration.test.ts +0 -328
  320. package/src/task/backend/sudocode/__tests__/integration/test-utils.ts +0 -175
  321. package/src/task/backend/sudocode/__tests__/mapping-edge-cases.test.ts +0 -265
  322. package/src/task/backend/sudocode/__tests__/server-client.test.ts +0 -675
  323. package/src/task/backend/sudocode/__tests__/sync-policy-edge-cases.test.ts +0 -521
  324. package/src/task/backend/sudocode/__tests__/sync-policy.test.ts +0 -519
  325. package/src/task/backend/sudocode/__tests__/tools.test.ts +0 -471
  326. package/src/task/backend/sudocode/backend.ts +0 -1237
  327. package/src/task/backend/sudocode/client.ts +0 -515
  328. package/src/task/backend/sudocode/index.ts +0 -120
  329. package/src/task/backend/sudocode/mapping.ts +0 -93
  330. package/src/task/backend/sudocode/server-client.ts +0 -522
  331. package/src/task/backend/sudocode/standalone-client.ts +0 -623
  332. package/src/task/backend/sudocode/sync-policy.ts +0 -387
  333. package/src/task/backend/sudocode/tools.ts +0 -896
  334. package/src/task/backend/tool-provider.ts +0 -506
  335. package/test_fixtures/fixtures/sudocode/index.ts +0 -29
  336. package/test_fixtures/fixtures/sudocode/issues.ts +0 -185
  337. package/test_fixtures/fixtures/sudocode/specs.ts +0 -159
@@ -0,0 +1,563 @@
1
+ /**
2
+ * E2E test: Spawned Agent Session Streaming
3
+ *
4
+ * Verifies that when an agent is spawned via the MCP bridge (`_macro/mcp/spawn_agent`),
5
+ * the fire-and-forget prompt emits MAP events that TUI clients can subscribe to:
6
+ * - session_user_message (before prompt starts)
7
+ * - session_update (for each streaming update during the prompt)
8
+ * - session_prompt_done (after the prompt completes)
9
+ *
10
+ * Also verifies that turns are recorded in EventStore for history persistence.
11
+ *
12
+ * REQUIRES: RUN_FULL_AGENT_TESTS=true environment variable (and authenticated Claude Code)
13
+ *
14
+ * Run with:
15
+ * RUN_FULL_AGENT_TESTS=true npx vitest run src/__tests__/e2e/spawn-session-streaming.e2e.test.ts
16
+ */
17
+
18
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
19
+ import { WebSocket } from "ws";
20
+ import * as fs from "fs";
21
+ import * as path from "path";
22
+ import * as os from "os";
23
+ import { createEventStore, type EventStore } from "../../store/event-store.js";
24
+ import {
25
+ createAgentManager,
26
+ type AgentManager,
27
+ } from "../../agent/agent-manager.js";
28
+ import { createTaskManager, type TaskManager } from "../../task/task-manager.js";
29
+ import {
30
+ createMessageRouter,
31
+ type MessageRouter,
32
+ } from "../../router/message-router.js";
33
+ import {
34
+ createCombinedServer,
35
+ type CombinedServer,
36
+ type CombinedServerServices,
37
+ } from "../../server/combined-server.js";
38
+ import { mapCall } from "../../mcp/map-client.js";
39
+
40
+ // =============================================================================
41
+ // Test Configuration
42
+ // =============================================================================
43
+
44
+ const RUN_FULL_AGENT = !!process.env.RUN_FULL_AGENT_TESTS;
45
+ const testFn = RUN_FULL_AGENT ? it : it.skip;
46
+
47
+ const TIMEOUT = {
48
+ SPAWN_AND_PROMPT: 120_000, // Agent spawn + prompt completion
49
+ EVENT_WAIT: 60_000, // Waiting for individual MAP events
50
+ };
51
+
52
+ const log = (msg: string) => {
53
+ if (RUN_FULL_AGENT) {
54
+ console.log(`[SpawnStreamE2E] ${msg}`);
55
+ }
56
+ };
57
+
58
+ // No event type filter needed — the TUI worker subscribes to ALL events
59
+ // by calling client.subscribe() without a filter. The e2e test mirrors this.
60
+
61
+ // =============================================================================
62
+ // Helpers
63
+ // =============================================================================
64
+
65
+ function getRandomPort(): number {
66
+ return 10000 + Math.floor(Math.random() * 50000);
67
+ }
68
+
69
+ interface JsonRpcMessage {
70
+ jsonrpc: "2.0";
71
+ id?: number;
72
+ method?: string;
73
+ params?: unknown;
74
+ result?: unknown;
75
+ error?: { code: number; message: string; data?: unknown };
76
+ }
77
+
78
+ /**
79
+ * WebSocket MAP client for subscribing to events.
80
+ */
81
+ class MAPTestClient {
82
+ private ws!: WebSocket;
83
+ private waiters: Map<
84
+ number,
85
+ { resolve: (r: JsonRpcMessage) => void; reject: (e: Error) => void }
86
+ > = new Map();
87
+ private nextId = 1;
88
+ private url: string;
89
+
90
+ readonly notifications: JsonRpcMessage[] = [];
91
+
92
+ private notificationWaiters: Array<{
93
+ check: (msg: JsonRpcMessage) => boolean;
94
+ resolve: (msg: JsonRpcMessage) => void;
95
+ reject: (e: Error) => void;
96
+ timeout: ReturnType<typeof setTimeout>;
97
+ }> = [];
98
+
99
+ constructor(url: string) {
100
+ this.url = url;
101
+ }
102
+
103
+ async connect(): Promise<void> {
104
+ this.ws = new WebSocket(this.url);
105
+ return new Promise((resolve, reject) => {
106
+ const timeout = setTimeout(
107
+ () => reject(new Error("Connection timeout")),
108
+ 5000,
109
+ );
110
+ this.ws.on("open", () => {
111
+ clearTimeout(timeout);
112
+ resolve();
113
+ });
114
+ this.ws.on("error", (err) => {
115
+ clearTimeout(timeout);
116
+ reject(err);
117
+ });
118
+ this.ws.on("message", (data: Buffer) => {
119
+ try {
120
+ const msg = JSON.parse(data.toString()) as JsonRpcMessage;
121
+ if (msg.id != null) {
122
+ const waiter = this.waiters.get(msg.id);
123
+ if (waiter) {
124
+ this.waiters.delete(msg.id);
125
+ waiter.resolve(msg);
126
+ }
127
+ } else if (msg.method) {
128
+ this.notifications.push(msg);
129
+ for (let i = this.notificationWaiters.length - 1; i >= 0; i--) {
130
+ const w = this.notificationWaiters[i];
131
+ if (w.check(msg)) {
132
+ clearTimeout(w.timeout);
133
+ this.notificationWaiters.splice(i, 1);
134
+ w.resolve(msg);
135
+ }
136
+ }
137
+ }
138
+ } catch {
139
+ // ignore
140
+ }
141
+ });
142
+ });
143
+ }
144
+
145
+ async request(method: string, params?: unknown): Promise<JsonRpcMessage> {
146
+ const id = this.nextId++;
147
+ return new Promise((resolve, reject) => {
148
+ const timeout = setTimeout(() => {
149
+ this.waiters.delete(id);
150
+ reject(new Error(`Request timeout: ${method}`));
151
+ }, 30000);
152
+ this.waiters.set(id, {
153
+ resolve: (r) => {
154
+ clearTimeout(timeout);
155
+ resolve(r);
156
+ },
157
+ reject: (e) => {
158
+ clearTimeout(timeout);
159
+ reject(e);
160
+ },
161
+ });
162
+ this.ws.send(JSON.stringify({ jsonrpc: "2.0", method, params, id }));
163
+ });
164
+ }
165
+
166
+ waitForNotification(
167
+ check: (msg: JsonRpcMessage) => boolean,
168
+ timeoutMs = 15000,
169
+ ): Promise<JsonRpcMessage> {
170
+ const existing = this.notifications.find(check);
171
+ if (existing) return Promise.resolve(existing);
172
+
173
+ return new Promise((resolve, reject) => {
174
+ const timeout = setTimeout(() => {
175
+ const idx = this.notificationWaiters.findIndex(
176
+ (w) => w.resolve === resolve,
177
+ );
178
+ if (idx >= 0) this.notificationWaiters.splice(idx, 1);
179
+ reject(
180
+ new Error(
181
+ `Timeout waiting for notification. Received ${this.notifications.length} notifications:\n` +
182
+ this.notifications
183
+ .map((n) => {
184
+ const p = n.params as Record<string, unknown> | undefined;
185
+ const evt = p?.event as Record<string, unknown> | undefined;
186
+ return ` ${n.method} → type=${evt?.type}, agentId=${(evt?.data as Record<string, unknown> | undefined)?.agentId}`;
187
+ })
188
+ .join("\n"),
189
+ ),
190
+ );
191
+ }, timeoutMs);
192
+ this.notificationWaiters.push({ check, resolve, reject, timeout });
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Collect all notifications matching a predicate that have arrived so far.
198
+ */
199
+ collectNotifications(check: (msg: JsonRpcMessage) => boolean): JsonRpcMessage[] {
200
+ return this.notifications.filter(check);
201
+ }
202
+
203
+ close(): void {
204
+ for (const w of this.notificationWaiters) {
205
+ clearTimeout(w.timeout);
206
+ }
207
+ this.notificationWaiters.length = 0;
208
+ try {
209
+ this.ws?.close();
210
+ } catch {
211
+ // ignore
212
+ }
213
+ }
214
+ }
215
+
216
+ function isEventOfType(type: string) {
217
+ return (msg: JsonRpcMessage) => {
218
+ if (msg.method !== "map/event") return false;
219
+ const params = msg.params as Record<string, unknown> | undefined;
220
+ const event = params?.event as Record<string, unknown> | undefined;
221
+ return event?.type === type;
222
+ };
223
+ }
224
+
225
+ function isEventForAgent(type: string, agentId: string) {
226
+ return (msg: JsonRpcMessage) => {
227
+ if (msg.method !== "map/event") return false;
228
+ const params = msg.params as Record<string, unknown> | undefined;
229
+ const event = params?.event as Record<string, unknown> | undefined;
230
+ if (event?.type !== type) return false;
231
+ const data = event?.data as Record<string, unknown> | undefined;
232
+ return data?.agentId === agentId;
233
+ };
234
+ }
235
+
236
+ function getEventData(msg: JsonRpcMessage): Record<string, unknown> {
237
+ const params = msg.params as Record<string, unknown>;
238
+ const event = params.event as Record<string, unknown>;
239
+ return event.data as Record<string, unknown>;
240
+ }
241
+
242
+ // =============================================================================
243
+ // Tests
244
+ // =============================================================================
245
+
246
+ describe("Spawned Agent Session Streaming E2E", () => {
247
+ let eventStore: EventStore;
248
+ let agentManager: AgentManager;
249
+ let taskManager: TaskManager;
250
+ let messageRouter: MessageRouter;
251
+ let server: CombinedServer;
252
+ let port: number;
253
+ let serverUrl: string;
254
+ let tmpDir: string;
255
+ const clients: MAPTestClient[] = [];
256
+
257
+ beforeEach(async () => {
258
+ if (!RUN_FULL_AGENT) return;
259
+
260
+ port = getRandomPort();
261
+ serverUrl = `http://localhost:${port}`;
262
+
263
+ // File-based EventStore so the Claude Code subprocess can access the same DB
264
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "spawn-stream-e2e-"));
265
+ const instanceId = `test-spawn-stream-${Date.now()}`;
266
+ log(`EventStore baseDir: ${tmpDir}, instanceId: ${instanceId}`);
267
+
268
+ eventStore = await createEventStore({ instanceId, baseDir: tmpDir });
269
+ messageRouter = createMessageRouter(eventStore);
270
+ agentManager = createAgentManager(eventStore, messageRouter, {
271
+ defaultPermissionMode: "auto-approve",
272
+ defaultCwd: process.cwd(),
273
+ });
274
+ taskManager = createTaskManager(eventStore);
275
+
276
+ const services: CombinedServerServices = {
277
+ eventStore,
278
+ agentManager,
279
+ taskManager,
280
+ messageRouter,
281
+ };
282
+
283
+ server = createCombinedServer(services, { port, host: "localhost" });
284
+ await server.start();
285
+ log(`Server started on port ${port}`);
286
+ });
287
+
288
+ afterEach(async () => {
289
+ if (!RUN_FULL_AGENT) return;
290
+
291
+ for (const c of clients) {
292
+ c.close();
293
+ }
294
+ clients.length = 0;
295
+
296
+ // Terminate any running agents
297
+ try {
298
+ for (const agent of agentManager.list()) {
299
+ if (agent.state === "running") {
300
+ try {
301
+ await agentManager.terminate(agent.id, "test_cleanup");
302
+ } catch { /* ignore */ }
303
+ }
304
+ }
305
+ } catch { /* ignore */ }
306
+
307
+ await server.stop().catch(() => {});
308
+ await agentManager.close();
309
+ await eventStore.close();
310
+
311
+ // Clean up temp dir
312
+ try {
313
+ fs.rmSync(tmpDir, { recursive: true, force: true });
314
+ } catch { /* ignore */ }
315
+
316
+ log("Cleanup complete");
317
+ });
318
+
319
+ function createClient(): MAPTestClient {
320
+ const client = new MAPTestClient(`ws://localhost:${port}/map`);
321
+ clients.push(client);
322
+ return client;
323
+ }
324
+
325
+ // ─────────────────────────────────────────────────────────────────
326
+ // Core: MAP event streaming during spawned agent prompt
327
+ // ─────────────────────────────────────────────────────────────────
328
+
329
+ testFn(
330
+ "subscriber receives session_user_message, session_update, and session_prompt_done when agent is spawned",
331
+ { timeout: TIMEOUT.SPAWN_AND_PROMPT },
332
+ async () => {
333
+ // Step 1: Subscribe to ALL events (no filter) — same as the real TUI worker
334
+ const subscriber = createClient();
335
+ await subscriber.connect();
336
+ const subRes = await subscriber.request("map/subscribe", {});
337
+ expect(subRes.error).toBeUndefined();
338
+ log("Subscriber connected and subscribed (no filter)");
339
+
340
+ // Step 2: Seed a root agent (the "caller" that spawns children)
341
+ eventStore.emit({
342
+ type: "spawn",
343
+ source: { agent_id: "system" },
344
+ payload: {
345
+ agent_id: "agent_caller",
346
+ session_id: "sess_caller",
347
+ task: "Root caller agent",
348
+ task_id: "task_caller",
349
+ parent: null,
350
+ config: {},
351
+ cwd: process.cwd(),
352
+ },
353
+ });
354
+ log("Root agent seeded");
355
+
356
+ // Step 3: Spawn a real agent via the MCP bridge
357
+ // Give it a simple task so it completes quickly
358
+ let spawnResult: { agent_id: string; session_id: string; task_id: string };
359
+ try {
360
+ spawnResult = await mapCall<{
361
+ agent_id: string;
362
+ session_id: string;
363
+ task_id: string;
364
+ }>(serverUrl, "_macro/mcp/spawn_agent", {
365
+ context: {
366
+ agent_id: "agent_caller",
367
+ session_id: "sess_caller",
368
+ task_id: "task_caller",
369
+ lineage: [],
370
+ cwd: process.cwd(),
371
+ },
372
+ task: "Say exactly: 'Hello from spawned agent'. Do not use any tools. Just respond with that text.",
373
+ });
374
+ log(`Agent spawned: ${spawnResult.agent_id} (session: ${spawnResult.session_id})`);
375
+ } catch (err) {
376
+ throw new Error(`spawn_agent failed: ${(err as Error).message}`);
377
+ }
378
+
379
+ const agentId = spawnResult.agent_id;
380
+
381
+ // Step 4: Wait for session_user_message
382
+ log("Waiting for session_user_message...");
383
+ const userMsg = await subscriber.waitForNotification(
384
+ isEventForAgent("session_user_message", agentId),
385
+ TIMEOUT.EVENT_WAIT,
386
+ );
387
+ const userMsgData = getEventData(userMsg);
388
+ log(`Got session_user_message: content=${JSON.stringify(userMsgData.content).slice(0, 80)}...`);
389
+
390
+ expect(userMsgData.agentId).toBe(agentId);
391
+ expect(userMsgData.sessionId).toBe(spawnResult.session_id);
392
+ expect(userMsgData.content).toContain("Hello from spawned agent");
393
+
394
+ // Step 5: Wait for session_prompt_done (agent should finish quickly)
395
+ log("Waiting for session_prompt_done...");
396
+ const promptDone = await subscriber.waitForNotification(
397
+ isEventForAgent("session_prompt_done", agentId),
398
+ TIMEOUT.EVENT_WAIT,
399
+ );
400
+ const promptDoneData = getEventData(promptDone);
401
+ log(`Got session_prompt_done: stopReason=${promptDoneData.stopReason}`);
402
+
403
+ expect(promptDoneData.agentId).toBe(agentId);
404
+ expect(promptDoneData.stopReason).toBe("end_turn");
405
+
406
+ // Step 6: Verify session_update events were received (at least one)
407
+ const sessionUpdates = subscriber.collectNotifications(
408
+ isEventForAgent("session_update", agentId),
409
+ );
410
+ log(`Got ${sessionUpdates.length} session_update events`);
411
+ expect(sessionUpdates.length).toBeGreaterThan(0);
412
+
413
+ // Verify at least one update contains an agent_message_chunk
414
+ const hasTextChunk = sessionUpdates.some((msg) => {
415
+ const data = getEventData(msg);
416
+ const update = data.update as Record<string, unknown> | undefined;
417
+ return update?.sessionUpdate === "agent_message_chunk";
418
+ });
419
+ log(`Has text chunk in updates: ${hasTextChunk}`);
420
+ expect(hasTextChunk).toBe(true);
421
+
422
+ // Step 7: Verify event ordering: user_message → updates → prompt_done
423
+ const allSessionEvents = subscriber.collectNotifications((msg) => {
424
+ if (msg.method !== "map/event") return false;
425
+ const params = msg.params as Record<string, unknown> | undefined;
426
+ const event = params?.event as Record<string, unknown> | undefined;
427
+ const data = event?.data as Record<string, unknown> | undefined;
428
+ if (data?.agentId !== agentId) return false;
429
+ const type = event?.type as string;
430
+ return type === "session_user_message" || type === "session_update" || type === "session_prompt_done";
431
+ });
432
+
433
+ const eventTypes = allSessionEvents.map((msg) => {
434
+ const params = msg.params as Record<string, unknown>;
435
+ const event = params.event as Record<string, unknown>;
436
+ return event.type;
437
+ });
438
+ log(`Event order: ${eventTypes.join(" → ")}`);
439
+
440
+ // First event should be user_message, last should be prompt_done
441
+ expect(eventTypes[0]).toBe("session_user_message");
442
+ expect(eventTypes[eventTypes.length - 1]).toBe("session_prompt_done");
443
+
444
+ // Step 8: Verify turns were recorded in EventStore for history persistence
445
+ // Wait a moment for the async turn recording to complete
446
+ await new Promise((r) => setTimeout(r, 2000));
447
+
448
+ const turnEvents = eventStore.query({
449
+ type: "turn",
450
+ source_agent_id: agentId,
451
+ });
452
+ log(`Turn events recorded: ${turnEvents.length}`);
453
+
454
+ // Should have at least a user turn and an assistant turn
455
+ const userTurns = turnEvents.filter(
456
+ (e) => (e.payload as Record<string, unknown>).participant === "user",
457
+ );
458
+ const assistantTurns = turnEvents.filter(
459
+ (e) => (e.payload as Record<string, unknown>).participant === agentId,
460
+ );
461
+
462
+ log(`User turns: ${userTurns.length}, Assistant turns: ${assistantTurns.length}`);
463
+ expect(userTurns.length).toBeGreaterThanOrEqual(1);
464
+ expect(assistantTurns.length).toBeGreaterThanOrEqual(1);
465
+
466
+ // Verify the user turn contains the task prompt
467
+ const userTurnPayload = userTurns[0].payload as Record<string, unknown>;
468
+ expect(userTurnPayload.content).toContain("Hello from spawned agent");
469
+ expect(userTurnPayload.conversation_id).toBe(spawnResult.session_id);
470
+
471
+ // Verify the assistant turn has content
472
+ const assistantPayload = assistantTurns[0].payload as Record<string, unknown>;
473
+ expect(assistantPayload.content_type).toBe("assistant_response");
474
+ const content = assistantPayload.content as { parts: Array<Record<string, unknown>> };
475
+ expect(content.parts.length).toBeGreaterThan(0);
476
+ expect(content.parts[0].type).toBe("text");
477
+ log(`Assistant response: ${JSON.stringify(content.parts[0].text).slice(0, 100)}...`);
478
+
479
+ log("All assertions passed!");
480
+ },
481
+ );
482
+
483
+ testFn(
484
+ "multiple subscribers each receive spawned agent session events",
485
+ { timeout: TIMEOUT.SPAWN_AND_PROMPT },
486
+ async () => {
487
+ // Connect two subscribers
488
+ const sub1 = createClient();
489
+ const sub2 = createClient();
490
+ await sub1.connect();
491
+ await sub2.connect();
492
+
493
+ await sub1.request("map/subscribe", {});
494
+ await sub2.request("map/subscribe", {});
495
+ log("Two subscribers connected");
496
+
497
+ // Seed root agent
498
+ eventStore.emit({
499
+ type: "spawn",
500
+ source: { agent_id: "system" },
501
+ payload: {
502
+ agent_id: "agent_multi_caller",
503
+ session_id: "sess_multi_caller",
504
+ task: "Multi-subscriber test caller",
505
+ task_id: "task_multi_caller",
506
+ parent: null,
507
+ config: {},
508
+ cwd: process.cwd(),
509
+ },
510
+ });
511
+
512
+ // Spawn agent
513
+ const spawnResult = await mapCall<{
514
+ agent_id: string;
515
+ session_id: string;
516
+ task_id: string;
517
+ }>(serverUrl, "_macro/mcp/spawn_agent", {
518
+ context: {
519
+ agent_id: "agent_multi_caller",
520
+ session_id: "sess_multi_caller",
521
+ task_id: "task_multi_caller",
522
+ lineage: [],
523
+ cwd: process.cwd(),
524
+ },
525
+ task: "Say exactly: 'test'. Do not use any tools.",
526
+ });
527
+ log(`Agent spawned: ${spawnResult.agent_id}`);
528
+
529
+ const agentId = spawnResult.agent_id;
530
+
531
+ // Both subscribers should receive session_prompt_done
532
+ const [done1, done2] = await Promise.all([
533
+ sub1.waitForNotification(
534
+ isEventForAgent("session_prompt_done", agentId),
535
+ TIMEOUT.EVENT_WAIT,
536
+ ),
537
+ sub2.waitForNotification(
538
+ isEventForAgent("session_prompt_done", agentId),
539
+ TIMEOUT.EVENT_WAIT,
540
+ ),
541
+ ]);
542
+
543
+ expect(getEventData(done1).stopReason).toBe("end_turn");
544
+ expect(getEventData(done2).stopReason).toBe("end_turn");
545
+
546
+ // Both should have received session_user_message
547
+ const userMsg1 = sub1.collectNotifications(isEventForAgent("session_user_message", agentId));
548
+ const userMsg2 = sub2.collectNotifications(isEventForAgent("session_user_message", agentId));
549
+ expect(userMsg1.length).toBe(1);
550
+ expect(userMsg2.length).toBe(1);
551
+
552
+ // Both should have received session_update events
553
+ const updates1 = sub1.collectNotifications(isEventForAgent("session_update", agentId));
554
+ const updates2 = sub2.collectNotifications(isEventForAgent("session_update", agentId));
555
+ expect(updates1.length).toBeGreaterThan(0);
556
+ expect(updates2.length).toBeGreaterThan(0);
557
+ expect(updates1.length).toBe(updates2.length);
558
+
559
+ log(`Both subscribers received ${updates1.length} session_update events`);
560
+ log("Multi-subscriber test passed!");
561
+ },
562
+ );
563
+ });