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,496 @@
1
+ /**
2
+ * Task Dispatch + OpenTasks Integration E2E Tests
3
+ *
4
+ * Tests the full dispatch lifecycle with REAL opentasks daemon AND
5
+ * REAL Claude Code agents. No mocking of task data or agent processes.
6
+ *
7
+ * End-to-end flow:
8
+ * opentasks daemon → tasks created → dispatch strategy polls →
9
+ * claims task → spawns real agent → agent works → done() →
10
+ * lifecycle listener → task transitioned → reconciliation
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-opentasks.e2e.test.ts
16
+ */
17
+
18
+ import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from "vitest";
19
+ import * as path from "node:path";
20
+ import * as os from "node:os";
21
+ import * as fs from "node:fs";
22
+ import { execFileSync } from "node:child_process";
23
+ import { bootV2, type MacroAgentSystemV2 } from "../../boot-v2.js";
24
+ import {
25
+ ensureOpentasksDaemon,
26
+ type DaemonHandle,
27
+ } from "../../adapters/opentasks-daemon.js";
28
+ import {
29
+ createTaskDispatcher,
30
+ type TaskDispatcher,
31
+ type DispatchAgentRuntime,
32
+ type DispatchTaskSource,
33
+ } from "swarm-dispatch";
34
+
35
+ // ─────────────────────────────────────────────────────────────────
36
+ // Configuration
37
+ // ─────────────────────────────────────────────────────────────────
38
+
39
+ const RUN_FULL_AGENT = !!process.env.RUN_FULL_AGENT_TESTS;
40
+ const describeFn = RUN_FULL_AGENT ? describe : describe.skip;
41
+
42
+ const TIMEOUT = {
43
+ SETUP: 30_000,
44
+ DISPATCH: 120_000,
45
+ MULTI: 180_000,
46
+ };
47
+
48
+ // ─────────────────────────────────────────────────────────────────
49
+ // Helpers
50
+ // ─────────────────────────────────────────────────────────────────
51
+
52
+ function createTempDir(prefix = "dispatch-ot"): string {
53
+ const suffix = Math.random().toString(36).slice(2, 8);
54
+ const dir = path.join(os.tmpdir(), `${prefix}-${suffix}`);
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ return dir;
57
+ }
58
+
59
+ function initGitRepo(dir: string): void {
60
+ execFileSync("git", ["init"], { cwd: dir, stdio: "pipe" });
61
+ execFileSync("git", ["config", "user.email", "test@e2e.dev"], {
62
+ cwd: dir,
63
+ stdio: "pipe",
64
+ });
65
+ execFileSync("git", ["config", "user.name", "E2E Test"], {
66
+ cwd: dir,
67
+ stdio: "pipe",
68
+ });
69
+ execFileSync("git", ["config", "commit.gpgsign", "false"], {
70
+ cwd: dir,
71
+ stdio: "pipe",
72
+ });
73
+ fs.writeFileSync(path.join(dir, "README.md"), "# Dispatch E2E\n");
74
+ execFileSync("git", ["add", "."], { cwd: dir, stdio: "pipe" });
75
+ execFileSync("git", ["commit", "-m", "init"], { cwd: dir, stdio: "pipe" });
76
+ }
77
+
78
+ function cleanupDir(dir: string): void {
79
+ try {
80
+ fs.rmSync(dir, { recursive: true, force: true });
81
+ } catch {
82
+ // best-effort
83
+ }
84
+ }
85
+
86
+ function log(msg: string): void {
87
+ console.log(`[DISPATCH-OT] ${msg}`);
88
+ }
89
+
90
+ function sleep(ms: number): Promise<void> {
91
+ return new Promise((resolve) => setTimeout(resolve, ms));
92
+ }
93
+
94
+ // ─────────────────────────────────────────────────────────────────
95
+ // Test Suite
96
+ // ─────────────────────────────────────────────────────────────────
97
+
98
+ describeFn("Task Dispatch + OpenTasks E2E", () => {
99
+ // Per-test: each test gets its own daemon for isolation
100
+ let daemonDir: string;
101
+ let registryDir: string;
102
+ let daemonHandle: DaemonHandle;
103
+ let canStartDaemon = false;
104
+
105
+ let system: MacroAgentSystemV2;
106
+ let dispatcher: TaskDispatcher;
107
+ let testRepoDir: string;
108
+
109
+ function createSourceAdapter(tasksAdapter: typeof system.tasksAdapter): DispatchTaskSource {
110
+ return {
111
+ queryReady: (opts) => tasksAdapter.queryReady(opts),
112
+ claim: async (taskId, claimantId) => {
113
+ try {
114
+ await tasksAdapter.assignTask(taskId, claimantId);
115
+ return { success: true as const };
116
+ } catch { return { success: false as const }; }
117
+ },
118
+ release: async (taskId) => tasksAdapter.unclaimTask(taskId),
119
+ transition: async (taskId, action) => tasksAdapter.transitionTask(taskId, action),
120
+ getTask: async (taskId) => tasksAdapter.getTask(taskId),
121
+ listInProgress: async () => tasksAdapter.listTasks({ status: "in_progress" }),
122
+ };
123
+ }
124
+
125
+ function createRuntimeAdapter(agentManager: typeof system.agentManager): DispatchAgentRuntime {
126
+ return {
127
+ spawn: async (opts) => {
128
+ const spawned = await agentManager.spawn({
129
+ task: opts.prompt, task_id: opts.taskId, role: opts.role, parent: null,
130
+ });
131
+ return { id: spawned.id };
132
+ },
133
+ terminate: async (agentId) => agentManager.terminate(agentId, "cancelled"),
134
+ onStopped: (cb) => agentManager.onLifecycleEvent((event) => {
135
+ if (event.type === "stopped") cb(event.agent.id, event.reason);
136
+ }),
137
+ };
138
+ }
139
+
140
+ beforeEach(async () => {
141
+ try {
142
+ daemonDir = createTempDir("ot-dmn");
143
+ registryDir = createTempDir("ot-reg");
144
+ initGitRepo(daemonDir);
145
+
146
+ daemonHandle = await ensureOpentasksDaemon(daemonDir, {
147
+ timeoutMs: 15_000,
148
+ registryPath: path.join(registryDir, "registry.json"),
149
+ });
150
+ canStartDaemon = true;
151
+ log(`Daemon started at ${daemonHandle.socketPath}`);
152
+ } catch (err) {
153
+ console.warn(
154
+ `[dispatch-opentasks] Skipping: daemon could not start: ${
155
+ err instanceof Error ? err.message : String(err)
156
+ }`
157
+ );
158
+ canStartDaemon = false;
159
+ return;
160
+ }
161
+
162
+ testRepoDir = createTempDir("dispatch-repo");
163
+ initGitRepo(testRepoDir);
164
+
165
+ const baseDir = path.join(testRepoDir, ".macro-agent");
166
+ fs.mkdirSync(baseDir, { recursive: true });
167
+
168
+ system = await bootV2({
169
+ cwd: testRepoDir,
170
+ baseDir,
171
+ defaultPermissionMode: "auto-approve",
172
+ inbox: { socketPath: path.join(baseDir, "inbox.sock") },
173
+ tasks: { socketPath: daemonHandle.socketPath },
174
+ });
175
+
176
+ dispatcher = createTaskDispatcher(
177
+ createSourceAdapter(system.tasksAdapter),
178
+ createRuntimeAdapter(system.agentManager),
179
+ {
180
+ claimantId: `test:${process.pid}:dispatch-ot`,
181
+ pollIntervalMs: 600_000,
182
+ defaultRole: "worker",
183
+ concurrency: { global: 3 },
184
+ retry: { maxRetries: 3, baseDelayMs: 1_000, maxDelayMs: 60_000 },
185
+ reconcile: { enabled: true, intervalMs: 600_000 },
186
+ }
187
+ );
188
+ await dispatcher.start();
189
+
190
+ log("System booted with real opentasks + swarm-dispatch");
191
+ });
192
+
193
+ afterEach(async () => {
194
+ if (dispatcher) await dispatcher.stop();
195
+ if (system) {
196
+ try {
197
+ const running = system.agentManager.list({ state: "running" } as any);
198
+ for (const agent of running) {
199
+ try {
200
+ await system.agentManager.terminate(agent.id, "cancelled");
201
+ } catch { /* best effort */ }
202
+ }
203
+ await system.shutdown();
204
+ } catch { /* best effort */ }
205
+ }
206
+ if (daemonHandle) {
207
+ try { await daemonHandle.stop(); } catch { /* best effort */ }
208
+ }
209
+ if (testRepoDir) cleanupDir(testRepoDir);
210
+ if (daemonDir) cleanupDir(daemonDir);
211
+ if (registryDir) cleanupDir(registryDir);
212
+ log("Cleanup complete");
213
+ }, 30_000);
214
+
215
+ // ── Full loop: create task → dispatch → agent completes ────
216
+
217
+ it(
218
+ "creates a real task, dispatches to a real agent, agent completes",
219
+ async () => {
220
+ if (!canStartDaemon) return;
221
+
222
+ // 1. Create a task in the real opentasks daemon
223
+ log("Creating task in opentasks...");
224
+ const taskId = await system.tasksAdapter.createTask({
225
+ title: "Write a haiku",
226
+ content:
227
+ 'Create a file called haiku.txt with a haiku about code. ' +
228
+ 'Then call the "done" MCP tool with status="completed" and summary="Created haiku.txt".',
229
+ tags: ["auto", "e2e"],
230
+ priority: 3,
231
+ });
232
+ log(`Task created: ${taskId}`);
233
+
234
+ // Verify task is queryable
235
+ const readyBefore = await system.tasksAdapter.queryReady();
236
+ log(`Ready tasks before dispatch: ${readyBefore.length}`);
237
+ expect(readyBefore.some((t) => t.id === taskId)).toBe(true);
238
+
239
+ // 2. Trigger dispatch
240
+ log("Triggering dispatch...");
241
+ await dispatcher.dispatchNow();
242
+
243
+ await sleep(3_000);
244
+
245
+ // 3. Verify agent was spawned
246
+ log(`Tracker active: ${dispatcher.tracker.activeCount()}`);
247
+ expect(dispatcher.tracker.activeCount()).toBeGreaterThanOrEqual(1);
248
+
249
+ const active = dispatcher.tracker.listActive();
250
+ const dispatch = active.find((d) => d.taskId === taskId);
251
+ expect(dispatch).toBeDefined();
252
+
253
+ const agentId = dispatch!.agentId;
254
+ log(`Dispatched agent: ${agentId}`);
255
+
256
+ const agentRecord = system.agentStore.getAgent(agentId);
257
+ expect(agentRecord).not.toBeNull();
258
+ expect(agentRecord!.state).toBe("running");
259
+ expect(agentRecord!.parent_id).toBeNull(); // Parentless
260
+
261
+ // 4. Prompt agent to complete its work
262
+ log("Prompting agent to complete task...");
263
+ const result = await system.agentManager.promptUntilDone(
264
+ agentId,
265
+ 'Complete your task: create haiku.txt with a haiku about code, then call done(status="completed", summary="Created haiku.txt").',
266
+ {
267
+ maxFollowUps: 3,
268
+ onUpdate: (update: any) => {
269
+ if (update.sessionUpdate === "tool_call") {
270
+ log(` [tool_call] ${update.title ?? "unknown"}`);
271
+ }
272
+ },
273
+ }
274
+ );
275
+
276
+ log(`promptUntilDone: doneCalled=${result.doneCalled}, status=${result.doneStatus}`);
277
+
278
+ // Wait for lifecycle listener
279
+ await sleep(3_000);
280
+
281
+ // 5. Verify completion
282
+ if (result.doneCalled && result.doneStatus === "completed") {
283
+ log("Agent called done(completed)");
284
+
285
+ // Task should be removed from tracker
286
+ expect(dispatcher.tracker.isTracked(taskId)).toBe(false);
287
+
288
+ // Agent should be stopped
289
+ const finalAgent = system.agentStore.getAgent(agentId);
290
+ expect(finalAgent?.state).toBe("stopped");
291
+
292
+ // Verify haiku.txt was created
293
+ const haikuPath = path.join(testRepoDir, "haiku.txt");
294
+ if (fs.existsSync(haikuPath)) {
295
+ const content = fs.readFileSync(haikuPath, "utf-8");
296
+ log(`haiku.txt: ${content.trim().substring(0, 80)}`);
297
+ expect(content.length).toBeGreaterThan(0);
298
+ } else {
299
+ log("haiku.txt not found (agent may have written elsewhere)");
300
+ }
301
+
302
+ // Verify the task was transitioned in opentasks
303
+ // (The lifecycle listener calls transitionTask("complete"))
304
+ const taskAfter = await system.tasksAdapter.getTask(taskId);
305
+ log(`Task status after completion: ${taskAfter.status}`);
306
+
307
+ // Ready list should no longer contain this task
308
+ const readyAfter = await system.tasksAdapter.queryReady();
309
+ expect(readyAfter.some((t) => t.id === taskId)).toBe(false);
310
+ } else {
311
+ log("Agent did not call done(completed) — LLM behavior variance");
312
+ }
313
+ },
314
+ TIMEOUT.MULTI
315
+ );
316
+
317
+ // ── Dispatch with dependencies: blocked task not dispatched ──
318
+
319
+ it(
320
+ "does not dispatch blocked tasks, dispatches when unblocked",
321
+ async () => {
322
+ if (!canStartDaemon) return;
323
+
324
+ // Create two tasks: taskB blocked by taskA
325
+ log("Creating tasks with dependency...");
326
+ const taskAId = await system.tasksAdapter.createTask({
327
+ title: "Prerequisite task A",
328
+ content: 'Say "done" and call done(status="completed").',
329
+ tags: ["auto"],
330
+ });
331
+ const taskBId = await system.tasksAdapter.createTask({
332
+ title: "Dependent task B",
333
+ content: 'Say "done" and call done(status="completed").',
334
+ tags: ["auto"],
335
+ });
336
+
337
+ // taskA blocks taskB
338
+ await system.tasksAdapter.addBlocker(taskBId, taskAId);
339
+ log(`Task ${taskAId} blocks ${taskBId}`);
340
+
341
+ // Verify only taskA is ready (taskB is blocked)
342
+ const readyBefore = await system.tasksAdapter.queryReady();
343
+ const readyIds = readyBefore.map((t) => t.id);
344
+ log(`Ready before: ${readyIds.join(", ")}`);
345
+ expect(readyIds).toContain(taskAId);
346
+ expect(readyIds).not.toContain(taskBId);
347
+
348
+ // Dispatch — should only pick up taskA
349
+ log("Triggering dispatch...");
350
+ await dispatcher.dispatchNow();
351
+
352
+ await sleep(3_000);
353
+
354
+ log(`Tracker active: ${dispatcher.tracker.activeCount()}`);
355
+ const active = dispatcher.tracker.listActive();
356
+ const dispatchedIds = active.map((d) => d.taskId);
357
+ log(`Dispatched: ${dispatchedIds.join(", ")}`);
358
+
359
+ // taskA should be dispatched, taskB should not
360
+ expect(dispatchedIds).toContain(taskAId);
361
+ expect(dispatchedIds).not.toContain(taskBId);
362
+
363
+ // Complete taskA to unblock taskB
364
+ log("Completing taskA to unblock taskB...");
365
+ const agentAId = active.find((d) => d.taskId === taskAId)!.agentId;
366
+ await system.agentManager.terminate(agentAId, "completed");
367
+ await sleep(1_000);
368
+
369
+ // Remove the blocker
370
+ await system.tasksAdapter.removeBlocker(taskBId, taskAId);
371
+
372
+ // Verify taskB is now ready
373
+ const readyAfter = await system.tasksAdapter.queryReady();
374
+ const readyAfterIds = readyAfter.map((t) => t.id);
375
+ log(`Ready after unblock: ${readyAfterIds.join(", ")}`);
376
+ expect(readyAfterIds).toContain(taskBId);
377
+
378
+ // Dispatch again — should pick up taskB
379
+ await dispatcher.dispatchNow();
380
+ await sleep(3_000);
381
+
382
+ const activeAfter = dispatcher.tracker.listActive();
383
+ const dispatchedAfter = activeAfter.map((d) => d.taskId);
384
+ log(`Dispatched after unblock: ${dispatchedAfter.join(", ")}`);
385
+ expect(dispatchedAfter).toContain(taskBId);
386
+ },
387
+ TIMEOUT.MULTI
388
+ );
389
+
390
+ // ── Multiple tasks dispatched concurrently ─────────────────
391
+
392
+ it(
393
+ "dispatches multiple ready tasks from opentasks concurrently",
394
+ async () => {
395
+ if (!canStartDaemon) return;
396
+
397
+ log("Creating 3 independent tasks...");
398
+ const ids: string[] = [];
399
+ for (let i = 0; i < 3; i++) {
400
+ const id = await system.tasksAdapter.createTask({
401
+ title: `Concurrent task ${i + 1}`,
402
+ content: "Wait for instructions.",
403
+ tags: ["auto", "concurrent"],
404
+ priority: 3,
405
+ });
406
+ ids.push(id);
407
+ }
408
+ log(`Created: ${ids.join(", ")}`);
409
+
410
+ // Verify all are ready
411
+ const ready = await system.tasksAdapter.queryReady();
412
+ const readyIds = ready.map((t) => t.id);
413
+ log(`Ready: ${readyIds.join(", ")}`);
414
+ for (const id of ids) {
415
+ expect(readyIds).toContain(id);
416
+ }
417
+
418
+ // Dispatch
419
+ log("Triggering dispatch...");
420
+ await dispatcher.dispatchNow();
421
+
422
+ await sleep(3_000);
423
+
424
+ log(`Tracker active: ${dispatcher.tracker.activeCount()}`);
425
+ expect(dispatcher.tracker.activeCount()).toBeGreaterThanOrEqual(3);
426
+
427
+ const active = dispatcher.tracker.listActive();
428
+ const dispatchedIds = active.map((d) => d.taskId);
429
+ log(`Dispatched: ${dispatchedIds.join(", ")}`);
430
+
431
+ // All 3 should be dispatched (global limit is 3)
432
+ for (const id of ids) {
433
+ expect(dispatchedIds).toContain(id);
434
+ }
435
+
436
+ // Verify all agents are running
437
+ for (const record of active) {
438
+ if (!ids.includes(record.taskId)) continue;
439
+ const agent = system.agentStore.getAgent(record.agentId);
440
+ expect(agent).not.toBeNull();
441
+ expect(agent!.state).toBe("running");
442
+ }
443
+ },
444
+ TIMEOUT.DISPATCH
445
+ );
446
+
447
+ // ── Reconcile detects externally completed task ────────────
448
+
449
+ it(
450
+ "reconciliation detects task completed externally in opentasks",
451
+ async () => {
452
+ if (!canStartDaemon) return;
453
+
454
+ // Create and dispatch a task
455
+ log("Creating task...");
456
+ const taskId = await system.tasksAdapter.createTask({
457
+ title: "Task to close externally",
458
+ content: "Wait for instructions.",
459
+ tags: ["auto"],
460
+ });
461
+
462
+ log("Dispatching...");
463
+ await dispatcher.dispatchNow();
464
+ await sleep(3_000);
465
+
466
+ log(`Tracker active: ${dispatcher.tracker.activeCount()}`);
467
+ expect(dispatcher.tracker.activeCount()).toBe(1);
468
+
469
+ const agentId = dispatcher.tracker.listActive().find((d) => d.taskId === taskId)!.agentId;
470
+ log(`Agent: ${agentId}`);
471
+
472
+ // Simulate external completion: transition task to closed directly
473
+ log("Closing task externally via opentasks...");
474
+ await system.tasksAdapter.transitionTask(taskId, "start");
475
+ await system.tasksAdapter.transitionTask(taskId, "complete");
476
+
477
+ const taskAfterClose = await system.tasksAdapter.getTask(taskId);
478
+ log(`Task status after external close: ${taskAfterClose.status}`);
479
+ expect(taskAfterClose.status).toBe("closed");
480
+
481
+ log("Triggering reconciliation...");
482
+ await dispatcher.reconcileNow();
483
+ await sleep(3_000);
484
+
485
+ // Task should be removed from tracker
486
+ log(`Tracker active after reconcile: ${dispatcher.tracker.activeCount()}`);
487
+ expect(dispatcher.tracker.isTracked(taskId)).toBe(false);
488
+
489
+ // Agent should be stopped
490
+ const agentAfter = system.agentStore.getAgent(agentId);
491
+ log(`Agent state after reconcile: ${agentAfter?.state}`);
492
+ expect(agentAfter?.state).toBe("stopped");
493
+ },
494
+ TIMEOUT.DISPATCH
495
+ );
496
+ });