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,257 @@
1
+ /**
2
+ * on_parent_advanced: sync_with_parent auto-sync e2e.
3
+ *
4
+ * Scenario: a role with `on_parent_advanced: sync_with_parent` has its
5
+ * stream automatically rebased onto its parent when the parent advances.
6
+ *
7
+ * Fixture:
8
+ * team_root (parent of both)
9
+ * ├── feature_owner stream (declares on_parent_advanced: sync_with_parent)
10
+ * └── (we simulate the parent advancing via commitChanges on team_root
11
+ * from a separate agent)
12
+ *
13
+ * Verifies:
14
+ * - Topology subscribes to stream:committed events on onTeamStart
15
+ * - syncWithParent is invoked on the feature_owner's stream when team_root
16
+ * gets a new commit
17
+ * - Coalescing: back-to-back commits within the debounce window produce
18
+ * only one sync call
19
+ *
20
+ * REQUIRES: RUN_E2E_TESTS=true
21
+ */
22
+
23
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
24
+ import * as fs from 'fs';
25
+ import * as path from 'path';
26
+ import * as os from 'os';
27
+ import { execSync } from 'child_process';
28
+ import { GitCascadeAdapter, createGitCascadeAdapter } from '../../workspace/git-cascade-adapter.js';
29
+ import {
30
+ DefaultWorkspaceManager,
31
+ createWorkspaceManagerWithAdapter,
32
+ } from '../../workspace/workspace-manager.js';
33
+ import { YamlDrivenTopology } from '../../workspace/topology/yaml-driven.js';
34
+ import { parseTeamWorkspaceConfig } from '../../workspace/yaml-schema.js';
35
+
36
+ const RUN_E2E = !!process.env.RUN_E2E_TESTS;
37
+ const describeFn = RUN_E2E ? describe : describe.skip;
38
+
39
+ describeFn('on_parent_advanced auto-sync', () => {
40
+ let tempDir: string;
41
+ let repoPath: string;
42
+ let adapter: GitCascadeAdapter;
43
+ let manager: DefaultWorkspaceManager;
44
+
45
+ beforeEach(() => {
46
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'auto-sync-'));
47
+ repoPath = path.join(tempDir, 'repo');
48
+ fs.mkdirSync(repoPath);
49
+
50
+ execSync('git init -b main', { cwd: repoPath, stdio: 'pipe' });
51
+ execSync('git config user.email "t@t.com"', { cwd: repoPath, stdio: 'pipe' });
52
+ execSync('git config user.name "T"', { cwd: repoPath, stdio: 'pipe' });
53
+ fs.writeFileSync(path.join(repoPath, 'README.md'), '# test\n');
54
+ execSync('git add .', { cwd: repoPath, stdio: 'pipe' });
55
+ execSync('git commit -m "init"', { cwd: repoPath, stdio: 'pipe' });
56
+
57
+ adapter = createGitCascadeAdapter({
58
+ enabled: true,
59
+ repoPath,
60
+ dbPath: path.join(tempDir, 'gc.db'),
61
+ skipRecovery: true,
62
+ });
63
+ manager = createWorkspaceManagerWithAdapter(adapter, {
64
+ worktreeBaseDir: path.join(tempDir, 'worktrees'),
65
+ }) as DefaultWorkspaceManager;
66
+ });
67
+
68
+ afterEach(() => {
69
+ manager.close();
70
+ adapter.close();
71
+ if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
72
+ });
73
+
74
+ it('subscribes to stream:committed and syncs affected children', async () => {
75
+ const config = parseTeamWorkspaceConfig({
76
+ roles: {
77
+ feature_owner: {
78
+ workspace: 'new_stream',
79
+ stream_lineage: 'fork_from_team_root',
80
+ landing: 'merge_to_parent_stream',
81
+ on_parent_advanced: 'sync_with_parent',
82
+ on_conflict: 'ours',
83
+ },
84
+ },
85
+ });
86
+ const topology = new YamlDrivenTopology(config!);
87
+ const startCtx = {
88
+ teamName: 'sync-test',
89
+ teamInstanceId: 'sync-1',
90
+ workspaceConfig: config,
91
+ workspaceManager: manager,
92
+ };
93
+ await topology.onTeamStart(startCtx);
94
+
95
+ const teamStream = manager
96
+ .listStreams()
97
+ .find((s) => s.agentId === 'team:sync-test');
98
+ expect(teamStream).toBeDefined();
99
+
100
+ // Spawn a feature_owner and allocate its workspace
101
+ const decision = await topology.onAgentSpawn({
102
+ agentId: 'agent-feat',
103
+ role: 'feature_owner',
104
+ workspaceManager: manager,
105
+ });
106
+ expect(decision.kind).toBe('new-stream');
107
+ let featStreamId: string | null = null;
108
+ if (decision.kind === 'new-stream') {
109
+ featStreamId = manager.createStreamV3(decision.streamSpec);
110
+ manager.allocateWorktree({
111
+ agentId: 'agent-feat',
112
+ streamId: featStreamId,
113
+ });
114
+ topology.recordAgentStream('agent-feat', featStreamId, 'feature_owner');
115
+ }
116
+ expect(featStreamId).not.toBeNull();
117
+
118
+ // Spy on syncWithParent to detect auto-sync calls
119
+ const syncSpy = vi.spyOn(manager, 'syncWithParent');
120
+
121
+ // Simulate team_root advancing: commit something via a separate agent
122
+ // on the team_root stream.
123
+ manager.allocateWorktree({
124
+ agentId: 'agent-on-root',
125
+ streamId: teamStream!.id,
126
+ });
127
+ const rootWorktree = manager.getWorktreeForAgent('agent-on-root')!;
128
+ fs.writeFileSync(path.join(rootWorktree.path, 'advance.txt'), 'new content\n');
129
+ manager.commitChanges({
130
+ agentId: 'agent-on-root',
131
+ streamId: teamStream!.id,
132
+ worktree: rootWorktree.path,
133
+ message: 'advance team root',
134
+ });
135
+
136
+ // Allow event handler to fire (it's sync but dispatch is async)
137
+ await new Promise((r) => setTimeout(r, 50));
138
+
139
+ expect(syncSpy).toHaveBeenCalled();
140
+ const call = syncSpy.mock.calls[0]?.[0];
141
+ expect(call?.streamId).toBe(featStreamId);
142
+ expect(call?.agentId).toBe('agent-feat');
143
+ expect(call?.onConflict).toBe('ours');
144
+
145
+ await topology.onTeamStop({
146
+ teamName: 'sync-test',
147
+ teamInstanceId: 'sync-1',
148
+ teamStreamId: teamStream!.id,
149
+ workspaceManager: manager,
150
+ });
151
+ });
152
+
153
+ it('coalesces back-to-back commits within debounce window', async () => {
154
+ const config = parseTeamWorkspaceConfig({
155
+ roles: {
156
+ feature_owner: {
157
+ workspace: 'new_stream',
158
+ stream_lineage: 'fork_from_team_root',
159
+ on_parent_advanced: 'sync_with_parent',
160
+ },
161
+ },
162
+ });
163
+ const topology = new YamlDrivenTopology(config!);
164
+ await topology.onTeamStart({
165
+ teamName: 'coalesce-test',
166
+ teamInstanceId: 'c-1',
167
+ workspaceConfig: config,
168
+ workspaceManager: manager,
169
+ });
170
+
171
+ const teamStream = manager
172
+ .listStreams()
173
+ .find((s) => s.agentId === 'team:coalesce-test');
174
+ expect(teamStream).toBeDefined();
175
+
176
+ const decision = await topology.onAgentSpawn({
177
+ agentId: 'agent-feat',
178
+ role: 'feature_owner',
179
+ workspaceManager: manager,
180
+ });
181
+ let featStreamId: string | null = null;
182
+ if (decision.kind === 'new-stream') {
183
+ featStreamId = manager.createStreamV3(decision.streamSpec);
184
+ manager.allocateWorktree({
185
+ agentId: 'agent-feat',
186
+ streamId: featStreamId,
187
+ });
188
+ topology.recordAgentStream('agent-feat', featStreamId, 'feature_owner');
189
+ }
190
+
191
+ const syncSpy = vi.spyOn(manager, 'syncWithParent');
192
+
193
+ manager.allocateWorktree({
194
+ agentId: 'agent-root',
195
+ streamId: teamStream!.id,
196
+ });
197
+ const rootWorktree = manager.getWorktreeForAgent('agent-root')!;
198
+
199
+ // 3 back-to-back commits — should coalesce to 1 sync
200
+ for (let i = 0; i < 3; i++) {
201
+ fs.writeFileSync(
202
+ path.join(rootWorktree.path, `c${i}.txt`),
203
+ `content ${i}\n`
204
+ );
205
+ manager.commitChanges({
206
+ agentId: 'agent-root',
207
+ streamId: teamStream!.id,
208
+ worktree: rootWorktree.path,
209
+ message: `commit ${i}`,
210
+ });
211
+ }
212
+
213
+ await new Promise((r) => setTimeout(r, 50));
214
+ expect(syncSpy.mock.calls.length).toBe(1);
215
+ });
216
+
217
+ it('does not subscribe when no role declares on_parent_advanced', async () => {
218
+ const config = parseTeamWorkspaceConfig({
219
+ roles: {
220
+ plain_role: {
221
+ workspace: 'new_stream',
222
+ stream_lineage: 'fork_from_team_root',
223
+ },
224
+ },
225
+ });
226
+ const topology = new YamlDrivenTopology(config!);
227
+ await topology.onTeamStart({
228
+ teamName: 'no-autosync',
229
+ teamInstanceId: 'n-1',
230
+ workspaceConfig: config,
231
+ workspaceManager: manager,
232
+ });
233
+
234
+ const teamStream = manager
235
+ .listStreams()
236
+ .find((s) => s.agentId === 'team:no-autosync');
237
+
238
+ const syncSpy = vi.spyOn(manager, 'syncWithParent');
239
+
240
+ // Even if we commit on team_root, no sync should fire
241
+ manager.allocateWorktree({
242
+ agentId: 'agent-x',
243
+ streamId: teamStream!.id,
244
+ });
245
+ const wt = manager.getWorktreeForAgent('agent-x')!;
246
+ fs.writeFileSync(path.join(wt.path, 'x.txt'), 'x\n');
247
+ manager.commitChanges({
248
+ agentId: 'agent-x',
249
+ streamId: teamStream!.id,
250
+ worktree: wt.path,
251
+ message: 'x',
252
+ });
253
+
254
+ await new Promise((r) => setTimeout(r, 50));
255
+ expect(syncSpy).not.toHaveBeenCalled();
256
+ });
257
+ });
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Bootstrap & Spawn-Options E2E Tests
3
+ *
4
+ * Boots macro-agent with the MAP server enabled and verifies:
5
+ * 1. `bootstrap.coordinator: true` (programmatic) auto-spawns a coordinator
6
+ * that's discoverable via the MAP protocol.
7
+ * 2. `MACRO_BOOTSTRAP_COORDINATOR=true` env var triggers the same path —
8
+ * this is how `openswarm` / openhive get bootstrap without modifying
9
+ * openswarm's whitelisted bootConfig pass-through.
10
+ * 3. `_macro/spawnAgent` extension forwards the full SpawnAgentOptions
11
+ * surface (permissionMode, agentType, customPrompt, config, taskRef)
12
+ * so the agent record carries the requested settings.
13
+ *
14
+ * Run:
15
+ * npx vitest run --config vitest.e2e.config.ts \
16
+ * src/__tests__/e2e/bootstrap.e2e.test.ts
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 { bootV2, type MacroAgentSystemV2 } from "../../boot-v2.js";
24
+ import { ClientConnection } from "@multi-agent-protocol/sdk";
25
+
26
+ /** Wait for a coordinator agent to land in the agent store. */
27
+ async function waitForCoordinator(
28
+ system: MacroAgentSystemV2,
29
+ timeoutMs = 5000,
30
+ ): Promise<{ id: string; cwd?: string | null; role: string } | null> {
31
+ const deadline = Date.now() + timeoutMs;
32
+ while (Date.now() < deadline) {
33
+ const agents = system.agentStore.listAgents({ role: "coordinator" });
34
+ if (agents.length > 0) return agents[0];
35
+ await new Promise((r) => setTimeout(r, 50));
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function freshDir(prefix: string): string {
41
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
42
+ }
43
+
44
+ // ─────────────────────────────────────────────────────────────────────────────
45
+ // Bootstrap: programmatic
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+
48
+ describe("Bootstrap E2E — programmatic config", () => {
49
+ let system: MacroAgentSystemV2 | null = null;
50
+ let client: ClientConnection | null = null;
51
+ let dir: string;
52
+
53
+ beforeEach(() => {
54
+ dir = freshDir("bootstrap-prog-");
55
+ });
56
+
57
+ afterEach(async () => {
58
+ if (client) {
59
+ try { await client.disconnect(); } catch { /* */ }
60
+ client = null;
61
+ }
62
+ if (system) {
63
+ await system.shutdown();
64
+ system = null;
65
+ }
66
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* */ }
67
+ });
68
+
69
+ it("auto-spawns a coordinator when bootstrap.coordinator: true", async () => {
70
+ system = await bootV2({
71
+ baseDir: dir,
72
+ cwd: dir,
73
+ defaultPermissionMode: "auto-approve",
74
+ mapServer: { enabled: true, port: 0, host: "127.0.0.1" },
75
+ bootstrap: { coordinator: true },
76
+ });
77
+
78
+ const agent = await waitForCoordinator(system);
79
+ expect(agent).not.toBeNull();
80
+ expect(agent!.role).toBe("coordinator");
81
+ expect(agent!.cwd).toBe(dir);
82
+
83
+ // Discoverable via MAP listAgents. The lifecycle handler that registers
84
+ // the agent in mapServer.agents fires after the AgentManager.spawn()
85
+ // promise resolves, but listAgents may race that registration in the
86
+ // first poll cycle. Poll briefly until it appears.
87
+ const url = system.mapServerInstance!.getUrl();
88
+ client = await ClientConnection.connect(url, { name: "bootstrap-test-client" });
89
+
90
+ let coordinators: unknown[] = [];
91
+ const deadline = Date.now() + 5000;
92
+ while (Date.now() < deadline) {
93
+ const result = await client.listAgents();
94
+ coordinators = result.agents.filter((a: any) => a.role === "coordinator");
95
+ if (coordinators.length >= 1) break;
96
+ await new Promise((r) => setTimeout(r, 100));
97
+ }
98
+ expect(coordinators.length).toBeGreaterThanOrEqual(1);
99
+ }, 20000);
100
+
101
+ it("uses bootstrap.coordinator.cwd when provided", async () => {
102
+ const projectDir = path.join(dir, "project-x");
103
+ fs.mkdirSync(projectDir, { recursive: true });
104
+
105
+ system = await bootV2({
106
+ baseDir: dir,
107
+ cwd: dir,
108
+ defaultPermissionMode: "auto-approve",
109
+ mapServer: { enabled: true, port: 0, host: "127.0.0.1" },
110
+ bootstrap: { coordinator: { cwd: projectDir } },
111
+ });
112
+
113
+ const agent = await waitForCoordinator(system);
114
+ expect(agent!.cwd).toBe(projectDir);
115
+ }, 20000);
116
+ });
117
+
118
+ // ─────────────────────────────────────────────────────────────────────────────
119
+ // Bootstrap: env-var bridge
120
+ // ─────────────────────────────────────────────────────────────────────────────
121
+
122
+ describe("Bootstrap E2E — env-var bridge", () => {
123
+ let system: MacroAgentSystemV2 | null = null;
124
+ let dir: string;
125
+
126
+ beforeEach(() => {
127
+ dir = freshDir("bootstrap-env-");
128
+ });
129
+
130
+ afterEach(async () => {
131
+ if (system) {
132
+ await system.shutdown();
133
+ system = null;
134
+ }
135
+ delete process.env.MACRO_BOOTSTRAP_COORDINATOR;
136
+ delete process.env.MACRO_BOOTSTRAP_CWD;
137
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* */ }
138
+ });
139
+
140
+ it("MACRO_BOOTSTRAP_COORDINATOR=true triggers bootstrap", async () => {
141
+ process.env.MACRO_BOOTSTRAP_COORDINATOR = "true";
142
+
143
+ system = await bootV2({
144
+ baseDir: dir,
145
+ cwd: dir,
146
+ defaultPermissionMode: "auto-approve",
147
+ mapServer: { enabled: true, port: 0, host: "127.0.0.1" },
148
+ });
149
+
150
+ const agent = await waitForCoordinator(system);
151
+ expect(agent).not.toBeNull();
152
+ expect(agent!.cwd).toBe(dir);
153
+ }, 20000);
154
+
155
+ it("MACRO_BOOTSTRAP_CWD overrides default cwd", async () => {
156
+ const projectDir = path.join(dir, "env-project");
157
+ fs.mkdirSync(projectDir, { recursive: true });
158
+
159
+ process.env.MACRO_BOOTSTRAP_COORDINATOR = "true";
160
+ process.env.MACRO_BOOTSTRAP_CWD = projectDir;
161
+
162
+ system = await bootV2({
163
+ baseDir: dir,
164
+ cwd: dir,
165
+ defaultPermissionMode: "auto-approve",
166
+ mapServer: { enabled: true, port: 0, host: "127.0.0.1" },
167
+ });
168
+
169
+ const agent = await waitForCoordinator(system);
170
+ expect(agent!.cwd).toBe(projectDir);
171
+ }, 20000);
172
+
173
+ it("does NOT spawn when env var is unset", async () => {
174
+ system = await bootV2({
175
+ baseDir: dir,
176
+ cwd: dir,
177
+ defaultPermissionMode: "auto-approve",
178
+ mapServer: { enabled: true, port: 0, host: "127.0.0.1" },
179
+ });
180
+
181
+ // Give any rogue spawn a chance to fire before asserting absence.
182
+ await new Promise((r) => setTimeout(r, 500));
183
+ const agents = system.agentStore.listAgents({ role: "coordinator" });
184
+ expect(agents).toHaveLength(0);
185
+ }, 20000);
186
+ });
187
+
188
+ // ─────────────────────────────────────────────────────────────────────────────
189
+ // Spawn options forwarding via _macro/spawnAgent
190
+ // ─────────────────────────────────────────────────────────────────────────────
191
+
192
+ describe("Spawn-options forwarding E2E — _macro/spawnAgent", () => {
193
+ let system: MacroAgentSystemV2 | null = null;
194
+ let client: ClientConnection | null = null;
195
+ let dir: string;
196
+
197
+ beforeEach(async () => {
198
+ dir = freshDir("spawn-opts-");
199
+ system = await bootV2({
200
+ baseDir: dir,
201
+ cwd: dir,
202
+ defaultPermissionMode: "auto-approve",
203
+ mapServer: { enabled: true, port: 0, host: "127.0.0.1" },
204
+ });
205
+ const url = system.mapServerInstance!.getUrl();
206
+ client = await ClientConnection.connect(url, { name: "spawn-opts-client" });
207
+ });
208
+
209
+ afterEach(async () => {
210
+ if (client) {
211
+ try { await client.disconnect(); } catch { /* */ }
212
+ client = null;
213
+ }
214
+ if (system) {
215
+ await system.shutdown();
216
+ system = null;
217
+ }
218
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* */ }
219
+ });
220
+
221
+ it("forwards role, cwd, task to the spawned agent record", async () => {
222
+ const projectDir = path.join(dir, "proj-a");
223
+ fs.mkdirSync(projectDir, { recursive: true });
224
+
225
+ const result = await client!.callExtension("_macro/spawnAgent", {
226
+ role: "coordinator",
227
+ cwd: projectDir,
228
+ task: "Test task description",
229
+ }) as { agent?: { id?: string; localId?: string } };
230
+
231
+ const localId = result.agent?.localId;
232
+ expect(localId).toBeTruthy();
233
+
234
+ const record = system!.agentStore.getAgent(localId!);
235
+ expect(record).not.toBeNull();
236
+ expect(record!.role).toBe("coordinator");
237
+ expect(record!.cwd).toBe(projectDir);
238
+ expect(record!.task).toBe("Test task description");
239
+ expect(record!.parent_id).toBeNull();
240
+ }, 15000);
241
+
242
+ it("forwards config (model, maxTokens, temperature) onto the agent record", async () => {
243
+ const result = await client!.callExtension("_macro/spawnAgent", {
244
+ role: "coordinator",
245
+ cwd: dir,
246
+ task: "Config test",
247
+ config: {
248
+ model: "claude-sonnet-4-6",
249
+ maxTokens: 4096,
250
+ temperature: 0.42,
251
+ },
252
+ }) as { agent?: { localId?: string } };
253
+
254
+ const localId = result.agent?.localId;
255
+ expect(localId).toBeTruthy();
256
+
257
+ const record = system!.agentStore.getAgent(localId!);
258
+ expect(record!.config).toBeDefined();
259
+ expect(record!.config!.model).toBe("claude-sonnet-4-6");
260
+ expect(record!.config!.maxTokens).toBe(4096);
261
+ expect(record!.config!.temperature).toBeCloseTo(0.42);
262
+ }, 15000);
263
+
264
+ it("forwards customPrompt by reflecting it in the agent's task surface", async () => {
265
+ // customPrompt is woven into the system prompt at spawn time; we can't
266
+ // easily inspect the assembled prompt from outside, but we can verify the
267
+ // spawn succeeds when customPrompt is set (no validation rejection /
268
+ // dropped-field bug). Combined with the unit check that the wire forwards
269
+ // the field, this confirms the round trip is intact.
270
+ const result = await client!.callExtension("_macro/spawnAgent", {
271
+ role: "coordinator",
272
+ cwd: dir,
273
+ task: "Custom prompt test",
274
+ customPrompt: "You are a meticulous code reviewer. Cite line numbers.",
275
+ }) as { agent?: { localId?: string; id?: string } };
276
+
277
+ expect(result.agent?.localId).toBeTruthy();
278
+ const record = system!.agentStore.getAgent(result.agent!.localId!);
279
+ expect(record).not.toBeNull();
280
+ expect(record!.role).toBe("coordinator");
281
+ }, 15000);
282
+
283
+ it("ignores unknown role when role is omitted (defaults to worker)", async () => {
284
+ const result = await client!.callExtension("_macro/spawnAgent", {
285
+ cwd: dir,
286
+ task: "No role specified",
287
+ }) as { agent?: { localId?: string } };
288
+
289
+ const localId = result.agent?.localId;
290
+ const record = system!.agentStore.getAgent(localId!);
291
+ // The handler defaults role to "worker" when omitted.
292
+ expect(record!.role).toBe("worker");
293
+ }, 15000);
294
+
295
+ it("multiple spawns with same cwd produce distinct agents (no implicit dedup)", async () => {
296
+ // The spawn endpoint always spawns. Get-or-create semantics live on the
297
+ // openhive caller side, not in macro-agent's _macro/spawnAgent.
298
+ const r1 = await client!.callExtension("_macro/spawnAgent", {
299
+ role: "coordinator",
300
+ cwd: dir,
301
+ task: "First",
302
+ }) as { agent?: { localId?: string } };
303
+
304
+ const r2 = await client!.callExtension("_macro/spawnAgent", {
305
+ role: "coordinator",
306
+ cwd: dir,
307
+ task: "Second",
308
+ }) as { agent?: { localId?: string } };
309
+
310
+ expect(r1.agent?.localId).toBeTruthy();
311
+ expect(r2.agent?.localId).toBeTruthy();
312
+ expect(r1.agent?.localId).not.toBe(r2.agent?.localId);
313
+
314
+ const r1Record = system!.agentStore.getAgent(r1.agent!.localId!);
315
+ const r2Record = system!.agentStore.getAgent(r2.agent!.localId!);
316
+ expect(r1Record!.task).toBe("First");
317
+ expect(r2Record!.task).toBe("Second");
318
+ }, 20000);
319
+ });