macro-agent 0.1.7 → 0.1.10

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 (259) hide show
  1. package/CLAUDE.md +179 -38
  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 +155 -6
  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/agent/agent-manager-v2.d.ts +21 -0
  14. package/dist/agent/agent-manager-v2.d.ts.map +1 -1
  15. package/dist/agent/agent-manager-v2.js +234 -71
  16. package/dist/agent/agent-manager-v2.js.map +1 -1
  17. package/dist/agent/agent-manager.d.ts +12 -0
  18. package/dist/agent/agent-manager.d.ts.map +1 -1
  19. package/dist/agent/agent-manager.js.map +1 -1
  20. package/dist/agent/types.d.ts +15 -2
  21. package/dist/agent/types.d.ts.map +1 -1
  22. package/dist/agent/types.js.map +1 -1
  23. package/dist/boot-v2.d.ts +41 -0
  24. package/dist/boot-v2.d.ts.map +1 -1
  25. package/dist/boot-v2.js +34 -37
  26. package/dist/boot-v2.js.map +1 -1
  27. package/dist/cli/index.js +56 -0
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/cognitive/macro-agent-backend.d.ts.map +1 -1
  30. package/dist/cognitive/macro-agent-backend.js +40 -22
  31. package/dist/cognitive/macro-agent-backend.js.map +1 -1
  32. package/dist/integrations/skilltree.d.ts.map +1 -1
  33. package/dist/integrations/skilltree.js +1 -0
  34. package/dist/integrations/skilltree.js.map +1 -1
  35. package/dist/lifecycle/cleanup.d.ts +33 -2
  36. package/dist/lifecycle/cleanup.d.ts.map +1 -1
  37. package/dist/lifecycle/cleanup.js +28 -6
  38. package/dist/lifecycle/cleanup.js.map +1 -1
  39. package/dist/lifecycle/handlers-v2.d.ts +7 -0
  40. package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
  41. package/dist/lifecycle/handlers-v2.js +28 -2
  42. package/dist/lifecycle/handlers-v2.js.map +1 -1
  43. package/dist/lifecycle/types.d.ts +11 -0
  44. package/dist/lifecycle/types.d.ts.map +1 -1
  45. package/dist/lifecycle/types.js.map +1 -1
  46. package/dist/map/acp-bridge.d.ts +9 -0
  47. package/dist/map/acp-bridge.d.ts.map +1 -1
  48. package/dist/map/acp-bridge.js +15 -2
  49. package/dist/map/acp-bridge.js.map +1 -1
  50. package/dist/map/cascade-bridge.d.ts +44 -0
  51. package/dist/map/cascade-bridge.d.ts.map +1 -0
  52. package/dist/map/cascade-bridge.js +257 -0
  53. package/dist/map/cascade-bridge.js.map +1 -0
  54. package/dist/map/lifecycle-bridge.d.ts +1 -8
  55. package/dist/map/lifecycle-bridge.d.ts.map +1 -1
  56. package/dist/map/lifecycle-bridge.js +76 -22
  57. package/dist/map/lifecycle-bridge.js.map +1 -1
  58. package/dist/map/server.d.ts.map +1 -1
  59. package/dist/map/server.js +47 -6
  60. package/dist/map/server.js.map +1 -1
  61. package/dist/map/sidecar.d.ts.map +1 -1
  62. package/dist/map/sidecar.js +33 -4
  63. package/dist/map/sidecar.js.map +1 -1
  64. package/dist/map/types.d.ts +20 -0
  65. package/dist/map/types.d.ts.map +1 -1
  66. package/dist/mcp/tools/done-v2.d.ts.map +1 -1
  67. package/dist/mcp/tools/done-v2.js +8 -0
  68. package/dist/mcp/tools/done-v2.js.map +1 -1
  69. package/dist/teams/team-manager-v2.d.ts.map +1 -1
  70. package/dist/teams/team-manager-v2.js +26 -0
  71. package/dist/teams/team-manager-v2.js.map +1 -1
  72. package/dist/teams/team-runtime-v2.d.ts.map +1 -1
  73. package/dist/teams/team-runtime-v2.js +16 -3
  74. package/dist/teams/team-runtime-v2.js.map +1 -1
  75. package/dist/workspace/config.d.ts +10 -10
  76. package/dist/workspace/config.d.ts.map +1 -1
  77. package/dist/workspace/config.js +4 -4
  78. package/dist/workspace/config.js.map +1 -1
  79. package/dist/workspace/git-cascade-adapter.d.ts +510 -0
  80. package/dist/workspace/git-cascade-adapter.d.ts.map +1 -0
  81. package/dist/workspace/git-cascade-adapter.js +908 -0
  82. package/dist/workspace/git-cascade-adapter.js.map +1 -0
  83. package/dist/workspace/index.d.ts +3 -3
  84. package/dist/workspace/index.d.ts.map +1 -1
  85. package/dist/workspace/index.js +4 -4
  86. package/dist/workspace/index.js.map +1 -1
  87. package/dist/workspace/landing/direct-push.d.ts +20 -0
  88. package/dist/workspace/landing/direct-push.d.ts.map +1 -0
  89. package/dist/workspace/landing/direct-push.js +74 -0
  90. package/dist/workspace/landing/direct-push.js.map +1 -0
  91. package/dist/workspace/landing/index.d.ts +29 -0
  92. package/dist/workspace/landing/index.d.ts.map +1 -0
  93. package/dist/workspace/landing/index.js +37 -0
  94. package/dist/workspace/landing/index.js.map +1 -0
  95. package/dist/workspace/landing/merge-to-parent.d.ts +41 -0
  96. package/dist/workspace/landing/merge-to-parent.d.ts.map +1 -0
  97. package/dist/workspace/landing/merge-to-parent.js +185 -0
  98. package/dist/workspace/landing/merge-to-parent.js.map +1 -0
  99. package/dist/workspace/landing/optimistic-push.d.ts +16 -0
  100. package/dist/workspace/landing/optimistic-push.d.ts.map +1 -0
  101. package/dist/workspace/landing/optimistic-push.js +27 -0
  102. package/dist/workspace/landing/optimistic-push.js.map +1 -0
  103. package/dist/workspace/landing/queue-to-branch.d.ts +24 -0
  104. package/dist/workspace/landing/queue-to-branch.d.ts.map +1 -0
  105. package/dist/workspace/landing/queue-to-branch.js +79 -0
  106. package/dist/workspace/landing/queue-to-branch.js.map +1 -0
  107. package/dist/workspace/merge-queue/merge-queue.d.ts +10 -0
  108. package/dist/workspace/merge-queue/merge-queue.d.ts.map +1 -1
  109. package/dist/workspace/merge-queue/merge-queue.js +10 -0
  110. package/dist/workspace/merge-queue/merge-queue.js.map +1 -1
  111. package/dist/workspace/merge-queue/types.d.ts +16 -2
  112. package/dist/workspace/merge-queue/types.d.ts.map +1 -1
  113. package/dist/workspace/merge-queue/types.js +9 -0
  114. package/dist/workspace/merge-queue/types.js.map +1 -1
  115. package/dist/workspace/pool/types.d.ts +1 -0
  116. package/dist/workspace/pool/types.d.ts.map +1 -1
  117. package/dist/workspace/pool/worktree-pool.d.ts.map +1 -1
  118. package/dist/workspace/pool/worktree-pool.js +1 -0
  119. package/dist/workspace/pool/worktree-pool.js.map +1 -1
  120. package/dist/workspace/recovery/abandon.d.ts +15 -0
  121. package/dist/workspace/recovery/abandon.d.ts.map +1 -0
  122. package/dist/workspace/recovery/abandon.js +45 -0
  123. package/dist/workspace/recovery/abandon.js.map +1 -0
  124. package/dist/workspace/recovery/auto-resolve.d.ts +27 -0
  125. package/dist/workspace/recovery/auto-resolve.d.ts.map +1 -0
  126. package/dist/workspace/recovery/auto-resolve.js +99 -0
  127. package/dist/workspace/recovery/auto-resolve.js.map +1 -0
  128. package/dist/workspace/recovery/defer.d.ts +15 -0
  129. package/dist/workspace/recovery/defer.d.ts.map +1 -0
  130. package/dist/workspace/recovery/defer.js +16 -0
  131. package/dist/workspace/recovery/defer.js.map +1 -0
  132. package/dist/workspace/recovery/escalate.d.ts +16 -0
  133. package/dist/workspace/recovery/escalate.d.ts.map +1 -0
  134. package/dist/workspace/recovery/escalate.js +24 -0
  135. package/dist/workspace/recovery/escalate.js.map +1 -0
  136. package/dist/workspace/recovery/index.d.ts +32 -0
  137. package/dist/workspace/recovery/index.d.ts.map +1 -0
  138. package/dist/workspace/recovery/index.js +45 -0
  139. package/dist/workspace/recovery/index.js.map +1 -0
  140. package/dist/workspace/recovery/spawn-resolver.d.ts +45 -0
  141. package/dist/workspace/recovery/spawn-resolver.d.ts.map +1 -0
  142. package/dist/workspace/recovery/spawn-resolver.js +111 -0
  143. package/dist/workspace/recovery/spawn-resolver.js.map +1 -0
  144. package/dist/workspace/recovery/types.d.ts +63 -0
  145. package/dist/workspace/recovery/types.d.ts.map +1 -0
  146. package/dist/workspace/recovery/types.js +12 -0
  147. package/dist/workspace/recovery/types.js.map +1 -0
  148. package/dist/workspace/topology/index.d.ts +9 -0
  149. package/dist/workspace/topology/index.d.ts.map +1 -0
  150. package/dist/workspace/topology/index.js +8 -0
  151. package/dist/workspace/topology/index.js.map +1 -0
  152. package/dist/workspace/topology/no-workspace.d.ts +18 -0
  153. package/dist/workspace/topology/no-workspace.d.ts.map +1 -0
  154. package/dist/workspace/topology/no-workspace.js +25 -0
  155. package/dist/workspace/topology/no-workspace.js.map +1 -0
  156. package/dist/workspace/topology/types.d.ts +97 -0
  157. package/dist/workspace/topology/types.d.ts.map +1 -0
  158. package/dist/workspace/topology/types.js +20 -0
  159. package/dist/workspace/topology/types.js.map +1 -0
  160. package/dist/workspace/topology/yaml-driven.d.ts +69 -0
  161. package/dist/workspace/topology/yaml-driven.d.ts.map +1 -0
  162. package/dist/workspace/topology/yaml-driven.js +273 -0
  163. package/dist/workspace/topology/yaml-driven.js.map +1 -0
  164. package/dist/workspace/types-v3.d.ts +110 -0
  165. package/dist/workspace/types-v3.d.ts.map +1 -0
  166. package/dist/workspace/types-v3.js +20 -0
  167. package/dist/workspace/types-v3.js.map +1 -0
  168. package/dist/workspace/types.d.ts +145 -17
  169. package/dist/workspace/types.d.ts.map +1 -1
  170. package/dist/workspace/workspace-manager.d.ts +92 -13
  171. package/dist/workspace/workspace-manager.d.ts.map +1 -1
  172. package/dist/workspace/workspace-manager.js +373 -13
  173. package/dist/workspace/workspace-manager.js.map +1 -1
  174. package/dist/workspace/yaml-schema.d.ts +254 -0
  175. package/dist/workspace/yaml-schema.d.ts.map +1 -0
  176. package/dist/workspace/yaml-schema.js +170 -0
  177. package/dist/workspace/yaml-schema.js.map +1 -0
  178. package/docs/conflict-recovery.md +472 -0
  179. package/docs/git-cascade-integration-gaps.md +678 -0
  180. package/docs/workspace-interfaces.md +731 -0
  181. package/docs/workspace-redesign-plan.md +302 -0
  182. package/package.json +4 -4
  183. package/src/__tests__/e2e/auto-sync.e2e.test.ts +257 -0
  184. package/src/__tests__/e2e/cascade-rebase.e2e.test.ts +254 -0
  185. package/src/__tests__/e2e/cli-run.e2e.test.ts +167 -0
  186. package/src/__tests__/e2e/self-driving-v3.e2e.test.ts +197 -0
  187. package/src/__tests__/e2e/spawn-resolver.e2e.test.ts +200 -0
  188. package/src/__tests__/e2e/workspace-lifecycle.e2e.test.ts +30 -22
  189. package/src/__tests__/e2e/workspace-v3.e2e.test.ts +413 -0
  190. package/src/acp/__tests__/claude-code-replay.test.ts +225 -0
  191. package/src/acp/__tests__/macro-agent.test.ts +39 -1
  192. package/src/acp/claude-code-replay.ts +208 -0
  193. package/src/acp/macro-agent.ts +167 -9
  194. package/src/acp/types.ts +10 -0
  195. package/src/agent/__tests__/agent-manager-topology.test.ts +73 -0
  196. package/src/agent/__tests__/agent-manager-v2.test.ts +71 -11
  197. package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
  198. package/src/agent/agent-manager-v2.ts +293 -77
  199. package/src/agent/agent-manager.ts +14 -0
  200. package/src/agent/types.ts +16 -2
  201. package/src/boot-v2.ts +87 -36
  202. package/src/cli/index.ts +61 -0
  203. package/src/cognitive/__tests__/macro-agent-backend.test.ts +47 -5
  204. package/src/cognitive/macro-agent-backend.ts +45 -29
  205. package/src/integrations/skilltree.ts +1 -0
  206. package/src/lifecycle/cleanup.ts +52 -3
  207. package/src/lifecycle/handlers-v2.ts +40 -3
  208. package/src/lifecycle/types.ts +12 -0
  209. package/src/map/__tests__/cascade-bridge.test.ts +229 -0
  210. package/src/map/__tests__/lifecycle-bridge.test.ts +165 -22
  211. package/src/map/acp-bridge.ts +26 -3
  212. package/src/map/cascade-bridge.ts +301 -0
  213. package/src/map/lifecycle-bridge.ts +77 -27
  214. package/src/map/server.ts +47 -6
  215. package/src/map/sidecar.ts +31 -3
  216. package/src/map/types.ts +20 -0
  217. package/src/mcp/tools/done-v2.ts +9 -0
  218. package/src/teams/team-manager-v2.ts +37 -0
  219. package/src/teams/team-runtime-v2.ts +23 -3
  220. package/src/workspace/__tests__/{dataplane-adapter.test.ts → git-cascade-adapter.test.ts} +209 -14
  221. package/src/workspace/__tests__/self-driving-yaml.test.ts +114 -0
  222. package/src/workspace/__tests__/shared-worktree-refcount.test.ts +154 -0
  223. package/src/workspace/__tests__/standalone-mode.test.ts +118 -0
  224. package/src/workspace/__tests__/workspace-manager-v3.test.ts +245 -0
  225. package/src/workspace/__tests__/yaml-schema.test.ts +210 -0
  226. package/src/workspace/config.ts +11 -11
  227. package/src/workspace/git-cascade-adapter.ts +1186 -0
  228. package/src/workspace/index.ts +11 -11
  229. package/src/workspace/landing/__tests__/strategies.test.ts +142 -0
  230. package/src/workspace/landing/direct-push.ts +91 -0
  231. package/src/workspace/landing/index.ts +40 -0
  232. package/src/workspace/landing/merge-to-parent.ts +228 -0
  233. package/src/workspace/landing/optimistic-push.ts +36 -0
  234. package/src/workspace/landing/queue-to-branch.ts +108 -0
  235. package/src/workspace/merge-queue/merge-queue.ts +10 -0
  236. package/src/workspace/merge-queue/types.ts +16 -2
  237. package/src/workspace/pool/__tests__/worktree-pool.integration.test.ts +5 -5
  238. package/src/workspace/pool/types.ts +1 -0
  239. package/src/workspace/pool/worktree-pool.ts +1 -0
  240. package/src/workspace/recovery/__tests__/auto-resolve-integration.test.ts +127 -0
  241. package/src/workspace/recovery/__tests__/spawn-resolver.test.ts +139 -0
  242. package/src/workspace/recovery/__tests__/strategies.test.ts +145 -0
  243. package/src/workspace/recovery/abandon.ts +51 -0
  244. package/src/workspace/recovery/auto-resolve.ts +119 -0
  245. package/src/workspace/recovery/defer.ts +23 -0
  246. package/src/workspace/recovery/escalate.ts +30 -0
  247. package/src/workspace/recovery/index.ts +58 -0
  248. package/src/workspace/recovery/spawn-resolver.ts +145 -0
  249. package/src/workspace/recovery/types.ts +54 -0
  250. package/src/workspace/topology/__tests__/yaml-driven.test.ts +345 -0
  251. package/src/workspace/topology/index.ts +18 -0
  252. package/src/workspace/topology/no-workspace.ts +39 -0
  253. package/src/workspace/topology/types.ts +116 -0
  254. package/src/workspace/topology/yaml-driven.ts +316 -0
  255. package/src/workspace/types-v3.ts +155 -0
  256. package/src/workspace/types.ts +191 -20
  257. package/src/workspace/workspace-manager.ts +474 -19
  258. package/src/workspace/yaml-schema.ts +216 -0
  259. package/src/workspace/dataplane-adapter.ts +0 -546
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Cascade rebase end-to-end via MergeToParentStrategy.
3
+ *
4
+ * Fixture: 3-level fork graph
5
+ * main → team_root → feat-A → feat-B → feat-C
6
+ *
7
+ * When we land feat-B into feat-A with `cascade: true`, feat-C is expected
8
+ * to rebase onto the new feat-A HEAD.
9
+ *
10
+ * Verifies:
11
+ * - The worktree provider finds live agents' worktrees
12
+ * - Allocates ephemeral worktrees for streams that have no agent
13
+ * - Ephemeral worktrees are cleaned up after cascade completes
14
+ * - cascadeStrategy options route as expected
15
+ *
16
+ * REQUIRES: RUN_E2E_TESTS=true
17
+ */
18
+
19
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
20
+ import * as fs from 'fs';
21
+ import * as path from 'path';
22
+ import * as os from 'os';
23
+ import { execSync } from 'child_process';
24
+ import { GitCascadeAdapter, createGitCascadeAdapter } from '../../workspace/git-cascade-adapter.js';
25
+ import {
26
+ DefaultWorkspaceManager,
27
+ createWorkspaceManagerWithAdapter,
28
+ } from '../../workspace/workspace-manager.js';
29
+ import { MergeToParentStrategy } from '../../workspace/landing/merge-to-parent.js';
30
+
31
+ const RUN_E2E = !!process.env.RUN_E2E_TESTS;
32
+ const describeFn = RUN_E2E ? describe : describe.skip;
33
+
34
+ describeFn('cascade rebase via merge-to-parent', () => {
35
+ let tempDir: string;
36
+ let repoPath: string;
37
+ let adapter: GitCascadeAdapter;
38
+ let manager: DefaultWorkspaceManager;
39
+
40
+ beforeEach(() => {
41
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cascade-e2e-'));
42
+ repoPath = path.join(tempDir, 'repo');
43
+ fs.mkdirSync(repoPath);
44
+
45
+ execSync('git init -b main', { cwd: repoPath, stdio: 'pipe' });
46
+ execSync('git config user.email "t@t.com"', { cwd: repoPath, stdio: 'pipe' });
47
+ execSync('git config user.name "T"', { cwd: repoPath, stdio: 'pipe' });
48
+ fs.writeFileSync(path.join(repoPath, 'README.md'), '# test\n');
49
+ execSync('git add .', { cwd: repoPath, stdio: 'pipe' });
50
+ execSync('git commit -m "init"', { cwd: repoPath, stdio: 'pipe' });
51
+
52
+ adapter = createGitCascadeAdapter({
53
+ enabled: true,
54
+ repoPath,
55
+ dbPath: path.join(tempDir, 'gc.db'),
56
+ skipRecovery: true,
57
+ });
58
+ manager = createWorkspaceManagerWithAdapter(adapter, {
59
+ worktreeBaseDir: path.join(tempDir, 'worktrees'),
60
+ }) as DefaultWorkspaceManager;
61
+ });
62
+
63
+ afterEach(() => {
64
+ manager.close();
65
+ adapter.close();
66
+ if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
67
+ });
68
+
69
+ it('cascades through 3-level fork: main → A → B → C', async () => {
70
+ // Build the stream graph
71
+ const streamA = manager.createStreamV3({
72
+ name: 'feat-A',
73
+ ownerId: 'agent-A',
74
+ forkFrom: 'main',
75
+ });
76
+ const streamB = manager.createStreamV3({
77
+ name: 'feat-B',
78
+ ownerId: 'agent-B',
79
+ parent: streamA,
80
+ });
81
+ const streamC = manager.createStreamV3({
82
+ name: 'feat-C',
83
+ ownerId: 'agent-C',
84
+ parent: streamB,
85
+ });
86
+
87
+ // Allocate worktrees for A and C (live agents). B gets a worktree,
88
+ // makes commits, then we land it into A.
89
+ const wtA = manager.allocateWorktree({ agentId: 'agent-A', streamId: streamA });
90
+ const wtB = manager.allocateWorktree({ agentId: 'agent-B', streamId: streamB });
91
+ const wtC = manager.allocateWorktree({ agentId: 'agent-C', streamId: streamC });
92
+
93
+ // Make a commit in B
94
+ fs.writeFileSync(path.join(wtB.path, 'b.txt'), 'B change\n');
95
+ manager.commitChanges({
96
+ agentId: 'agent-B',
97
+ streamId: streamB,
98
+ worktree: wtB.path,
99
+ message: 'feat-B: add b.txt',
100
+ });
101
+
102
+ // Land B → A with cascade=true (C should rebase onto new A)
103
+ const strategy = new MergeToParentStrategy();
104
+ const result = await strategy.land({
105
+ agentId: 'agent-B',
106
+ streamId: streamB,
107
+ sourceWorktree: wtB.path,
108
+ targetStreamId: streamA,
109
+ strategyConfig: { cascade: true, cascadeStrategy: 'defer_conflicts' },
110
+ workspaceManager: manager,
111
+ });
112
+
113
+ expect(result.success).toBe(true);
114
+
115
+ // Verify A has B's file
116
+ expect(fs.existsSync(path.join(wtA.path, 'b.txt'))).toBe(true);
117
+
118
+ // Verify C got rebased onto new A (the file should now exist in C's
119
+ // worktree after a `git pull` or the cascade picks it up).
120
+ // git-cascade's cascadeRebase operates on the branch level — the worktree
121
+ // may need a refresh. The key check: the stream's baseCommit in the DB
122
+ // should have moved.
123
+ const updatedC = adapter.getStream(streamC);
124
+ expect(updatedC).toBeDefined();
125
+ // baseCommit should reflect the cascade update — if it didn't move,
126
+ // cascade was a no-op. (Exact commit comparison depends on git-cascade
127
+ // internals; we just check that the stream is still active.)
128
+ expect(updatedC?.status).toBe('active');
129
+ });
130
+
131
+ it('allocates ephemeral worktree for dependent stream without live agent', async () => {
132
+ const streamA = manager.createStreamV3({
133
+ name: 'feat-A',
134
+ ownerId: 'agent-A',
135
+ forkFrom: 'main',
136
+ });
137
+ const streamB = manager.createStreamV3({
138
+ name: 'feat-B',
139
+ ownerId: 'agent-B',
140
+ parent: streamA,
141
+ });
142
+ // stream-C has no agent allocated — cascade must create an ephemeral
143
+ const streamC = manager.createStreamV3({
144
+ name: 'feat-C',
145
+ ownerId: 'pseudo:C',
146
+ parent: streamB,
147
+ });
148
+
149
+ const wtA = manager.allocateWorktree({ agentId: 'agent-A', streamId: streamA });
150
+ const wtB = manager.allocateWorktree({ agentId: 'agent-B', streamId: streamB });
151
+
152
+ fs.writeFileSync(path.join(wtB.path, 'b.txt'), 'B\n');
153
+ manager.commitChanges({
154
+ agentId: 'agent-B',
155
+ streamId: streamB,
156
+ worktree: wtB.path,
157
+ message: 'feat-B',
158
+ });
159
+
160
+ const beforeWorktrees = adapter.listWorktrees().length;
161
+
162
+ const strategy = new MergeToParentStrategy();
163
+ await strategy.land({
164
+ agentId: 'agent-B',
165
+ streamId: streamB,
166
+ sourceWorktree: wtB.path,
167
+ targetStreamId: streamA,
168
+ strategyConfig: { cascade: true },
169
+ workspaceManager: manager,
170
+ });
171
+
172
+ // After cascade, ephemeral worktrees should have been cleaned up
173
+ const afterWorktrees = adapter.listWorktrees().length;
174
+ expect(afterWorktrees).toBeLessThanOrEqual(beforeWorktrees);
175
+ });
176
+
177
+ it('cascade is a no-op when strategyConfig.cascade is false', async () => {
178
+ const streamA = manager.createStreamV3({
179
+ name: 'feat-A',
180
+ ownerId: 'agent-A',
181
+ forkFrom: 'main',
182
+ });
183
+ const streamB = manager.createStreamV3({
184
+ name: 'feat-B',
185
+ ownerId: 'agent-B',
186
+ parent: streamA,
187
+ });
188
+
189
+ const wtA = manager.allocateWorktree({ agentId: 'agent-A', streamId: streamA });
190
+ const wtB = manager.allocateWorktree({ agentId: 'agent-B', streamId: streamB });
191
+
192
+ fs.writeFileSync(path.join(wtB.path, 'b.txt'), 'B\n');
193
+ manager.commitChanges({
194
+ agentId: 'agent-B',
195
+ streamId: streamB,
196
+ worktree: wtB.path,
197
+ message: 'feat-B',
198
+ });
199
+
200
+ const strategy = new MergeToParentStrategy();
201
+ const result = await strategy.land({
202
+ agentId: 'agent-B',
203
+ streamId: streamB,
204
+ sourceWorktree: wtB.path,
205
+ targetStreamId: streamA,
206
+ // No cascade config — should stop after mergeStream
207
+ workspaceManager: manager,
208
+ });
209
+
210
+ expect(result.success).toBe(true);
211
+ });
212
+
213
+ it('cleans up ephemeral worktrees even when cascade throws internally', async () => {
214
+ // Simulate a cascade scenario where provider-allocated worktrees must
215
+ // be cleaned up. We construct a minimal graph and verify the tracker
216
+ // doesn't accumulate ephemeral records.
217
+ const streamA = manager.createStreamV3({
218
+ name: 'feat-A',
219
+ ownerId: 'agent-A',
220
+ forkFrom: 'main',
221
+ });
222
+ const streamB = manager.createStreamV3({
223
+ name: 'feat-B',
224
+ ownerId: 'agent-B',
225
+ parent: streamA,
226
+ });
227
+
228
+ const wtA = manager.allocateWorktree({ agentId: 'agent-A', streamId: streamA });
229
+ const wtB = manager.allocateWorktree({ agentId: 'agent-B', streamId: streamB });
230
+ fs.writeFileSync(path.join(wtB.path, 'b.txt'), 'B\n');
231
+ manager.commitChanges({
232
+ agentId: 'agent-B',
233
+ streamId: streamB,
234
+ worktree: wtB.path,
235
+ message: 'feat-B',
236
+ });
237
+
238
+ const strategy = new MergeToParentStrategy();
239
+ await strategy.land({
240
+ agentId: 'agent-B',
241
+ streamId: streamB,
242
+ sourceWorktree: wtB.path,
243
+ targetStreamId: streamA,
244
+ strategyConfig: { cascade: true },
245
+ workspaceManager: manager,
246
+ });
247
+
248
+ // No worktree records should include the `system:cascade-*` pseudo-id
249
+ const cascadeEphemerals = adapter
250
+ .listWorktrees()
251
+ .filter((wt) => wt.agentId.startsWith('system:cascade-'));
252
+ expect(cascadeEphemerals.length).toBe(0);
253
+ });
254
+ });
@@ -0,0 +1,167 @@
1
+ /**
2
+ * `multiagent-cli run <team>` CLI e2e.
3
+ *
4
+ * Spawns the CLI as a real subprocess with a minimal team fixture (so we
5
+ * don't need a full team YAML tree in the test repo). Verifies:
6
+ * - CLI boots successfully
7
+ * - Team starts (log output confirms)
8
+ * - SIGINT shutdown exits cleanly
9
+ *
10
+ * REQUIRES: RUN_E2E_TESTS=true
11
+ */
12
+
13
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import * as os from 'os';
17
+ import { spawn, type ChildProcess } from 'child_process';
18
+
19
+ const RUN_E2E = !!process.env.RUN_E2E_TESTS;
20
+ const describeFn = RUN_E2E ? describe : describe.skip;
21
+
22
+ describeFn('multiagent-cli run <team>', () => {
23
+ let testDir: string;
24
+ let cliProcess: ChildProcess | null = null;
25
+ const cliPath = path.resolve(process.cwd(), 'dist/cli/index.js');
26
+
27
+ beforeEach(() => {
28
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-run-e2e-'));
29
+
30
+ // Minimal project scaffolding: a git repo + .multiagent/teams/<name>/
31
+ const repoPath = path.join(testDir, 'repo');
32
+ fs.mkdirSync(repoPath);
33
+ // initialize git so git-cascade can attach
34
+ const { execSync } = require('child_process');
35
+ execSync('git init -b main', { cwd: repoPath, stdio: 'pipe' });
36
+ execSync('git config user.email "t@t.com"', { cwd: repoPath, stdio: 'pipe' });
37
+ execSync('git config user.name "T"', { cwd: repoPath, stdio: 'pipe' });
38
+ fs.writeFileSync(path.join(repoPath, 'README.md'), '# test');
39
+ execSync('git add .', { cwd: repoPath, stdio: 'pipe' });
40
+ execSync('git commit -m "init"', { cwd: repoPath, stdio: 'pipe' });
41
+
42
+ // Minimal team template (no macro_agent.workspace — simplest path)
43
+ const teamDir = path.join(repoPath, '.multiagent/teams/minimal');
44
+ fs.mkdirSync(teamDir, { recursive: true });
45
+ fs.mkdirSync(path.join(teamDir, 'prompts'));
46
+ fs.writeFileSync(
47
+ path.join(teamDir, 'team.yaml'),
48
+ `name: minimal
49
+ description: Minimal team for CLI e2e
50
+ version: 1
51
+
52
+ roles:
53
+ - worker
54
+
55
+ topology:
56
+ root:
57
+ role: worker
58
+ prompt: prompts/worker.md
59
+
60
+ communication:
61
+ enforcement: permissive
62
+ `
63
+ );
64
+ fs.writeFileSync(path.join(teamDir, 'prompts/worker.md'), '# Worker\nDo work.\n');
65
+ });
66
+
67
+ afterEach(async () => {
68
+ if (cliProcess && !cliProcess.killed) {
69
+ cliProcess.kill('SIGINT');
70
+ await new Promise((resolve) => setTimeout(resolve, 500));
71
+ if (!cliProcess.killed) cliProcess.kill('SIGKILL');
72
+ }
73
+ cliProcess = null;
74
+ if (fs.existsSync(testDir)) {
75
+ fs.rmSync(testDir, { recursive: true, force: true });
76
+ }
77
+ });
78
+
79
+ it('boots, starts the team, and responds to SIGINT with clean shutdown', async () => {
80
+ const repoPath = path.join(testDir, 'repo');
81
+ const baseDir = path.join(testDir, 'state');
82
+ fs.mkdirSync(baseDir);
83
+
84
+ cliProcess = spawn(
85
+ 'node',
86
+ [cliPath, 'run', 'minimal', '--cwd', repoPath, '--base-path', repoPath],
87
+ {
88
+ env: {
89
+ ...process.env,
90
+ MACRO_BASE_DIR: baseDir,
91
+ },
92
+ stdio: ['ignore', 'pipe', 'pipe'],
93
+ }
94
+ );
95
+
96
+ let stdout = '';
97
+ let stderr = '';
98
+ cliProcess.stdout?.on('data', (chunk: Buffer) => {
99
+ stdout += chunk.toString();
100
+ });
101
+ cliProcess.stderr?.on('data', (chunk: Buffer) => {
102
+ stderr += chunk.toString();
103
+ });
104
+
105
+ // Wait for the CLI to reach "Team started" or timeout
106
+ const booted = await new Promise<boolean>((resolve) => {
107
+ const timeout = setTimeout(() => resolve(false), 15_000);
108
+ const check = setInterval(() => {
109
+ if (stdout.includes('Team started') || stdout.includes('team started')) {
110
+ clearTimeout(timeout);
111
+ clearInterval(check);
112
+ resolve(true);
113
+ }
114
+ }, 100);
115
+ });
116
+
117
+ if (!booted) {
118
+ console.error('stdout:', stdout);
119
+ console.error('stderr:', stderr);
120
+ }
121
+ expect(booted).toBe(true);
122
+ expect(stdout).toContain('booted');
123
+
124
+ // Send SIGINT and verify clean exit
125
+ const exitPromise = new Promise<number | null>((resolve) => {
126
+ cliProcess!.once('exit', (code) => resolve(code));
127
+ });
128
+
129
+ cliProcess.kill('SIGINT');
130
+ const exitCode = await Promise.race([
131
+ exitPromise,
132
+ new Promise<number | null>((resolve) => setTimeout(() => resolve(-1), 5_000)),
133
+ ]);
134
+
135
+ expect(exitCode).toBe(0);
136
+ });
137
+
138
+ it('prints an error and exits non-zero when team does not exist', async () => {
139
+ const repoPath = path.join(testDir, 'repo');
140
+ const baseDir = path.join(testDir, 'state');
141
+ fs.mkdirSync(baseDir);
142
+
143
+ cliProcess = spawn(
144
+ 'node',
145
+ [cliPath, 'run', 'no-such-team', '--cwd', repoPath, '--base-path', repoPath],
146
+ {
147
+ env: { ...process.env, MACRO_BASE_DIR: baseDir },
148
+ stdio: ['ignore', 'pipe', 'pipe'],
149
+ }
150
+ );
151
+
152
+ let stderr = '';
153
+ let stdout = '';
154
+ cliProcess.stderr?.on('data', (c: Buffer) => (stderr += c.toString()));
155
+ cliProcess.stdout?.on('data', (c: Buffer) => (stdout += c.toString()));
156
+
157
+ const exitCode: number | null = await new Promise((resolve) => {
158
+ cliProcess!.once('exit', (code) => resolve(code));
159
+ setTimeout(() => resolve(-1), 15_000);
160
+ });
161
+
162
+ expect(exitCode).not.toBe(0);
163
+ // Accept error text from either stdout (chalk.red) or stderr
164
+ const combined = stdout + stderr;
165
+ expect(combined.toLowerCase()).toMatch(/fail|error|no-such-team/);
166
+ });
167
+ });
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Self-driving team V3 end-to-end test.
3
+ *
4
+ * Exercises the complete path from a real team YAML on disk:
5
+ * .multiagent/teams/self-driving/team.yaml
6
+ * → TeamManagerV2.startTeam("self-driving")
7
+ * → extracts macro_agent.workspace block
8
+ * → constructs YamlDrivenTopology, installs on AgentManager
9
+ * → onTeamStart creates the team root stream
10
+ * → agents spawn with the correct V3 WorkspaceDecision per role
11
+ *
12
+ * Uses mocked acp-factory to avoid spawning real Claude Code — the purpose
13
+ * here is verifying the wiring, not agent behavior.
14
+ *
15
+ * REQUIRES: RUN_E2E_TESTS=true
16
+ */
17
+
18
+ import {
19
+ describe,
20
+ it,
21
+ expect,
22
+ beforeEach,
23
+ afterEach,
24
+ vi,
25
+ } from 'vitest';
26
+ import * as fs from 'fs';
27
+ import * as os from 'os';
28
+ import * as path from 'path';
29
+ import { execSync } from 'child_process';
30
+ import { bootV2, type MacroAgentSystemV2 } from '../../boot-v2.js';
31
+ import { GitCascadeAdapter } from '../../workspace/git-cascade-adapter.js';
32
+ import {
33
+ DefaultWorkspaceManager,
34
+ createWorkspaceManagerWithAdapter,
35
+ } from '../../workspace/workspace-manager.js';
36
+ import { TeamManagerV2 } from '../../teams/team-manager-v2.js';
37
+
38
+ const RUN_E2E = !!process.env.RUN_E2E_TESTS;
39
+ const describeFn = RUN_E2E ? describe : describe.skip;
40
+
41
+ // Mock acp-factory so we don't spawn real Claude Code
42
+ vi.mock('acp-factory', () => ({
43
+ AgentFactory: {
44
+ spawn: vi.fn().mockResolvedValue({
45
+ createSession: vi.fn().mockResolvedValue({
46
+ id: `session-${Date.now()}`,
47
+ prompt: vi.fn().mockReturnValue({
48
+ [Symbol.asyncIterator]: () => ({
49
+ next: () => Promise.resolve({ done: true, value: undefined }),
50
+ }),
51
+ }),
52
+ forkWithFlush: vi.fn().mockResolvedValue({ id: `forked-${Date.now()}` }),
53
+ }),
54
+ loadSession: vi.fn().mockResolvedValue({ id: `loaded-${Date.now()}` }),
55
+ close: vi.fn().mockResolvedValue(undefined),
56
+ isRunning: vi.fn().mockReturnValue(true),
57
+ }),
58
+ },
59
+ }));
60
+
61
+ vi.mock('opentasks', () => ({
62
+ OpenTasksClient: vi.fn().mockImplementation(() => ({
63
+ connect: vi.fn().mockRejectedValue(new Error('No daemon')),
64
+ disconnect: vi.fn(),
65
+ query: vi.fn().mockResolvedValue({ items: [] }),
66
+ link: vi.fn().mockResolvedValue({ success: true }),
67
+ task: vi.fn().mockResolvedValue({ id: 't-1' }),
68
+ })),
69
+ }));
70
+
71
+ describeFn('Self-driving team V3 auto-wire', () => {
72
+ let system: MacroAgentSystemV2;
73
+ let testDir: string;
74
+ let repoPath: string;
75
+ let adapter: GitCascadeAdapter;
76
+ let workspaceManager: DefaultWorkspaceManager;
77
+ let teamManager: TeamManagerV2;
78
+ const projectRoot = process.cwd();
79
+
80
+ beforeEach(async () => {
81
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'self-driving-e2e-'));
82
+ repoPath = path.join(testDir, 'repo');
83
+ fs.mkdirSync(repoPath);
84
+
85
+ execSync('git init -b main', { cwd: repoPath, stdio: 'pipe' });
86
+ execSync('git config user.email "t@t.com"', { cwd: repoPath, stdio: 'pipe' });
87
+ execSync('git config user.name "T"', { cwd: repoPath, stdio: 'pipe' });
88
+ fs.writeFileSync(path.join(repoPath, 'README.md'), '# test');
89
+ execSync('git add .', { cwd: repoPath, stdio: 'pipe' });
90
+ execSync('git commit -m "init"', { cwd: repoPath, stdio: 'pipe' });
91
+
92
+ adapter = new GitCascadeAdapter({
93
+ enabled: true,
94
+ repoPath,
95
+ dbPath: path.join(testDir, 'gc.db'),
96
+ skipRecovery: true,
97
+ });
98
+ workspaceManager = createWorkspaceManagerWithAdapter(adapter, {
99
+ worktreeBaseDir: path.join(repoPath, '.worktrees'),
100
+ }) as DefaultWorkspaceManager;
101
+
102
+ system = await bootV2({
103
+ cwd: repoPath,
104
+ baseDir: testDir,
105
+ inbox: { socketPath: path.join(testDir, 'inbox.sock') },
106
+ workspaceManager,
107
+ });
108
+
109
+ teamManager = new TeamManagerV2({
110
+ agentManager: system.agentManager,
111
+ inboxAdapter: system.inboxAdapter,
112
+ tasksAdapter: system.tasksAdapter,
113
+ workspaceManager,
114
+ });
115
+ teamManager.install();
116
+ });
117
+
118
+ afterEach(async () => {
119
+ if (system) await system.shutdown();
120
+ if (workspaceManager) workspaceManager.close();
121
+ if (adapter) adapter.close();
122
+ if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
123
+ });
124
+
125
+ it('startTeam auto-wires YamlDrivenTopology from macro_agent.workspace', async () => {
126
+ // Start the real self-driving team template from .multiagent/teams/
127
+ await teamManager.startTeam('self-driving', projectRoot);
128
+
129
+ // Team root stream should have been created by onTeamStart
130
+ const streams = workspaceManager.listStreams();
131
+ const teamRoot = streams.find((s) => s.agentId === 'team:self-driving');
132
+ expect(teamRoot).toBeDefined();
133
+ expect(teamRoot!.name).toBe('self-driving');
134
+ });
135
+
136
+ it('spawning a grinder via the V3 path forks a stream off team root', async () => {
137
+ await teamManager.startTeam('self-driving', projectRoot);
138
+
139
+ const teamStreams = workspaceManager.listStreams();
140
+ const teamRoot = teamStreams.find((s) => s.agentId === 'team:self-driving');
141
+ expect(teamRoot).toBeDefined();
142
+
143
+ const grinder = await system.agentManager.spawn({
144
+ role: 'grinder',
145
+ task: 'do a thing',
146
+ });
147
+
148
+ const record = system.agentStore.getAgent(grinder.id);
149
+ expect(record?.workspace_path).toBeDefined();
150
+ expect(fs.existsSync(record!.workspace_path!)).toBe(true);
151
+
152
+ const grinderStream = workspaceManager
153
+ .listStreams()
154
+ .find((s) => s.agentId === grinder.id);
155
+ expect(grinderStream).toBeDefined();
156
+ expect(grinderStream?.parentStream).toBe(teamRoot!.id);
157
+ });
158
+
159
+ it('judge spawn returns workspace: none (no worktree allocated)', async () => {
160
+ await teamManager.startTeam('self-driving', projectRoot);
161
+
162
+ const judge = await system.agentManager.spawn({
163
+ role: 'judge',
164
+ task: 'review',
165
+ });
166
+
167
+ const record = system.agentStore.getAgent(judge.id);
168
+ // workspace: none → no workspace_path assigned
169
+ expect(record?.workspace_path).toBeFalsy();
170
+ });
171
+
172
+ it('planner attaches to team root stream (not a new fork)', async () => {
173
+ // Note: self-driving's topology already bootstraps the planner as the
174
+ // root agent; we spawn explicitly to verify the decision path.
175
+ await teamManager.startTeam('self-driving', projectRoot);
176
+
177
+ const plannerBeforeBootstrap = system.agentStore.listAgents({
178
+ state: 'running',
179
+ });
180
+
181
+ // The bootstrap planner is the "root" — already spawned.
182
+ const rootPlanner = plannerBeforeBootstrap.find((a) => a.role === 'planner');
183
+ expect(rootPlanner).toBeDefined();
184
+
185
+ // Verify its workspace_path (if allocated) is on the team root branch
186
+ if (rootPlanner?.workspace_path) {
187
+ expect(fs.existsSync(rootPlanner.workspace_path)).toBe(true);
188
+ }
189
+
190
+ // The team_root stream should still be the only top-level stream
191
+ // (planner did not fork a new stream — it attached)
192
+ const topLevelStreams = workspaceManager
193
+ .listStreams()
194
+ .filter((s) => !s.parentStream);
195
+ expect(topLevelStreams.length).toBe(1);
196
+ });
197
+ });