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.
- package/CLAUDE.md +179 -38
- package/README.md +781 -131
- package/dist/acp/claude-code-replay.d.ts +11 -0
- package/dist/acp/claude-code-replay.d.ts.map +1 -0
- package/dist/acp/claude-code-replay.js +190 -0
- package/dist/acp/claude-code-replay.js.map +1 -0
- package/dist/acp/macro-agent.d.ts.map +1 -1
- package/dist/acp/macro-agent.js +155 -6
- package/dist/acp/macro-agent.js.map +1 -1
- package/dist/acp/types.d.ts +9 -0
- package/dist/acp/types.d.ts.map +1 -1
- package/dist/acp/types.js.map +1 -1
- package/dist/agent/agent-manager-v2.d.ts +21 -0
- package/dist/agent/agent-manager-v2.d.ts.map +1 -1
- package/dist/agent/agent-manager-v2.js +234 -71
- package/dist/agent/agent-manager-v2.js.map +1 -1
- package/dist/agent/agent-manager.d.ts +12 -0
- package/dist/agent/agent-manager.d.ts.map +1 -1
- package/dist/agent/agent-manager.js.map +1 -1
- package/dist/agent/types.d.ts +15 -2
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/boot-v2.d.ts +41 -0
- package/dist/boot-v2.d.ts.map +1 -1
- package/dist/boot-v2.js +34 -37
- package/dist/boot-v2.js.map +1 -1
- package/dist/cli/index.js +56 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cognitive/macro-agent-backend.d.ts.map +1 -1
- package/dist/cognitive/macro-agent-backend.js +40 -22
- package/dist/cognitive/macro-agent-backend.js.map +1 -1
- package/dist/integrations/skilltree.d.ts.map +1 -1
- package/dist/integrations/skilltree.js +1 -0
- package/dist/integrations/skilltree.js.map +1 -1
- package/dist/lifecycle/cleanup.d.ts +33 -2
- package/dist/lifecycle/cleanup.d.ts.map +1 -1
- package/dist/lifecycle/cleanup.js +28 -6
- package/dist/lifecycle/cleanup.js.map +1 -1
- package/dist/lifecycle/handlers-v2.d.ts +7 -0
- package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
- package/dist/lifecycle/handlers-v2.js +28 -2
- package/dist/lifecycle/handlers-v2.js.map +1 -1
- package/dist/lifecycle/types.d.ts +11 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/dist/map/acp-bridge.d.ts +9 -0
- package/dist/map/acp-bridge.d.ts.map +1 -1
- package/dist/map/acp-bridge.js +15 -2
- package/dist/map/acp-bridge.js.map +1 -1
- package/dist/map/cascade-bridge.d.ts +44 -0
- package/dist/map/cascade-bridge.d.ts.map +1 -0
- package/dist/map/cascade-bridge.js +257 -0
- package/dist/map/cascade-bridge.js.map +1 -0
- package/dist/map/lifecycle-bridge.d.ts +1 -8
- package/dist/map/lifecycle-bridge.d.ts.map +1 -1
- package/dist/map/lifecycle-bridge.js +76 -22
- package/dist/map/lifecycle-bridge.js.map +1 -1
- package/dist/map/server.d.ts.map +1 -1
- package/dist/map/server.js +47 -6
- package/dist/map/server.js.map +1 -1
- package/dist/map/sidecar.d.ts.map +1 -1
- package/dist/map/sidecar.js +33 -4
- package/dist/map/sidecar.js.map +1 -1
- package/dist/map/types.d.ts +20 -0
- package/dist/map/types.d.ts.map +1 -1
- package/dist/mcp/tools/done-v2.d.ts.map +1 -1
- package/dist/mcp/tools/done-v2.js +8 -0
- package/dist/mcp/tools/done-v2.js.map +1 -1
- package/dist/teams/team-manager-v2.d.ts.map +1 -1
- package/dist/teams/team-manager-v2.js +26 -0
- package/dist/teams/team-manager-v2.js.map +1 -1
- package/dist/teams/team-runtime-v2.d.ts.map +1 -1
- package/dist/teams/team-runtime-v2.js +16 -3
- package/dist/teams/team-runtime-v2.js.map +1 -1
- package/dist/workspace/config.d.ts +10 -10
- package/dist/workspace/config.d.ts.map +1 -1
- package/dist/workspace/config.js +4 -4
- package/dist/workspace/config.js.map +1 -1
- package/dist/workspace/git-cascade-adapter.d.ts +510 -0
- package/dist/workspace/git-cascade-adapter.d.ts.map +1 -0
- package/dist/workspace/git-cascade-adapter.js +908 -0
- package/dist/workspace/git-cascade-adapter.js.map +1 -0
- package/dist/workspace/index.d.ts +3 -3
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +4 -4
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/landing/direct-push.d.ts +20 -0
- package/dist/workspace/landing/direct-push.d.ts.map +1 -0
- package/dist/workspace/landing/direct-push.js +74 -0
- package/dist/workspace/landing/direct-push.js.map +1 -0
- package/dist/workspace/landing/index.d.ts +29 -0
- package/dist/workspace/landing/index.d.ts.map +1 -0
- package/dist/workspace/landing/index.js +37 -0
- package/dist/workspace/landing/index.js.map +1 -0
- package/dist/workspace/landing/merge-to-parent.d.ts +41 -0
- package/dist/workspace/landing/merge-to-parent.d.ts.map +1 -0
- package/dist/workspace/landing/merge-to-parent.js +185 -0
- package/dist/workspace/landing/merge-to-parent.js.map +1 -0
- package/dist/workspace/landing/optimistic-push.d.ts +16 -0
- package/dist/workspace/landing/optimistic-push.d.ts.map +1 -0
- package/dist/workspace/landing/optimistic-push.js +27 -0
- package/dist/workspace/landing/optimistic-push.js.map +1 -0
- package/dist/workspace/landing/queue-to-branch.d.ts +24 -0
- package/dist/workspace/landing/queue-to-branch.d.ts.map +1 -0
- package/dist/workspace/landing/queue-to-branch.js +79 -0
- package/dist/workspace/landing/queue-to-branch.js.map +1 -0
- package/dist/workspace/merge-queue/merge-queue.d.ts +10 -0
- package/dist/workspace/merge-queue/merge-queue.d.ts.map +1 -1
- package/dist/workspace/merge-queue/merge-queue.js +10 -0
- package/dist/workspace/merge-queue/merge-queue.js.map +1 -1
- package/dist/workspace/merge-queue/types.d.ts +16 -2
- package/dist/workspace/merge-queue/types.d.ts.map +1 -1
- package/dist/workspace/merge-queue/types.js +9 -0
- package/dist/workspace/merge-queue/types.js.map +1 -1
- package/dist/workspace/pool/types.d.ts +1 -0
- package/dist/workspace/pool/types.d.ts.map +1 -1
- package/dist/workspace/pool/worktree-pool.d.ts.map +1 -1
- package/dist/workspace/pool/worktree-pool.js +1 -0
- package/dist/workspace/pool/worktree-pool.js.map +1 -1
- package/dist/workspace/recovery/abandon.d.ts +15 -0
- package/dist/workspace/recovery/abandon.d.ts.map +1 -0
- package/dist/workspace/recovery/abandon.js +45 -0
- package/dist/workspace/recovery/abandon.js.map +1 -0
- package/dist/workspace/recovery/auto-resolve.d.ts +27 -0
- package/dist/workspace/recovery/auto-resolve.d.ts.map +1 -0
- package/dist/workspace/recovery/auto-resolve.js +99 -0
- package/dist/workspace/recovery/auto-resolve.js.map +1 -0
- package/dist/workspace/recovery/defer.d.ts +15 -0
- package/dist/workspace/recovery/defer.d.ts.map +1 -0
- package/dist/workspace/recovery/defer.js +16 -0
- package/dist/workspace/recovery/defer.js.map +1 -0
- package/dist/workspace/recovery/escalate.d.ts +16 -0
- package/dist/workspace/recovery/escalate.d.ts.map +1 -0
- package/dist/workspace/recovery/escalate.js +24 -0
- package/dist/workspace/recovery/escalate.js.map +1 -0
- package/dist/workspace/recovery/index.d.ts +32 -0
- package/dist/workspace/recovery/index.d.ts.map +1 -0
- package/dist/workspace/recovery/index.js +45 -0
- package/dist/workspace/recovery/index.js.map +1 -0
- package/dist/workspace/recovery/spawn-resolver.d.ts +45 -0
- package/dist/workspace/recovery/spawn-resolver.d.ts.map +1 -0
- package/dist/workspace/recovery/spawn-resolver.js +111 -0
- package/dist/workspace/recovery/spawn-resolver.js.map +1 -0
- package/dist/workspace/recovery/types.d.ts +63 -0
- package/dist/workspace/recovery/types.d.ts.map +1 -0
- package/dist/workspace/recovery/types.js +12 -0
- package/dist/workspace/recovery/types.js.map +1 -0
- package/dist/workspace/topology/index.d.ts +9 -0
- package/dist/workspace/topology/index.d.ts.map +1 -0
- package/dist/workspace/topology/index.js +8 -0
- package/dist/workspace/topology/index.js.map +1 -0
- package/dist/workspace/topology/no-workspace.d.ts +18 -0
- package/dist/workspace/topology/no-workspace.d.ts.map +1 -0
- package/dist/workspace/topology/no-workspace.js +25 -0
- package/dist/workspace/topology/no-workspace.js.map +1 -0
- package/dist/workspace/topology/types.d.ts +97 -0
- package/dist/workspace/topology/types.d.ts.map +1 -0
- package/dist/workspace/topology/types.js +20 -0
- package/dist/workspace/topology/types.js.map +1 -0
- package/dist/workspace/topology/yaml-driven.d.ts +69 -0
- package/dist/workspace/topology/yaml-driven.d.ts.map +1 -0
- package/dist/workspace/topology/yaml-driven.js +273 -0
- package/dist/workspace/topology/yaml-driven.js.map +1 -0
- package/dist/workspace/types-v3.d.ts +110 -0
- package/dist/workspace/types-v3.d.ts.map +1 -0
- package/dist/workspace/types-v3.js +20 -0
- package/dist/workspace/types-v3.js.map +1 -0
- package/dist/workspace/types.d.ts +145 -17
- package/dist/workspace/types.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.d.ts +92 -13
- package/dist/workspace/workspace-manager.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.js +373 -13
- package/dist/workspace/workspace-manager.js.map +1 -1
- package/dist/workspace/yaml-schema.d.ts +254 -0
- package/dist/workspace/yaml-schema.d.ts.map +1 -0
- package/dist/workspace/yaml-schema.js +170 -0
- package/dist/workspace/yaml-schema.js.map +1 -0
- package/docs/conflict-recovery.md +472 -0
- package/docs/git-cascade-integration-gaps.md +678 -0
- package/docs/workspace-interfaces.md +731 -0
- package/docs/workspace-redesign-plan.md +302 -0
- package/package.json +4 -4
- package/src/__tests__/e2e/auto-sync.e2e.test.ts +257 -0
- package/src/__tests__/e2e/cascade-rebase.e2e.test.ts +254 -0
- package/src/__tests__/e2e/cli-run.e2e.test.ts +167 -0
- package/src/__tests__/e2e/self-driving-v3.e2e.test.ts +197 -0
- package/src/__tests__/e2e/spawn-resolver.e2e.test.ts +200 -0
- package/src/__tests__/e2e/workspace-lifecycle.e2e.test.ts +30 -22
- package/src/__tests__/e2e/workspace-v3.e2e.test.ts +413 -0
- package/src/acp/__tests__/claude-code-replay.test.ts +225 -0
- package/src/acp/__tests__/macro-agent.test.ts +39 -1
- package/src/acp/claude-code-replay.ts +208 -0
- package/src/acp/macro-agent.ts +167 -9
- package/src/acp/types.ts +10 -0
- package/src/agent/__tests__/agent-manager-topology.test.ts +73 -0
- package/src/agent/__tests__/agent-manager-v2.test.ts +71 -11
- package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
- package/src/agent/agent-manager-v2.ts +293 -77
- package/src/agent/agent-manager.ts +14 -0
- package/src/agent/types.ts +16 -2
- package/src/boot-v2.ts +87 -36
- package/src/cli/index.ts +61 -0
- package/src/cognitive/__tests__/macro-agent-backend.test.ts +47 -5
- package/src/cognitive/macro-agent-backend.ts +45 -29
- package/src/integrations/skilltree.ts +1 -0
- package/src/lifecycle/cleanup.ts +52 -3
- package/src/lifecycle/handlers-v2.ts +40 -3
- package/src/lifecycle/types.ts +12 -0
- package/src/map/__tests__/cascade-bridge.test.ts +229 -0
- package/src/map/__tests__/lifecycle-bridge.test.ts +165 -22
- package/src/map/acp-bridge.ts +26 -3
- package/src/map/cascade-bridge.ts +301 -0
- package/src/map/lifecycle-bridge.ts +77 -27
- package/src/map/server.ts +47 -6
- package/src/map/sidecar.ts +31 -3
- package/src/map/types.ts +20 -0
- package/src/mcp/tools/done-v2.ts +9 -0
- package/src/teams/team-manager-v2.ts +37 -0
- package/src/teams/team-runtime-v2.ts +23 -3
- package/src/workspace/__tests__/{dataplane-adapter.test.ts → git-cascade-adapter.test.ts} +209 -14
- package/src/workspace/__tests__/self-driving-yaml.test.ts +114 -0
- package/src/workspace/__tests__/shared-worktree-refcount.test.ts +154 -0
- package/src/workspace/__tests__/standalone-mode.test.ts +118 -0
- package/src/workspace/__tests__/workspace-manager-v3.test.ts +245 -0
- package/src/workspace/__tests__/yaml-schema.test.ts +210 -0
- package/src/workspace/config.ts +11 -11
- package/src/workspace/git-cascade-adapter.ts +1186 -0
- package/src/workspace/index.ts +11 -11
- package/src/workspace/landing/__tests__/strategies.test.ts +142 -0
- package/src/workspace/landing/direct-push.ts +91 -0
- package/src/workspace/landing/index.ts +40 -0
- package/src/workspace/landing/merge-to-parent.ts +228 -0
- package/src/workspace/landing/optimistic-push.ts +36 -0
- package/src/workspace/landing/queue-to-branch.ts +108 -0
- package/src/workspace/merge-queue/merge-queue.ts +10 -0
- package/src/workspace/merge-queue/types.ts +16 -2
- package/src/workspace/pool/__tests__/worktree-pool.integration.test.ts +5 -5
- package/src/workspace/pool/types.ts +1 -0
- package/src/workspace/pool/worktree-pool.ts +1 -0
- package/src/workspace/recovery/__tests__/auto-resolve-integration.test.ts +127 -0
- package/src/workspace/recovery/__tests__/spawn-resolver.test.ts +139 -0
- package/src/workspace/recovery/__tests__/strategies.test.ts +145 -0
- package/src/workspace/recovery/abandon.ts +51 -0
- package/src/workspace/recovery/auto-resolve.ts +119 -0
- package/src/workspace/recovery/defer.ts +23 -0
- package/src/workspace/recovery/escalate.ts +30 -0
- package/src/workspace/recovery/index.ts +58 -0
- package/src/workspace/recovery/spawn-resolver.ts +145 -0
- package/src/workspace/recovery/types.ts +54 -0
- package/src/workspace/topology/__tests__/yaml-driven.test.ts +345 -0
- package/src/workspace/topology/index.ts +18 -0
- package/src/workspace/topology/no-workspace.ts +39 -0
- package/src/workspace/topology/types.ts +116 -0
- package/src/workspace/topology/yaml-driven.ts +316 -0
- package/src/workspace/types-v3.ts +155 -0
- package/src/workspace/types.ts +191 -20
- package/src/workspace/workspace-manager.ts +474 -19
- package/src/workspace/yaml-schema.ts +216 -0
- 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
|
-
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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.
|
|
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
|
-
|
|
359
|
+
await flushAsync();
|
|
360
|
+
|
|
361
|
+
expect(conn.callExtension).toHaveBeenCalledWith(
|
|
362
|
+
"map/agents/register",
|
|
220
363
|
expect.objectContaining({ name: "agent-99" }),
|
|
221
364
|
);
|
|
222
365
|
});
|
package/src/map/acp-bridge.ts
CHANGED
|
@@ -39,7 +39,8 @@ interface ACPEnvelope {
|
|
|
39
39
|
/** Active ACP stream state */
|
|
40
40
|
interface ACPStreamState {
|
|
41
41
|
streamId: string;
|
|
42
|
-
|
|
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) =>
|
|
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();
|