macro-agent 0.1.8 → 0.1.11

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 (306) hide show
  1. package/CLAUDE.md +263 -33
  2. package/README.md +781 -131
  3. package/dist/acp/claude-code-replay.d.ts +11 -0
  4. package/dist/acp/claude-code-replay.d.ts.map +1 -0
  5. package/dist/acp/claude-code-replay.js +190 -0
  6. package/dist/acp/claude-code-replay.js.map +1 -0
  7. package/dist/acp/macro-agent.d.ts.map +1 -1
  8. package/dist/acp/macro-agent.js +192 -7
  9. package/dist/acp/macro-agent.js.map +1 -1
  10. package/dist/acp/types.d.ts +9 -0
  11. package/dist/acp/types.d.ts.map +1 -1
  12. package/dist/acp/types.js.map +1 -1
  13. package/dist/adapters/tasks-adapter.d.ts.map +1 -1
  14. package/dist/adapters/tasks-adapter.js +3 -0
  15. package/dist/adapters/tasks-adapter.js.map +1 -1
  16. package/dist/adapters/types.d.ts +1 -0
  17. package/dist/adapters/types.d.ts.map +1 -1
  18. package/dist/agent/agent-manager-v2.d.ts +21 -0
  19. package/dist/agent/agent-manager-v2.d.ts.map +1 -1
  20. package/dist/agent/agent-manager-v2.js +308 -54
  21. package/dist/agent/agent-manager-v2.js.map +1 -1
  22. package/dist/agent/agent-manager.d.ts +12 -0
  23. package/dist/agent/agent-manager.d.ts.map +1 -1
  24. package/dist/agent/agent-manager.js.map +1 -1
  25. package/dist/agent/agent-store.d.ts +10 -0
  26. package/dist/agent/agent-store.d.ts.map +1 -1
  27. package/dist/agent/agent-store.js +22 -0
  28. package/dist/agent/agent-store.js.map +1 -1
  29. package/dist/agent/types.d.ts +15 -2
  30. package/dist/agent/types.d.ts.map +1 -1
  31. package/dist/agent/types.js.map +1 -1
  32. package/dist/boot-v2.d.ts +129 -1
  33. package/dist/boot-v2.d.ts.map +1 -1
  34. package/dist/boot-v2.js +359 -8
  35. package/dist/boot-v2.js.map +1 -1
  36. package/dist/cli/acp.js +4 -0
  37. package/dist/cli/acp.js.map +1 -1
  38. package/dist/cli/index.js +56 -0
  39. package/dist/cli/index.js.map +1 -1
  40. package/dist/cognitive/macro-agent-backend.d.ts.map +1 -1
  41. package/dist/cognitive/macro-agent-backend.js +40 -22
  42. package/dist/cognitive/macro-agent-backend.js.map +1 -1
  43. package/dist/integrations/skilltree.d.ts.map +1 -1
  44. package/dist/integrations/skilltree.js +1 -0
  45. package/dist/integrations/skilltree.js.map +1 -1
  46. package/dist/lifecycle/cascade.d.ts +25 -2
  47. package/dist/lifecycle/cascade.d.ts.map +1 -1
  48. package/dist/lifecycle/cascade.js +70 -2
  49. package/dist/lifecycle/cascade.js.map +1 -1
  50. package/dist/lifecycle/cleanup.d.ts +33 -2
  51. package/dist/lifecycle/cleanup.d.ts.map +1 -1
  52. package/dist/lifecycle/cleanup.js +28 -6
  53. package/dist/lifecycle/cleanup.js.map +1 -1
  54. package/dist/lifecycle/handlers-v2.d.ts +7 -0
  55. package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
  56. package/dist/lifecycle/handlers-v2.js +28 -2
  57. package/dist/lifecycle/handlers-v2.js.map +1 -1
  58. package/dist/lifecycle/types.d.ts +11 -0
  59. package/dist/lifecycle/types.d.ts.map +1 -1
  60. package/dist/lifecycle/types.js.map +1 -1
  61. package/dist/map/acp-bridge.d.ts +9 -0
  62. package/dist/map/acp-bridge.d.ts.map +1 -1
  63. package/dist/map/acp-bridge.js +15 -2
  64. package/dist/map/acp-bridge.js.map +1 -1
  65. package/dist/map/cascade-action-handler.d.ts +24 -0
  66. package/dist/map/cascade-action-handler.d.ts.map +1 -0
  67. package/dist/map/cascade-action-handler.js +170 -0
  68. package/dist/map/cascade-action-handler.js.map +1 -0
  69. package/dist/map/cascade-bridge.d.ts +44 -0
  70. package/dist/map/cascade-bridge.d.ts.map +1 -0
  71. package/dist/map/cascade-bridge.js +294 -0
  72. package/dist/map/cascade-bridge.js.map +1 -0
  73. package/dist/map/coordination-handler.d.ts.map +1 -1
  74. package/dist/map/coordination-handler.js +12 -1
  75. package/dist/map/coordination-handler.js.map +1 -1
  76. package/dist/map/lifecycle-bridge.d.ts +1 -1
  77. package/dist/map/lifecycle-bridge.d.ts.map +1 -1
  78. package/dist/map/lifecycle-bridge.js +58 -23
  79. package/dist/map/lifecycle-bridge.js.map +1 -1
  80. package/dist/map/server.d.ts.map +1 -1
  81. package/dist/map/server.js +219 -7
  82. package/dist/map/server.js.map +1 -1
  83. package/dist/map/sidecar.d.ts.map +1 -1
  84. package/dist/map/sidecar.js +49 -2
  85. package/dist/map/sidecar.js.map +1 -1
  86. package/dist/map/types.d.ts +22 -0
  87. package/dist/map/types.d.ts.map +1 -1
  88. package/dist/mcp/tools/done-v2.d.ts.map +1 -1
  89. package/dist/mcp/tools/done-v2.js +8 -0
  90. package/dist/mcp/tools/done-v2.js.map +1 -1
  91. package/dist/teams/team-manager-v2.d.ts.map +1 -1
  92. package/dist/teams/team-manager-v2.js +26 -0
  93. package/dist/teams/team-manager-v2.js.map +1 -1
  94. package/dist/teams/team-runtime-v2.d.ts.map +1 -1
  95. package/dist/teams/team-runtime-v2.js +16 -3
  96. package/dist/teams/team-runtime-v2.js.map +1 -1
  97. package/dist/workspace/config.d.ts +10 -10
  98. package/dist/workspace/config.d.ts.map +1 -1
  99. package/dist/workspace/config.js +4 -4
  100. package/dist/workspace/config.js.map +1 -1
  101. package/dist/workspace/git-cascade-adapter.d.ts +510 -0
  102. package/dist/workspace/git-cascade-adapter.d.ts.map +1 -0
  103. package/dist/workspace/git-cascade-adapter.js +934 -0
  104. package/dist/workspace/git-cascade-adapter.js.map +1 -0
  105. package/dist/workspace/index.d.ts +3 -3
  106. package/dist/workspace/index.d.ts.map +1 -1
  107. package/dist/workspace/index.js +4 -4
  108. package/dist/workspace/index.js.map +1 -1
  109. package/dist/workspace/landing/direct-push.d.ts +20 -0
  110. package/dist/workspace/landing/direct-push.d.ts.map +1 -0
  111. package/dist/workspace/landing/direct-push.js +74 -0
  112. package/dist/workspace/landing/direct-push.js.map +1 -0
  113. package/dist/workspace/landing/index.d.ts +29 -0
  114. package/dist/workspace/landing/index.d.ts.map +1 -0
  115. package/dist/workspace/landing/index.js +37 -0
  116. package/dist/workspace/landing/index.js.map +1 -0
  117. package/dist/workspace/landing/merge-to-parent.d.ts +41 -0
  118. package/dist/workspace/landing/merge-to-parent.d.ts.map +1 -0
  119. package/dist/workspace/landing/merge-to-parent.js +186 -0
  120. package/dist/workspace/landing/merge-to-parent.js.map +1 -0
  121. package/dist/workspace/landing/optimistic-push.d.ts +16 -0
  122. package/dist/workspace/landing/optimistic-push.d.ts.map +1 -0
  123. package/dist/workspace/landing/optimistic-push.js +27 -0
  124. package/dist/workspace/landing/optimistic-push.js.map +1 -0
  125. package/dist/workspace/landing/queue-to-branch.d.ts +24 -0
  126. package/dist/workspace/landing/queue-to-branch.d.ts.map +1 -0
  127. package/dist/workspace/landing/queue-to-branch.js +79 -0
  128. package/dist/workspace/landing/queue-to-branch.js.map +1 -0
  129. package/dist/workspace/merge-queue/merge-queue.d.ts +10 -0
  130. package/dist/workspace/merge-queue/merge-queue.d.ts.map +1 -1
  131. package/dist/workspace/merge-queue/merge-queue.js +10 -0
  132. package/dist/workspace/merge-queue/merge-queue.js.map +1 -1
  133. package/dist/workspace/merge-queue/types.d.ts +16 -2
  134. package/dist/workspace/merge-queue/types.d.ts.map +1 -1
  135. package/dist/workspace/merge-queue/types.js +9 -0
  136. package/dist/workspace/merge-queue/types.js.map +1 -1
  137. package/dist/workspace/pool/types.d.ts +1 -0
  138. package/dist/workspace/pool/types.d.ts.map +1 -1
  139. package/dist/workspace/pool/worktree-pool.d.ts.map +1 -1
  140. package/dist/workspace/pool/worktree-pool.js +1 -0
  141. package/dist/workspace/pool/worktree-pool.js.map +1 -1
  142. package/dist/workspace/recovery/abandon.d.ts +15 -0
  143. package/dist/workspace/recovery/abandon.d.ts.map +1 -0
  144. package/dist/workspace/recovery/abandon.js +45 -0
  145. package/dist/workspace/recovery/abandon.js.map +1 -0
  146. package/dist/workspace/recovery/auto-resolve.d.ts +27 -0
  147. package/dist/workspace/recovery/auto-resolve.d.ts.map +1 -0
  148. package/dist/workspace/recovery/auto-resolve.js +99 -0
  149. package/dist/workspace/recovery/auto-resolve.js.map +1 -0
  150. package/dist/workspace/recovery/defer.d.ts +15 -0
  151. package/dist/workspace/recovery/defer.d.ts.map +1 -0
  152. package/dist/workspace/recovery/defer.js +16 -0
  153. package/dist/workspace/recovery/defer.js.map +1 -0
  154. package/dist/workspace/recovery/escalate.d.ts +16 -0
  155. package/dist/workspace/recovery/escalate.d.ts.map +1 -0
  156. package/dist/workspace/recovery/escalate.js +24 -0
  157. package/dist/workspace/recovery/escalate.js.map +1 -0
  158. package/dist/workspace/recovery/index.d.ts +32 -0
  159. package/dist/workspace/recovery/index.d.ts.map +1 -0
  160. package/dist/workspace/recovery/index.js +45 -0
  161. package/dist/workspace/recovery/index.js.map +1 -0
  162. package/dist/workspace/recovery/spawn-resolver.d.ts +45 -0
  163. package/dist/workspace/recovery/spawn-resolver.d.ts.map +1 -0
  164. package/dist/workspace/recovery/spawn-resolver.js +118 -0
  165. package/dist/workspace/recovery/spawn-resolver.js.map +1 -0
  166. package/dist/workspace/recovery/types.d.ts +63 -0
  167. package/dist/workspace/recovery/types.d.ts.map +1 -0
  168. package/dist/workspace/recovery/types.js +12 -0
  169. package/dist/workspace/recovery/types.js.map +1 -0
  170. package/dist/workspace/topology/index.d.ts +9 -0
  171. package/dist/workspace/topology/index.d.ts.map +1 -0
  172. package/dist/workspace/topology/index.js +8 -0
  173. package/dist/workspace/topology/index.js.map +1 -0
  174. package/dist/workspace/topology/no-workspace.d.ts +18 -0
  175. package/dist/workspace/topology/no-workspace.d.ts.map +1 -0
  176. package/dist/workspace/topology/no-workspace.js +25 -0
  177. package/dist/workspace/topology/no-workspace.js.map +1 -0
  178. package/dist/workspace/topology/types.d.ts +97 -0
  179. package/dist/workspace/topology/types.d.ts.map +1 -0
  180. package/dist/workspace/topology/types.js +20 -0
  181. package/dist/workspace/topology/types.js.map +1 -0
  182. package/dist/workspace/topology/yaml-driven.d.ts +69 -0
  183. package/dist/workspace/topology/yaml-driven.d.ts.map +1 -0
  184. package/dist/workspace/topology/yaml-driven.js +273 -0
  185. package/dist/workspace/topology/yaml-driven.js.map +1 -0
  186. package/dist/workspace/types-v3.d.ts +117 -0
  187. package/dist/workspace/types-v3.d.ts.map +1 -0
  188. package/dist/workspace/types-v3.js +20 -0
  189. package/dist/workspace/types-v3.js.map +1 -0
  190. package/dist/workspace/types.d.ts +162 -17
  191. package/dist/workspace/types.d.ts.map +1 -1
  192. package/dist/workspace/workspace-manager.d.ts +101 -13
  193. package/dist/workspace/workspace-manager.d.ts.map +1 -1
  194. package/dist/workspace/workspace-manager.js +416 -13
  195. package/dist/workspace/workspace-manager.js.map +1 -1
  196. package/dist/workspace/yaml-schema.d.ts +254 -0
  197. package/dist/workspace/yaml-schema.d.ts.map +1 -0
  198. package/dist/workspace/yaml-schema.js +170 -0
  199. package/dist/workspace/yaml-schema.js.map +1 -0
  200. package/docs/conflict-recovery.md +472 -0
  201. package/docs/design/task-dispatcher.md +880 -0
  202. package/docs/git-cascade-integration-gaps.md +678 -0
  203. package/docs/workspace-interfaces.md +731 -0
  204. package/docs/workspace-redesign-plan.md +302 -0
  205. package/package.json +6 -5
  206. package/src/__tests__/boot-v2.test.ts +435 -0
  207. package/src/__tests__/e2e/acp-over-map.e2e.test.ts +92 -0
  208. package/src/__tests__/e2e/auto-sync.e2e.test.ts +257 -0
  209. package/src/__tests__/e2e/bootstrap.e2e.test.ts +319 -0
  210. package/src/__tests__/e2e/cascade-rebase.e2e.test.ts +254 -0
  211. package/src/__tests__/e2e/cli-run.e2e.test.ts +167 -0
  212. package/src/__tests__/e2e/dispatch-coordination.e2e.test.ts +495 -0
  213. package/src/__tests__/e2e/dispatch-live.e2e.test.ts +564 -0
  214. package/src/__tests__/e2e/dispatch-opentasks.e2e.test.ts +496 -0
  215. package/src/__tests__/e2e/dispatch-phase2-live.e2e.test.ts +456 -0
  216. package/src/__tests__/e2e/dispatch-phase2.e2e.test.ts +386 -0
  217. package/src/__tests__/e2e/dispatch.e2e.test.ts +376 -0
  218. package/src/__tests__/e2e/self-driving-v3.e2e.test.ts +197 -0
  219. package/src/__tests__/e2e/spawn-resolver.e2e.test.ts +200 -0
  220. package/src/__tests__/e2e/workspace-lifecycle.e2e.test.ts +30 -22
  221. package/src/__tests__/e2e/workspace-v3.e2e.test.ts +413 -0
  222. package/src/acp/__tests__/claude-code-replay.test.ts +225 -0
  223. package/src/acp/__tests__/macro-agent.test.ts +39 -1
  224. package/src/acp/claude-code-replay.ts +208 -0
  225. package/src/acp/macro-agent.ts +203 -10
  226. package/src/acp/types.ts +10 -0
  227. package/src/adapters/__tests__/tasks-adapter.test.ts +1 -0
  228. package/src/adapters/tasks-adapter.ts +3 -0
  229. package/src/adapters/types.ts +1 -0
  230. package/src/agent/__tests__/agent-manager-topology.test.ts +73 -0
  231. package/src/agent/__tests__/agent-manager-v2.test.ts +66 -0
  232. package/src/agent/__tests__/agent-store.test.ts +52 -0
  233. package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
  234. package/src/agent/agent-manager-v2.ts +372 -59
  235. package/src/agent/agent-manager.ts +14 -0
  236. package/src/agent/agent-store.ts +24 -0
  237. package/src/agent/types.ts +16 -2
  238. package/src/boot-v2.ts +589 -35
  239. package/src/cli/acp.ts +4 -0
  240. package/src/cli/index.ts +61 -0
  241. package/src/cognitive/macro-agent-backend.ts +45 -29
  242. package/src/integrations/skilltree.ts +1 -0
  243. package/src/lifecycle/__tests__/cascade-consolidation.test.ts +240 -0
  244. package/src/lifecycle/cascade.ts +77 -2
  245. package/src/lifecycle/cleanup.ts +52 -3
  246. package/src/lifecycle/handlers-v2.ts +40 -3
  247. package/src/lifecycle/types.ts +12 -0
  248. package/src/map/__tests__/cascade-bridge.test.ts +229 -0
  249. package/src/map/__tests__/emit-event.test.ts +71 -0
  250. package/src/map/__tests__/lifecycle-bridge.test.ts +86 -10
  251. package/src/map/acp-bridge.ts +26 -3
  252. package/src/map/cascade-action-handler.ts +205 -0
  253. package/src/map/cascade-bridge.ts +339 -0
  254. package/src/map/coordination-handler.ts +13 -1
  255. package/src/map/lifecycle-bridge.ts +52 -17
  256. package/src/map/server.ts +225 -7
  257. package/src/map/sidecar.ts +48 -1
  258. package/src/map/types.ts +23 -0
  259. package/src/mcp/tools/done-v2.ts +9 -0
  260. package/src/teams/team-manager-v2.ts +37 -0
  261. package/src/teams/team-runtime-v2.ts +23 -3
  262. package/src/workspace/__tests__/{dataplane-adapter.test.ts → git-cascade-adapter.test.ts} +209 -14
  263. package/src/workspace/__tests__/land-dispatch.test.ts +214 -0
  264. package/src/workspace/__tests__/self-driving-yaml.test.ts +114 -0
  265. package/src/workspace/__tests__/shared-worktree-refcount.test.ts +154 -0
  266. package/src/workspace/__tests__/standalone-mode.test.ts +118 -0
  267. package/src/workspace/__tests__/workspace-manager-v3.test.ts +245 -0
  268. package/src/workspace/__tests__/yaml-schema.test.ts +210 -0
  269. package/src/workspace/config.ts +11 -11
  270. package/src/workspace/git-cascade-adapter.ts +1213 -0
  271. package/src/workspace/index.ts +11 -11
  272. package/src/workspace/landing/__tests__/strategies.test.ts +184 -0
  273. package/src/workspace/landing/direct-push.ts +91 -0
  274. package/src/workspace/landing/index.ts +40 -0
  275. package/src/workspace/landing/merge-to-parent.ts +229 -0
  276. package/src/workspace/landing/optimistic-push.ts +36 -0
  277. package/src/workspace/landing/queue-to-branch.ts +108 -0
  278. package/src/workspace/merge-queue/merge-queue.ts +10 -0
  279. package/src/workspace/merge-queue/types.ts +16 -2
  280. package/src/workspace/pool/__tests__/worktree-pool.integration.test.ts +5 -5
  281. package/src/workspace/pool/types.ts +1 -0
  282. package/src/workspace/pool/worktree-pool.ts +1 -0
  283. package/src/workspace/recovery/__tests__/auto-resolve-integration.test.ts +127 -0
  284. package/src/workspace/recovery/__tests__/spawn-resolver.test.ts +139 -0
  285. package/src/workspace/recovery/__tests__/strategies.test.ts +145 -0
  286. package/src/workspace/recovery/abandon.ts +51 -0
  287. package/src/workspace/recovery/auto-resolve.ts +119 -0
  288. package/src/workspace/recovery/defer.ts +23 -0
  289. package/src/workspace/recovery/escalate.ts +30 -0
  290. package/src/workspace/recovery/index.ts +58 -0
  291. package/src/workspace/recovery/spawn-resolver.ts +152 -0
  292. package/src/workspace/recovery/types.ts +54 -0
  293. package/src/workspace/topology/__tests__/yaml-driven.test.ts +345 -0
  294. package/src/workspace/topology/index.ts +18 -0
  295. package/src/workspace/topology/no-workspace.ts +39 -0
  296. package/src/workspace/topology/types.ts +116 -0
  297. package/src/workspace/topology/yaml-driven.ts +316 -0
  298. package/src/workspace/types-v3.ts +162 -0
  299. package/src/workspace/types.ts +211 -20
  300. package/src/workspace/workspace-manager.ts +533 -19
  301. package/src/workspace/yaml-schema.ts +216 -0
  302. package/dist/workspace/dataplane-adapter.d.ts +0 -260
  303. package/dist/workspace/dataplane-adapter.d.ts.map +0 -1
  304. package/dist/workspace/dataplane-adapter.js +0 -416
  305. package/dist/workspace/dataplane-adapter.js.map +0 -1
  306. package/src/workspace/dataplane-adapter.ts +0 -546
