macro-agent 0.1.10 → 0.1.12

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 (111) hide show
  1. package/CLAUDE.md +97 -0
  2. package/dist/acp/macro-agent.d.ts.map +1 -1
  3. package/dist/acp/macro-agent.js +42 -6
  4. package/dist/acp/macro-agent.js.map +1 -1
  5. package/dist/adapters/tasks-adapter.d.ts.map +1 -1
  6. package/dist/adapters/tasks-adapter.js +3 -0
  7. package/dist/adapters/tasks-adapter.js.map +1 -1
  8. package/dist/adapters/types.d.ts +1 -0
  9. package/dist/adapters/types.d.ts.map +1 -1
  10. package/dist/agent/agent-manager-v2.d.ts.map +1 -1
  11. package/dist/agent/agent-manager-v2.js +74 -11
  12. package/dist/agent/agent-manager-v2.js.map +1 -1
  13. package/dist/agent/agent-store.d.ts +10 -0
  14. package/dist/agent/agent-store.d.ts.map +1 -1
  15. package/dist/agent/agent-store.js +22 -0
  16. package/dist/agent/agent-store.js.map +1 -1
  17. package/dist/boot-v2.d.ts +88 -1
  18. package/dist/boot-v2.d.ts.map +1 -1
  19. package/dist/boot-v2.js +343 -7
  20. package/dist/boot-v2.js.map +1 -1
  21. package/dist/cli/acp.js +4 -0
  22. package/dist/cli/acp.js.map +1 -1
  23. package/dist/lifecycle/cascade.d.ts +25 -2
  24. package/dist/lifecycle/cascade.d.ts.map +1 -1
  25. package/dist/lifecycle/cascade.js +70 -2
  26. package/dist/lifecycle/cascade.js.map +1 -1
  27. package/dist/map/cascade-action-handler.d.ts +24 -0
  28. package/dist/map/cascade-action-handler.d.ts.map +1 -0
  29. package/dist/map/cascade-action-handler.js +170 -0
  30. package/dist/map/cascade-action-handler.js.map +1 -0
  31. package/dist/map/cascade-bridge.d.ts.map +1 -1
  32. package/dist/map/cascade-bridge.js +42 -5
  33. package/dist/map/cascade-bridge.js.map +1 -1
  34. package/dist/map/coordination-handler.d.ts.map +1 -1
  35. package/dist/map/coordination-handler.js +12 -1
  36. package/dist/map/coordination-handler.js.map +1 -1
  37. package/dist/map/server.d.ts.map +1 -1
  38. package/dist/map/server.js +172 -1
  39. package/dist/map/server.js.map +1 -1
  40. package/dist/map/sidecar.d.ts.map +1 -1
  41. package/dist/map/sidecar.js +18 -2
  42. package/dist/map/sidecar.js.map +1 -1
  43. package/dist/map/types.d.ts +2 -0
  44. package/dist/map/types.d.ts.map +1 -1
  45. package/dist/teams/seed-defaults.d.ts.map +1 -1
  46. package/dist/teams/seed-defaults.js +6 -2
  47. package/dist/teams/seed-defaults.js.map +1 -1
  48. package/dist/teams/team-loader.d.ts.map +1 -1
  49. package/dist/teams/team-loader.js +17 -1
  50. package/dist/teams/team-loader.js.map +1 -1
  51. package/dist/workspace/git-cascade-adapter.d.ts +1 -1
  52. package/dist/workspace/git-cascade-adapter.d.ts.map +1 -1
  53. package/dist/workspace/git-cascade-adapter.js +26 -0
  54. package/dist/workspace/git-cascade-adapter.js.map +1 -1
  55. package/dist/workspace/landing/merge-to-parent.d.ts.map +1 -1
  56. package/dist/workspace/landing/merge-to-parent.js +1 -0
  57. package/dist/workspace/landing/merge-to-parent.js.map +1 -1
  58. package/dist/workspace/recovery/spawn-resolver.d.ts.map +1 -1
  59. package/dist/workspace/recovery/spawn-resolver.js +8 -1
  60. package/dist/workspace/recovery/spawn-resolver.js.map +1 -1
  61. package/dist/workspace/types-v3.d.ts +7 -0
  62. package/dist/workspace/types-v3.d.ts.map +1 -1
  63. package/dist/workspace/types-v3.js.map +1 -1
  64. package/dist/workspace/types.d.ts +17 -0
  65. package/dist/workspace/types.d.ts.map +1 -1
  66. package/dist/workspace/workspace-manager.d.ts +9 -0
  67. package/dist/workspace/workspace-manager.d.ts.map +1 -1
  68. package/dist/workspace/workspace-manager.js +45 -2
  69. package/dist/workspace/workspace-manager.js.map +1 -1
  70. package/docs/design/task-dispatcher.md +880 -0
  71. package/package.json +3 -3
  72. package/src/__tests__/boot-v2.test.ts +435 -0
  73. package/src/__tests__/e2e/acp-over-map.e2e.test.ts +92 -0
  74. package/src/__tests__/e2e/bootstrap.e2e.test.ts +319 -0
  75. package/src/__tests__/e2e/dispatch-coordination.e2e.test.ts +495 -0
  76. package/src/__tests__/e2e/dispatch-live.e2e.test.ts +564 -0
  77. package/src/__tests__/e2e/dispatch-opentasks.e2e.test.ts +496 -0
  78. package/src/__tests__/e2e/dispatch-phase2-live.e2e.test.ts +456 -0
  79. package/src/__tests__/e2e/dispatch-phase2.e2e.test.ts +386 -0
  80. package/src/__tests__/e2e/dispatch.e2e.test.ts +376 -0
  81. package/src/acp/macro-agent.ts +41 -6
  82. package/src/adapters/__tests__/tasks-adapter.test.ts +1 -0
  83. package/src/adapters/tasks-adapter.ts +3 -0
  84. package/src/adapters/types.ts +1 -0
  85. package/src/agent/__tests__/agent-store.test.ts +52 -0
  86. package/src/agent/agent-manager-v2.ts +79 -11
  87. package/src/agent/agent-store.ts +24 -0
  88. package/src/boot-v2.ts +522 -35
  89. package/src/cli/acp.ts +4 -0
  90. package/src/lifecycle/__tests__/cascade-consolidation.test.ts +240 -0
  91. package/src/lifecycle/cascade.ts +77 -2
  92. package/src/map/__tests__/emit-event.test.ts +71 -0
  93. package/src/map/cascade-action-handler.ts +205 -0
  94. package/src/map/cascade-bridge.ts +43 -5
  95. package/src/map/coordination-handler.ts +13 -1
  96. package/src/map/server.ts +178 -1
  97. package/src/map/sidecar.ts +19 -2
  98. package/src/map/types.ts +3 -0
  99. package/src/teams/seed-defaults.ts +6 -2
  100. package/src/teams/team-loader.ts +18 -1
  101. package/src/workspace/__tests__/land-dispatch.test.ts +214 -0
  102. package/src/workspace/__tests__/self-driving-yaml.test.ts +10 -2
  103. package/src/workspace/git-cascade-adapter.ts +30 -3
  104. package/src/workspace/landing/__tests__/strategies.test.ts +42 -0
  105. package/src/workspace/landing/merge-to-parent.ts +1 -0
  106. package/src/workspace/recovery/spawn-resolver.ts +8 -1
  107. package/src/workspace/types-v3.ts +7 -0
  108. package/src/workspace/types.ts +20 -0
  109. package/src/workspace/workspace-manager.ts +61 -2
  110. package/templates/teams/self-driving/team.yaml +142 -0
  111. package/tsconfig.json +2 -1
