macro-agent 0.1.8 → 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 (258) hide show
  1. package/CLAUDE.md +166 -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 +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 -43
  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 +16 -1
  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 -1
  55. package/dist/map/lifecycle-bridge.d.ts.map +1 -1
  56. package/dist/map/lifecycle-bridge.js +58 -23
  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 -2
  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 +66 -0
  197. package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
  198. package/src/agent/agent-manager-v2.ts +293 -48
  199. package/src/agent/agent-manager.ts +14 -0
  200. package/src/agent/types.ts +16 -2
  201. package/src/boot-v2.ts +68 -1
  202. package/src/cli/index.ts +61 -0
  203. package/src/cognitive/macro-agent-backend.ts +45 -29
  204. package/src/integrations/skilltree.ts +1 -0
  205. package/src/lifecycle/cleanup.ts +52 -3
  206. package/src/lifecycle/handlers-v2.ts +40 -3
  207. package/src/lifecycle/types.ts +12 -0
  208. package/src/map/__tests__/cascade-bridge.test.ts +229 -0
  209. package/src/map/__tests__/lifecycle-bridge.test.ts +86 -10
  210. package/src/map/acp-bridge.ts +26 -3
  211. package/src/map/cascade-bridge.ts +301 -0
  212. package/src/map/lifecycle-bridge.ts +52 -17
  213. package/src/map/server.ts +47 -6
  214. package/src/map/sidecar.ts +31 -1
  215. package/src/map/types.ts +20 -0
  216. package/src/mcp/tools/done-v2.ts +9 -0
  217. package/src/teams/team-manager-v2.ts +37 -0
  218. package/src/teams/team-runtime-v2.ts +23 -3
  219. package/src/workspace/__tests__/{dataplane-adapter.test.ts → git-cascade-adapter.test.ts} +209 -14
  220. package/src/workspace/__tests__/self-driving-yaml.test.ts +114 -0
  221. package/src/workspace/__tests__/shared-worktree-refcount.test.ts +154 -0
  222. package/src/workspace/__tests__/standalone-mode.test.ts +118 -0
  223. package/src/workspace/__tests__/workspace-manager-v3.test.ts +245 -0
  224. package/src/workspace/__tests__/yaml-schema.test.ts +210 -0
  225. package/src/workspace/config.ts +11 -11
  226. package/src/workspace/git-cascade-adapter.ts +1186 -0
  227. package/src/workspace/index.ts +11 -11
  228. package/src/workspace/landing/__tests__/strategies.test.ts +142 -0
  229. package/src/workspace/landing/direct-push.ts +91 -0
  230. package/src/workspace/landing/index.ts +40 -0
  231. package/src/workspace/landing/merge-to-parent.ts +228 -0
  232. package/src/workspace/landing/optimistic-push.ts +36 -0
  233. package/src/workspace/landing/queue-to-branch.ts +108 -0
  234. package/src/workspace/merge-queue/merge-queue.ts +10 -0
  235. package/src/workspace/merge-queue/types.ts +16 -2
  236. package/src/workspace/pool/__tests__/worktree-pool.integration.test.ts +5 -5
  237. package/src/workspace/pool/types.ts +1 -0
  238. package/src/workspace/pool/worktree-pool.ts +1 -0
  239. package/src/workspace/recovery/__tests__/auto-resolve-integration.test.ts +127 -0
  240. package/src/workspace/recovery/__tests__/spawn-resolver.test.ts +139 -0
  241. package/src/workspace/recovery/__tests__/strategies.test.ts +145 -0
  242. package/src/workspace/recovery/abandon.ts +51 -0
  243. package/src/workspace/recovery/auto-resolve.ts +119 -0
  244. package/src/workspace/recovery/defer.ts +23 -0
  245. package/src/workspace/recovery/escalate.ts +30 -0
  246. package/src/workspace/recovery/index.ts +58 -0
  247. package/src/workspace/recovery/spawn-resolver.ts +145 -0
  248. package/src/workspace/recovery/types.ts +54 -0
  249. package/src/workspace/topology/__tests__/yaml-driven.test.ts +345 -0
  250. package/src/workspace/topology/index.ts +18 -0
  251. package/src/workspace/topology/no-workspace.ts +39 -0
  252. package/src/workspace/topology/types.ts +116 -0
  253. package/src/workspace/topology/yaml-driven.ts +316 -0
  254. package/src/workspace/types-v3.ts +155 -0
  255. package/src/workspace/types.ts +191 -20
  256. package/src/workspace/workspace-manager.ts +474 -19
  257. package/src/workspace/yaml-schema.ts +216 -0
  258. package/src/workspace/dataplane-adapter.ts +0 -546
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Unit tests for createCascadeBridge.
3
+ *
4
+ * Exercises the GitCascadeEvent → x-cascade/* MAP call translation directly
5
+ * via a mock adapter + mock connection, avoiding real git operations. E2E
6
+ * coverage lives elsewhere (workspace-v3 tests + OpenHive hub round-trips).
7
+ */
8
+
9
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
10
+ import { createCascadeBridge } from '../cascade-bridge.js';
11
+ import type {
12
+ GitCascadeEvent,
13
+ GitCascadeEventCallback,
14
+ GitCascadeAdapter,
15
+ } from '../../workspace/git-cascade-adapter.js';
16
+ import type { LifecycleBridgeConnection } from '../lifecycle-bridge.js';
17
+
18
+ interface MockConnection extends LifecycleBridgeConnection {
19
+ calls: Array<{ method: string; params: unknown }>;
20
+ connected: boolean;
21
+ }
22
+
23
+ function createMockConnection(connected = true): MockConnection {
24
+ const calls: Array<{ method: string; params: unknown }> = [];
25
+ return {
26
+ calls,
27
+ connected,
28
+ get isConnected() {
29
+ return this.connected;
30
+ },
31
+ async callExtension(method: string, params?: unknown): Promise<unknown> {
32
+ this.calls.push({ method, params });
33
+ return { ok: true };
34
+ },
35
+ };
36
+ }
37
+
38
+ interface MockAdapter {
39
+ emit: (event: GitCascadeEvent) => void;
40
+ onEvent(callback: GitCascadeEventCallback): () => void;
41
+ }
42
+
43
+ function createMockAdapter(): MockAdapter {
44
+ const listeners = new Set<GitCascadeEventCallback>();
45
+ return {
46
+ emit(event: GitCascadeEvent) {
47
+ for (const listener of listeners) {
48
+ listener(event);
49
+ }
50
+ },
51
+ onEvent(callback: GitCascadeEventCallback): () => void {
52
+ listeners.add(callback);
53
+ return () => listeners.delete(callback);
54
+ },
55
+ };
56
+ }
57
+
58
+ function mkEvent<T extends GitCascadeEvent['type']>(
59
+ type: T,
60
+ data: Record<string, unknown>
61
+ ): GitCascadeEvent {
62
+ return { type, timestamp: Date.now(), data } as GitCascadeEvent;
63
+ }
64
+
65
+ describe('createCascadeBridge', () => {
66
+ let connection: MockConnection;
67
+ let adapter: MockAdapter;
68
+
69
+ beforeEach(() => {
70
+ connection = createMockConnection();
71
+ adapter = createMockAdapter();
72
+ });
73
+
74
+ async function flushMicrotasks() {
75
+ // The bridge's callExtension is fire-and-forget (void + .catch). Yield
76
+ // to the microtask queue so the promise settles before assertions.
77
+ await new Promise((resolve) => setImmediate(resolve));
78
+ }
79
+
80
+ it('forwards stream:created as x-cascade/stream.opened with snake_case params', async () => {
81
+ createCascadeBridge(connection, adapter as unknown as GitCascadeAdapter);
82
+ adapter.emit(
83
+ mkEvent('stream:created', {
84
+ streamId: 's1',
85
+ name: 'feat',
86
+ agentId: 'a1',
87
+ baseCommit: 'base',
88
+ branchName: 'stream/s1',
89
+ metadata: { task_ref: { resource_id: 'r', node_id: 'n' } },
90
+ })
91
+ );
92
+ await flushMicrotasks();
93
+
94
+ expect(connection.calls).toHaveLength(1);
95
+ expect(connection.calls[0].method).toBe('x-cascade/stream.opened');
96
+ expect(connection.calls[0].params).toMatchObject({
97
+ stream_id: 's1',
98
+ name: 'feat',
99
+ agent_id: 'a1',
100
+ base_commit: 'base',
101
+ branch_name: 'stream/s1',
102
+ metadata: { task_ref: { resource_id: 'r', node_id: 'n' } },
103
+ });
104
+ });
105
+
106
+ it('forwards stream:committed with commit_hash and files_touched', async () => {
107
+ createCascadeBridge(connection, adapter as unknown as GitCascadeAdapter);
108
+ adapter.emit(
109
+ mkEvent('stream:committed', {
110
+ streamId: 's1',
111
+ commit: 'abc',
112
+ changeId: 'c-123',
113
+ agentId: 'a',
114
+ messageSummary: 'feat: x',
115
+ filesTouched: ['a.ts', 'b.ts'],
116
+ parentCommit: 'base',
117
+ })
118
+ );
119
+ await flushMicrotasks();
120
+
121
+ expect(connection.calls[0].method).toBe('x-cascade/stream.committed');
122
+ expect(connection.calls[0].params).toMatchObject({
123
+ stream_id: 's1',
124
+ commit_hash: 'abc',
125
+ change_id: 'c-123',
126
+ files_touched: ['a.ts', 'b.ts'],
127
+ parent_commit: 'base',
128
+ });
129
+ });
130
+
131
+ it('forwards cascade:rebased with new_commits array', async () => {
132
+ createCascadeBridge(connection, adapter as unknown as GitCascadeAdapter);
133
+ adapter.emit(
134
+ mkEvent('cascade:rebased', {
135
+ streamId: 'dep',
136
+ agentId: 'a',
137
+ triggeredByStreamId: 'root',
138
+ triggeredByAgentId: 'a',
139
+ newBaseCommit: 'nb',
140
+ newHead: 'nh',
141
+ newCommits: [
142
+ {
143
+ commit_hash: 'r1',
144
+ change_id: 'chg',
145
+ parent_commit: 'nb',
146
+ message_summary: 'rebased',
147
+ files_touched: ['x.ts'],
148
+ },
149
+ ],
150
+ })
151
+ );
152
+ await flushMicrotasks();
153
+
154
+ expect(connection.calls[0].method).toBe('x-cascade/cascade.rebased');
155
+ expect(connection.calls[0].params).toMatchObject({
156
+ stream_id: 'dep',
157
+ triggered_by_stream_id: 'root',
158
+ new_base_commit: 'nb',
159
+ new_head: 'nh',
160
+ });
161
+ expect((connection.calls[0].params as { new_commits: unknown[] }).new_commits).toHaveLength(1);
162
+ });
163
+
164
+ it('forwards cascade:completed with summary fields', async () => {
165
+ createCascadeBridge(connection, adapter as unknown as GitCascadeAdapter);
166
+ adapter.emit(
167
+ mkEvent('cascade:completed', {
168
+ rootStreamId: 'root',
169
+ agentId: 'a',
170
+ strategy: 'stop_on_conflict',
171
+ updatedStreams: ['d1'],
172
+ failedStreams: [],
173
+ skippedStreams: [],
174
+ })
175
+ );
176
+ await flushMicrotasks();
177
+
178
+ expect(connection.calls[0].method).toBe('x-cascade/cascade.completed');
179
+ expect(connection.calls[0].params).toMatchObject({
180
+ root_stream_id: 'root',
181
+ strategy: 'stop_on_conflict',
182
+ updated_streams: ['d1'],
183
+ });
184
+ });
185
+
186
+ it('drops events while disconnected (standalone mode)', async () => {
187
+ connection.connected = false;
188
+ createCascadeBridge(connection, adapter as unknown as GitCascadeAdapter);
189
+ adapter.emit(mkEvent('stream:created', { streamId: 's', name: 'n', agentId: 'a' }));
190
+ await flushMicrotasks();
191
+ expect(connection.calls).toHaveLength(0);
192
+ });
193
+
194
+ it('ignores events that have no MAP counterpart (e.g. worktree:created, mergeQueue:added)', async () => {
195
+ createCascadeBridge(connection, adapter as unknown as GitCascadeAdapter);
196
+ adapter.emit(mkEvent('worktree:created', { agentId: 'a' }));
197
+ adapter.emit(mkEvent('mergeQueue:added', { streamId: 's' }));
198
+ adapter.emit(mkEvent('task:started', { taskId: 't' }));
199
+ await flushMicrotasks();
200
+ expect(connection.calls).toHaveLength(0);
201
+ });
202
+
203
+ it('dispose() unsubscribes — no more forwarding after call', async () => {
204
+ const disposable = createCascadeBridge(connection, adapter as unknown as GitCascadeAdapter);
205
+ adapter.emit(mkEvent('stream:created', { streamId: 's1', name: 'x', agentId: 'a' }));
206
+ await flushMicrotasks();
207
+ expect(connection.calls).toHaveLength(1);
208
+
209
+ disposable.dispose();
210
+
211
+ adapter.emit(mkEvent('stream:created', { streamId: 's2', name: 'y', agentId: 'a' }));
212
+ await flushMicrotasks();
213
+ expect(connection.calls).toHaveLength(1);
214
+ });
215
+
216
+ it('swallows callExtension errors (does not throw or block the event loop)', async () => {
217
+ const errConnection = createMockConnection();
218
+ errConnection.callExtension = vi.fn().mockRejectedValue(new Error('hub broke'));
219
+ createCascadeBridge(errConnection, adapter as unknown as GitCascadeAdapter);
220
+
221
+ expect(() =>
222
+ adapter.emit(
223
+ mkEvent('stream:abandoned', { streamId: 's', reason: 'testing' })
224
+ )
225
+ ).not.toThrow();
226
+ await flushMicrotasks();
227
+ expect(errConnection.callExtension).toHaveBeenCalledTimes(1);
228
+ });
229
+ });
@@ -48,6 +48,13 @@ function mockTaskBridge(): TaskBridge & {
48
48
  };
