macro-agent 0.0.10 → 0.0.12

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 (518) hide show
  1. package/.macro-agent/teams/self-driving/prompts/grinder.md +27 -0
  2. package/.macro-agent/teams/self-driving/prompts/judge.md +27 -0
  3. package/.macro-agent/teams/self-driving/prompts/planner.md +33 -0
  4. package/.macro-agent/teams/self-driving/roles/grinder.yaml +17 -0
  5. package/.macro-agent/teams/self-driving/roles/judge.yaml +24 -0
  6. package/.macro-agent/teams/self-driving/roles/planner.yaml +18 -0
  7. package/.macro-agent/teams/self-driving/team.yaml +103 -0
  8. package/.macro-agent/teams/structured/prompts/developer.md +26 -0
  9. package/.macro-agent/teams/structured/prompts/lead.md +25 -0
  10. package/.macro-agent/teams/structured/prompts/reviewer.md +24 -0
  11. package/.macro-agent/teams/structured/roles/developer.yaml +12 -0
  12. package/.macro-agent/teams/structured/roles/lead.yaml +11 -0
  13. package/.macro-agent/teams/structured/roles/reviewer.yaml +19 -0
  14. package/.macro-agent/teams/structured/team.yaml +89 -0
  15. package/.sudocode/issues.jsonl +56 -51
  16. package/.sudocode/specs.jsonl +8 -1
  17. package/CLAUDE.md +121 -30
  18. package/README.md +60 -3
  19. package/dist/acp/macro-agent.d.ts +4 -0
  20. package/dist/acp/macro-agent.d.ts.map +1 -1
  21. package/dist/acp/macro-agent.js +50 -4
  22. package/dist/acp/macro-agent.js.map +1 -1
  23. package/dist/acp/session-mapper.d.ts +20 -1
  24. package/dist/acp/session-mapper.d.ts.map +1 -1
  25. package/dist/acp/session-mapper.js +90 -1
  26. package/dist/acp/session-mapper.js.map +1 -1
  27. package/dist/acp/types.d.ts +24 -1
  28. package/dist/acp/types.d.ts.map +1 -1
  29. package/dist/acp/types.js.map +1 -1
  30. package/dist/agent/agent-manager.d.ts +40 -1
  31. package/dist/agent/agent-manager.d.ts.map +1 -1
  32. package/dist/agent/agent-manager.js +172 -8
  33. package/dist/agent/agent-manager.js.map +1 -1
  34. package/dist/agent/types.d.ts +22 -0
  35. package/dist/agent/types.d.ts.map +1 -1
  36. package/dist/agent/types.js.map +1 -1
  37. package/dist/agent/wake.d.ts +15 -0
  38. package/dist/agent/wake.d.ts.map +1 -1
  39. package/dist/agent/wake.js +15 -0
  40. package/dist/agent/wake.js.map +1 -1
  41. package/dist/agent-detection/command-builder.d.ts +30 -0
  42. package/dist/agent-detection/command-builder.d.ts.map +1 -0
  43. package/dist/agent-detection/command-builder.js +71 -0
  44. package/dist/agent-detection/command-builder.js.map +1 -0
  45. package/dist/agent-detection/detector.d.ts +84 -0
  46. package/dist/agent-detection/detector.d.ts.map +1 -0
  47. package/dist/agent-detection/detector.js +240 -0
  48. package/dist/agent-detection/detector.js.map +1 -0
  49. package/dist/agent-detection/index.d.ts +12 -0
  50. package/dist/agent-detection/index.d.ts.map +1 -0
  51. package/dist/agent-detection/index.js +14 -0
  52. package/dist/agent-detection/index.js.map +1 -0
  53. package/dist/agent-detection/registry.d.ts +53 -0
  54. package/dist/agent-detection/registry.d.ts.map +1 -0
  55. package/dist/agent-detection/registry.js +177 -0
  56. package/dist/agent-detection/registry.js.map +1 -0
  57. package/dist/agent-detection/types.d.ts +121 -0
  58. package/dist/agent-detection/types.d.ts.map +1 -0
  59. package/dist/agent-detection/types.js +20 -0
  60. package/dist/agent-detection/types.js.map +1 -0
  61. package/dist/api/server.d.ts +5 -1
  62. package/dist/api/server.d.ts.map +1 -1
  63. package/dist/api/server.js +362 -0
  64. package/dist/api/server.js.map +1 -1
  65. package/dist/api/types.d.ts +50 -1
  66. package/dist/api/types.d.ts.map +1 -1
  67. package/dist/cli/acp.d.ts +2 -0
  68. package/dist/cli/acp.d.ts.map +1 -1
  69. package/dist/cli/acp.js +8 -1
  70. package/dist/cli/acp.js.map +1 -1
  71. package/dist/cli/index.js +29 -0
  72. package/dist/cli/index.js.map +1 -1
  73. package/dist/cli/mcp.js +38 -0
  74. package/dist/cli/mcp.js.map +1 -1
  75. package/dist/config/index.d.ts +2 -0
  76. package/dist/config/index.d.ts.map +1 -0
  77. package/dist/config/index.js +2 -0
  78. package/dist/config/index.js.map +1 -0
  79. package/dist/config/project-config.d.ts +46 -0
  80. package/dist/config/project-config.d.ts.map +1 -0
  81. package/dist/config/project-config.js +68 -0
  82. package/dist/config/project-config.js.map +1 -0
  83. package/dist/lifecycle/cascade.d.ts +1 -1
  84. package/dist/lifecycle/cascade.d.ts.map +1 -1
  85. package/dist/lifecycle/handlers/index.d.ts +4 -0
  86. package/dist/lifecycle/handlers/index.d.ts.map +1 -1
  87. package/dist/lifecycle/handlers/index.js +2 -0
  88. package/dist/lifecycle/handlers/index.js.map +1 -1
  89. package/dist/lifecycle/handlers/worker.d.ts +4 -0
  90. package/dist/lifecycle/handlers/worker.d.ts.map +1 -1
  91. package/dist/lifecycle/handlers/worker.js +35 -3
  92. package/dist/lifecycle/handlers/worker.js.map +1 -1
  93. package/dist/mail/conversation-map.d.ts +33 -0
  94. package/dist/mail/conversation-map.d.ts.map +1 -0
  95. package/dist/mail/conversation-map.js +61 -0
  96. package/dist/mail/conversation-map.js.map +1 -0
  97. package/dist/mail/index.d.ts +11 -0
  98. package/dist/mail/index.d.ts.map +1 -0
  99. package/dist/mail/index.js +11 -0
  100. package/dist/mail/index.js.map +1 -0
  101. package/dist/mail/mail-service.d.ts +85 -0
  102. package/dist/mail/mail-service.d.ts.map +1 -0
  103. package/dist/mail/mail-service.js +121 -0
  104. package/dist/mail/mail-service.js.map +1 -0
  105. package/dist/mail/stores/eventstore-conversation-store.d.ts +40 -0
  106. package/dist/mail/stores/eventstore-conversation-store.d.ts.map +1 -0
  107. package/dist/mail/stores/eventstore-conversation-store.js +131 -0
  108. package/dist/mail/stores/eventstore-conversation-store.js.map +1 -0
  109. package/dist/mail/stores/eventstore-participant-store.d.ts +43 -0
  110. package/dist/mail/stores/eventstore-participant-store.d.ts.map +1 -0
  111. package/dist/mail/stores/eventstore-participant-store.js +145 -0
  112. package/dist/mail/stores/eventstore-participant-store.js.map +1 -0
  113. package/dist/mail/stores/eventstore-thread-store.d.ts +46 -0
  114. package/dist/mail/stores/eventstore-thread-store.d.ts.map +1 -0
  115. package/dist/mail/stores/eventstore-thread-store.js +118 -0
  116. package/dist/mail/stores/eventstore-thread-store.js.map +1 -0
  117. package/dist/mail/stores/eventstore-turn-store.d.ts +47 -0
  118. package/dist/mail/stores/eventstore-turn-store.d.ts.map +1 -0
  119. package/dist/mail/stores/eventstore-turn-store.js +153 -0
  120. package/dist/mail/stores/eventstore-turn-store.js.map +1 -0
  121. package/dist/mail/stores/index.d.ts +12 -0
  122. package/dist/mail/stores/index.d.ts.map +1 -0
  123. package/dist/mail/stores/index.js +12 -0
  124. package/dist/mail/stores/index.js.map +1 -0
  125. package/dist/mail/stores/types.d.ts +146 -0
  126. package/dist/mail/stores/types.d.ts.map +1 -0
  127. package/dist/mail/stores/types.js +13 -0
  128. package/dist/mail/stores/types.js.map +1 -0
  129. package/dist/mail/turn-recorder.d.ts +30 -0
  130. package/dist/mail/turn-recorder.d.ts.map +1 -0
  131. package/dist/mail/turn-recorder.js +98 -0
  132. package/dist/mail/turn-recorder.js.map +1 -0
  133. package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
  134. package/dist/map/adapter/acp-over-map.js +32 -2
  135. package/dist/map/adapter/acp-over-map.js.map +1 -1
  136. package/dist/map/adapter/event-translator.d.ts.map +1 -1
  137. package/dist/map/adapter/event-translator.js +4 -0
  138. package/dist/map/adapter/event-translator.js.map +1 -1
  139. package/dist/map/adapter/extensions/agent-detection.d.ts +49 -0
  140. package/dist/map/adapter/extensions/agent-detection.d.ts.map +1 -0
  141. package/dist/map/adapter/extensions/agent-detection.js +91 -0
  142. package/dist/map/adapter/extensions/agent-detection.js.map +1 -0
  143. package/dist/map/adapter/extensions/index.d.ts +10 -1
  144. package/dist/map/adapter/extensions/index.d.ts.map +1 -1
  145. package/dist/map/adapter/extensions/index.js +39 -0
  146. package/dist/map/adapter/extensions/index.js.map +1 -1
  147. package/dist/map/adapter/extensions/resume.d.ts +47 -0
  148. package/dist/map/adapter/extensions/resume.d.ts.map +1 -0
  149. package/dist/map/adapter/extensions/resume.js +59 -0
  150. package/dist/map/adapter/extensions/resume.js.map +1 -0
  151. package/dist/map/adapter/extensions/workspace-files.d.ts +42 -0
  152. package/dist/map/adapter/extensions/workspace-files.d.ts.map +1 -0
  153. package/dist/map/adapter/extensions/workspace-files.js +338 -0
  154. package/dist/map/adapter/extensions/workspace-files.js.map +1 -0
  155. package/dist/map/adapter/mail-handler-adapter.d.ts +27 -0
  156. package/dist/map/adapter/mail-handler-adapter.d.ts.map +1 -0
  157. package/dist/map/adapter/mail-handler-adapter.js +292 -0
  158. package/dist/map/adapter/mail-handler-adapter.js.map +1 -0
  159. package/dist/map/adapter/map-adapter.d.ts +34 -10
  160. package/dist/map/adapter/map-adapter.d.ts.map +1 -1
  161. package/dist/map/adapter/map-adapter.js +110 -14
  162. package/dist/map/adapter/map-adapter.js.map +1 -1
  163. package/dist/map/adapter/rpc-handler.d.ts +4 -1
  164. package/dist/map/adapter/rpc-handler.d.ts.map +1 -1
  165. package/dist/map/adapter/rpc-handler.js +6 -0
  166. package/dist/map/adapter/rpc-handler.js.map +1 -1
  167. package/dist/map/index.d.ts +1 -0
  168. package/dist/map/index.d.ts.map +1 -1
  169. package/dist/map/index.js +2 -0
  170. package/dist/map/index.js.map +1 -1
  171. package/dist/map/types.d.ts +3 -1
  172. package/dist/map/types.d.ts.map +1 -1
  173. package/dist/map/types.js.map +1 -1
  174. package/dist/mcp/mcp-server.d.ts +6 -0
  175. package/dist/mcp/mcp-server.d.ts.map +1 -1
  176. package/dist/mcp/mcp-server.js +45 -0
  177. package/dist/mcp/mcp-server.js.map +1 -1
  178. package/dist/mcp/tools/claim_task.d.ts +35 -0
  179. package/dist/mcp/tools/claim_task.d.ts.map +1 -0
  180. package/dist/mcp/tools/claim_task.js +58 -0
  181. package/dist/mcp/tools/claim_task.js.map +1 -0
  182. package/dist/mcp/tools/done.d.ts +15 -2
  183. package/dist/mcp/tools/done.d.ts.map +1 -1
  184. package/dist/mcp/tools/done.js +45 -10
  185. package/dist/mcp/tools/done.js.map +1 -1
  186. package/dist/mcp/tools/list_claimable_tasks.d.ts +38 -0
  187. package/dist/mcp/tools/list_claimable_tasks.d.ts.map +1 -0
  188. package/dist/mcp/tools/list_claimable_tasks.js +63 -0
  189. package/dist/mcp/tools/list_claimable_tasks.js.map +1 -0
  190. package/dist/mcp/tools/unclaim_task.d.ts +31 -0
  191. package/dist/mcp/tools/unclaim_task.d.ts.map +1 -0
  192. package/dist/mcp/tools/unclaim_task.js +47 -0
  193. package/dist/mcp/tools/unclaim_task.js.map +1 -0
  194. package/dist/metrics/index.d.ts +2 -0
  195. package/dist/metrics/index.d.ts.map +1 -0
  196. package/dist/metrics/index.js +2 -0
  197. package/dist/metrics/index.js.map +1 -0
  198. package/dist/metrics/metrics.d.ts +79 -0
  199. package/dist/metrics/metrics.d.ts.map +1 -0
  200. package/dist/metrics/metrics.js +166 -0
  201. package/dist/metrics/metrics.js.map +1 -0
  202. package/dist/roles/capabilities.d.ts +1 -0
  203. package/dist/roles/capabilities.d.ts.map +1 -1
  204. package/dist/roles/capabilities.js +3 -0
  205. package/dist/roles/capabilities.js.map +1 -1
  206. package/dist/roles/types.d.ts +1 -1
  207. package/dist/roles/types.d.ts.map +1 -1
  208. package/dist/router/channels.d.ts +2 -4
  209. package/dist/router/channels.d.ts.map +1 -1
  210. package/dist/router/channels.js.map +1 -1
  211. package/dist/router/message-router.d.ts +85 -9
  212. package/dist/router/message-router.d.ts.map +1 -1
  213. package/dist/router/message-router.js +203 -14
  214. package/dist/router/message-router.js.map +1 -1
  215. package/dist/router/role-resolver.d.ts +10 -1
  216. package/dist/router/role-resolver.d.ts.map +1 -1
  217. package/dist/router/role-resolver.js +15 -1
  218. package/dist/router/role-resolver.js.map +1 -1
  219. package/dist/router/types.d.ts +30 -1
  220. package/dist/router/types.d.ts.map +1 -1
  221. package/dist/router/types.js.map +1 -1
  222. package/dist/server/combined-server.d.ts +6 -0
  223. package/dist/server/combined-server.d.ts.map +1 -1
  224. package/dist/server/combined-server.js +24 -2
  225. package/dist/server/combined-server.js.map +1 -1
  226. package/dist/store/event-store.d.ts +14 -1
  227. package/dist/store/event-store.d.ts.map +1 -1
  228. package/dist/store/event-store.js +456 -4
  229. package/dist/store/event-store.js.map +1 -1
  230. package/dist/store/types/agents.d.ts +1 -1
  231. package/dist/store/types/agents.d.ts.map +1 -1
  232. package/dist/store/types/conversations.d.ts +91 -0
  233. package/dist/store/types/conversations.d.ts.map +1 -0
  234. package/dist/store/types/conversations.js +8 -0
  235. package/dist/store/types/conversations.js.map +1 -0
  236. package/dist/store/types/events.d.ts +1 -1
  237. package/dist/store/types/events.d.ts.map +1 -1
  238. package/dist/store/types/events.js.map +1 -1
  239. package/dist/store/types/index.d.ts +2 -0
  240. package/dist/store/types/index.d.ts.map +1 -1
  241. package/dist/store/types/index.js +2 -0
  242. package/dist/store/types/index.js.map +1 -1
  243. package/dist/store/types/sessions.d.ts +44 -0
  244. package/dist/store/types/sessions.d.ts.map +1 -0
  245. package/dist/store/types/sessions.js +9 -0
  246. package/dist/store/types/sessions.js.map +1 -0
  247. package/dist/store/types/tasks.d.ts +2 -0
  248. package/dist/store/types/tasks.d.ts.map +1 -1
  249. package/dist/task/backend/memory.d.ts +4 -1
  250. package/dist/task/backend/memory.d.ts.map +1 -1
  251. package/dist/task/backend/memory.js +81 -0
  252. package/dist/task/backend/memory.js.map +1 -1
  253. package/dist/task/backend/types.d.ts +30 -0
  254. package/dist/task/backend/types.d.ts.map +1 -1
  255. package/dist/task/backend/types.js.map +1 -1
  256. package/dist/teams/index.d.ts +4 -0
  257. package/dist/teams/index.d.ts.map +1 -0
  258. package/dist/teams/index.js +4 -0
  259. package/dist/teams/index.js.map +1 -0
  260. package/dist/teams/team-loader.d.ts +20 -0
  261. package/dist/teams/team-loader.d.ts.map +1 -0
  262. package/dist/teams/team-loader.js +293 -0
  263. package/dist/teams/team-loader.js.map +1 -0
  264. package/dist/teams/team-runtime.d.ts +139 -0
  265. package/dist/teams/team-runtime.d.ts.map +1 -0
  266. package/dist/teams/team-runtime.js +613 -0
  267. package/dist/teams/team-runtime.js.map +1 -0
  268. package/dist/teams/types.d.ts +266 -0
  269. package/dist/teams/types.d.ts.map +1 -0
  270. package/dist/teams/types.js +20 -0
  271. package/dist/teams/types.js.map +1 -0
  272. package/dist/trigger/router/trigger-router.d.ts +30 -3
  273. package/dist/trigger/router/trigger-router.d.ts.map +1 -1
  274. package/dist/trigger/router/trigger-router.js +30 -3
  275. package/dist/trigger/router/trigger-router.js.map +1 -1
  276. package/dist/trigger/wake/types.d.ts +31 -5
  277. package/dist/trigger/wake/types.d.ts.map +1 -1
  278. package/dist/trigger/wake/types.js +19 -0
  279. package/dist/trigger/wake/types.js.map +1 -1
  280. package/dist/workspace/dataplane-adapter.d.ts +1 -1
  281. package/dist/workspace/dataplane-adapter.d.ts.map +1 -1
  282. package/dist/workspace/dataplane-adapter.js +1 -1
  283. package/dist/workspace/dataplane-adapter.js.map +1 -1
  284. package/dist/workspace/index.d.ts +1 -1
  285. package/dist/workspace/index.d.ts.map +1 -1
  286. package/dist/workspace/strategies/index.d.ts +6 -0
  287. package/dist/workspace/strategies/index.d.ts.map +1 -0
  288. package/dist/workspace/strategies/index.js +5 -0
  289. package/dist/workspace/strategies/index.js.map +1 -0
  290. package/dist/workspace/strategies/optimistic.d.ts +26 -0
  291. package/dist/workspace/strategies/optimistic.d.ts.map +1 -0
  292. package/dist/workspace/strategies/optimistic.js +121 -0
  293. package/dist/workspace/strategies/optimistic.js.map +1 -0
  294. package/dist/workspace/strategies/queue.d.ts +26 -0
  295. package/dist/workspace/strategies/queue.d.ts.map +1 -0
  296. package/dist/workspace/strategies/queue.js +67 -0
  297. package/dist/workspace/strategies/queue.js.map +1 -0
  298. package/dist/workspace/strategies/registry.d.ts +37 -0
  299. package/dist/workspace/strategies/registry.d.ts.map +1 -0
  300. package/dist/workspace/strategies/registry.js +63 -0
  301. package/dist/workspace/strategies/registry.js.map +1 -0
  302. package/dist/workspace/strategies/trunk.d.ts +20 -0
  303. package/dist/workspace/strategies/trunk.d.ts.map +1 -0
  304. package/dist/workspace/strategies/trunk.js +108 -0
  305. package/dist/workspace/strategies/trunk.js.map +1 -0
  306. package/dist/workspace/strategies/types.d.ts +104 -0
  307. package/dist/workspace/strategies/types.d.ts.map +1 -0
  308. package/dist/workspace/strategies/types.js +11 -0
  309. package/dist/workspace/strategies/types.js.map +1 -0
  310. package/dist/workspace/types.d.ts +1 -1
  311. package/dist/workspace/types.d.ts.map +1 -1
  312. package/dist/workspace/workspace-manager.d.ts +1 -1
  313. package/dist/workspace/workspace-manager.d.ts.map +1 -1
  314. package/docs/implementation-details.md +1127 -0
  315. package/docs/implementation-summary.md +448 -0
  316. package/docs/mail-integration.md +608 -0
  317. package/docs/plan-self-driving-support.md +433 -0
  318. package/docs/spec-self-driving-support.md +462 -0
  319. package/docs/team-templates.md +860 -0
  320. package/docs/teams.md +233 -0
  321. package/package.json +5 -3
  322. package/src/acp/__tests__/integration.test.ts +161 -1
  323. package/src/acp/__tests__/macro-agent.test.ts +95 -0
  324. package/src/acp/__tests__/session-persistence.test.ts +276 -0
  325. package/src/acp/macro-agent.ts +79 -7
  326. package/src/acp/session-mapper.ts +108 -1
  327. package/src/acp/types.ts +33 -1
  328. package/src/agent/agent-manager.ts +278 -6
  329. package/src/agent/types.ts +27 -0
  330. package/src/agent/wake.ts +15 -0
  331. package/src/agent-detection/__tests__/command-builder.test.ts +336 -0
  332. package/src/agent-detection/__tests__/detector.test.ts +768 -0
  333. package/src/agent-detection/__tests__/registry.test.ts +254 -0
  334. package/src/agent-detection/command-builder.ts +90 -0
  335. package/src/agent-detection/detector.ts +307 -0
  336. package/src/agent-detection/index.ts +36 -0
  337. package/src/agent-detection/registry.ts +200 -0
  338. package/src/agent-detection/types.ts +184 -0
  339. package/src/api/__tests__/conversation-api.test.ts +468 -0
  340. package/src/api/server.ts +425 -1
  341. package/src/api/types.ts +64 -1
  342. package/src/cli/acp.ts +9 -1
  343. package/src/cli/index.ts +44 -0
  344. package/src/cli/mcp.ts +47 -0
  345. package/src/config/index.ts +9 -0
  346. package/src/config/project-config.ts +107 -0
  347. package/src/lifecycle/cascade.ts +1 -1
  348. package/src/lifecycle/handlers/index.ts +8 -0
  349. package/src/lifecycle/handlers/worker.ts +48 -3
  350. package/src/mail/__tests__/conversation-lifecycle.test.ts +409 -0
  351. package/src/mail/__tests__/eventstore-stores.test.ts +1073 -0
  352. package/src/mail/__tests__/mail-full-agent.e2e.test.ts +575 -0
  353. package/src/mail/__tests__/mail-integration.test.ts +759 -0
  354. package/src/mail/__tests__/mail-map-protocol.e2e.test.ts +1068 -0
  355. package/src/mail/__tests__/mail-service.test.ts +506 -0
  356. package/src/mail/__tests__/turn-recorder.test.ts +328 -0
  357. package/src/mail/conversation-map.ts +107 -0
  358. package/src/mail/index.ts +25 -0
  359. package/src/mail/mail-service.ts +257 -0
  360. package/src/mail/stores/eventstore-conversation-store.ts +146 -0
  361. package/src/mail/stores/eventstore-participant-store.ts +172 -0
  362. package/src/mail/stores/eventstore-thread-store.ts +129 -0
  363. package/src/mail/stores/eventstore-turn-store.ts +173 -0
  364. package/src/mail/stores/index.ts +12 -0
  365. package/src/mail/stores/types.ts +160 -0
  366. package/src/mail/turn-recorder.ts +124 -0
  367. package/src/map/README.md +79 -0
  368. package/src/map/adapter/__tests__/extensions.test.ts +359 -0
  369. package/src/map/adapter/__tests__/map-adapter.test.ts +90 -0
  370. package/src/map/adapter/__tests__/workspace-files.test.ts +673 -0
  371. package/src/map/adapter/acp-over-map.ts +45 -2
  372. package/src/map/adapter/event-translator.ts +4 -0
  373. package/src/map/adapter/extensions/agent-detection.ts +201 -0
  374. package/src/map/adapter/extensions/index.ts +63 -0
  375. package/src/map/adapter/extensions/resume.ts +114 -0
  376. package/src/map/adapter/extensions/workspace-files.ts +449 -0
  377. package/src/map/adapter/mail-handler-adapter.ts +429 -0
  378. package/src/map/adapter/map-adapter.ts +173 -27
  379. package/src/map/adapter/rpc-handler.ts +8 -1
  380. package/src/map/index.ts +3 -0
  381. package/src/map/types.ts +3 -1
  382. package/src/mcp/mcp-server.ts +67 -0
  383. package/src/mcp/tools/claim_task.ts +86 -0
  384. package/src/mcp/tools/done.ts +59 -10
  385. package/src/mcp/tools/list_claimable_tasks.ts +93 -0
  386. package/src/mcp/tools/unclaim_task.ts +71 -0
  387. package/src/metrics/index.ts +9 -0
  388. package/src/metrics/metrics.ts +280 -0
  389. package/src/roles/capabilities.ts +3 -0
  390. package/src/roles/types.ts +2 -1
  391. package/src/router/README.md +120 -0
  392. package/src/router/__tests__/message-router.test.ts +561 -0
  393. package/src/router/channels.ts +3 -4
  394. package/src/router/message-router.ts +308 -22
  395. package/src/router/role-resolver.ts +22 -1
  396. package/src/router/types.ts +36 -1
  397. package/src/server/combined-server.ts +36 -2
  398. package/src/store/README.md +134 -0
  399. package/src/store/event-store.ts +546 -3
  400. package/src/store/types/agents.ts +1 -1
  401. package/src/store/types/conversations.ts +129 -0
  402. package/src/store/types/events.ts +5 -1
  403. package/src/store/types/index.ts +2 -0
  404. package/src/store/types/sessions.ts +53 -0
  405. package/src/store/types/tasks.ts +3 -0
  406. package/src/task/backend/memory.ts +116 -0
  407. package/src/task/backend/types.ts +43 -0
  408. package/src/teams/__tests__/cross-subsystem.integration.test.ts +983 -0
  409. package/src/teams/__tests__/e2e/team-runtime.e2e.test.ts +553 -0
  410. package/src/teams/__tests__/team-system.test.ts +1280 -0
  411. package/src/teams/index.ts +13 -0
  412. package/src/teams/team-loader.ts +434 -0
  413. package/src/teams/team-runtime.ts +727 -0
  414. package/src/teams/types.ts +377 -0
  415. package/src/trigger/router/trigger-router.ts +30 -3
  416. package/src/trigger/wake/types.ts +32 -5
  417. package/src/trigger/wake/wake-manager.ts +2 -2
  418. package/src/workspace/dataplane-adapter.ts +1 -1
  419. package/src/workspace/index.ts +1 -1
  420. package/src/workspace/strategies/index.ts +18 -0
  421. package/src/workspace/strategies/optimistic.ts +136 -0
  422. package/src/workspace/strategies/queue.ts +81 -0
  423. package/src/workspace/strategies/registry.ts +89 -0
  424. package/src/workspace/strategies/trunk.ts +123 -0
  425. package/src/workspace/strategies/types.ts +145 -0
  426. package/src/workspace/types.ts +1 -1
  427. package/src/workspace/workspace-manager.ts +1 -1
  428. package/.claude/settings.local.json +0 -59
  429. package/dist/map/utils/address-translation.d.ts +0 -99
  430. package/dist/map/utils/address-translation.d.ts.map +0 -1
  431. package/dist/map/utils/address-translation.js +0 -285
  432. package/dist/map/utils/address-translation.js.map +0 -1
  433. package/dist/map/utils/index.d.ts +0 -7
  434. package/dist/map/utils/index.d.ts.map +0 -1
  435. package/dist/map/utils/index.js +0 -7
  436. package/dist/map/utils/index.js.map +0 -1
  437. package/openspec/AGENTS.md +0 -456
  438. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/design.md +0 -128
  439. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/proposal.md +0 -49
  440. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/agent-manager/spec.md +0 -150
  441. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/cli-api/spec.md +0 -258
  442. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/event-store/spec.md +0 -160
  443. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/mcp-tools/spec.md +0 -224
  444. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/message-router/spec.md +0 -153
  445. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/task-manager/spec.md +0 -136
  446. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/tasks.md +0 -147
  447. package/openspec/project.md +0 -31
  448. package/openspec/specs/agent-manager/spec.md +0 -154
  449. package/openspec/specs/cli-api/spec.md +0 -262
  450. package/openspec/specs/event-store/spec.md +0 -164
  451. package/openspec/specs/mcp-tools/spec.md +0 -228
  452. package/openspec/specs/message-router/spec.md +0 -157
  453. package/openspec/specs/task-manager/spec.md +0 -140
  454. package/references/acp-factory-ref/CHANGELOG.md +0 -33
  455. package/references/acp-factory-ref/LICENSE +0 -21
  456. package/references/acp-factory-ref/README.md +0 -341
  457. package/references/acp-factory-ref/package-lock.json +0 -3102
  458. package/references/acp-factory-ref/package.json +0 -96
  459. package/references/acp-factory-ref/python/CHANGELOG.md +0 -33
  460. package/references/acp-factory-ref/python/LICENSE +0 -21
  461. package/references/acp-factory-ref/python/Makefile +0 -57
  462. package/references/acp-factory-ref/python/README.md +0 -253
  463. package/references/acp-factory-ref/python/pyproject.toml +0 -73
  464. package/references/acp-factory-ref/python/tests/__init__.py +0 -0
  465. package/references/acp-factory-ref/python/tests/e2e/__init__.py +0 -1
  466. package/references/acp-factory-ref/python/tests/e2e/test_codex_e2e.py +0 -349
  467. package/references/acp-factory-ref/python/tests/e2e/test_gemini_e2e.py +0 -165
  468. package/references/acp-factory-ref/python/tests/e2e/test_opencode_e2e.py +0 -296
  469. package/references/acp-factory-ref/python/tests/test_client_handler.py +0 -543
  470. package/references/acp-factory-ref/python/tests/test_pushable.py +0 -199
  471. package/references/claude-code-acp/.github/workflows/ci.yml +0 -45
  472. package/references/claude-code-acp/.github/workflows/publish.yml +0 -34
  473. package/references/claude-code-acp/.prettierrc.json +0 -4
  474. package/references/claude-code-acp/CHANGELOG.md +0 -249
  475. package/references/claude-code-acp/LICENSE +0 -222
  476. package/references/claude-code-acp/README.md +0 -53
  477. package/references/claude-code-acp/docs/RELEASES.md +0 -24
  478. package/references/claude-code-acp/eslint.config.js +0 -48
  479. package/references/claude-code-acp/package-lock.json +0 -4570
  480. package/references/claude-code-acp/package.json +0 -88
  481. package/references/claude-code-acp/scripts/release.sh +0 -119
  482. package/references/claude-code-acp/src/acp-agent.ts +0 -2065
  483. package/references/claude-code-acp/src/index.ts +0 -26
  484. package/references/claude-code-acp/src/lib.ts +0 -38
  485. package/references/claude-code-acp/src/mcp-server.ts +0 -911
  486. package/references/claude-code-acp/src/settings.ts +0 -522
  487. package/references/claude-code-acp/src/tests/.claude/commands/quick-math.md +0 -5
  488. package/references/claude-code-acp/src/tests/.claude/commands/say-hello.md +0 -6
  489. package/references/claude-code-acp/src/tests/acp-agent-fork.test.ts +0 -479
  490. package/references/claude-code-acp/src/tests/acp-agent.test.ts +0 -1502
  491. package/references/claude-code-acp/src/tests/extract-lines.test.ts +0 -103
  492. package/references/claude-code-acp/src/tests/fork-session.test.ts +0 -335
  493. package/references/claude-code-acp/src/tests/replace-and-calculate-location.test.ts +0 -334
  494. package/references/claude-code-acp/src/tests/settings.test.ts +0 -617
  495. package/references/claude-code-acp/src/tests/skills-options.test.ts +0 -187
  496. package/references/claude-code-acp/src/tests/tools.test.ts +0 -318
  497. package/references/claude-code-acp/src/tests/typescript-declarations.test.ts +0 -558
  498. package/references/claude-code-acp/src/tools.ts +0 -819
  499. package/references/claude-code-acp/src/utils.ts +0 -171
  500. package/references/claude-code-acp/tsconfig.json +0 -18
  501. package/references/claude-code-acp/vitest.config.ts +0 -19
  502. package/references/multi-agent-protocol/.sudocode/issues.jsonl +0 -82
  503. package/references/multi-agent-protocol/.sudocode/specs.jsonl +0 -9
  504. package/references/multi-agent-protocol/LICENSE +0 -21
  505. package/references/multi-agent-protocol/README.md +0 -113
  506. package/references/multi-agent-protocol/docs/00-design-specification.md +0 -460
  507. package/references/multi-agent-protocol/docs/01-open-questions.md +0 -1050
  508. package/references/multi-agent-protocol/docs/02-wire-protocol.md +0 -296
  509. package/references/multi-agent-protocol/docs/03-streaming-semantics.md +0 -252
  510. package/references/multi-agent-protocol/docs/04-error-handling.md +0 -231
  511. package/references/multi-agent-protocol/docs/05-connection-model.md +0 -244
  512. package/references/multi-agent-protocol/docs/06-visibility-permissions.md +0 -243
  513. package/references/multi-agent-protocol/docs/07-federation.md +0 -259
  514. package/references/multi-agent-protocol/docs/08-macro-agent-migration.md +0 -253
  515. package/references/multi-agent-protocol/package-lock.json +0 -3239
  516. package/references/multi-agent-protocol/package.json +0 -56
  517. package/references/multi-agent-protocol/schema/meta.json +0 -337
  518. package/references/multi-agent-protocol/schema/schema.json +0 -1828
