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