@@ -0,0 +1,564 @@
1
+ /**
2
+ * Task Dispatch Live Agent E2E Tests
3
+ *
4
+ * Tests the full dispatch lifecycle with REAL Claude Code agents:
5
+ * - Dispatch strategy spawns real agents for tasks
6
+ * - Agents complete work and call done()
7
+ * - Lifecycle listener detects completion and updates tracker
8
+ * - Reconciliation handles external state changes
9
+ *
10
+ * These tests hit the Claude API and require authenticated Claude Code.
11
+ *
12
+ * REQUIRES: RUN_FULL_AGENT_TESTS=true
13
+ *
14
+ * Run with:
15
+ * RUN_FULL_AGENT_TESTS=true npx vitest run --config vitest.e2e.config.ts src/__tests__/e2e/dispatch-live.e2e.test.ts
16
+ */
17
+
18
+ import {
19
+ describe,
20
+ it,
21
+ expect,
22
+ beforeEach,
23
+ afterEach,
24
+ vi,
25
+ } from "vitest";
26
+ import * as path from "path";
27
+ import * as os from "os";
28
+ import * as fs from "fs";
29
+ import { execSync } from "child_process";
30
+ import { bootV2, type MacroAgentSystemV2 } from "../../boot-v2.js";
31
+ import {
32
+ createTaskDispatcher,
33
+ type TaskDispatcher,
34
+ type DispatchAgentRuntime,
35
+ type DispatchTaskSource,
36
+ } from "swarm-dispatch";
37
+ import type { TaskRecord } from "../../adapters/types.js";
38
+
39
+ // ─────────────────────────────────────────────────────────────────
40
+ // Configuration
41
+ // ─────────────────────────────────────────────────────────────────
42
+
43
+ const RUN_FULL_AGENT = !!process.env.RUN_FULL_AGENT_TESTS;
44
+ const describeFn = RUN_FULL_AGENT ? describe : describe.skip;
45
+
46
+ const TIMEOUT = {
47
+ SPAWN: 60_000,
48
+ DISPATCH: 120_000,
49
+ MULTI: 180_000,
50
+ };
51
+
52
+ // ─────────────────────────────────────────────────────────────────
53
+ // Helpers
54
+ // ─────────────────────────────────────────────────────────────────
55
+
56
+ function createTestRepo(prefix: string): { path: string; cleanup: () => void } {
57
+ const tmpDir = fs.mkdtempSync(
58
+ path.join(os.tmpdir(), `dispatch-live-${prefix}-`)
59
+ );
60
+ const repoPath = path.join(tmpDir, "test-repo");
61
+ fs.mkdirSync(repoPath);
62
+ execSync("git init", { cwd: repoPath, stdio: "pipe" });
63
+ execSync('git config user.email "test@test.com"', {
64
+ cwd: repoPath,
65
+ stdio: "pipe",
66
+ });
67
+ execSync('git config user.name "Test User"', {
68
+ cwd: repoPath,
69
+ stdio: "pipe",
70
+ });
71
+ fs.writeFileSync(path.join(repoPath, "README.md"), "# Test Repo\n");
72
+ execSync("git add -A", { cwd: repoPath, stdio: "pipe" });
73
+ execSync('git commit -m "Initial commit"', { cwd: repoPath, stdio: "pipe" });
74
+
75
+ return {
76
+ path: repoPath,
77
+ cleanup: () => fs.rmSync(tmpDir, { recursive: true, force: true }),
78
+ };
79
+ }
80
+
81
+ function log(msg: string): void {
82
+ console.log(`[DISPATCH-LIVE] ${msg}`);
83
+ }
84
+
85
+ function sleep(ms: number): Promise<void> {
86
+ return new Promise((resolve) => setTimeout(resolve, ms));
87
+ }
88
+
89
+ /**
90
+ * Wait for a condition to become true, polling at intervals.
91
+ */
92
+ async function waitFor(
93
+ condition: () => boolean,
94
+ timeoutMs: number = 30_000,
95
+ pollMs: number = 500
96
+ ): Promise<boolean> {
97
+ const deadline = Date.now() + timeoutMs;
98
+ while (Date.now() < deadline) {
99
+ if (condition()) return true;
100
+ await sleep(pollMs);
101
+ }
102
+ return condition();
103
+ }
104
+
105
+ // ─────────────────────────────────────────────────────────────────
106
+ // Tests
107
+ // ─────────────────────────────────────────────────────────────────
108
+
109
+ describeFn("Task Dispatch Live Agent E2E", () => {
110
+ let system: MacroAgentSystemV2;
111
+ let testRepo: { path: string; cleanup: () => void };
112
+ let baseDir: string;
113
+ let dispatcher: TaskDispatcher;
114
+
115
+ function createSourceAdapter(tasksAdapter: typeof system.tasksAdapter): DispatchTaskSource {
116
+ return {
117
+ queryReady: (opts) => tasksAdapter.queryReady(opts),
118
+ claim: async (taskId, claimantId) => {
119
+ try {
120
+ await tasksAdapter.assignTask(taskId, claimantId);
121
+ return { success: true as const };
122
+ } catch { return { success: false as const }; }
123
+ },
124
+ release: async (taskId) => tasksAdapter.unclaimTask(taskId),
125
+ transition: async (taskId, action) => tasksAdapter.transitionTask(taskId, action),
126
+ getTask: async (taskId) => tasksAdapter.getTask(taskId),
127
+ listInProgress: async () => tasksAdapter.listTasks({ status: "in_progress" }),
128
+ };
129
+ }
130
+
131
+ function createRuntimeAdapter(agentManager: typeof system.agentManager): DispatchAgentRuntime {
132
+ return {
133
+ spawn: async (opts) => {
134
+ const spawned = await agentManager.spawn({
135
+ task: opts.prompt, task_id: opts.taskId, role: opts.role, parent: null,
136
+ });
137
+ return { id: spawned.id };
138
+ },
139
+ terminate: async (agentId) => agentManager.terminate(agentId, "cancelled"),
140
+ onStopped: (cb) => agentManager.onLifecycleEvent((event) => {
141
+ if (event.type === "stopped") cb(event.agent.id, event.reason);
142
+ }),
143
+ };
144
+ }
145
+
146
+ beforeEach(async () => {
147
+ testRepo = createTestRepo("dispatch");
148
+ baseDir = path.join(testRepo.path, ".macro-agent");
149
+ fs.mkdirSync(baseDir, { recursive: true });
150
+
151
+ system = await bootV2({
152
+ cwd: testRepo.path,
153
+ baseDir,
154
+ defaultPermissionMode: "auto-approve",
155
+ inbox: { socketPath: path.join(baseDir, "inbox.sock") },
156
+ });
157
+
158
+ dispatcher = createTaskDispatcher(
159
+ createSourceAdapter(system.tasksAdapter),
160
+ createRuntimeAdapter(system.agentManager),
161
+ {
162
+ claimantId: `test:${process.pid}:dispatch-live`,
163
+ pollIntervalMs: 600_000,
164
+ defaultRole: "worker",
165
+ concurrency: { global: 3 },
166
+ retry: { maxRetries: 3, baseDelayMs: 1_000, maxDelayMs: 60_000 },
167
+ reconcile: { enabled: true, intervalMs: 600_000 },
168
+ }
169
+ );
170
+ await dispatcher.start();
171
+
172
+ log("System booted with swarm-dispatch");
173
+ });
174
+
175
+ afterEach(async () => {
176
+ if (dispatcher) {
177
+ await dispatcher.stop();
178
+ }
179
+ if (system) {
180
+ try {
181
+ const running = system.agentManager.list({ state: "running" });
182
+ for (const agent of running) {
183
+ try {
184
+ await system.agentManager.terminate(agent.id, "cancelled");
185
+ } catch {
186
+ // Best effort
187
+ }
188
+ }
189
+ await system.shutdown();
190
+ } catch {
191
+ // Best effort
192
+ }
193
+ }
194
+ testRepo?.cleanup();
195
+ log("Cleanup complete");
196
+ });
197
+
198
+ // ── Dispatch spawns a real agent that completes ─────────────
199
+
200
+ it(
201
+ "dispatches a task and agent completes via done()",
202
+ async () => {
203
+ log("Setting up mock task data on tasksAdapter...");
204
+
205
+ const task: TaskRecord = {
206
+ id: "live-task-1",
207
+ title: "Write a greeting",
208
+ content:
209
+ 'Create a file called greeting.txt with the text "Hello World". ' +
210
+ 'Then call the "done" MCP tool with status="completed" and summary="Created greeting.txt".',
211
+ status: "open",
212
+ tags: ["auto"],
213
+ priority: 3,
214
+ };
215
+
216
+ // Override tasksAdapter methods to serve our test task
217
+ const tasksAdapter = system.tasksAdapter;
218
+ (tasksAdapter as any).queryReady = vi
219
+ .fn()
220
+ .mockResolvedValueOnce([task])
221
+ .mockResolvedValue([]); // Empty after first poll
222
+ (tasksAdapter as any).assignTask = vi.fn().mockResolvedValue(undefined);
223
+ (tasksAdapter as any).transitionTask = vi.fn().mockResolvedValue(undefined);
224
+ (tasksAdapter as any).getTask = vi.fn().mockResolvedValue(task);
225
+ (tasksAdapter as any).listTasks = vi.fn().mockResolvedValue([]);
226
+
227
+ log("Triggering dispatch...");
228
+ await dispatcher.dispatchNow();
229
+ await sleep(2_000);
230
+
231
+ log(`Tracker active: ${dispatcher.tracker.activeCount()}`);
232
+ expect(dispatcher.tracker.activeCount()).toBe(1);
233
+
234
+ const active = dispatcher.tracker.listActive();
235
+ const agentId = active[0]?.agentId;
236
+ log(`Dispatched agent: ${agentId}`);
237
+ expect(agentId).toBeDefined();
238
+
239
+ // Verify agent is running
240
+ const agentRecord = system.agentStore.getAgent(agentId);
241
+ expect(agentRecord).not.toBeNull();
242
+ expect(agentRecord!.state).toBe("running");
243
+
244
+ // Verify it was spawned parentless (root agent)
245
+ expect(agentRecord!.parent_id).toBeNull();
246
+
247
+ // Verify task was claimed
248
+ expect((tasksAdapter as any).assignTask).toHaveBeenCalledWith(
249
+ "live-task-1",
250
+ expect.any(String)
251
+ );
252
+ expect((tasksAdapter as any).transitionTask).toHaveBeenCalledWith(
253
+ "live-task-1",
254
+ "start"
255
+ );
256
+
257
+ // Now prompt the agent to do its work and call done
258
+ log("Waiting for agent to complete...");
259
+ const result = await system.agentManager.promptUntilDone(
260
+ agentId,
261
+ 'Complete your task: create greeting.txt with "Hello World", then call done(status="completed", summary="Created greeting.txt").',
262
+ {
263
+ maxFollowUps: 3,
264
+ onUpdate: (update: any) => {
265
+ if (update.sessionUpdate === "tool_call") {
266
+ log(` [tool_call] ${update.title ?? "unknown"}`);
267
+ }
268
+ },
269
+ }
270
+ );
271
+
272
+ log(
273
+ `promptUntilDone result: doneCalled=${result.doneCalled}, status=${result.doneStatus}`
274
+ );
275
+
276
+ // Wait for lifecycle listener to process the stop event
277
+ await waitFor(
278
+ () => system.agentStore.getAgent(agentId)?.state === "stopped",
279
+ 15_000,
280
+ 500
281
+ );
282
+
283
+ // Verify the lifecycle listener detected completion
284
+ log(`Tracker active after done: ${dispatcher.tracker.activeCount()}`);
285
+ log(`Tracker retries: ${dispatcher.tracker.listRetries().length}`);
286
+
287
+ if (result.doneCalled && result.doneStatus === "completed") {
288
+ // Agent called done(completed) → lifecycle listener should have
289
+ // completed the task in the tracker
290
+ const stillTracked = dispatcher.tracker.isTracked("live-task-1");
291
+ log(`Task still tracked: ${stillTracked}`);
292
+
293
+ // The agent should be stopped now
294
+ const finalRecord = system.agentStore.getAgent(agentId);
295
+ log(`Agent final state: ${finalRecord?.state}`);
296
+ expect(finalRecord?.state).toBe("stopped");
297
+
298
+ // Verify greeting.txt was created
299
+ const greetingPath = path.join(testRepo.path, "greeting.txt");
300
+ if (fs.existsSync(greetingPath)) {
301
+ const content = fs.readFileSync(greetingPath, "utf-8");
302
+ log(`greeting.txt content: ${content.trim()}`);
303
+ expect(content).toContain("Hello World");
304
+ } else {
305
+ log("greeting.txt not found (agent may have written elsewhere)");
306
+ }
307
+ } else {
308
+ log("Agent did not call done(completed) — checking alternative outcomes");
309
+ // Agent may have been terminated or may still be running
310
+ // This is acceptable in live tests where LLM behavior varies
311
+ }
312
+ },
313
+ TIMEOUT.MULTI
314
+ );
315
+
316
+ // ── Dispatch respects concurrency with real agents ──────────
317
+
318
+ it(
319
+ "dispatches multiple tasks respecting concurrency",
320
+ async () => {
321
+ log("Setting up 3 tasks with concurrency limit of 2...");
322
+
323
+ // Recreate tracker with global limit of 2
324
+ // (We can't change the existing tracker's config, but we
325
+ // can verify the strategy respects the concurrency config
326
+ // it was initialized with — which is global: 3)
327
+
328
+ const tasks: TaskRecord[] = [
329
+ {
330
+ id: "multi-1",
331
+ title: "Task one",
332
+ content: 'Say "Task one done" and call done(status="completed").',
333
+ status: "open",
334
+ tags: ["auto"],
335
+ },
336
+ {
337
+ id: "multi-2",
338
+ title: "Task two",
339
+ content: 'Say "Task two done" and call done(status="completed").',
340
+ status: "open",
341
+ tags: ["auto"],
342
+ },
343
+ ];
344
+
345
+ const tasksAdapter = system.tasksAdapter;
346
+ (tasksAdapter as any).queryReady = vi.fn().mockResolvedValue(tasks);
347
+ (tasksAdapter as any).assignTask = vi.fn().mockResolvedValue(undefined);
348
+ (tasksAdapter as any).transitionTask = vi.fn().mockResolvedValue(undefined);
349
+ (tasksAdapter as any).getTask = vi.fn().mockImplementation(async (id: string) =>
350
+ tasks.find((t) => t.id === id) ?? { id, title: "Unknown", status: "open" }
351
+ );
352
+ (tasksAdapter as any).listTasks = vi.fn().mockResolvedValue([]);
353
+
354
+ log("Triggering dispatch...");
355
+ await dispatcher.dispatchNow();
356
+ await sleep(3_000);
357
+
358
+ log(`Tracker active: ${dispatcher.tracker.activeCount()}`);
359
+ log(`Agents spawned: ${dispatcher.tracker.listActive().map((d) => d.agentId).flat().join(", ")}`);
360
+
361
+ // Both tasks should be dispatched (within global limit of 3)
362
+ expect(dispatcher.tracker.activeCount()).toBe(2);
363
+ expect((tasksAdapter as any).assignTask).toHaveBeenCalledTimes(2);
364
+
365
+ // Verify both agents are running
366
+ const active = dispatcher.tracker.listActive();
367
+ for (const record of active) {
368
+ const agentId = record.agentId;
369
+ const agentRecord = system.agentStore.getAgent(agentId);
370
+ expect(agentRecord).not.toBeNull();
371
+ expect(agentRecord!.state).toBe("running");
372
+ log(`Agent ${agentId} is ${agentRecord!.state}`);
373
+ }
374
+ },
375
+ TIMEOUT.DISPATCH
376
+ );
377
+
378
+ // ── Lifecycle listener tracks agent termination ─────────────
379
+
380
+ it(
381
+ "lifecycle listener detects external agent termination",
382
+ async () => {
383
+ log("Dispatching a task...");
384
+
385
+ const task: TaskRecord = {
386
+ id: "terminate-task",
387
+ title: "Long running task",
388
+ content: "Wait for instructions. Do not call done().",
389
+ status: "open",
390
+ tags: ["auto"],
391
+ };
392
+
393
+ const tasksAdapter = system.tasksAdapter;
394
+ (tasksAdapter as any).queryReady = vi
395
+ .fn()
396
+ .mockResolvedValueOnce([task])
397
+ .mockResolvedValue([]);
398
+ (tasksAdapter as any).assignTask = vi.fn().mockResolvedValue(undefined);
399
+ (tasksAdapter as any).transitionTask = vi.fn().mockResolvedValue(undefined);
400
+ (tasksAdapter as any).getTask = vi.fn().mockResolvedValue(task);
401
+ (tasksAdapter as any).listTasks = vi.fn().mockResolvedValue([]);
402
+
403
+ log("Triggering dispatch...");
404
+ await dispatcher.dispatchNow();
405
+ await sleep(2_000);
406
+
407
+ expect(dispatcher.tracker.activeCount()).toBe(1);
408
+ const agentId = dispatcher.tracker.listActive()[0].agentId;
409
+ log(`Agent spawned: ${agentId}`);
410
+
411
+ log("Terminating agent externally...");
412
+ await system.agentManager.terminate(agentId, "cancelled");
413
+
414
+ // Wait for lifecycle listener to process
415
+ await waitFor(
416
+ () => dispatcher.tracker.activeCount() === 0,
417
+ 15_000,
418
+ 500
419
+ );
420
+
421
+ log(`Tracker active after terminate: ${dispatcher.tracker.activeCount()}`);
422
+ log(`Tracker retries: ${dispatcher.tracker.listRetries().length}`);
423
+
424
+ // Agent was cancelled (not "completed") → lifecycle listener
425
+ // should have called dispatcher.tracker.fail(), which queues a retry
426
+ // (since maxRetries > 0 and this is attempt 0)
427
+ const isTracked = dispatcher.tracker.isTracked("terminate-task");
428
+ log(`Task still tracked (in retry queue): ${isTracked}`);
429
+ expect(isTracked).toBe(true);
430
+ expect(dispatcher.tracker.listRetries()).toHaveLength(1);
431
+ expect(dispatcher.tracker.listRetries()[0].taskId).toBe("terminate-task");
432
+ },
433
+ TIMEOUT.DISPATCH
434
+ );
435
+
436
+ // ── Reconciliation detects closed task ──────────────────────
437
+
438
+ it(
439
+ "reconciliation terminates agent when task is closed externally",
440
+ async () => {
441
+ log("Dispatching a task...");
442
+
443
+ const task: TaskRecord = {
444
+ id: "reconcile-task",
445
+ title: "Task to be closed externally",
446
+ content: "Wait for instructions.",
447
+ status: "open",
448
+ tags: ["auto"],
449
+ };
450
+
451
+ const tasksAdapter = system.tasksAdapter;
452
+ (tasksAdapter as any).queryReady = vi
453
+ .fn()
454
+ .mockResolvedValueOnce([task])
455
+ .mockResolvedValue([]);
456
+ (tasksAdapter as any).assignTask = vi.fn().mockResolvedValue(undefined);
457
+ (tasksAdapter as any).transitionTask = vi.fn().mockResolvedValue(undefined);
458
+ // getTask returns open initially (dispatch doesn't call getTask for new tasks,
459
+ // only for retries — so this won't be consumed during dispatch)
460
+ (tasksAdapter as any).getTask = vi.fn().mockResolvedValue(task);
461
+ (tasksAdapter as any).listTasks = vi.fn().mockResolvedValue([]);
462
+
463
+ log("Dispatching...");
464
+ await dispatcher.dispatchNow();
465
+ await sleep(2_000);
466
+
467
+ expect(dispatcher.tracker.activeCount()).toBe(1);
468
+ const agentId = dispatcher.tracker.listActive()[0].agentId;
469
+ log(`Agent spawned: ${agentId}`);
470
+ expect(system.agentStore.getAgent(agentId)!.state).toBe("running");
471
+
472
+ // Simulate external state change: task closed
473
+ (tasksAdapter as any).getTask = vi.fn().mockResolvedValue({
474
+ ...task,
475
+ status: "closed",
476
+ });
477
+
478
+ log("Triggering reconciliation (task now closed externally)...");
479
+ await dispatcher.reconcileNow();
480
+ await waitFor(
481
+ () => system.agentStore.getAgent(agentId)?.state === "stopped",
482
+ 15_000,
483
+ 500
484
+ );
485
+
486
+ log(`Tracker active after reconcile: ${dispatcher.tracker.activeCount()}`);
487
+
488
+ // Task should be removed from tracker (completed, not retried)
489
+ expect(dispatcher.tracker.isTracked("reconcile-task")).toBe(false);
490
+ expect(dispatcher.tracker.activeCount()).toBe(0);
491
+
492
+ // Agent should be stopped
493
+ const finalRecord = system.agentStore.getAgent(agentId);
494
+ log(`Agent final state: ${finalRecord?.state}`);
495
+ expect(finalRecord?.state).toBe("stopped");
496
+ },
497
+ TIMEOUT.DISPATCH
498
+ );
499
+
500
+ // ── Prompt pipeline produces correct prompt ─────────────────
501
+
502
+ it(
503
+ "dispatched agent receives prompt built from task metadata",
504
+ async () => {
505
+ log("Dispatching task with rich metadata...");
506
+
507
+ const task: TaskRecord = {
508
+ id: "prompt-task",
509
+ title: "Fix authentication bug",
510
+ content: "The login form fails when password contains special characters.",
511
+ status: "open",
512
+ tags: ["backend", "auth"],
513
+ priority: 5,
514
+ metadata: {
515
+ criteria: ["Login works with special chars", "No regression in tests"],
516
+ files: ["src/auth.ts", "src/login.tsx"],
517
+ sourceUrl: "https://linear.app/team/issue/AUTH-42",
518
+ },
519
+ };
520
+
521
+ const tasksAdapter = system.tasksAdapter;
522
+ (tasksAdapter as any).queryReady = vi
523
+ .fn()
524
+ .mockResolvedValueOnce([task])
525
+ .mockResolvedValue([]);
526
+ (tasksAdapter as any).assignTask = vi.fn().mockResolvedValue(undefined);
527
+ (tasksAdapter as any).transitionTask = vi.fn().mockResolvedValue(undefined);
528
+ (tasksAdapter as any).getTask = vi.fn().mockResolvedValue(task);
529
+ (tasksAdapter as any).listTasks = vi.fn().mockResolvedValue([]);
530
+
531
+ // Capture the spawn call to inspect the prompt
532
+ const originalSpawn = system.agentManager.spawn.bind(system.agentManager);
533
+ let capturedPrompt = "";
534
+ (system.agentManager as any).spawn = async (opts: any) => {
535
+ capturedPrompt = opts.task;
536
+ return originalSpawn(opts);
537
+ };
538
+
539
+ log("Triggering dispatch...");
540
+ await dispatcher.dispatchNow();
541
+ await sleep(2_000);
542
+
543
+ log(`Captured prompt length: ${capturedPrompt.length}`);
544
+ log(`Prompt preview: ${capturedPrompt.substring(0, 200)}...`);
545
+
546
+ // Verify prompt contains task metadata
547
+ expect(capturedPrompt).toContain("## Task: Fix authentication bug");
548
+ expect(capturedPrompt).toContain("login form fails");
549
+ expect(capturedPrompt).toContain("Task ID: prompt-task");
550
+ expect(capturedPrompt).toContain("Tags: backend, auth");
551
+ expect(capturedPrompt).toContain("Priority: 5");
552
+ expect(capturedPrompt).toContain("### Acceptance Criteria");
553
+ expect(capturedPrompt).toContain("Login works with special chars");
554
+ expect(capturedPrompt).toContain("### Relevant Files");
555
+ expect(capturedPrompt).toContain("src/auth.ts");
556
+ expect(capturedPrompt).toContain("Source: https://linear.app/team/issue/AUTH-42");
557
+ expect(capturedPrompt).toContain("**worker** role");
558
+
559
+ // Should NOT contain retry context on first attempt
560
+ expect(capturedPrompt).not.toContain("Retry");
561
+ },
562
+ TIMEOUT.DISPATCH
563
+ );
564
+ });