@@ -0,0 +1,1280 @@
1
+ /**
2
+ * Team System Tests
3
+ *
4
+ * Tests loading team templates, runtime initialization, bootstrap,
5
+ * and integration between team subsystems (roles, communication,
6
+ * strategies, task modes).
7
+ */
8
+
9
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
10
+ import * as path from "path";
11
+ import { loadTeam } from "../team-loader.js";
12
+ import { TeamRuntime, type TeamServices } from "../team-runtime.js";
13
+ import { DefaultRoleRegistry } from "../../roles/registry.js";
14
+ import type { RoleDefinition } from "../../roles/types.js";
15
+ import type { AgentManager, SpawnInterceptor } from "../../agent/agent-manager.js";
16
+ import type { MessageRouter } from "../../router/message-router.js";
17
+ import type { EventStore } from "../../store/event-store.js";
18
+ import type { SpawnAgentOptions } from "../../agent/types.js";
19
+ import type { AgentId, Event } from "../../store/types/index.js";
20
+
21
+ // =============================================================================
22
+ // Helpers
23
+ // =============================================================================
24
+
25
+ const PROJECT_ROOT = path.resolve(import.meta.dirname, "../../..");
26
+
27
+ function createMockEventStore(): EventStore {
28
+ const events: Event[] = [];
29
+ return {
30
+ emit: vi.fn((input: Record<string, unknown>) => {
31
+ const event = {
32
+ id: `evt_${events.length}`,
33
+ type: input.type,
34
+ timestamp: Date.now(),
35
+ source: input.source,
36
+ target: input.target,
37
+ payload: input.payload,
38
+ } as unknown as Event;
39
+ events.push(event);
40
+ return event;
41
+ }),
42
+ persist: vi.fn().mockResolvedValue(undefined),
43
+ close: vi.fn().mockResolvedValue(undefined),
44
+ query: vi.fn().mockReturnValue([]),
45
+ getAgent: vi.fn().mockReturnValue(null),
46
+ getTask: vi.fn().mockReturnValue(null),
47
+ listAgents: vi.fn().mockReturnValue([]),
48
+ onAgentChange: vi.fn(),
49
+ onTaskChange: vi.fn(),
50
+ instanceId: "test-instance",
51
+ _events: events,
52
+ } as unknown as EventStore & { _events: Event[] };
53
+ }
54
+
55
+ function createMockMessageRouter(): MessageRouter {
56
+ return {
57
+ sendToAddress: vi.fn().mockResolvedValue({ delivered: true }),
58
+ emitStatus: vi.fn(),
59
+ getMessages: vi.fn().mockReturnValue([]),
60
+ subscribe: vi.fn(),
61
+ unsubscribe: vi.fn(),
62
+ getSubscriptions: vi.fn().mockReturnValue([]),
63
+ setupDefaultSubscriptions: vi.fn(),
64
+ setSignalFilter: vi.fn(),
65
+ setEmissionValidator: vi.fn(),
66
+ } as unknown as MessageRouter;
67
+ }
68
+
69
+ let spawnCounter = 0;
70
+ let capturedInterceptor: SpawnInterceptor | null = null;
71
+ let interceptedSpawnOptions: SpawnAgentOptions[] = [];
72
+
73
+ function createMockAgentManager(roleRegistry: DefaultRoleRegistry): AgentManager {
74
+ capturedInterceptor = null;
75
+ spawnCounter = 0;
76
+ interceptedSpawnOptions = [];
77
+
78
+ return {
79
+ spawn: vi.fn(async (options: SpawnAgentOptions) => {
80
+ // Apply interceptor if set and record the intercepted options
81
+ const opts = capturedInterceptor ? capturedInterceptor(options) : options;
82
+ interceptedSpawnOptions.push(opts);
83
+ const id = `agent_${spawnCounter++}`;
84
+ return {
85
+ id,
86
+ session_id: `session_${id}`,
87
+ task: opts.task ?? "test",
88
+ state: "running" as const,
89
+ created_at: Date.now(),
90
+ parent: opts.parent ?? null,
91
+ role: opts.role,
92
+ config: opts.config,
93
+ _spawnOptions: opts,
94
+ };
95
+ }),
96
+ terminate: vi.fn().mockResolvedValue(undefined),
97
+ get: vi.fn().mockReturnValue(null),
98
+ list: vi.fn().mockReturnValue([]),
99
+ getChildren: vi.fn().mockReturnValue([]),
100
+ getHierarchy: vi.fn().mockReturnValue(null),
101
+ getSession: vi.fn().mockReturnValue(null),
102
+ hasActiveSession: vi.fn().mockReturnValue(false),
103
+ setSpawnInterceptor: vi.fn((interceptor: SpawnInterceptor | null) => {
104
+ capturedInterceptor = interceptor;
105
+ }),
106
+ getRoleRegistry: vi.fn(() => roleRegistry),
107
+ onLifecycleEvent: vi.fn(() => vi.fn()),
108
+ continueAgent: vi.fn().mockResolvedValue({ id: "continued_0" }),
109
+ close: vi.fn().mockResolvedValue(undefined),
110
+ getOrCreateHeadManager: vi.fn(),
111
+ prompt: vi.fn(),
112
+ isPrompting: vi.fn().mockReturnValue(false),
113
+ } as unknown as AgentManager;
114
+ }
115
+
116
+ // =============================================================================
117
+ // Tests: Team Loading
118
+ // =============================================================================
119
+
120
+ describe("Team Template Loading", () => {
121
+ let roleRegistry: DefaultRoleRegistry;
122
+
123
+ beforeEach(() => {
124
+ roleRegistry = new DefaultRoleRegistry();
125
+ });
126
+
127
+ it("loads self-driving team template", async () => {
128
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
129
+
130
+ expect(manifest.name).toBe("self-driving");
131
+ expect(manifest.version).toBe(1);
132
+ expect(manifest.roles).toEqual(["planner", "grinder", "judge"]);
133
+ });
134
+
135
+ it("resolves self-driving roles with correct base roles", async () => {
136
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
137
+
138
+ const planner = manifest._resolvedRoles.get("planner");
139
+ expect(planner).toBeDefined();
140
+ expect(planner!.baseRole).toBe("coordinator");
141
+
142
+ const grinder = manifest._resolvedRoles.get("grinder");
143
+ expect(grinder).toBeDefined();
144
+ expect(grinder!.baseRole).toBe("worker");
145
+
146
+ const judge = manifest._resolvedRoles.get("judge");
147
+ expect(judge).toBeDefined();
148
+ expect(judge!.baseRole).toBe("monitor");
149
+ });
150
+
151
+ it("resolves capability additions and removals", async () => {
152
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
153
+
154
+ const planner = manifest._resolvedRoles.get("planner");
155
+ expect(planner!.capabilities).toContain("task.claim");
156
+ expect(planner!.capabilities).not.toContain("agent.spawn.integrator");
157
+ expect(planner!.capabilities).not.toContain("agent.spawn.monitor");
158
+
159
+ const grinder = manifest._resolvedRoles.get("grinder");
160
+ expect(grinder!.capabilities).toContain("task.claim");
161
+ expect(grinder!.capabilities).toContain("git.push");
162
+ });
163
+
164
+ it("translates spawn_rules into capabilities", async () => {
165
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
166
+
167
+ const planner = manifest._resolvedRoles.get("planner");
168
+ expect(planner!.capabilities).toContain("agent.spawn.grinder");
169
+ expect(planner!.capabilities).toContain("agent.spawn.planner");
170
+
171
+ // Judge and grinder have no spawn rules → no spawn capabilities
172
+ const judge = manifest._resolvedRoles.get("judge");
173
+ expect(judge!.capabilities.filter((c) => c.startsWith("agent.spawn."))).toEqual([]);
174
+ });
175
+
176
+ it("loads prompt files", async () => {
177
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
178
+
179
+ expect(manifest._loadedPrompts.has("prompts/planner.md")).toBe(true);
180
+ expect(manifest._loadedPrompts.has("prompts/grinder.md")).toBe(true);
181
+ expect(manifest._loadedPrompts.has("prompts/judge.md")).toBe(true);
182
+
183
+ const plannerPrompt = manifest._loadedPrompts.get("prompts/planner.md")!;
184
+ expect(plannerPrompt).toContain("Planner");
185
+ });
186
+
187
+ it("parses macro_agent extensions", async () => {
188
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
189
+
190
+ expect(manifest.macro_agent.task_assignment?.mode).toBe("pull");
191
+ expect(manifest.macro_agent.integration?.strategy).toBe("trunk");
192
+ expect(manifest.macro_agent.lifecycle?.continuations?.enabled).toBe(true);
193
+ expect(manifest.macro_agent.lifecycle?.scaling?.max_workers).toBe(20);
194
+ });
195
+
196
+ it("validates communication topology", async () => {
197
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
198
+
199
+ expect(manifest.communication.channels).toBeDefined();
200
+ expect(Object.keys(manifest.communication.channels!)).toContain("task_updates");
201
+ expect(Object.keys(manifest.communication.channels!)).toContain("work_coordination");
202
+ expect(Object.keys(manifest.communication.channels!)).toContain("health");
203
+
204
+ expect(manifest.communication.subscriptions?.planner).toBeDefined();
205
+ expect(manifest.communication.emissions?.planner).toContain("TASK_CREATED");
206
+ });
207
+
208
+ it("loads structured team template", async () => {
209
+ const manifest = await loadTeam("structured", roleRegistry, PROJECT_ROOT);
210
+
211
+ expect(manifest.name).toBe("structured");
212
+ expect(manifest.roles).toEqual(["lead", "developer", "reviewer"]);
213
+ expect(manifest.macro_agent.task_assignment?.mode).toBe("push");
214
+ expect(manifest.macro_agent.integration?.strategy).toBe("queue");
215
+ });
216
+
217
+ it("resolves structured roles", async () => {
218
+ const manifest = await loadTeam("structured", roleRegistry, PROJECT_ROOT);
219
+
220
+ const lead = manifest._resolvedRoles.get("lead");
221
+ expect(lead!.baseRole).toBe("coordinator");
222
+
223
+ const developer = manifest._resolvedRoles.get("developer");
224
+ expect(developer!.baseRole).toBe("worker");
225
+
226
+ const reviewer = manifest._resolvedRoles.get("reviewer");
227
+ expect(reviewer!.baseRole).toBe("monitor");
228
+ expect(reviewer!.capabilities).toContain("exec.build");
229
+ expect(reviewer!.capabilities).toContain("exec.test");
230
+ });
231
+ });
232
+
233
+ // =============================================================================
234
+ // Tests: TeamRuntime
235
+ // =============================================================================
236
+
237
+ describe("TeamRuntime", () => {
238
+ let roleRegistry: DefaultRoleRegistry;
239
+ let agentManager: AgentManager;
240
+ let messageRouter: MessageRouter;
241
+ let eventStore: EventStore & { _events: Event[] };
242
+ let services: TeamServices;
243
+
244
+ beforeEach(() => {
245
+ roleRegistry = new DefaultRoleRegistry();
246
+ eventStore = createMockEventStore() as EventStore & { _events: Event[] };
247
+ messageRouter = createMockMessageRouter();
248
+ agentManager = createMockAgentManager(roleRegistry);
249
+ services = { agentManager, messageRouter, eventStore };
250
+ });
251
+
252
+ describe("initialize()", () => {
253
+ it("registers team roles in the role registry", async () => {
254
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
255
+ const runtime = new TeamRuntime(manifest, services);
256
+
257
+ await runtime.initialize();
258
+
259
+ // Roles should be registered in the registry
260
+ expect(roleRegistry.getRole("planner")).toBeDefined();
261
+ expect(roleRegistry.getRole("grinder")).toBeDefined();
262
+ expect(roleRegistry.getRole("judge")).toBeDefined();
263
+ });
264
+
265
+ it("emits team_config event to EventStore", async () => {
266
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
267
+ const runtime = new TeamRuntime(manifest, services);
268
+
269
+ await runtime.initialize();
270
+
271
+ expect(eventStore.emit).toHaveBeenCalledWith(
272
+ expect.objectContaining({
273
+ type: "status",
274
+ payload: expect.objectContaining({
275
+ status_type: "discovery",
276
+ team_config: expect.objectContaining({
277
+ teamName: "self-driving",
278
+ strategy: "trunk",
279
+ taskMode: "pull",
280
+ }),
281
+ }),
282
+ })
283
+ );
284
+ });
285
+
286
+ it("sets spawn interceptor on agent manager", async () => {
287
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
288
+ const runtime = new TeamRuntime(manifest, services);
289
+
290
+ await runtime.initialize();
291
+
292
+ expect(agentManager.setSpawnInterceptor).toHaveBeenCalledWith(
293
+ expect.any(Function)
294
+ );
295
+ });
296
+ });
297
+
298
+ describe("bootstrap()", () => {
299
+ it("spawns root and companion agents", async () => {
300
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
301
+ const runtime = new TeamRuntime(manifest, services);
302
+
303
+ await runtime.initialize();
304
+ const result = await runtime.bootstrap();
305
+
306
+ expect(result.rootId).toBeDefined();
307
+ expect(result.companionIds).toHaveLength(1); // judge is the companion
308
+
309
+ // Two spawn calls: planner (root) + judge (companion)
310
+ expect(agentManager.spawn).toHaveBeenCalledTimes(2);
311
+ });
312
+
313
+ it("spawns root with correct role and model", async () => {
314
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
315
+ const runtime = new TeamRuntime(manifest, services);
316
+
317
+ await runtime.initialize();
318
+ await runtime.bootstrap();
319
+
320
+ const spawnCalls = vi.mocked(agentManager.spawn).mock.calls;
321
+ const rootCall = spawnCalls[0][0];
322
+
323
+ expect(rootCall.role).toBe("planner");
324
+ expect(rootCall.config?.model).toBe("sonnet");
325
+ expect(rootCall.parent).toBeNull();
326
+ });
327
+
328
+ it("spawns companion as peer (not child)", async () => {
329
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
330
+ const runtime = new TeamRuntime(manifest, services);
331
+
332
+ await runtime.initialize();
333
+ await runtime.bootstrap();
334
+
335
+ const spawnCalls = vi.mocked(agentManager.spawn).mock.calls;
336
+ const companionCall = spawnCalls[1][0];
337
+
338
+ expect(companionCall.role).toBe("judge");
339
+ expect(companionCall.config?.model).toBe("haiku");
340
+ expect(companionCall.parent).toBeNull();
341
+ });
342
+
343
+ it("wires config-driven peer subscriptions from routing.peers", async () => {
344
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
345
+ const runtime = new TeamRuntime(manifest, services);
346
+
347
+ await runtime.initialize();
348
+ const result = await runtime.bootstrap();
349
+
350
+ // self-driving has 2 peer entries: judge→planner + planner→judge, both via: "direct"
351
+ // Each creates one directional subtree subscription
352
+ expect(messageRouter.subscribe).toHaveBeenCalledTimes(2);
353
+
354
+ // judge (agent_1) subscribes to planner's (agent_0) subtree
355
+ expect(messageRouter.subscribe).toHaveBeenCalledWith(
356
+ result.companionIds[0], // judge = agent_1
357
+ { type: "subtree", target: result.rootId } // planner = agent_0
358
+ );
359
+
360
+ // planner (agent_0) subscribes to judge's (agent_1) subtree
361
+ expect(messageRouter.subscribe).toHaveBeenCalledWith(
362
+ result.rootId, // planner = agent_0
363
+ { type: "subtree", target: result.companionIds[0] } // judge = agent_1
364
+ );
365
+ });
366
+
367
+ it("injects interaction patterns for pull mode", async () => {
368
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
369
+ const runtime = new TeamRuntime(manifest, services);
370
+
371
+ await runtime.initialize();
372
+ await runtime.bootstrap();
373
+
374
+ const spawnCalls = vi.mocked(agentManager.spawn).mock.calls;
375
+ const rootCall = spawnCalls[0][0];
376
+
377
+ expect(rootCall.interactionPatterns).toBeDefined();
378
+ expect(rootCall.interactionPatterns!.length).toBeGreaterThan(0);
379
+ expect(rootCall.interactionPatterns!.some((p) => p.includes("PULL mode"))).toBe(true);
380
+ expect(rootCall.interactionPatterns!.some((p) => p.includes("trunk"))).toBe(true);
381
+ });
382
+
383
+ it("provides team prompts to spawned agents", async () => {
384
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
385
+ const runtime = new TeamRuntime(manifest, services);
386
+
387
+ await runtime.initialize();
388
+ await runtime.bootstrap();
389
+
390
+ const spawnCalls = vi.mocked(agentManager.spawn).mock.calls;
391
+ const rootCall = spawnCalls[0][0];
392
+
393
+ expect(rootCall.customPrompt).toBeDefined();
394
+ expect(rootCall.customPrompt).toContain("Planner");
395
+ });
396
+ });
397
+
398
+ describe("spawn interceptor", () => {
399
+ it("injects team topics into spawned agent options", async () => {
400
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
401
+ const runtime = new TeamRuntime(manifest, services);
402
+
403
+ await runtime.initialize();
404
+ await runtime.bootstrap();
405
+
406
+ // Now spawn a grinder through the interceptor
407
+ await agentManager.spawn({
408
+ task: "test grinder task",
409
+ role: "grinder",
410
+ parent: "agent_0",
411
+ });
412
+
413
+ // Check the intercepted options (not the original args)
414
+ const lastOpts = interceptedSpawnOptions.at(-1)!;
415
+ // Interceptor should have added topics for grinder subscriptions
416
+ expect(lastOpts.topics).toBeDefined();
417
+ expect(lastOpts.topics).toContain("work_coordination");
418
+ });
419
+
420
+ it("injects team environment variables", async () => {
421
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
422
+ const runtime = new TeamRuntime(manifest, services);
423
+
424
+ await runtime.initialize();
425
+ await runtime.bootstrap();
426
+
427
+ // Spawn a grinder
428
+ await agentManager.spawn({
429
+ task: "test task",
430
+ role: "grinder",
431
+ parent: "agent_0",
432
+ });
433
+
434
+ const lastOpts = interceptedSpawnOptions.at(-1)!;
435
+ expect(lastOpts.config?.env?.MACRO_TEAM_NAME).toBe("self-driving");
436
+ expect(lastOpts.config?.env?.MACRO_TASK_MODE).toBe("pull");
437
+ expect(lastOpts.config?.env?.MACRO_INTEGRATION_STRATEGY).toBe("trunk");
438
+ });
439
+
440
+ it("does not override caller-provided options", async () => {
441
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
442
+ const runtime = new TeamRuntime(manifest, services);
443
+
444
+ await runtime.initialize();
445
+ await runtime.bootstrap();
446
+
447
+ const customPrompt = "My custom prompt";
448
+ await agentManager.spawn({
449
+ task: "test task",
450
+ role: "grinder",
451
+ parent: "agent_0",
452
+ customPrompt,
453
+ });
454
+
455
+ const lastOpts = interceptedSpawnOptions.at(-1)!;
456
+ expect(lastOpts.customPrompt).toBe(customPrompt);
457
+ });
458
+ });
459
+
460
+ describe("getters", () => {
461
+ it("returns task mode and strategy", async () => {
462
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
463
+ const runtime = new TeamRuntime(manifest, services);
464
+
465
+ expect(runtime.getTaskMode()).toBe("pull");
466
+ expect(runtime.getStrategyName()).toBe("trunk");
467
+ });
468
+
469
+ it("returns agent IDs after bootstrap", async () => {
470
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
471
+ const runtime = new TeamRuntime(manifest, services);
472
+
473
+ await runtime.initialize();
474
+ const result = await runtime.bootstrap();
475
+
476
+ expect(runtime.getRootAgentId()).toBe(result.rootId);
477
+ expect(runtime.getCompanionAgentIds()).toEqual(result.companionIds);
478
+ });
479
+ });
480
+
481
+ describe("teardown()", () => {
482
+ it("clears spawn interceptor", async () => {
483
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
484
+ const runtime = new TeamRuntime(manifest, services);
485
+
486
+ await runtime.initialize();
487
+ await runtime.bootstrap();
488
+ await runtime.teardown();
489
+
490
+ expect(agentManager.setSpawnInterceptor).toHaveBeenLastCalledWith(null);
491
+ });
492
+ });
493
+
494
+ describe("peer routing", () => {
495
+ it("stores signal filters from peer connections", async () => {
496
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
497
+ const runtime = new TeamRuntime(manifest, services);
498
+
499
+ await runtime.initialize();
500
+ const result = await runtime.bootstrap();
501
+
502
+ const filters = runtime.getPeerSignalFilters();
503
+
504
+ // judge→planner has signals: [FIXUP_CREATED, GREEN_SNAPSHOT]
505
+ const judgeToPlanner = filters.get(`${result.companionIds[0]}→${result.rootId}`);
506
+ expect(judgeToPlanner).toEqual(["FIXUP_CREATED", "GREEN_SNAPSHOT"]);
507
+
508
+ // planner→judge has signals: [CONVERGENCE_CHECK]
509
+ const plannerToJudge = filters.get(`${result.rootId}→${result.companionIds[0]}`);
510
+ expect(plannerToJudge).toEqual(["CONVERGENCE_CHECK"]);
511
+ });
512
+
513
+ it("falls back to legacy subtree subscriptions when no peers config", async () => {
514
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
515
+
516
+ // Remove routing.peers to test fallback
517
+ manifest.communication.routing = { status: "upstream" };
518
+
519
+ const runtime = new TeamRuntime(manifest, services);
520
+
521
+ await runtime.initialize();
522
+ const result = await runtime.bootstrap();
523
+
524
+ // Legacy: 2 bidirectional subtree subs (root→companion + companion→root)
525
+ expect(messageRouter.subscribe).toHaveBeenCalledTimes(2);
526
+ expect(messageRouter.subscribe).toHaveBeenCalledWith(
527
+ result.rootId,
528
+ { type: "subtree", target: result.companionIds[0] }
529
+ );
530
+ expect(messageRouter.subscribe).toHaveBeenCalledWith(
531
+ result.companionIds[0],
532
+ { type: "subtree", target: result.rootId }
533
+ );
534
+ });
535
+
536
+ it("defers wiring for roles not spawned at bootstrap", async () => {
537
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
538
+
539
+ // Add a peer connection involving grinder (not spawned at bootstrap)
540
+ manifest.communication.routing!.peers!.push({
541
+ from: "grinder",
542
+ to: "planner",
543
+ via: "direct",
544
+ signals: ["WORKER_DONE"],
545
+ });
546
+
547
+ const runtime = new TeamRuntime(manifest, services);
548
+
549
+ await runtime.initialize();
550
+ const result = await runtime.bootstrap();
551
+
552
+ // 2 wired at bootstrap (judge↔planner) + 1 deferred (grinder→planner)
553
+ expect(messageRouter.subscribe).toHaveBeenCalledTimes(2);
554
+
555
+ // onLifecycleEvent should have been called twice: once for deferred wiring, once for continuations
556
+ expect(agentManager.onLifecycleEvent).toHaveBeenCalledTimes(2);
557
+
558
+ // Simulate grinder spawn via lifecycle event
559
+ const lifecycleCallbacks = vi.mocked(agentManager.onLifecycleEvent).mock.calls;
560
+ // The deferred wiring callback is the first one registered (wirePeerRoutes before monitorContinuations)
561
+ const deferredCallback = lifecycleCallbacks[0][0];
562
+
563
+ deferredCallback({
564
+ type: "spawned",
565
+ agent: { id: "grinder_agent", role: "grinder", state: "running" },
566
+ } as any);
567
+
568
+ // Now the deferred route should be wired
569
+ expect(messageRouter.subscribe).toHaveBeenCalledTimes(3);
570
+ expect(messageRouter.subscribe).toHaveBeenCalledWith(
571
+ "grinder_agent",
572
+ { type: "subtree", target: result.rootId }
573
+ );
574
+
575
+ // Signal filter should be stored
576
+ const filters = runtime.getPeerSignalFilters();
577
+ expect(filters.get(`grinder_agent→${result.rootId}`)).toEqual(["WORKER_DONE"]);
578
+ });
579
+
580
+ it("serializes peerRoutes in team_config event", async () => {
581
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
582
+ const runtime = new TeamRuntime(manifest, services);
583
+
584
+ await runtime.initialize();
585
+
586
+ expect(eventStore.emit).toHaveBeenCalledWith(
587
+ expect.objectContaining({
588
+ type: "status",
589
+ payload: expect.objectContaining({
590
+ team_config: expect.objectContaining({
591
+ peerRoutes: expect.arrayContaining([
592
+ expect.objectContaining({
593
+ from: "judge",
594
+ to: "planner",
595
+ via: "direct",
596
+ signals: ["FIXUP_CREATED", "GREEN_SNAPSHOT"],
597
+ }),
598
+ ]),
599
+ }),
600
+ }),
601
+ })
602
+ );
603
+ });
604
+
605
+ it("teardown cleans up deferred wiring listener", async () => {
606
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
607
+
608
+ // Add a deferred route to ensure the wiring listener is set up
609
+ manifest.communication.routing!.peers!.push({
610
+ from: "grinder",
611
+ to: "judge",
612
+ via: "direct",
613
+ });
614
+
615
+ const runtime = new TeamRuntime(manifest, services);
616
+
617
+ await runtime.initialize();
618
+ await runtime.bootstrap();
619
+
620
+ // onLifecycleEvent called twice: deferred wiring + continuations
621
+ expect(agentManager.onLifecycleEvent).toHaveBeenCalledTimes(2);
622
+
623
+ // Both return unsubscribe fns (index 0 = deferred wiring, index 1 = continuations)
624
+ const peerWiringUnsub = vi.mocked(agentManager.onLifecycleEvent).mock.results[0].value;
625
+ const continuationsUnsub = vi.mocked(agentManager.onLifecycleEvent).mock.results[1].value;
626
+
627
+ await runtime.teardown();
628
+
629
+ // Both unsubscribe fns should be called
630
+ expect(peerWiringUnsub).toHaveBeenCalled();
631
+ expect(continuationsUnsub).toHaveBeenCalled();
632
+ });
633
+
634
+ it("via topic creates shared topic subscription", async () => {
635
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
636
+
637
+ // Replace peers with a topic-based connection
638
+ manifest.communication.routing!.peers = [
639
+ { from: "planner", to: "judge", via: "topic" },
640
+ ];
641
+
642
+ const runtime = new TeamRuntime(manifest, services);
643
+
644
+ await runtime.initialize();
645
+ const result = await runtime.bootstrap();
646
+
647
+ // Topic creates 2 subscriptions: both agents to the same topic
648
+ expect(messageRouter.subscribe).toHaveBeenCalledTimes(2);
649
+ expect(messageRouter.subscribe).toHaveBeenCalledWith(
650
+ result.rootId, // planner
651
+ { type: "topic", target: "peer:planner:judge" }
652
+ );
653
+ expect(messageRouter.subscribe).toHaveBeenCalledWith(
654
+ result.companionIds[0], // judge
655
+ { type: "topic", target: "peer:planner:judge" }
656
+ );
657
+ });
658
+
659
+ it("via scope creates role subscription", async () => {
660
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
661
+
662
+ // Replace peers with a scope-based connection
663
+ manifest.communication.routing!.peers = [
664
+ { from: "planner", to: "judge", via: "scope" },
665
+ ];
666
+
667
+ const runtime = new TeamRuntime(manifest, services);
668
+
669
+ await runtime.initialize();
670
+ const result = await runtime.bootstrap();
671
+
672
+ // Scope creates 1 subscription: from subscribes to to's role channel
673
+ expect(messageRouter.subscribe).toHaveBeenCalledTimes(1);
674
+ expect(messageRouter.subscribe).toHaveBeenCalledWith(
675
+ result.rootId, // planner
676
+ { type: "role", target: "judge" }
677
+ );
678
+ });
679
+ });
680
+
681
+ describe("signal filtering", () => {
682
+ it("installs signal filter on message router after bootstrap", async () => {
683
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
684
+ const runtime = new TeamRuntime(manifest, services);
685
+
686
+ await runtime.initialize();
687
+ await runtime.bootstrap();
688
+
689
+ expect(messageRouter.setSignalFilter).toHaveBeenCalledTimes(1);
690
+ expect(messageRouter.setSignalFilter).toHaveBeenCalledWith(expect.any(Function));
691
+ });
692
+
693
+ it("peer connection filter allows matching signals", async () => {
694
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
695
+ const runtime = new TeamRuntime(manifest, services);
696
+
697
+ await runtime.initialize();
698
+ const result = await runtime.bootstrap();
699
+
700
+ // Extract the installed filter
701
+ const filterFn = vi.mocked(messageRouter.setSignalFilter).mock.calls[0][0] as (
702
+ from: string, to: string, signal: string | undefined
703
+ ) => boolean;
704
+
705
+ const judgeId = result.companionIds[0]; // judge
706
+ const plannerId = result.rootId; // planner
707
+
708
+ // judge→planner peer has signals: [FIXUP_CREATED, GREEN_SNAPSHOT]
709
+ expect(filterFn(judgeId, plannerId, "FIXUP_CREATED")).toBe(true);
710
+ expect(filterFn(judgeId, plannerId, "GREEN_SNAPSHOT")).toBe(true);
711
+ });
712
+
713
+ it("peer connection filter blocks non-matching signals", async () => {
714
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
715
+ const runtime = new TeamRuntime(manifest, services);
716
+
717
+ await runtime.initialize();
718
+ const result = await runtime.bootstrap();
719
+
720
+ const filterFn = vi.mocked(messageRouter.setSignalFilter).mock.calls[0][0] as (
721
+ from: string, to: string, signal: string | undefined
722
+ ) => boolean;
723
+
724
+ const judgeId = result.companionIds[0];
725
+ const plannerId = result.rootId;
726
+
727
+ // judge→planner peer does NOT include WORKER_DONE
728
+ expect(filterFn(judgeId, plannerId, "WORKER_DONE")).toBe(false);
729
+ });
730
+
731
+ it("untagged status events always pass through", async () => {
732
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
733
+ const runtime = new TeamRuntime(manifest, services);
734
+
735
+ await runtime.initialize();
736
+ const result = await runtime.bootstrap();
737
+
738
+ const filterFn = vi.mocked(messageRouter.setSignalFilter).mock.calls[0][0] as (
739
+ from: string, to: string, signal: string | undefined
740
+ ) => boolean;
741
+
742
+ const judgeId = result.companionIds[0];
743
+ const plannerId = result.rootId;
744
+
745
+ // No signal (undefined) should always pass
746
+ expect(filterFn(judgeId, plannerId, undefined)).toBe(true);
747
+ });
748
+
749
+ it("channel subscription filter allows role's configured signals", async () => {
750
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
751
+ const runtime = new TeamRuntime(manifest, services);
752
+
753
+ await runtime.initialize();
754
+ const result = await runtime.bootstrap();
755
+
756
+ const filterFn = vi.mocked(messageRouter.setSignalFilter).mock.calls[0][0] as (
757
+ from: string, to: string, signal: string | undefined
758
+ ) => boolean;
759
+
760
+ // Use a "grinder" agent as recipient - grinder only allows WORK_ASSIGNED
761
+ // Simulate spawning a grinder by triggering deferred wiring
762
+ // But grinder has no peer connection, so we test channel sub filter directly
763
+ // by spawning through the lifecycle event to populate agentRoleMap
764
+
765
+ // Add a grinder peer route so deferred wiring populates agentRoleMap
766
+ manifest.communication.routing!.peers!.push({
767
+ from: "grinder",
768
+ to: "planner",
769
+ via: "direct",
770
+ });
771
+
772
+ // Re-bootstrap with updated manifest
773
+ const runtime2 = new TeamRuntime(manifest, services);
774
+ await runtime2.initialize();
775
+ const result2 = await runtime2.bootstrap();
776
+
777
+ // Simulate grinder spawn via lifecycle event
778
+ const deferredCallback = vi.mocked(agentManager.onLifecycleEvent).mock.calls.at(-2)![0];
779
+ deferredCallback({
780
+ type: "spawned",
781
+ agent: { id: "grinder_1", role: "grinder", state: "running" },
782
+ } as any);
783
+
784
+ // Get the latest filter (from runtime2's installSignalFilter)
785
+ const filterFn2 = vi.mocked(messageRouter.setSignalFilter).mock.calls.at(-1)![0] as (
786
+ from: string, to: string, signal: string | undefined
787
+ ) => boolean;
788
+
789
+ // grinder allows WORK_ASSIGNED from channel subs
790
+ // But grinder→planner is a peer route (no signal filter), so test from a non-peer source
791
+ // From planner to grinder_1 (no peer filter exists for this direction)
792
+ expect(filterFn2(result2.rootId, "grinder_1", "WORK_ASSIGNED")).toBe(true);
793
+ expect(filterFn2(result2.rootId, "grinder_1", "WORKER_DONE")).toBe(false);
794
+ });
795
+
796
+ it("roles with any unfiltered subscription receive all signals", async () => {
797
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
798
+ const runtime = new TeamRuntime(manifest, services);
799
+
800
+ await runtime.initialize();
801
+ const result = await runtime.bootstrap();
802
+
803
+ const filterFn = vi.mocked(messageRouter.setSignalFilter).mock.calls[0][0] as (
804
+ from: string, to: string, signal: string | undefined
805
+ ) => boolean;
806
+
807
+ // planner has task_updates subscription with no signals filter → receives all
808
+ // But planner's peer connections have explicit filters, so test from a non-peer agent
809
+ // From an unknown agent to planner — falls through to channel sub filter
810
+ expect(filterFn("unknown_agent", result.rootId, "ANY_SIGNAL")).toBe(true);
811
+ expect(filterFn("unknown_agent", result.rootId, "RANDOM")).toBe(true);
812
+ });
813
+
814
+ it("peer filter takes precedence over channel subscription filter", async () => {
815
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
816
+ const runtime = new TeamRuntime(manifest, services);
817
+
818
+ await runtime.initialize();
819
+ const result = await runtime.bootstrap();
820
+
821
+ const filterFn = vi.mocked(messageRouter.setSignalFilter).mock.calls[0][0] as (
822
+ from: string, to: string, signal: string | undefined
823
+ ) => boolean;
824
+
825
+ const judgeId = result.companionIds[0];
826
+ const plannerId = result.rootId;
827
+
828
+ // judge→planner peer only allows FIXUP_CREATED, GREEN_SNAPSHOT
829
+ // Even though planner's channel subs allow "all" (via unfiltered task_updates),
830
+ // the peer filter takes precedence
831
+ expect(filterFn(judgeId, plannerId, "TASK_CREATED")).toBe(false);
832
+ });
833
+ });
834
+
835
+ describe("emission validation", () => {
836
+ it("installs emission validator on message router after bootstrap", async () => {
837
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
838
+ const runtime = new TeamRuntime(manifest, services);
839
+
840
+ await runtime.initialize();
841
+ await runtime.bootstrap();
842
+
843
+ expect(messageRouter.setEmissionValidator).toHaveBeenCalledTimes(1);
844
+ expect(messageRouter.setEmissionValidator).toHaveBeenCalledWith(expect.any(Function));
845
+ });
846
+
847
+ it("allows emissions in role's allowed list", async () => {
848
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
849
+ const runtime = new TeamRuntime(manifest, services);
850
+
851
+ await runtime.initialize();
852
+ const result = await runtime.bootstrap();
853
+
854
+ const validatorFn = vi.mocked(messageRouter.setEmissionValidator).mock.calls[0][0] as (
855
+ agentId: string, signal: string | undefined
856
+ ) => { action: string; message?: string };
857
+
858
+ const plannerId = result.rootId;
859
+
860
+ // planner's emissions: [TASK_CREATED, WORK_ASSIGNED]
861
+ expect(validatorFn(plannerId, "TASK_CREATED").action).toBe("allow");
862
+ expect(validatorFn(plannerId, "WORK_ASSIGNED").action).toBe("allow");
863
+ });
864
+
865
+ it("rejects disallowed emissions in strict mode", async () => {
866
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
867
+ manifest.communication.enforcement = "strict";
868
+ const runtime = new TeamRuntime(manifest, services);
869
+
870
+ await runtime.initialize();
871
+ const result = await runtime.bootstrap();
872
+
873
+ const validatorFn = vi.mocked(messageRouter.setEmissionValidator).mock.calls[0][0] as (
874
+ agentId: string, signal: string | undefined
875
+ ) => { action: string; message?: string };
876
+
877
+ const plannerId = result.rootId;
878
+
879
+ // WORKER_DONE is not in planner's allowed emissions
880
+ const res = validatorFn(plannerId, "WORKER_DONE");
881
+ expect(res.action).toBe("reject");
882
+ expect(res.message).toContain("WORKER_DONE");
883
+ expect(res.message).toContain("planner");
884
+ });
885
+
886
+ it("warns on disallowed emissions in permissive mode", async () => {
887
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
888
+ manifest.communication.enforcement = "permissive";
889
+ const runtime = new TeamRuntime(manifest, services);
890
+
891
+ await runtime.initialize();
892
+ const result = await runtime.bootstrap();
893
+
894
+ const validatorFn = vi.mocked(messageRouter.setEmissionValidator).mock.calls[0][0] as (
895
+ agentId: string, signal: string | undefined
896
+ ) => { action: string; message?: string };
897
+
898
+ const plannerId = result.rootId;
899
+
900
+ const res = validatorFn(plannerId, "HEALTH_CHECK");
901
+ expect(res.action).toBe("warn");
902
+ expect(res.message).toContain("HEALTH_CHECK");
903
+ });
904
+
905
+ it("audits disallowed emissions in audit mode", async () => {
906
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
907
+ manifest.communication.enforcement = "audit";
908
+ const runtime = new TeamRuntime(manifest, services);
909
+
910
+ await runtime.initialize();
911
+ const result = await runtime.bootstrap();
912
+
913
+ const validatorFn = vi.mocked(messageRouter.setEmissionValidator).mock.calls[0][0] as (
914
+ agentId: string, signal: string | undefined
915
+ ) => { action: string; message?: string };
916
+
917
+ const plannerId = result.rootId;
918
+
919
+ const res = validatorFn(plannerId, "FORBIDDEN");
920
+ expect(res.action).toBe("audit");
921
+ expect(res.message).toContain("FORBIDDEN");
922
+ });
923
+
924
+ it("allows untagged emissions for any role", async () => {
925
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
926
+ manifest.communication.enforcement = "strict";
927
+ const runtime = new TeamRuntime(manifest, services);
928
+
929
+ await runtime.initialize();
930
+ const result = await runtime.bootstrap();
931
+
932
+ const validatorFn = vi.mocked(messageRouter.setEmissionValidator).mock.calls[0][0] as (
933
+ agentId: string, signal: string | undefined
934
+ ) => { action: string; message?: string };
935
+
936
+ // Undefined signal always passes even in strict mode
937
+ expect(validatorFn(result.rootId, undefined).action).toBe("allow");
938
+ expect(validatorFn(result.companionIds[0], undefined).action).toBe("allow");
939
+ });
940
+
941
+ it("allows emissions from agents with no role mapping", async () => {
942
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
943
+ manifest.communication.enforcement = "strict";
944
+ const runtime = new TeamRuntime(manifest, services);
945
+
946
+ await runtime.initialize();
947
+ await runtime.bootstrap();
948
+
949
+ const validatorFn = vi.mocked(messageRouter.setEmissionValidator).mock.calls[0][0] as (
950
+ agentId: string, signal: string | undefined
951
+ ) => { action: string; message?: string };
952
+
953
+ // Unknown agent — no role mapping, so allowed
954
+ expect(validatorFn("unknown_agent", "ANYTHING").action).toBe("allow");
955
+ });
956
+
957
+ it("does not install validator when no emissions config", async () => {
958
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
959
+ manifest.communication.emissions = undefined;
960
+ const runtime = new TeamRuntime(manifest, services);
961
+
962
+ await runtime.initialize();
963
+ await runtime.bootstrap();
964
+
965
+ expect(messageRouter.setEmissionValidator).not.toHaveBeenCalled();
966
+ });
967
+
968
+ it("serializes emissions in team_config event", async () => {
969
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
970
+ const runtime = new TeamRuntime(manifest, services);
971
+
972
+ await runtime.initialize();
973
+
974
+ expect(eventStore.emit).toHaveBeenCalledWith(
975
+ expect.objectContaining({
976
+ type: "status",
977
+ payload: expect.objectContaining({
978
+ team_config: expect.objectContaining({
979
+ emissions: expect.objectContaining({
980
+ planner: ["TASK_CREATED", "WORK_ASSIGNED"],
981
+ judge: ["HEALTH_CHECK", "GREEN_SNAPSHOT", "FIXUP_CREATED"],
982
+ grinder: ["WORKER_DONE"],
983
+ }),
984
+ }),
985
+ }),
986
+ })
987
+ );
988
+ });
989
+ });
990
+
991
+ describe("monitorContinuations()", () => {
992
+ it("auto-continues root agent on unexpected stop", async () => {
993
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
994
+ const runtime = new TeamRuntime(manifest, services);
995
+
996
+ await runtime.initialize();
997
+ const result = await runtime.bootstrap();
998
+
999
+ // Capture the lifecycle callback registered during bootstrap
1000
+ const onLifecycleEventMock = vi.mocked(agentManager.onLifecycleEvent);
1001
+ expect(onLifecycleEventMock).toHaveBeenCalled();
1002
+ const lifecycleCallback = onLifecycleEventMock.mock.calls[0][0];
1003
+
1004
+ // Simulate unexpected stop of root agent (no reason = unexpected)
1005
+ lifecycleCallback({
1006
+ type: "stopped",
1007
+ agent: { id: result.rootId, role: "planner", state: "stopped" },
1008
+ } as any);
1009
+
1010
+ // Wait for the setTimeout (1s) + async continuation
1011
+ await vi.waitFor(
1012
+ () => {
1013
+ expect(agentManager.continueAgent).toHaveBeenCalledWith(result.rootId);
1014
+ },
1015
+ { timeout: 3000 }
1016
+ );
1017
+
1018
+ // Root agent ID should be updated to the continued agent
1019
+ expect(runtime.getRootAgentId()).toBe("continued_0");
1020
+ });
1021
+
1022
+ it("auto-continues companion agent on unexpected stop", async () => {
1023
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
1024
+ const runtime = new TeamRuntime(manifest, services);
1025
+
1026
+ await runtime.initialize();
1027
+ const result = await runtime.bootstrap();
1028
+ const companionId = result.companionIds[0];
1029
+
1030
+ const lifecycleCallback = vi.mocked(agentManager.onLifecycleEvent).mock.calls[0][0];
1031
+
1032
+ // Simulate unexpected stop of companion
1033
+ lifecycleCallback({
1034
+ type: "stopped",
1035
+ agent: { id: companionId, role: "judge", state: "stopped" },
1036
+ } as any);
1037
+
1038
+ await vi.waitFor(
1039
+ () => {
1040
+ expect(agentManager.continueAgent).toHaveBeenCalledWith(companionId);
1041
+ },
1042
+ { timeout: 3000 }
1043
+ );
1044
+
1045
+ // Companion ID should be updated
1046
+ expect(runtime.getCompanionAgentIds()).toContain("continued_0");
1047
+ });
1048
+
1049
+ it("does NOT auto-continue on completed stop", async () => {
1050
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
1051
+ const runtime = new TeamRuntime(manifest, services);
1052
+
1053
+ await runtime.initialize();
1054
+ const result = await runtime.bootstrap();
1055
+
1056
+ const lifecycleCallback = vi.mocked(agentManager.onLifecycleEvent).mock.calls[0][0];
1057
+
1058
+ // Simulate completed stop (should NOT trigger continuation)
1059
+ lifecycleCallback({
1060
+ type: "stopped",
1061
+ agent: { id: result.rootId, role: "planner", state: "stopped" },
1062
+ reason: "completed",
1063
+ } as any);
1064
+
1065
+ // Wait a bit to ensure no continuation is triggered
1066
+ await new Promise((resolve) => setTimeout(resolve, 1500));
1067
+ expect(agentManager.continueAgent).not.toHaveBeenCalled();
1068
+ });
1069
+
1070
+ it("does NOT auto-continue on cancelled stop", async () => {
1071
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
1072
+ const runtime = new TeamRuntime(manifest, services);
1073
+
1074
+ await runtime.initialize();
1075
+ const result = await runtime.bootstrap();
1076
+
1077
+ const lifecycleCallback = vi.mocked(agentManager.onLifecycleEvent).mock.calls[0][0];
1078
+
1079
+ lifecycleCallback({
1080
+ type: "stopped",
1081
+ agent: { id: result.rootId, role: "planner", state: "stopped" },
1082
+ reason: "cancelled",
1083
+ } as any);
1084
+
1085
+ await new Promise((resolve) => setTimeout(resolve, 1500));
1086
+ expect(agentManager.continueAgent).not.toHaveBeenCalled();
1087
+ });
1088
+
1089
+ it("does NOT trigger for non-monitored agents", async () => {
1090
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
1091
+ const runtime = new TeamRuntime(manifest, services);
1092
+
1093
+ await runtime.initialize();
1094
+ await runtime.bootstrap();
1095
+
1096
+ const lifecycleCallback = vi.mocked(agentManager.onLifecycleEvent).mock.calls[0][0];
1097
+
1098
+ // Simulate stop of an unrelated agent
1099
+ lifecycleCallback({
1100
+ type: "stopped",
1101
+ agent: { id: "unrelated_agent", role: "worker", state: "stopped" },
1102
+ } as any);
1103
+
1104
+ await new Promise((resolve) => setTimeout(resolve, 1500));
1105
+ expect(agentManager.continueAgent).not.toHaveBeenCalled();
1106
+ });
1107
+
1108
+ it("unsubscribes lifecycle listener on teardown", async () => {
1109
+ const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
1110
+ const runtime = new TeamRuntime(manifest, services);
1111
+
1112
+ await runtime.initialize();
1113
+ await runtime.bootstrap();
1114
+
1115
+ // onLifecycleEvent returns an unsubscribe function
1116
+ const unsubscribeFn = vi.mocked(agentManager.onLifecycleEvent).mock.results[0].value;
1117
+
1118
+ await runtime.teardown();
1119
+
1120
+ // The unsubscribe function should have been called
1121
+ expect(unsubscribeFn).toHaveBeenCalled();
1122
+ });
1123
+ });
1124
+
1125
+ describe("structured team", () => {
1126
+ it("bootstraps with push mode and queue strategy", async () => {
1127
+ const manifest = await loadTeam("structured", roleRegistry, PROJECT_ROOT);
1128
+ const runtime = new TeamRuntime(manifest, services);
1129
+
1130
+ expect(runtime.getTaskMode()).toBe("push");
1131
+ expect(runtime.getStrategyName()).toBe("queue");
1132
+
1133
+ await runtime.initialize();
1134
+ const result = await runtime.bootstrap();
1135
+
1136
+ // Root (lead) + 1 companion (reviewer)
1137
+ expect(agentManager.spawn).toHaveBeenCalledTimes(2);
1138
+ expect(result.companionIds).toHaveLength(1);
1139
+
1140
+ // Verify no pull-mode interaction patterns injected
1141
+ const rootCall = vi.mocked(agentManager.spawn).mock.calls[0][0];
1142
+ const pullPatterns = rootCall.interactionPatterns?.filter((p) =>
1143
+ p.includes("PULL mode")
1144
+ ) ?? [];
1145
+ expect(pullPatterns).toHaveLength(0);
1146
+ });
1147
+ });
1148
+ });
1149
+
1150
+ // =============================================================================
1151
+ // Tests: Integration Strategies
1152
+ // =============================================================================
1153
+
1154
+ describe("Integration Strategies", () => {
1155
+ it("imports trunk strategy module", async () => {
1156
+ const { TrunkIntegrationStrategy } = await import(
1157
+ "../../workspace/strategies/trunk.js"
1158
+ );
1159
+ const strategy = new TrunkIntegrationStrategy();
1160
+ expect(strategy.name).toBe("trunk");
1161
+ });
1162
+
1163
+ it("imports optimistic strategy module", async () => {
1164
+ const { OptimisticIntegrationStrategy } = await import(
1165
+ "../../workspace/strategies/optimistic.js"
1166
+ );
1167
+ const strategy = new OptimisticIntegrationStrategy();
1168
+ expect(strategy.name).toBe("optimistic");
1169
+ });
1170
+
1171
+ it("imports queue strategy module", async () => {
1172
+ const { QueueIntegrationStrategy } = await import(
1173
+ "../../workspace/strategies/queue.js"
1174
+ );
1175
+ const strategy = new QueueIntegrationStrategy();
1176
+ expect(strategy.name).toBe("queue");
1177
+ });
1178
+
1179
+ it("registry provides all built-in strategies", async () => {
1180
+ const { defaultStrategyRegistry } = await import(
1181
+ "../../workspace/strategies/registry.js"
1182
+ );
1183
+ expect(defaultStrategyRegistry.has("queue")).toBe(true);
1184
+ expect(defaultStrategyRegistry.has("trunk")).toBe(true);
1185
+ expect(defaultStrategyRegistry.has("optimistic")).toBe(true);
1186
+ });
1187
+ });
1188
+
1189
+ // =============================================================================
1190
+ // Tests: Task Pull Model
1191
+ // =============================================================================
1192
+
1193
+ describe("Task Pull Model", () => {
1194
+ it("claim_task tool module exports correctly", async () => {
1195
+ const { CLAIM_TASK_TOOL_INFO, ClaimTaskSchema, createClaimTaskHandler } =
1196
+ await import("../../mcp/tools/claim_task.js");
1197
+
1198
+ expect(CLAIM_TASK_TOOL_INFO.name).toBe("claim_task");
1199
+ expect(createClaimTaskHandler).toBeInstanceOf(Function);
1200
+ expect(ClaimTaskSchema).toBeDefined();
1201
+ });
1202
+
1203
+ it("unclaim_task tool module exports correctly", async () => {
1204
+ const { UNCLAIM_TASK_TOOL_INFO, createUnclaimTaskHandler } = await import(
1205
+ "../../mcp/tools/unclaim_task.js"
1206
+ );
1207
+
1208
+ expect(UNCLAIM_TASK_TOOL_INFO.name).toBe("unclaim_task");
1209
+ expect(createUnclaimTaskHandler).toBeInstanceOf(Function);
1210
+ });
1211
+
1212
+ it("list_claimable_tasks tool module exports correctly", async () => {
1213
+ const { LIST_CLAIMABLE_TASKS_TOOL_INFO, createListClaimableTasksHandler } =
1214
+ await import("../../mcp/tools/list_claimable_tasks.js");
1215
+
1216
+ expect(LIST_CLAIMABLE_TASKS_TOOL_INFO.name).toBe("list_claimable_tasks");
1217
+ expect(createListClaimableTasksHandler).toBeInstanceOf(Function);
1218
+ });
1219
+
1220
+ it("task.claim capability is registered", async () => {
1221
+ const { TASK_CAPABILITIES, ALL_CAPABILITIES } = await import(
1222
+ "../../roles/capabilities.js"
1223
+ );
1224
+
1225
+ expect(TASK_CAPABILITIES.CLAIM).toBe("task.claim");
1226
+ expect(ALL_CAPABILITIES.has("task.claim")).toBe(true);
1227
+ });
1228
+ });
1229
+
1230
+ // =============================================================================
1231
+ // Tests: Metrics Module
1232
+ // =============================================================================
1233
+
1234
+ describe("Metrics Module", () => {
1235
+ it("exports all metric functions", async () => {
1236
+ const {
1237
+ getThroughputMetrics,
1238
+ getUtilizationMetrics,
1239
+ getErrorMetrics,
1240
+ } = await import("../../metrics/index.js");
1241
+
1242
+ expect(getThroughputMetrics).toBeInstanceOf(Function);
1243
+ expect(getUtilizationMetrics).toBeInstanceOf(Function);
1244
+ expect(getErrorMetrics).toBeInstanceOf(Function);
1245
+ });
1246
+
1247
+ it("computes throughput metrics from empty store", async () => {
1248
+ const { getThroughputMetrics } = await import("../../metrics/index.js");
1249
+ const store = createMockEventStore();
1250
+
1251
+ const metrics = getThroughputMetrics(store, 60000);
1252
+
1253
+ expect(metrics.tasksCompleted).toBe(0);
1254
+ expect(metrics.tasksFailed).toBe(0);
1255
+ expect(metrics.tasksCreated).toBe(0);
1256
+ expect(metrics.completedPerMinute).toBe(0);
1257
+ expect(metrics.avgCompletionTimeMs).toBeNull();
1258
+ });
1259
+
1260
+ it("computes utilization metrics from empty store", async () => {
1261
+ const { getUtilizationMetrics } = await import("../../metrics/index.js");
1262
+ const store = createMockEventStore();
1263
+
1264
+ const metrics = getUtilizationMetrics(store);
1265
+
1266
+ expect(metrics.activeAgents).toBe(0);
1267
+ expect(metrics.totalSpawned).toBe(0);
1268
+ expect(metrics.totalStopped).toBe(0);
1269
+ });
1270
+
1271
+ it("computes error metrics from empty store", async () => {
1272
+ const { getErrorMetrics } = await import("../../metrics/index.js");
1273
+ const store = createMockEventStore();
1274
+
1275
+ const metrics = getErrorMetrics(store);
1276
+
1277
+ expect(metrics.totalErrors).toBe(0);
1278
+ expect(metrics.recentErrors).toEqual([]);
1279
+ });
1280
+ });