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,413 @@
1
+ /**
2
+ * Workspace V3 End-to-End Tests (Phase 4/5/7 integration gate)
3
+ *
4
+ * Exercises the full stream-first path:
5
+ * YAML → parseTeamWorkspaceConfig → YamlDrivenTopology → AgentManagerV2.spawn
6
+ * → executeWorkspaceDecision → WorkspaceManager V3 → real git-cascade + git
7
+ *
8
+ * These tests prevent the silent-failure class that mocked unit tests miss.
9
+ *
10
+ * REQUIRES: RUN_E2E_TESTS=true
11
+ *
12
+ * Run with:
13
+ * RUN_E2E_TESTS=true npx vitest run --config vitest.e2e.config.ts \
14
+ * src/__tests__/e2e/workspace-v3.e2e.test.ts
15
+ */
16
+
17
+ import {
18
+ describe,
19
+ it,
20
+ expect,
21
+ beforeEach,
22
+ afterEach,
23
+ vi,
24
+ } from 'vitest';
25
+ import * as path from 'path';
26
+ import * as os from 'os';
27
+ import * as fs from 'fs';
28
+ import { execSync } from 'child_process';
29
+ import { bootV2, type MacroAgentSystemV2 } from '../../boot-v2.js';
30
+ import { GitCascadeAdapter } from '../../workspace/git-cascade-adapter.js';
31
+ import {
32
+ DefaultWorkspaceManager,
33
+ createWorkspaceManagerWithAdapter,
34
+ } from '../../workspace/workspace-manager.js';
35
+ import { YamlDrivenTopology } from '../../workspace/topology/yaml-driven.js';
36
+ import {
37
+ parseTeamWorkspaceConfig,
38
+ type TeamWorkspaceConfig,
39
+ } from '../../workspace/yaml-schema.js';
40
+ import {
41
+ registerBuiltinLandingStrategies,
42
+ MergeToParentStrategy,
43
+ } from '../../workspace/landing/index.js';
44
+ import {
45
+ DeferStrategy,
46
+ AbandonStrategy,
47
+ buildBuiltinRecoveryRegistry,
48
+ } from '../../workspace/recovery/index.js';
49
+
50
+ // ─────────────────────────────────────────────────────────────────
51
+ // Harness
52
+ // ─────────────────────────────────────────────────────────────────
53
+
54
+ const RUN_E2E = !!process.env.RUN_E2E_TESTS;
55
+ const describeFn = RUN_E2E ? describe : describe.skip;
56
+
57
+ // Mock acp-factory — no real Claude Code sessions
58
+ vi.mock('acp-factory', () => ({
59
+ AgentFactory: {
60
+ spawn: vi.fn().mockResolvedValue({
61
+ createSession: vi.fn().mockResolvedValue({
62
+ id: `session-${Date.now()}`,
63
+ prompt: vi.fn().mockReturnValue({
64
+ [Symbol.asyncIterator]: () => ({
65
+ next: () => Promise.resolve({ done: true, value: undefined }),
66
+ }),
67
+ }),
68
+ forkWithFlush: vi.fn().mockResolvedValue({ id: `forked-${Date.now()}` }),
69
+ }),
70
+ loadSession: vi.fn().mockResolvedValue({ id: `loaded-${Date.now()}` }),
71
+ close: vi.fn().mockResolvedValue(undefined),
72
+ isRunning: vi.fn().mockReturnValue(true),
73
+ }),
74
+ },
75
+ }));
76
+
77
+ vi.mock('opentasks', () => ({
78
+ OpenTasksClient: vi.fn().mockImplementation(() => ({
79
+ connect: vi.fn().mockRejectedValue(new Error('No daemon')),
80
+ disconnect: vi.fn(),
81
+ query: vi.fn().mockResolvedValue({ items: [] }),
82
+ link: vi.fn().mockResolvedValue({ success: true }),
83
+ task: vi.fn().mockResolvedValue({ id: 't-1' }),
84
+ })),
85
+ }));
86
+
87
+ function createTestDir(): string {
88
+ const dir = path.join(
89
+ os.tmpdir(),
90
+ `ws-v3-e2e-${Date.now()}-${Math.random().toString(36).slice(2)}`
91
+ );
92
+ fs.mkdirSync(dir, { recursive: true });
93
+ return dir;
94
+ }
95
+
96
+ function createGitRepo(baseDir: string): string {
97
+ const repoPath = path.join(baseDir, 'repo');
98
+ fs.mkdirSync(repoPath, { recursive: true });
99
+ execSync('git init -b main', { cwd: repoPath, stdio: 'pipe' });
100
+ execSync('git config user.email "test@test.com"', { cwd: repoPath, stdio: 'pipe' });
101
+ execSync('git config user.name "Test User"', { cwd: repoPath, stdio: 'pipe' });
102
+ fs.writeFileSync(path.join(repoPath, 'README.md'), '# Test\n');
103
+ execSync('git add .', { cwd: repoPath, stdio: 'pipe' });
104
+ execSync('git commit -m "init"', { cwd: repoPath, stdio: 'pipe' });
105
+ return repoPath;
106
+ }
107
+
108
+ // ─────────────────────────────────────────────────────────────────
109
+ // Tests
110
+ // ─────────────────────────────────────────────────────────────────
111
+
112
+ describeFn('Workspace V3 E2E', () => {
113
+ let system: MacroAgentSystemV2;
114
+ let testDir: string;
115
+ let repoPath: string;
116
+ let adapter: GitCascadeAdapter;
117
+ let workspaceManager: DefaultWorkspaceManager;
118
+
119
+ beforeEach(async () => {
120
+ testDir = createTestDir();
121
+ repoPath = createGitRepo(testDir);
122
+ const dbPath = path.join(testDir, 'git-cascade.db');
123
+
124
+ adapter = new GitCascadeAdapter({
125
+ enabled: true,
126
+ repoPath,
127
+ dbPath,
128
+ });
129
+ workspaceManager = createWorkspaceManagerWithAdapter(adapter, {
130
+ worktreeBaseDir: path.join(repoPath, '.worktrees'),
131
+ }) as DefaultWorkspaceManager;
132
+
133
+ // Register built-in landing strategies
134
+ registerBuiltinLandingStrategies(workspaceManager);
135
+
136
+ system = await bootV2({
137
+ cwd: repoPath,
138
+ baseDir: testDir,
139
+ inbox: { socketPath: path.join(testDir, 'inbox.sock') },
140
+ workspaceManager,
141
+ });
142
+ });
143
+
144
+ afterEach(async () => {
145
+ if (system) await system.shutdown();
146
+ if (workspaceManager) workspaceManager.close();
147
+ if (adapter) adapter.close();
148
+ if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
149
+ });
150
+
151
+ // ═══════════════════════════════════════════════════════════════
152
+ // TEST 1: Peer swarm — YAML → Topology → Spawn → V3 allocation
153
+ // ═══════════════════════════════════════════════════════════════
154
+
155
+ describe('peer swarm (V3)', () => {
156
+ let topology: YamlDrivenTopology;
157
+ let config: TeamWorkspaceConfig;
158
+
159
+ beforeEach(async () => {
160
+ const parsed = parseTeamWorkspaceConfig({
161
+ roles: {
162
+ orchestrator: { workspace: 'none' },
163
+ peer: {
164
+ workspace: 'new_stream',
165
+ stream_lineage: 'fork_from_team_root',
166
+ landing: 'merge_to_parent_stream',
167
+ capabilities: ['workspace.commit', 'workspace.land'],
168
+ },
169
+ },
170
+ });
171
+ if (!parsed) throw new Error('config should parse');
172
+ config = parsed;
173
+ topology = new YamlDrivenTopology(config);
174
+ system.agentManager.setTopologyPolicy(topology);
175
+
176
+ // Simulate team start (TeamManager would do this; we call manually)
177
+ await topology.onTeamStart({
178
+ teamName: 'peer-swarm-test',
179
+ teamInstanceId: 'ps-1',
180
+ workspaceConfig: config,
181
+ workspaceManager,
182
+ });
183
+ });
184
+
185
+ it('creates a team root stream at onTeamStart', () => {
186
+ const streams = workspaceManager.listStreams();
187
+ const teamRoot = streams.find((s) => s.agentId === 'team:peer-swarm-test');
188
+ expect(teamRoot).toBeDefined();
189
+ expect(teamRoot?.name).toBe('peer-swarm-test');
190
+ });
191
+
192
+ it('spawns orchestrator with no workspace (workspace: none)', async () => {
193
+ const orch = await system.agentManager.spawn({
194
+ role: 'orchestrator',
195
+ task: 'coordinate',
196
+ });
197
+ const record = system.agentStore.getAgent(orch.id);
198
+ // workspace: none → no workspace_path assigned
199
+ expect(record?.workspace_path).toBeFalsy();
200
+ });
201
+
202
+ it('spawns peer with a new stream forked off team root', async () => {
203
+ const peer = await system.agentManager.spawn({
204
+ role: 'peer',
205
+ task: 'investigate',
206
+ });
207
+ const record = system.agentStore.getAgent(peer.id);
208
+
209
+ expect(record?.workspace_path).toBeDefined();
210
+ expect(fs.existsSync(record!.workspace_path!)).toBe(true);
211
+
212
+ const streams = workspaceManager.listStreams();
213
+ const teamRoot = streams.find((s) => s.agentId === 'team:peer-swarm-test');
214
+ const peerStream = streams.find((s) => s.agentId === peer.id);
215
+
216
+ expect(peerStream).toBeDefined();
217
+ expect(peerStream?.parentStream).toBe(teamRoot?.id);
218
+ });
219
+
220
+ it('emits stream:created and worktree:allocated events on peer spawn', async () => {
221
+ const events: string[] = [];
222
+ workspaceManager.onEvent((e) => events.push(e.type));
223
+
224
+ await system.agentManager.spawn({ role: 'peer', task: 'investigate' });
225
+
226
+ expect(events).toContain('stream:forked');
227
+ expect(events).toContain('worktree:allocated');
228
+ });
229
+ });
230
+
231
+ // ═══════════════════════════════════════════════════════════════
232
+ // TEST 2: Landing — real merge-to-parent through a real merge
233
+ // ═══════════════════════════════════════════════════════════════
234
+
235
+ describe('merge-to-parent landing (V3)', () => {
236
+ it('merges a child stream into its parent via the strategy', async () => {
237
+ // Team root owned by a pseudo-principal
238
+ const parentStreamId = workspaceManager.createStreamV3({
239
+ name: 'parent',
240
+ ownerId: 'team:landing-test',
241
+ forkFrom: 'main',
242
+ });
243
+
244
+ // Agent owns a child forked off parent
245
+ const childStreamId = workspaceManager.forkStream({
246
+ parentStreamId,
247
+ name: 'child',
248
+ ownerId: 'agent-auth',
249
+ });
250
+
251
+ // Allocate worktree for the agent on the child branch
252
+ const worktree = workspaceManager.allocateWorktree({
253
+ agentId: 'agent-auth',
254
+ streamId: childStreamId,
255
+ });
256
+
257
+ // Make a real commit via commitChanges (Change-Id tracked)
258
+ fs.writeFileSync(path.join(worktree.path, 'auth.ts'), 'export const X = 1;');
259
+ const { commit, changeId } = workspaceManager.commitChanges({
260
+ agentId: 'agent-auth',
261
+ streamId: childStreamId,
262
+ worktree: worktree.path,
263
+ message: 'feat: add auth module',
264
+ });
265
+
266
+ expect(commit).toMatch(/^[0-9a-f]+$/);
267
+ expect(changeId).toMatch(/^c-/);
268
+
269
+ // Invoke the landing strategy directly
270
+ const strategy = new MergeToParentStrategy();
271
+ const result = await strategy.land({
272
+ agentId: 'agent-auth',
273
+ streamId: childStreamId,
274
+ sourceWorktree: worktree.path,
275
+ workspaceManager,
276
+ });
277
+
278
+ expect(result.success).toBe(true);
279
+
280
+ // Verify parent stream advanced
281
+ const parent = workspaceManager.listStreams().find((s) => s.id === parentStreamId);
282
+ expect(parent?.status).toBe('active');
283
+
284
+ // Verify the Change-Id is tracked and findable
285
+ const change = workspaceManager.getChange(changeId);
286
+ expect(change).not.toBeNull();
287
+ });
288
+
289
+ it('fails gracefully when source has no parent', async () => {
290
+ // Orphan stream (no parent)
291
+ const streamId = workspaceManager.createStreamV3({
292
+ name: 'orphan',
293
+ ownerId: 'agent-x',
294
+ forkFrom: 'main',
295
+ });
296
+ const worktree = workspaceManager.allocateWorktree({
297
+ agentId: 'agent-x',
298
+ streamId,
299
+ });
300
+
301
+ const strategy = new MergeToParentStrategy();
302
+ const result = await strategy.land({
303
+ agentId: 'agent-x',
304
+ streamId,
305
+ sourceWorktree: worktree.path,
306
+ workspaceManager,
307
+ });
308
+
309
+ expect(result.success).toBe(false);
310
+ expect(result.error).toMatch(/no target stream/);
311
+ });
312
+ });
313
+
314
+ // ═══════════════════════════════════════════════════════════════
315
+ // TEST 3: Conflict recovery — real conflict → real abandon
316
+ // ═══════════════════════════════════════════════════════════════
317
+
318
+ describe('conflict recovery dispatch (V3)', () => {
319
+ it('routes through buildBuiltinRecoveryRegistry to the abandon strategy', async () => {
320
+ // Create a stream so abandon has something to act on
321
+ const streamId = workspaceManager.createStreamV3({
322
+ name: 'doomed',
323
+ ownerId: 'agent-1',
324
+ forkFrom: 'main',
325
+ });
326
+
327
+ // Simulate a conflict record created by git-cascade
328
+ const conflictId = adapter.createConflict({
329
+ streamId,
330
+ conflictingCommit: '0'.repeat(40),
331
+ targetCommit: '0'.repeat(40),
332
+ conflictedFiles: ['conflict.ts'],
333
+ });
334
+
335
+ const registry = buildBuiltinRecoveryRegistry();
336
+ const abandon = registry.get('abandon');
337
+ expect(abandon).toBeDefined();
338
+
339
+ const resolution = await abandon!.recover({
340
+ conflictId,
341
+ streamId,
342
+ paths: ['conflict.ts'],
343
+ operation: 'merge',
344
+ recoveryDepth: 0,
345
+ workspaceManager,
346
+ });
347
+
348
+ expect(resolution.kind).toBe('abandoned');
349
+ // Verify the stream is actually abandoned in git-cascade
350
+ const stream = workspaceManager.listStreams().find((s) => s.id === streamId);
351
+ expect(stream?.status).toBe('abandoned');
352
+ });
353
+
354
+ it('defer leaves the conflict record in place', async () => {
355
+ const streamId = workspaceManager.createStreamV3({
356
+ name: 'deferred',
357
+ ownerId: 'agent-1',
358
+ forkFrom: 'main',
359
+ });
360
+ const conflictId = adapter.createConflict({
361
+ streamId,
362
+ conflictingCommit: '0'.repeat(40),
363
+ targetCommit: '0'.repeat(40),
364
+ conflictedFiles: ['x.ts'],
365
+ });
366
+
367
+ const defer = new DeferStrategy();
368
+ const resolution = await defer.recover({
369
+ conflictId,
370
+ streamId,
371
+ paths: ['x.ts'],
372
+ operation: 'merge',
373
+ recoveryDepth: 0,
374
+ workspaceManager,
375
+ });
376
+
377
+ expect(resolution.kind).toBe('deferred');
378
+ // Stream is untouched (still active; conflict record still exists)
379
+ const stream = workspaceManager.listStreams().find((s) => s.id === streamId);
380
+ expect(stream?.status).toBe('active');
381
+ const conflict = adapter.getConflict(conflictId);
382
+ expect(conflict).not.toBeNull();
383
+ });
384
+ });
385
+
386
+ // ═══════════════════════════════════════════════════════════════
387
+ // TEST 4: Regression — legacy path still works unchanged
388
+ // ═══════════════════════════════════════════════════════════════
389
+
390
+ describe('regression: legacy path (no TopologyPolicy set)', () => {
391
+ it('still allocates workspace via role-name dispatch', async () => {
392
+ // Do NOT set a topology policy; ensure legacy path is active
393
+ const streamId = workspaceManager.createIntegrationStream('coord-legacy', {
394
+ name: 'legacy-feature',
395
+ });
396
+ const taskId = workspaceManager.createTask(streamId, {
397
+ title: 'legacy task',
398
+ });
399
+
400
+ const worker = await system.agentManager.spawn({
401
+ role: 'worker',
402
+ task: 'do work',
403
+ streamId,
404
+ gitCascadeTaskId: taskId,
405
+ capabilities: ['workspace.worktree'],
406
+ });
407
+
408
+ const record = system.agentStore.getAgent(worker.id);
409
+ expect(record?.workspace_path).toBeDefined();
410
+ expect(fs.existsSync(record!.workspace_path!)).toBe(true);
411
+ });
412
+ });
413
+ });
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Tests for the Claude Code JSONL → ACP SessionUpdate converter.
3
+ *
4
+ * Verifies that the replay fallback correctly parses real-world Claude Code
5
+ * transcript entries and emits the right sessionUpdate events in order.
6
+ */
7
+
8
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
9
+ import * as fs from "node:fs/promises";
10
+ import * as path from "node:path";
11
+ import * as os from "node:os";
12
+ import { randomBytes } from "node:crypto";
13
+ import { replayClaudeCodeTranscript } from "../claude-code-replay.js";
14
+
15
+ function jsonl(obj: unknown): string {
16
+ return JSON.stringify(obj) + "\n";
17
+ }
18
+
19
+ async function collectUpdates(sessionId: string): Promise<any[]> {
20
+ const updates: any[] = [];
21
+ for await (const u of replayClaudeCodeTranscript(sessionId)) {
22
+ updates.push(u);
23
+ }
24
+ return updates;
25
+ }
26
+
27
+ describe("replayClaudeCodeTranscript", () => {
28
+ const testProjectDir = `-test-replay-${randomBytes(4).toString("hex")}`;
29
+ const claudeProjectsRoot = path.join(os.homedir(), ".claude", "projects");
30
+ const fixtureDir = path.join(claudeProjectsRoot, testProjectDir);
31
+ const nonExistentSessionId = "00000000-0000-0000-0000-000000000000";
32
+
33
+ beforeAll(async () => {
34
+ await fs.mkdir(fixtureDir, { recursive: true });
35
+ });
36
+
37
+ afterAll(async () => {
38
+ await fs.rm(fixtureDir, { recursive: true, force: true });
39
+ });
40
+
41
+ it("returns no updates when transcript file doesn't exist", async () => {
42
+ const updates = await collectUpdates(nonExistentSessionId);
43
+ expect(updates).toEqual([]);
44
+ });
45
+
46
+ it("converts a simple user/assistant exchange", async () => {
47
+ const sessionId = `sess-simple-${randomBytes(4).toString("hex")}`;
48
+ const transcript =
49
+ jsonl({ type: "queue-operation", operation: "enqueue" }) + // skip
50
+ jsonl({
51
+ type: "user",
52
+ isMeta: false,
53
+ message: { role: "user", content: [{ type: "text", text: "hello" }] },
54
+ }) +
55
+ jsonl({
56
+ type: "assistant",
57
+ message: {
58
+ role: "assistant",
59
+ content: [{ type: "text", text: "hi there" }],
60
+ },
61
+ });
62
+ await fs.writeFile(path.join(fixtureDir, `${sessionId}.jsonl`), transcript);
63
+
64
+ const updates = await collectUpdates(sessionId);
65
+ expect(updates).toHaveLength(2);
66
+ expect(updates[0]).toEqual({
67
+ sessionUpdate: "user_message_chunk",
68
+ content: { type: "text", text: "hello" },
69
+ });
70
+ expect(updates[1]).toEqual({
71
+ sessionUpdate: "agent_message_chunk",
72
+ content: { type: "text", text: "hi there" },
73
+ });
74
+ });
75
+
76
+ it("skips meta messages and internal commands", async () => {
77
+ const sessionId = `sess-meta-${randomBytes(4).toString("hex")}`;
78
+ const transcript =
79
+ jsonl({
80
+ type: "user",
81
+ isMeta: true, // should skip
82
+ message: { role: "user", content: "system context" },
83
+ }) +
84
+ jsonl({
85
+ type: "user",
86
+ message: { role: "user", content: "<command-name>/model</command-name>" },
87
+ }) + // string starting with <command- → skip
88
+ jsonl({
89
+ type: "user",
90
+ message: { role: "user", content: "<local-command-stdout>ok</local-command-stdout>" },
91
+ }) + // skip
92
+ jsonl({
93
+ type: "user",
94
+ message: { role: "user", content: [{ type: "text", text: "real message" }] },
95
+ });
96
+ await fs.writeFile(path.join(fixtureDir, `${sessionId}.jsonl`), transcript);
97
+
98
+ const updates = await collectUpdates(sessionId);
99
+ expect(updates).toHaveLength(1);
100
+ expect(updates[0]).toMatchObject({
101
+ sessionUpdate: "user_message_chunk",
102
+ content: { type: "text", text: "real message" },
103
+ });
104
+ });
105
+
106
+ it("converts tool_use and tool_result blocks", async () => {
107
+ const sessionId = `sess-tools-${randomBytes(4).toString("hex")}`;
108
+ const transcript =
109
+ jsonl({
110
+ type: "assistant",
111
+ message: {
112
+ role: "assistant",
113
+ content: [
114
+ { type: "text", text: "Let me check." },
115
+ {
116
+ type: "tool_use",
117
+ id: "tc_1",
118
+ name: "Read",
119
+ input: { path: "/tmp/foo" },
120
+ },
121
+ ],
122
+ },
123
+ }) +
124
+ jsonl({
125
+ type: "user",
126
+ message: {
127
+ role: "user",
128
+ content: [
129
+ {
130
+ type: "tool_result",
131
+ tool_use_id: "tc_1",
132
+ content: "file contents",
133
+ },
134
+ ],
135
+ },
136
+ });
137
+ await fs.writeFile(path.join(fixtureDir, `${sessionId}.jsonl`), transcript);
138
+
139
+ const updates = await collectUpdates(sessionId);
140
+ expect(updates).toHaveLength(3);
141
+ expect(updates[0]).toMatchObject({
142
+ sessionUpdate: "agent_message_chunk",
143
+ content: { type: "text", text: "Let me check." },
144
+ });
145
+ expect(updates[1]).toMatchObject({
146
+ sessionUpdate: "tool_call",
147
+ toolCallId: "tc_1",
148
+ title: "Read",
149
+ rawInput: { path: "/tmp/foo" },
150
+ });
151
+ expect(updates[2]).toMatchObject({
152
+ sessionUpdate: "tool_call_update",
153
+ toolCallId: "tc_1",
154
+ output: "file contents",
155
+ status: "completed",
156
+ });
157
+ });
158
+
159
+ it("handles assistant thinking blocks", async () => {
160
+ const sessionId = `sess-think-${randomBytes(4).toString("hex")}`;
161
+ const transcript = jsonl({
162
+ type: "assistant",
163
+ message: {
164
+ role: "assistant",
165
+ content: [
166
+ { type: "thinking", thinking: "Let me reason about this..." },
167
+ { type: "text", text: "Here's my answer." },
168
+ ],
169
+ },
170
+ });
171
+ await fs.writeFile(path.join(fixtureDir, `${sessionId}.jsonl`), transcript);
172
+
173
+ const updates = await collectUpdates(sessionId);
174
+ expect(updates).toHaveLength(2);
175
+ expect(updates[0]).toMatchObject({
176
+ sessionUpdate: "agent_thought_chunk",
177
+ content: { type: "text", text: "Let me reason about this..." },
178
+ });
179
+ expect(updates[1]).toMatchObject({
180
+ sessionUpdate: "agent_message_chunk",
181
+ });
182
+ });
183
+
184
+ it("ignores malformed JSONL lines and continues", async () => {
185
+ const sessionId = `sess-bad-${randomBytes(4).toString("hex")}`;
186
+ const transcript =
187
+ "not-json\n" +
188
+ "{\n" + // incomplete
189
+ jsonl({
190
+ type: "user",
191
+ message: { role: "user", content: [{ type: "text", text: "after garbage" }] },
192
+ }) +
193
+ "\n"; // blank line
194
+ await fs.writeFile(path.join(fixtureDir, `${sessionId}.jsonl`), transcript);
195
+
196
+ const updates = await collectUpdates(sessionId);
197
+ expect(updates).toHaveLength(1);
198
+ expect(updates[0]).toMatchObject({
199
+ content: { type: "text", text: "after garbage" },
200
+ });
201
+ });
202
+
203
+ it("produces events in chronological (file) order", async () => {
204
+ const sessionId = `sess-order-${randomBytes(4).toString("hex")}`;
205
+ const transcript = Array.from({ length: 5 }, (_, i) =>
206
+ jsonl({
207
+ type: i % 2 === 0 ? "user" : "assistant",
208
+ message: {
209
+ role: i % 2 === 0 ? "user" : "assistant",
210
+ content: [{ type: "text", text: `msg-${i}` }],
211
+ },
212
+ }),
213
+ ).join("");
214
+ await fs.writeFile(path.join(fixtureDir, `${sessionId}.jsonl`), transcript);
215
+
216
+ const updates = await collectUpdates(sessionId);
217
+ expect(updates.map((u) => u.content?.text)).toEqual([
218
+ "msg-0",
219
+ "msg-1",
220
+ "msg-2",
221
+ "msg-3",
222
+ "msg-4",
223
+ ]);
224
+ });
225
+ });
@@ -141,7 +141,7 @@ describe("createMacroAgent", () => {
141
141
  // ── newSession ──────────────────────────────────────────────
142
142
 
143
143
  describe("newSession", () => {
144
- it("should create head manager and return session ID", async () => {
144
+ it("should create head manager and return session ID (cwd fallback)", async () => {
145
145
  const agent = createAgent();
146
146
  const result = await agent.newSession({
147
147
  cwd: "/tmp/test",
@@ -153,6 +153,44 @@ describe("createMacroAgent", () => {
153
153
  cwd: "/tmp/test",
154
154
  });
155
155
  });
156
+
157
+ it("binds the session to initConfig.targetAgentId when provided (skips cwd lookup)", async () => {
158
+ // Stub getActiveAgentSession on the mock system
159
+ system.agentManager.getActiveAgentSession = vi.fn().mockReturnValue({
160
+ id: "worker-7",
161
+ session_id: "worker-7-session",
162
+ agent: { id: "worker-7", role: "worker", state: "running" },
163
+ session: {},
164
+ });
165
+
166
+ // Bind this MacroAgent to a specific (non-coordinator) agent
167
+ const agent = createMacroAgent(connection, {
168
+ system,
169
+ initConfig: { targetAgentId: "worker-7" },
170
+ });
171
+ await agent.newSession({ cwd: "/tmp/whatever", mcpServers: [] });
172
+
173
+ expect(system.agentManager.getActiveAgentSession).toHaveBeenCalledWith("worker-7");
174
+ // cwd-based lookup must NOT run when a target is bound
175
+ expect(system.agentManager.getOrCreateHeadManager).not.toHaveBeenCalled();
176
+ });
177
+
178
+ it("throws AGENT_NOT_FOUND when targetAgentId has no live session", async () => {
179
+ system.agentManager.getActiveAgentSession = vi.fn().mockReturnValue(null);
180
+
181
+ const agent = createMacroAgent(connection, {
182
+ system,
183
+ initConfig: { targetAgentId: "ghost-agent" },
184
+ });
185
+
186
+ await expect(
187
+ agent.newSession({ cwd: "/tmp/test", mcpServers: [] }),
188
+ ).rejects.toMatchObject({
189
+ name: "ACPError",
190
+ code: "AGENT_NOT_FOUND",
191
+ });
192
+ expect(system.agentManager.getOrCreateHeadManager).not.toHaveBeenCalled();
193
+ });
156
194
  });
157
195
 
158
196
  // ── prompt ──────────────────────────────────────────────────