@@ -0,0 +1,386 @@
1
+ /**
2
+ * Task Dispatch Phase 2 E2E Tests (mocked agents)
3
+ *
4
+ * Tests dispatch boot wiring for Phase 2 ports: MessagePort (agent-inbox),
5
+ * AgentRoster (inbox agent listing), dispatch mode selection, snapshot,
6
+ * and mail routing configuration.
7
+ *
8
+ * Uses the same mock strategy as dispatch.e2e.test.ts — mocked acp-factory
9
+ * and opentasks (no real agents or daemon).
10
+ *
11
+ * REQUIRES: RUN_E2E_TESTS=true
12
+ *
13
+ * Run with:
14
+ * RUN_E2E_TESTS=true npx vitest run --config vitest.e2e.config.ts src/__tests__/e2e/dispatch-phase2.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 { bootV2, type MacroAgentSystemV2 } from "../../boot-v2.js";
29
+
30
+ // ─────────────────────────────────────────────────────────────────
31
+ // Configuration
32
+ // ─────────────────────────────────────────────────────────────────
33
+
34
+ const RUN_E2E = !!process.env.RUN_E2E_TESTS;
35
+ const describeFn = RUN_E2E ? describe : describe.skip;
36
+
37
+ // ─────────────────────────────────────────────────────────────────
38
+ // Mocks (same as dispatch.e2e.test.ts)
39
+ // ─────────────────────────────────────────────────────────────────
40
+
41
+ vi.mock("acp-factory", () => ({
42
+ AgentFactory: {
43
+ spawn: vi.fn().mockResolvedValue({
44
+ createSession: vi.fn().mockResolvedValue({
45
+ id: `session-${Date.now()}`,
46
+ prompt: vi.fn().mockReturnValue({
47
+ [Symbol.asyncIterator]: () => ({
48
+ next: () => Promise.resolve({ done: true, value: undefined }),
49
+ }),
50
+ }),
51
+ forkWithFlush: vi.fn().mockResolvedValue({ id: `forked-${Date.now()}` }),
52
+ }),
53
+ loadSession: vi.fn().mockResolvedValue({ id: `loaded-${Date.now()}` }),
54
+ close: vi.fn().mockResolvedValue(undefined),
55
+ isRunning: vi.fn().mockReturnValue(true),
56
+ }),
57
+ },
58
+ }));
59
+
60
+ vi.mock("opentasks", () => ({
61
+ OpenTasksClient: vi.fn().mockImplementation(() => ({
62
+ connect: vi.fn().mockRejectedValue(new Error("No daemon")),
63
+ disconnect: vi.fn(),
64
+ query: vi.fn().mockResolvedValue({ items: [] }),
65
+ link: vi.fn().mockResolvedValue({ success: true }),
66
+ task: vi.fn().mockResolvedValue({ id: "t-1" }),
67
+ })),
68
+ }));
69
+
70
+ // ─────────────────────────────────────────────────────────────────
71
+ // Helpers
72
+ // ─────────────────────────────────────────────────────────────────
73
+
74
+ function createTestDir(): string {
75
+ const dir = path.join(
76
+ os.tmpdir(),
77
+ `dispatch-p2-e2e-${Date.now()}-${Math.random().toString(36).slice(2)}`
78
+ );
79
+ fs.mkdirSync(dir, { recursive: true });
80
+ return dir;
81
+ }
82
+
83
+ // ─────────────────────────────────────────────────────────────────
84
+ // Tests
85
+ // ─────────────────────────────────────────────────────────────────
86
+
87
+ describeFn("Task Dispatch Phase 2 E2E", () => {
88
+ let system: MacroAgentSystemV2;
89
+ let testDir: string;
90
+
91
+ beforeEach(async () => {
92
+ testDir = createTestDir();
93
+ });
94
+
95
+ afterEach(async () => {
96
+ if (system) {
97
+ try {
98
+ const running = system.agentManager.list({ state: "running" } as any);
99
+ for (const agent of running) {
100
+ await system.agentManager.terminate(agent.id, "cancelled");
101
+ }
102
+ } catch { /* best effort */ }
103
+ await system.shutdown();
104
+ }
105
+ if (fs.existsSync(testDir)) {
106
+ fs.rmSync(testDir, { recursive: true, force: true });
107
+ }
108
+ });
109
+
110
+ // ── Phase 2 boot wiring ───────────────────────────────────
111
+
112
+ describe("Phase 2 Boot", () => {
113
+ it("boots with mail routing and roster enabled by default", async () => {
114
+ system = await bootV2({
115
+ cwd: testDir,
116
+ baseDir: testDir,
117
+ inbox: { socketPath: path.join(testDir, "inbox.sock") },
118
+ dispatch: {
119
+ enabled: true,
120
+ pollIntervalMs: 600_000,
121
+ maxConcurrent: 3,
122
+ },
123
+ });
124
+
125
+ expect(system.taskDispatcher).toBeDefined();
126
+ expect(system.taskDispatcher!.running).toBe(true);
127
+ });
128
+
129
+ it("boots with mail routing explicitly disabled", async () => {
130
+ system = await bootV2({
131
+ cwd: testDir,
132
+ baseDir: testDir,
133
+ inbox: { socketPath: path.join(testDir, "inbox.sock") },
134
+ dispatch: {
135
+ enabled: true,
136
+ pollIntervalMs: 600_000,
137
+ enableMailRouting: false,
138
+ },
139
+ });
140
+
141
+ expect(system.taskDispatcher).toBeDefined();
142
+ expect(system.taskDispatcher!.running).toBe(true);
143
+ });
144
+
145
+ it("boots with roster explicitly disabled", async () => {
146
+ system = await bootV2({
147
+ cwd: testDir,
148
+ baseDir: testDir,
149
+ inbox: { socketPath: path.join(testDir, "inbox.sock") },
150
+ dispatch: {
151
+ enabled: true,
152
+ pollIntervalMs: 600_000,
153
+ enableRoster: false,
154
+ },
155
+ });
156
+
157
+ expect(system.taskDispatcher).toBeDefined();
158
+ expect(system.taskDispatcher!.running).toBe(true);
159
+ });
160
+
161
+ it("boots with custom dispatchMode override", async () => {
162
+ system = await bootV2({
163
+ cwd: testDir,
164
+ baseDir: testDir,
165
+ inbox: { socketPath: path.join(testDir, "inbox.sock") },
166
+ dispatch: {
167
+ enabled: true,
168
+ pollIntervalMs: 600_000,
169
+ dispatchMode: "spawn-only",
170
+ },
171
+ });
172
+
173
+ expect(system.taskDispatcher).toBeDefined();
174
+ expect(system.taskDispatcher!.running).toBe(true);
175
+ });
176
+
177
+ it("boots with route-only mode", async () => {
178
+ system = await bootV2({
179
+ cwd: testDir,
180
+ baseDir: testDir,
181
+ inbox: { socketPath: path.join(testDir, "inbox.sock") },
182
+ dispatch: {
183
+ enabled: true,
184
+ pollIntervalMs: 600_000,
185
+ dispatchMode: "route-only",
186
+ },
187
+ });
188
+
189
+ expect(system.taskDispatcher).toBeDefined();
190
+ expect(system.taskDispatcher!.running).toBe(true);
191
+ });
192
+
193
+ it("boots with continuation config", async () => {
194
+ system = await bootV2({
195
+ cwd: testDir,
196
+ baseDir: testDir,
197
+ inbox: { socketPath: path.join(testDir, "inbox.sock") },
198
+ dispatch: {
199
+ enabled: true,
200
+ pollIntervalMs: 600_000,
201
+ continuation: { delayMs: 2_000, maxTurns: 10 },
202
+ },
203
+ });
204
+
205
+ expect(system.taskDispatcher).toBeDefined();
206
+ expect(system.taskDispatcher!.running).toBe(true);
207
+ });
208
+
209
+ it("boots with stall timeout configured", async () => {
210
+ system = await bootV2({
211
+ cwd: testDir,
212
+ baseDir: testDir,
213
+ inbox: { socketPath: path.join(testDir, "inbox.sock") },
214
+ dispatch: {
215
+ enabled: true,
216
+ pollIntervalMs: 600_000,
217
+ reconcile: { enabled: true, intervalMs: 60_000, stallTimeoutMs: 120_000 },
218
+ },
219
+ });
220
+
221
+ expect(system.taskDispatcher).toBeDefined();
222
+ expect(system.taskDispatcher!.running).toBe(true);
223
+ });
224
+ });
225
+
226
+ // ── Snapshot ──────────────────────────────────────────────
227
+
228
+ describe("Snapshot", () => {
229
+ it("snapshot returns Phase 2 fields", async () => {
230
+ system = await bootV2({
231
+ cwd: testDir,
232
+ baseDir: testDir,
233
+ inbox: { socketPath: path.join(testDir, "inbox.sock") },
234
+ dispatch: {
235
+ enabled: true,
236
+ pollIntervalMs: 600_000,
237
+ maxConcurrent: 3,
238
+ },
239
+ });
240
+
241
+ const snap = system.taskDispatcher!.snapshot();
242
+ expect(snap).toBeDefined();
243
+ expect(snap.generatedAt).toBeTruthy();
244
+ expect(snap.counts).toMatchObject({
245
+ running: 0,
246
+ retryQueued: 0,
247
+ continuing: 0,
248
+ claimed: 0,
249
+ });
250
+ expect(snap.running).toHaveLength(0);
251
+ expect(snap.retryQueued).toHaveLength(0);
252
+ expect(snap.subscriberErrorCount).toBe(0);
253
+ // Phase 2: totals are present (even if zero)
254
+ expect(snap.totals).toBeDefined();
255
+ expect(snap.totals!.tokens).toMatchObject({
256
+ inputTokens: 0,
257
+ outputTokens: 0,
258
+ totalTokens: 0,
259
+ });
260
+ expect(snap.totals!.agentSeconds).toBeGreaterThanOrEqual(0);
261
+ });
262
+ });
263
+
264
+ // ── Dispatch + Reconcile ──────────────────────────────────
265
+
266
+ describe("Dispatch with Phase 2 ports", () => {
267
+ it("dispatchNow works with mail routing enabled", async () => {
268
+ system = await bootV2({
269
+ cwd: testDir,
270
+ baseDir: testDir,
271
+ inbox: { socketPath: path.join(testDir, "inbox.sock") },
272
+ dispatch: {
273
+ enabled: true,
274
+ pollIntervalMs: 600_000,
275
+ maxConcurrent: 5,
276
+ },
277
+ });
278
+
279
+ // No tasks ready — should complete cleanly.
280
+ await system.taskDispatcher!.dispatchNow();
281
+ expect(system.taskDispatcher!.tracker.activeCount()).toBe(0);
282
+ });
283
+
284
+ it("reconcileNow works with stall detection configured", async () => {
285
+ system = await bootV2({
286
+ cwd: testDir,
287
+ baseDir: testDir,
288
+ inbox: { socketPath: path.join(testDir, "inbox.sock") },
289
+ dispatch: {
290
+ enabled: true,
291
+ pollIntervalMs: 600_000,
292
+ maxConcurrent: 3,
293
+ reconcile: { enabled: true, intervalMs: 600_000, stallTimeoutMs: 10_000 },
294
+ },
295
+ });
296
+
297
+ // No active dispatches — reconcile should complete cleanly.
298
+ await system.taskDispatcher!.reconcileNow();
299
+ });
300
+
301
+ it("emits poll events with Phase 2 config", async () => {
302
+ system = await bootV2({
303
+ cwd: testDir,
304
+ baseDir: testDir,
305
+ inbox: { socketPath: path.join(testDir, "inbox.sock") },
306
+ dispatch: {
307
+ enabled: true,
308
+ pollIntervalMs: 600_000,
309
+ dispatchMode: "prefer-route",
310
+ },
311
+ });
312
+
313
+ const events: any[] = [];
314
+ system.taskDispatcher!.onEvent((e) => events.push(e));
315
+ await system.taskDispatcher!.dispatchNow();
316
+
317
+ const poll = events.find((e: any) => e.type === "poll");
318
+ expect(poll).toBeDefined();
319
+ expect(poll.dispatched).toBe(0);
320
+ expect(poll.active).toBe(0);
321
+ });
322
+
323
+ it("emits reconciled events with stall/cancelled counts", async () => {
324
+ system = await bootV2({
325
+ cwd: testDir,
326
+ baseDir: testDir,
327
+ inbox: { socketPath: path.join(testDir, "inbox.sock") },
328
+ dispatch: {
329
+ enabled: true,
330
+ pollIntervalMs: 600_000,
331
+ reconcile: { enabled: true, intervalMs: 600_000, stallTimeoutMs: 10_000 },
332
+ },
333
+ });
334
+
335
+ const events: any[] = [];
336
+ system.taskDispatcher!.onEvent((e) => events.push(e));
337
+ await system.taskDispatcher!.reconcileNow();
338
+
339
+ const reconciled = events.find((e: any) => e.type === "reconciled");
340
+ expect(reconciled).toBeDefined();
341
+ expect(reconciled.checked).toBe(0);
342
+ expect(reconciled.cancelled).toBe(0);
343
+ expect(reconciled.stalled).toBe(0);
344
+ });
345
+ });
346
+
347
+ // ── Shutdown ──────────────────────────────────────────────
348
+
349
+ describe("Shutdown with Phase 2 ports", () => {
350
+ it("shuts down cleanly with all Phase 2 ports enabled", async () => {
351
+ system = await bootV2({
352
+ cwd: testDir,
353
+ baseDir: testDir,
354
+ inbox: { socketPath: path.join(testDir, "inbox.sock") },
355
+ dispatch: {
356
+ enabled: true,
357
+ pollIntervalMs: 60_000,
358
+ maxConcurrent: 3,
359
+ dispatchMode: "prefer-route",
360
+ continuation: { delayMs: 1_000, maxTurns: 5 },
361
+ reconcile: { enabled: true, intervalMs: 120_000, stallTimeoutMs: 300_000 },
362
+ },
363
+ });
364
+
365
+ expect(system.taskDispatcher!.running).toBe(true);
366
+ await system.shutdown();
367
+ system = undefined as any;
368
+ });
369
+
370
+ it("shuts down cleanly with route-only mode", async () => {
371
+ system = await bootV2({
372
+ cwd: testDir,
373
+ baseDir: testDir,
374
+ inbox: { socketPath: path.join(testDir, "inbox.sock") },
375
+ dispatch: {
376
+ enabled: true,
377
+ pollIntervalMs: 60_000,
378
+ dispatchMode: "route-only",
379
+ },
380
+ });
381
+
382
+ await system.shutdown();
383
+ system = undefined as any;
384
+ });
385
+ });
386
+ });