49
49
  }
50
50
 
51
+ /** Flush microtasks and any pending setTimeout(0)-ish waits used by the bridge */
52
+ async function flushAsync(iterations = 5): Promise<void> {
53
+ for (let i = 0; i < iterations; i++) {
54
+ await new Promise((r) => setTimeout(r, 25));
55
+ }
56
+ }
57
+
51
58
  describe("LifecycleBridge", () => {
52
59
  let conn: ReturnType<typeof mockConnection>;
53
60
  const scope = "swarm:test";
@@ -56,7 +63,7 @@ describe("LifecycleBridge", () => {
56
63
  conn = mockConnection();
57
64
  });
58
65
 
59
- it("registers agent with MAP hub on spawn event", () => {
66
+ it("registers agent with MAP hub on spawn event", async () => {
60
67
  const { callback } = createLifecycleBridge(
61
68
  conn,
62
69
  {} as AgentStore,
@@ -68,6 +75,8 @@ describe("LifecycleBridge", () => {
68
75
  agent: mockAgent({ id: "agent-1", name: "worker-1", role: "worker" }),
69
76
  });
70
77
 
78
+ await flushAsync();
79
+
71
80
  expect(conn.callExtension).toHaveBeenCalledWith(
72
81
  "map/agents/register",
73
82
  expect.objectContaining({
@@ -77,7 +86,7 @@ describe("LifecycleBridge", () => {
77
86
  );
78
87
  });
79
88
 
80
- it("includes per-agent ACP capabilities for coordinators", () => {
89
+ it("includes per-agent ACP capabilities for coordinators", async () => {
81
90
  const { callback } = createLifecycleBridge(
82
91
  conn,
83
92
  {} as AgentStore,
@@ -89,6 +98,8 @@ describe("LifecycleBridge", () => {
89
98
  agent: mockAgent({ id: "coord-1", name: "coordinator-1", role: "coordinator" }),
90
99
  });
91
100
 
101
+ await flushAsync();
102
+
92
103
  expect(conn.callExtension).toHaveBeenCalledWith(
93
104
  "map/agents/register",
94
105
  expect.objectContaining({
@@ -103,7 +114,7 @@ describe("LifecycleBridge", () => {
103
114
  );
104
115
  });
105
116
 
106
- it("does not include ACP capabilities for workers", () => {
117
+ it("does not include ACP capabilities for workers", async () => {
107
118
  const { callback } = createLifecycleBridge(
108
119
  conn,
109
120
  {} as AgentStore,
@@ -115,6 +126,8 @@ describe("LifecycleBridge", () => {
115
126
  agent: mockAgent({ id: "worker-1", name: "worker-1", role: "worker" }),
116
127
  });
117
128
 
129
+ await flushAsync();
130
+
118
131
  const call = conn.callExtension.mock.calls[0];
119
132
  const params = call[1] as Record<string, unknown>;
120
133
  const caps = params.capabilities as Record<string, unknown>;
@@ -123,7 +136,63 @@ describe("LifecycleBridge", () => {
123
136
  expect(caps.messaging).toEqual({ canReceive: true });
124
137
  });
125
138
 
126
- it("unregisters agent from MAP hub on stop event", () => {
139
+ it("includes peerMapId in metadata when getLocalMapId resolves", async () => {
140
+ const getLocalMapId = vi.fn((id: string) =>
141
+ id === "coord-1" ? "map-ulid-local" : undefined,
142
+ );
143
+ const { callback } = createLifecycleBridge(
144
+ conn,
145
+ {} as AgentStore,
146
+ scope,
147
+ undefined,
148
+ getLocalMapId,
149
+ );
150
+
151
+ callback({
152
+ type: "spawned",
153
+ agent: mockAgent({ id: "coord-1", name: "coordinator-1", role: "coordinator" }),
154
+ });
155
+
156
+ await flushAsync();
157
+
158
+ const call = conn.callExtension.mock.calls.find(
159
+ (c: any[]) => c[0] === "map/agents/register",
160
+ );
161
+ expect(call).toBeDefined();
162
+ const params = call![1] as Record<string, unknown>;
163
+ const metadata = params.metadata as Record<string, unknown>;
164
+ expect(metadata.peerMapId).toBe("map-ulid-local");
165
+ expect(metadata.peerAgentId).toBe("coord-1");
166
+ });
167
+
168
+ it("registers without peerMapId when lookup returns undefined", async () => {
169
+ const getLocalMapId = vi.fn(() => undefined);
170
+ const { callback } = createLifecycleBridge(
171
+ conn,
172
+ {} as AgentStore,
173
+ scope,
174
+ undefined,
175
+ getLocalMapId,
176
+ );
177
+
178
+ callback({
179
+ type: "spawned",
180
+ agent: mockAgent({ id: "coord-1", role: "coordinator" }),
181
+ });
182
+
183
+ // Use a longer wait since the bridge will poll ~500ms for the local MAP ID
184
+ await new Promise((r) => setTimeout(r, 600));
185
+
186
+ const call = conn.callExtension.mock.calls.find(
187
+ (c: any[]) => c[0] === "map/agents/register",
188
+ );
189
+ expect(call).toBeDefined();
190
+ const params = call![1] as Record<string, unknown>;
191
+ const metadata = params.metadata as Record<string, unknown>;
192
+ expect(metadata.peerMapId).toBeUndefined();
193
+ });
194
+
195
+ it("unregisters agent from MAP hub on stop event", async () => {
127
196
  const { callback } = createLifecycleBridge(
128
197
  conn,
129
198
  {} as AgentStore,
@@ -132,6 +201,7 @@ describe("LifecycleBridge", () => {
132
201
 
133
202
  // First spawn, then stop
134
203
  callback({ type: "spawned", agent: mockAgent({ id: "agent-1" }) });
204
+ await flushAsync();
135
205
  callback({
136
206
  type: "stopped",
137
207
  agent: mockAgent({ id: "agent-1" }),
@@ -157,8 +227,8 @@ describe("LifecycleBridge", () => {
157
227
 
158
228
  callback({ type: "spawned", agent: mockAgent({ id: "agent-1" }) });
159
229
 
160
- // Wait for the .then() that stores mapId
161
- await new Promise(r => setTimeout(r, 10));
230
+ // Wait for spawn registration + mapId capture
231
+ await flushAsync();
162
232
 
163
233
  callback({
164
234
  type: "stopped",
@@ -174,7 +244,7 @@ describe("LifecycleBridge", () => {
174
244
  );
175
245
  });
176
246
 
177
- it("does nothing when disconnected", () => {
247
+ it("does nothing when disconnected", async () => {
178
248
  const disconnected = {
179
249
  ...conn,
180
250
  get isConnected() {
@@ -189,6 +259,8 @@ describe("LifecycleBridge", () => {
189
259
 
190
260
  callback({ type: "spawned", agent: mockAgent() });
191
261
 
262
+ await flushAsync();
263
+
192
264
  expect(conn.callExtension).not.toHaveBeenCalled();
193
265
  });
194
266
 
@@ -246,16 +318,16 @@ describe("LifecycleBridge", () => {
246
318
  callback({ type: "spawned", agent: mockAgent({ id: "a1" }) });
247
319
  callback({ type: "spawned", agent: mockAgent({ id: "a2" }) });
248
320
 
321
+ await flushAsync();
249
322
  await cleanup();
250
323
 
251
- // 2 register calls + 2 unregister calls
252
324
  const unregisterCalls = conn.callExtension.mock.calls.filter(
253
325
  (c: any[]) => c[0] === "map/agents/unregister",
254
326
  );
255
327
  expect(unregisterCalls).toHaveLength(2);
256
328
  });
257
329
 
258
- it("silently handles MAP call failures", () => {
330
+ it("silently handles MAP call failures", async () => {
259
331
  conn.callExtension.mockRejectedValue(new Error("network error"));
260
332
 
261
333
  const { callback } = createLifecycleBridge(
@@ -268,9 +340,11 @@ describe("LifecycleBridge", () => {
268
340
  expect(() => {
269
341
  callback({ type: "spawned", agent: mockAgent() });
270
342
  }).not.toThrow();
343
+
344
+ await flushAsync();
271
345
  });
272
346
 
273
- it("uses agent.id as fallback name when name is undefined", () => {
347
+ it("uses agent.id as fallback name when name is undefined", async () => {
274
348
  const { callback } = createLifecycleBridge(
275
349
  conn,
276
350
  {} as AgentStore,
@@ -282,6 +356,8 @@ describe("LifecycleBridge", () => {
282
356
  agent: mockAgent({ id: "agent-99", name: undefined }),
283
357
  });
284
358
 
359
+ await flushAsync();
360
+
285
361
  expect(conn.callExtension).toHaveBeenCalledWith(
286
362
  "map/agents/register",
287
363
  expect.objectContaining({ name: "agent-99" }),
@@ -39,7 +39,8 @@ interface ACPEnvelope {
39
39
  /** Active ACP stream state */
40
40
  interface ACPStreamState {
41
41
  streamId: string;
42
- agentId: string;
42
+ /** Macro-agent's internal store id (the agent the stream targets). */
43
+ peerAgentId: string;
43
44
  /** Push inbound ACP messages into the readable side */
44
45
  push: (message: AnyMessage) => void;
45
46
  /** Close the stream */
@@ -59,6 +60,13 @@ export interface ACPBridge {
59
60
  */
60
61
  handleDelivery(agentId: string, message: any): boolean;
61
62
 
63
+ /**
64
+ * Inspect which agent each open ACP stream is bound to. Used for
65
+ * observability and routing tests — the binding is otherwise per-stream
66
+ * in-memory state and not externally observable.
67
+ */
68
+ getStreamBindings(): Array<{ streamId: string; peerAgentId: string }>;
69
+
62
70
  /** Close all active streams */
63
71
  close(): void;
64
72
  }
@@ -212,14 +220,22 @@ export function createACPBridge(
212
220
  // Create the AgentSideConnection.
213
221
  // All outbound messages (responses + notifications) go through the
214
222
  // writable side of the stream, which calls sendToClient above.
223
+ //
224
+ // Bind the MacroAgent to this stream's target agent so `session/new`
225
+ // creates a session for the agent the MAP stream was opened against,
226
+ // not whichever head manager happens to share the same cwd.
215
227
  const conn = new AgentSideConnection(
216
- (agentConn) => createMacroAgent(agentConn, { system }),
228
+ (agentConn) =>
229
+ createMacroAgent(agentConn, {
230
+ system,
231
+ initConfig: { targetAgentId: agentId },
232
+ }),
217
233
  stream,
218
234
  );
219
235
 
220
236
  const state: ACPStreamState = {
221
237
  streamId,
222
- agentId,
238
+ peerAgentId: agentId,
223
239
  push,
224
240
  close,
225
241
  connection: conn,
@@ -260,6 +276,13 @@ export function createACPBridge(
260
276
  return true;
261
277
  },
262
278
 
279
+ getStreamBindings(): Array<{ streamId: string; peerAgentId: string }> {
280
+ return Array.from(streams.values()).map((s) => ({
281
+ streamId: s.streamId,
282
+ peerAgentId: s.peerAgentId,
283
+ }));
284
+ },
285
+
263
286
  close(): void {
264
287
  for (const state of streams.values()) {
265
288
  state.close();