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,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
+ });
@@ -12,11 +12,9 @@ import type { TaskBridge } from "../types.js";
12
12
  import type { Agent } from "../../store/types/index.js";
13
13
 
14
14
  function mockConnection(): LifecycleBridgeConnection & {
15
- spawn: ReturnType<typeof vi.fn>;
16
15
  callExtension: ReturnType<typeof vi.fn>;
17
16
  } {
18
17
  return {
19
- spawn: vi.fn().mockResolvedValue({}),
20
18
  callExtension: vi.fn().mockResolvedValue({}),
21
19
  get isConnected() {
22
20
  return true;
@@ -50,6 +48,13 @@ function mockTaskBridge(): TaskBridge & {
50
48
  };
51
49
  }
52
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
+
53
58
  describe("LifecycleBridge", () => {
54
59
  let conn: ReturnType<typeof mockConnection>;
55
60
  const scope = "swarm:test";
@@ -58,7 +63,7 @@ describe("LifecycleBridge", () => {
58
63
  conn = mockConnection();
59
64
  });
60
65
 
61
- it("registers agent with MAP hub on spawn event", () => {
66
+ it("registers agent with MAP hub on spawn event", async () => {
62
67
  const { callback } = createLifecycleBridge(
63
68
  conn,
64
69
  {} as AgentStore,
@@ -70,17 +75,124 @@ describe("LifecycleBridge", () => {
70
75
  agent: mockAgent({ id: "agent-1", name: "worker-1", role: "worker" }),
71
76
  });
72
77
 
73
- expect(conn.spawn).toHaveBeenCalledWith(
78
+ await flushAsync();
79
+
80
+ expect(conn.callExtension).toHaveBeenCalledWith(
81
+ "map/agents/register",
74
82
  expect.objectContaining({
75
- agentId: "agent-1",
76
83
  name: "worker-1",
77
84
  role: "worker",
78
- scopes: [scope],
79
85
  }),
80
86
  );
81
87
  });
82
88
 
83
- it("unregisters agent from MAP hub on stop event", () => {
89
+ it("includes per-agent ACP capabilities for coordinators", async () => {
90
+ const { callback } = createLifecycleBridge(
91
+ conn,
92
+ {} as AgentStore,
93
+ scope,
94
+ );
95
+
96
+ callback({
97
+ type: "spawned",
98
+ agent: mockAgent({ id: "coord-1", name: "coordinator-1", role: "coordinator" }),
99
+ });
100
+
101
+ await flushAsync();
102
+
103
+ expect(conn.callExtension).toHaveBeenCalledWith(
104
+ "map/agents/register",
105
+ expect.objectContaining({
106
+ name: "coordinator-1",
107
+ role: "coordinator",
108
+ capabilities: expect.objectContaining({
109
+ protocols: ["acp"],
110
+ acp: { version: "2024-10-07" },
111
+ messaging: { canReceive: true },
112
+ }),
113
+ }),
114
+ );
115
+ });
116
+
117
+ it("does not include ACP capabilities for workers", async () => {
118
+ const { callback } = createLifecycleBridge(
119
+ conn,
120
+ {} as AgentStore,
121
+ scope,
122
+ );
123
+
124
+ callback({
125
+ type: "spawned",
126
+ agent: mockAgent({ id: "worker-1", name: "worker-1", role: "worker" }),
127
+ });
128
+
129
+ await flushAsync();
130
+
131
+ const call = conn.callExtension.mock.calls[0];
132
+ const params = call[1] as Record<string, unknown>;
133
+ const caps = params.capabilities as Record<string, unknown>;
134
+ expect(caps.protocols).toBeUndefined();
135
+ expect(caps.acp).toBeUndefined();
136
+ expect(caps.messaging).toEqual({ canReceive: true });
137
+ });
138
+
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 () => {
84
196
  const { callback } = createLifecycleBridge(
85
197
  conn,
86
198
  {} as AgentStore,
@@ -89,6 +201,7 @@ describe("LifecycleBridge", () => {
89
201
 
90
202
  // First spawn, then stop
91
203
  callback({ type: "spawned", agent: mockAgent({ id: "agent-1" }) });
204
+ await flushAsync();
92
205
  callback({
93
206
  type: "stopped",
94
207
  agent: mockAgent({ id: "agent-1" }),
@@ -98,13 +211,40 @@ describe("LifecycleBridge", () => {
98
211
  expect(conn.callExtension).toHaveBeenCalledWith(
99
212
  "map/agents/unregister",
100
213
  expect.objectContaining({
101
- agentId: "agent-1",
102
214
  reason: "completed",
103
215
  }),
104
216
  );
105
217
  });
106
218
 
107
- it("does nothing when disconnected", () => {
219
+ it("uses MAP-assigned ID for unregistration when available", async () => {
220
+ conn.callExtension.mockResolvedValueOnce({ agent: { id: "map-ulid-1" } });
221
+
222
+ const { callback } = createLifecycleBridge(
223
+ conn,
224
+ {} as AgentStore,
225
+ scope,
226
+ );
227
+
228
+ callback({ type: "spawned", agent: mockAgent({ id: "agent-1" }) });
229
+
230
+ // Wait for spawn registration + mapId capture
231
+ await flushAsync();
232
+
233
+ callback({
234
+ type: "stopped",
235
+ agent: mockAgent({ id: "agent-1" }),
236
+ reason: "completed",
237
+ });
238
+
239
+ expect(conn.callExtension).toHaveBeenLastCalledWith(
240
+ "map/agents/unregister",
241
+ expect.objectContaining({
242
+ agentId: "map-ulid-1",
243
+ }),
244
+ );
245
+ });
246
+
247
+ it("does nothing when disconnected", async () => {
108
248
  const disconnected = {
109
249
  ...conn,
110
250
  get isConnected() {
@@ -119,7 +259,9 @@ describe("LifecycleBridge", () => {
119
259
 
120
260
  callback({ type: "spawned", agent: mockAgent() });
121
261
 
122
- expect(conn.spawn).not.toHaveBeenCalled();
262
+ await flushAsync();
263
+
264
+ expect(conn.callExtension).not.toHaveBeenCalled();
123
265
  });
124
266
 
125
267
  it("bridges task creation on spawn when agent has task_id", () => {
@@ -176,21 +318,17 @@ describe("LifecycleBridge", () => {
176
318
  callback({ type: "spawned", agent: mockAgent({ id: "a1" }) });
177
319
  callback({ type: "spawned", agent: mockAgent({ id: "a2" }) });
178
320
 
321
+ await flushAsync();
179
322
  await cleanup();
180
323
 
181
- expect(conn.callExtension).toHaveBeenCalledTimes(2);
182
- expect(conn.callExtension).toHaveBeenCalledWith(
183
- "map/agents/unregister",
184
- expect.objectContaining({ agentId: "a1" }),
185
- );
186
- expect(conn.callExtension).toHaveBeenCalledWith(
187
- "map/agents/unregister",
188
- expect.objectContaining({ agentId: "a2" }),
324
+ const unregisterCalls = conn.callExtension.mock.calls.filter(
325
+ (c: any[]) => c[0] === "map/agents/unregister",
189
326
  );
327
+ expect(unregisterCalls).toHaveLength(2);
190
328
  });
191
329
 
192
- it("silently handles MAP call failures", () => {
193
- conn.spawn.mockRejectedValue(new Error("network error"));
330
+ it("silently handles MAP call failures", async () => {
331
+ conn.callExtension.mockRejectedValue(new Error("network error"));
194
332
 
195
333
  const { callback } = createLifecycleBridge(
196
334
  conn,
@@ -202,9 +340,11 @@ describe("LifecycleBridge", () => {
202
340
  expect(() => {
203
341
  callback({ type: "spawned", agent: mockAgent() });
204
342
  }).not.toThrow();
343
+
344
+ await flushAsync();
205
345
  });
206
346
 
207
- it("uses agent.id as fallback name when name is undefined", () => {
347
+ it("uses agent.id as fallback name when name is undefined", async () => {
208
348
  const { callback } = createLifecycleBridge(
209
349
  conn,
210
350
  {} as AgentStore,
@@ -216,7 +356,10 @@ describe("LifecycleBridge", () => {
216
356
  agent: mockAgent({ id: "agent-99", name: undefined }),
217
357
  });
218
358
 
219
- expect(conn.spawn).toHaveBeenCalledWith(
359
+ await flushAsync();
360
+
361
+ expect(conn.callExtension).toHaveBeenCalledWith(
362
+ "map/agents/register",
220
363
  expect.objectContaining({ name: "agent-99" }),
221
364
  );
222
365
  });
@@ -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();