macro-agent 0.1.5 → 0.1.7

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 (45) hide show
  1. package/.claude/settings.json +128 -1
  2. package/.sessionlog/settings.json +4 -0
  3. package/CLAUDE.md +125 -10
  4. package/README.md +93 -31
  5. package/dist/acp/macro-agent.d.ts.map +1 -1
  6. package/dist/acp/macro-agent.js +1 -3
  7. package/dist/acp/macro-agent.js.map +1 -1
  8. package/dist/boot-v2.d.ts +1 -0
  9. package/dist/boot-v2.d.ts.map +1 -1
  10. package/dist/boot-v2.js +1 -0
  11. package/dist/boot-v2.js.map +1 -1
  12. package/dist/cognitive/workspace-handler.d.ts +17 -9
  13. package/dist/cognitive/workspace-handler.d.ts.map +1 -1
  14. package/dist/cognitive/workspace-handler.js +10 -11
  15. package/dist/cognitive/workspace-handler.js.map +1 -1
  16. package/dist/map/coordination-handler.d.ts +7 -23
  17. package/dist/map/coordination-handler.d.ts.map +1 -1
  18. package/dist/map/coordination-handler.js +124 -100
  19. package/dist/map/coordination-handler.js.map +1 -1
  20. package/dist/map/server.d.ts.map +1 -1
  21. package/dist/map/server.js +13 -3
  22. package/dist/map/server.js.map +1 -1
  23. package/dist/map/sidecar.d.ts.map +1 -1
  24. package/dist/map/sidecar.js +13 -15
  25. package/dist/map/sidecar.js.map +1 -1
  26. package/dist/map/trajectory-reporter.d.ts +4 -9
  27. package/dist/map/trajectory-reporter.d.ts.map +1 -1
  28. package/dist/map/trajectory-reporter.js +15 -129
  29. package/dist/map/trajectory-reporter.js.map +1 -1
  30. package/dist/map/types.d.ts +39 -0
  31. package/dist/map/types.d.ts.map +1 -1
  32. package/package.json +2 -3
  33. package/src/__tests__/e2e/cognitive-workspace.e2e.test.ts +1 -1
  34. package/src/acp/macro-agent.ts +1 -4
  35. package/src/boot-v2.ts +2 -0
  36. package/src/cognitive/__tests__/workspace-handler.test.ts +2 -10
  37. package/src/cognitive/workspace-handler.ts +18 -15
  38. package/src/map/__tests__/trajectory-reporter.test.ts +2 -254
  39. package/src/map/coordination-handler.ts +137 -120
  40. package/src/map/server.ts +14 -2
  41. package/src/map/sidecar.ts +13 -20
  42. package/src/map/trajectory-reporter.ts +16 -154
  43. package/src/map/types.ts +43 -2
  44. package/src/__tests__/e2e/trajectory-content.e2e.test.ts +0 -708
  45. package/src/map/__tests__/coordination-handler.test.ts +0 -598
@@ -1,598 +0,0 @@
1
- /**
2
- * Tests for Coordination Handler — inbound MAP task messages + notifications.
3
- */
4
-
5
- import { describe, it, expect, beforeEach, vi } from "vitest";
6
- import {
7
- setupCoordinationHandlers,
8
- type CoordinationConnection,
9
- type CoordinationDeps,
10
- type MAPMessage,
11
- } from "../coordination-handler.js";
12
- import type { InboxAdapter, TasksAdapter } from "../../adapters/types.js";
13
-
14
- // =============================================================================
15
- // Mock helpers
16
- // =============================================================================
17
-
18
- type MessageHandler = (message: MAPMessage) => void | Promise<void>;
19
- type NotificationHandler = (params: unknown) => void | Promise<void>;
20
-
21
- function mockConnection() {
22
- const messageHandlers = new Set<MessageHandler>();
23
- const notificationHandlers = new Map<string, Set<NotificationHandler>>();
24
-
25
- const conn: CoordinationConnection = {
26
- onMessage(handler: MessageHandler) {
27
- messageHandlers.add(handler);
28
- },
29
- offMessage(handler: MessageHandler) {
30
- messageHandlers.delete(handler);
31
- },
32
- onNotification(method: string, handler: NotificationHandler) {
33
- if (!notificationHandlers.has(method)) {
34
- notificationHandlers.set(method, new Set());
35
- }
36
- notificationHandlers.get(method)!.add(handler);
37
- },
38
- offNotification(method: string, handler: NotificationHandler) {
39
- notificationHandlers.get(method)?.delete(handler);
40
- },
41
- sendNotification: vi.fn().mockResolvedValue(undefined),
42
- };
43
-
44
- return {
45
- conn,
46
- messageHandlers,
47
- notificationHandlers,
48
- /** Simulate an incoming MAP scope message */
49
- async emitMessage(payload: Record<string, unknown>) {
50
- const msg: MAPMessage = {
51
- id: `msg-${Date.now()}`,
52
- from: "hub",
53
- to: { scope: "swarm:test" },
54
- timestamp: new Date().toISOString(),
55
- payload,
56
- };
57
- for (const handler of messageHandlers) {
58
- await handler(msg);
59
- }
60
- },
61
- /** Simulate an incoming JSON-RPC notification */
62
- async emitNotification(method: string, params: unknown) {
63
- const handlers = notificationHandlers.get(method);
64
- if (handlers) {
65
- for (const handler of handlers) {
66
- await handler(params);
67
- }
68
- }
69
- },
70
- };
71
- }
72
-
73
- function mockInboxAdapter() {
74
- return {
75
- send: vi.fn().mockResolvedValue("msg-1"),
76
- } as unknown as InboxAdapter;
77
- }
78
-
79
- function mockTasksAdapter() {
80
- return {
81
- createTask: vi.fn().mockResolvedValue("task-100"),
82
- assignTask: vi.fn().mockResolvedValue(undefined),
83
- transitionTask: vi.fn().mockResolvedValue(undefined),
84
- connected: true,
85
- } as unknown as TasksAdapter;
86
- }
87
-
88
- function createDeps(overrides: Partial<CoordinationDeps> = {}): CoordinationDeps & {
89
- mock: ReturnType<typeof mockConnection>;
90
- } {
91
- const mock = mockConnection();
92
- return {
93
- connection: mock.conn,
94
- inboxAdapter: mockInboxAdapter(),
95
- tasksAdapter: mockTasksAdapter(),
96
- ...overrides,
97
- mock,
98
- };
99
- }
100
-
101
- // =============================================================================
102
- // Tests — Task messages (MAP scope messages)
103
- // =============================================================================
104
-
105
- describe("CoordinationHandler — task messages", () => {
106
- let deps: ReturnType<typeof createDeps>;
107
-
108
- beforeEach(() => {
109
- deps = createDeps();
110
- setupCoordinationHandlers(deps);
111
- });
112
-
113
- describe("task.created", () => {
114
- it("creates task in opentasks", async () => {
115
- await deps.mock.emitMessage({
116
- type: "task.created",
117
- task: { id: "t-1", title: "Fix bug", status: "open", assignee: "agent-1" },
118
- });
119
-
120
- expect(deps.tasksAdapter.createTask).toHaveBeenCalledWith(
121
- expect.objectContaining({
122
- title: "Fix bug",
123
- assignee: "agent-1",
124
- }),
125
- );
126
- });
127
-
128
- it("notifies assignee via inbox", async () => {
129
- await deps.mock.emitMessage({
130
- type: "task.created",
131
- task: { id: "t-1", title: "Fix bug", status: "open", assignee: "agent-1" },
132
- });
133
-
134
- expect(deps.inboxAdapter.send).toHaveBeenCalledWith(
135
- "system",
136
- "agent-1",
137
- expect.objectContaining({
138
- type: "event",
139
- event: "TASK_ASSIGNED",
140
- data: expect.objectContaining({ title: "Fix bug" }),
141
- }),
142
- );
143
- });
144
-
145
- it("skips inbox notification when no assignee", async () => {
146
- await deps.mock.emitMessage({
147
- type: "task.created",
148
- task: { id: "t-1", title: "Unassigned task", status: "open" },
149
- });
150
-
151
- expect(deps.tasksAdapter.createTask).toHaveBeenCalled();
152
- expect(deps.inboxAdapter.send).not.toHaveBeenCalled();
153
- });
154
-
155
- it("passes task description to opentasks", async () => {
156
- await deps.mock.emitMessage({
157
- type: "task.created",
158
- task: { id: "t-1", title: "Fix bug", status: "open", description: "Segfault on startup" },
159
- });
160
-
161
- expect(deps.tasksAdapter.createTask).toHaveBeenCalledWith(
162
- expect.objectContaining({
163
- title: "Fix bug",
164
- content: "Segfault on startup",
165
- }),
166
- );
167
- });
168
-
169
- it("ignores messages without a title", async () => {
170
- await deps.mock.emitMessage({
171
- type: "task.created",
172
- task: { id: "t-1", status: "open" },
173
- });
174
-
175
- expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
176
- });
177
-
178
- it("ignores messages without a task object", async () => {
179
- await deps.mock.emitMessage({ type: "task.created" });
180
-
181
- expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
182
- });
183
- });
184
-
185
- describe("task.assigned", () => {
186
- it("assigns task in opentasks and notifies agent", async () => {
187
- await deps.mock.emitMessage({
188
- type: "task.assigned",
189
- taskId: "t-1",
190
- assignee: "agent-2",
191
- });
192
-
193
- expect(deps.tasksAdapter.assignTask).toHaveBeenCalledWith("t-1", "agent-2");
194
- expect(deps.inboxAdapter.send).toHaveBeenCalledWith(
195
- "system",
196
- "agent-2",
197
- expect.objectContaining({
198
- event: "TASK_ASSIGNED",
199
- data: expect.objectContaining({ taskId: "t-1" }),
200
- }),
201
- );
202
- });
203
-
204
- it("ignores messages without taskId", async () => {
205
- await deps.mock.emitMessage({
206
- type: "task.assigned",
207
- assignee: "agent-2",
208
- });
209
-
210
- expect(deps.tasksAdapter.assignTask).not.toHaveBeenCalled();
211
- });
212
-
213
- it("ignores messages without assignee", async () => {
214
- await deps.mock.emitMessage({
215
- type: "task.assigned",
216
- taskId: "t-1",
217
- });
218
-
219
- expect(deps.tasksAdapter.assignTask).not.toHaveBeenCalled();
220
- });
221
- });
222
-
223
- describe("task.status", () => {
224
- it("transitions task to in_progress", async () => {
225
- await deps.mock.emitMessage({
226
- type: "task.status",
227
- taskId: "t-1",
228
- previous: "open",
229
- current: "in_progress",
230
- });
231
-
232
- expect(deps.tasksAdapter.transitionTask).toHaveBeenCalledWith("t-1", "start");
233
- });
234
-
235
- it("transitions task to completed", async () => {
236
- await deps.mock.emitMessage({
237
- type: "task.status",
238
- taskId: "t-1",
239
- previous: "in_progress",
240
- current: "completed",
241
- });
242
-
243
- expect(deps.tasksAdapter.transitionTask).toHaveBeenCalledWith("t-1", "complete");
244
- });
245
-
246
- it("transitions task to blocked", async () => {
247
- await deps.mock.emitMessage({
248
- type: "task.status",
249
- taskId: "t-1",
250
- previous: "in_progress",
251
- current: "blocked",
252
- });
253
-
254
- expect(deps.tasksAdapter.transitionTask).toHaveBeenCalledWith("t-1", "block");
255
- });
256
-
257
- it("transitions task to failed", async () => {
258
- await deps.mock.emitMessage({
259
- type: "task.status",
260
- taskId: "t-1",
261
- previous: "in_progress",
262
- current: "failed",
263
- });
264
-
265
- expect(deps.tasksAdapter.transitionTask).toHaveBeenCalledWith("t-1", "fail");
266
- });
267
-
268
- it("transitions closed to complete", async () => {
269
- await deps.mock.emitMessage({
270
- type: "task.status",
271
- taskId: "t-1",
272
- previous: "in_progress",
273
- current: "closed",
274
- });
275
-
276
- expect(deps.tasksAdapter.transitionTask).toHaveBeenCalledWith("t-1", "complete");
277
- });
278
-
279
- it("reopens task", async () => {
280
- await deps.mock.emitMessage({
281
- type: "task.status",
282
- taskId: "t-1",
283
- previous: "blocked",
284
- current: "open",
285
- });
286
-
287
- expect(deps.tasksAdapter.transitionTask).toHaveBeenCalledWith("t-1", "reopen");
288
- });
289
-
290
- it("ignores unknown status values", async () => {
291
- await deps.mock.emitMessage({
292
- type: "task.status",
293
- taskId: "t-1",
294
- previous: "open",
295
- current: "unknown_status",
296
- });
297
-
298
- expect(deps.tasksAdapter.transitionTask).not.toHaveBeenCalled();
299
- });
300
-
301
- it("ignores messages without taskId", async () => {
302
- await deps.mock.emitMessage({
303
- type: "task.status",
304
- current: "completed",
305
- });
306
-
307
- expect(deps.tasksAdapter.transitionTask).not.toHaveBeenCalled();
308
- });
309
-
310
- it("ignores messages without current status", async () => {
311
- await deps.mock.emitMessage({
312
- type: "task.status",
313
- taskId: "t-1",
314
- });
315
-
316
- expect(deps.tasksAdapter.transitionTask).not.toHaveBeenCalled();
317
- });
318
- });
319
-
320
- describe("echo prevention", () => {
321
- it("skips messages with _origin macro-agent", async () => {
322
- await deps.mock.emitMessage({
323
- type: "task.created",
324
- task: { id: "t-1", title: "My own task", status: "open" },
325
- _origin: "macro-agent",
326
- });
327
-
328
- expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
329
- });
330
-
331
- it("processes messages from other origins", async () => {
332
- await deps.mock.emitMessage({
333
- type: "task.created",
334
- task: { id: "t-1", title: "External task", status: "open" },
335
- _origin: "cc-swarm",
336
- });
337
-
338
- expect(deps.tasksAdapter.createTask).toHaveBeenCalled();
339
- });
340
-
341
- it("processes messages with no origin", async () => {
342
- await deps.mock.emitMessage({
343
- type: "task.created",
344
- task: { id: "t-1", title: "No origin task", status: "open" },
345
- });
346
-
347
- expect(deps.tasksAdapter.createTask).toHaveBeenCalled();
348
- });
349
- });
350
-
351
- describe("error handling", () => {
352
- it("warns on tasksAdapter failure without throwing", async () => {
353
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
354
- (deps.tasksAdapter.createTask as ReturnType<typeof vi.fn>).mockRejectedValue(
355
- new Error("opentasks down"),
356
- );
357
-
358
- await deps.mock.emitMessage({
359
- type: "task.created",
360
- task: { id: "t-1", title: "Will fail", status: "open" },
361
- });
362
-
363
- expect(warnSpy).toHaveBeenCalledWith(
364
- expect.stringContaining("opentasks down"),
365
- );
366
- warnSpy.mockRestore();
367
- });
368
-
369
- it("continues processing after inbox send failure", async () => {
370
- (deps.inboxAdapter.send as ReturnType<typeof vi.fn>).mockRejectedValue(
371
- new Error("inbox error"),
372
- );
373
-
374
- // Should not throw — inbox errors are caught
375
- await deps.mock.emitMessage({
376
- type: "task.created",
377
- task: { id: "t-1", title: "Task", status: "open", assignee: "agent-1" },
378
- });
379
-
380
- expect(deps.tasksAdapter.createTask).toHaveBeenCalled();
381
- });
382
- });
383
-
384
- describe("ignored message types", () => {
385
- it("ignores task.completed (informational only)", async () => {
386
- await deps.mock.emitMessage({
387
- type: "task.completed",
388
- taskId: "t-1",
389
- });
390
-
391
- expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
392
- expect(deps.tasksAdapter.transitionTask).not.toHaveBeenCalled();
393
- });
394
-
395
- it("ignores messages without payload", async () => {
396
- const msg: MAPMessage = {
397
- id: "msg-1",
398
- from: "hub",
399
- to: { scope: "swarm:test" },
400
- timestamp: new Date().toISOString(),
401
- };
402
- for (const handler of deps.mock.messageHandlers) {
403
- await handler(msg);
404
- }
405
-
406
- expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
407
- });
408
-
409
- it("ignores messages without type field", async () => {
410
- await deps.mock.emitMessage({ taskId: "t-1", status: "open" });
411
-
412
- expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
413
- });
414
- });
415
- });
416
-
417
- // =============================================================================
418
- // Tests — Context and messaging NOT handled here (agent-inbox flow)
419
- // =============================================================================
420
-
421
- describe("CoordinationHandler — context/messaging excluded", () => {
422
- let deps: ReturnType<typeof createDeps>;
423
-
424
- beforeEach(() => {
425
- deps = createDeps();
426
- setupCoordinationHandlers(deps);
427
- });
428
-
429
- it("does not handle context.shared messages (handled by agent-inbox)", async () => {
430
- await deps.mock.emitMessage({
431
- type: "context.shared",
432
- context_type: "code_review",
433
- data: { file: "main.ts" },
434
- source_swarm_id: "swarm-A",
435
- });
436
-
437
- // No task adapter calls
438
- expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
439
- expect(deps.tasksAdapter.transitionTask).not.toHaveBeenCalled();
440
- // No inbox calls from coordination handler
441
- expect(deps.inboxAdapter.send).not.toHaveBeenCalled();
442
- });
443
-
444
- it("does not handle message type messages (handled by agent-inbox)", async () => {
445
- await deps.mock.emitMessage({
446
- type: "message",
447
- from_swarm_id: "swarm-A",
448
- to_swarm_id: "agent-1",
449
- content_type: "text",
450
- content: "Hello",
451
- });
452
-
453
- expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
454
- expect(deps.inboxAdapter.send).not.toHaveBeenCalled();
455
- });
456
-
457
- it("does not register x-openhive/context.share notification handler", () => {
458
- expect(deps.mock.notificationHandlers.has("x-openhive/context.share")).toBe(false);
459
- });
460
-
461
- it("does not register x-openhive/message.send notification handler", () => {
462
- expect(deps.mock.notificationHandlers.has("x-openhive/message.send")).toBe(false);
463
- });
464
-
465
- it("does not register x-openhive/task.assign notification handler", () => {
466
- expect(deps.mock.notificationHandlers.has("x-openhive/task.assign")).toBe(false);
467
- });
468
-
469
- it("does not register x-openhive/task.status notification handler", () => {
470
- expect(deps.mock.notificationHandlers.has("x-openhive/task.status")).toBe(false);
471
- });
472
- });
473
-
474
- // =============================================================================
475
- // Tests — Workspace notifications
476
- // =============================================================================
477
-
478
- describe("CoordinationHandler — workspace notifications", () => {
479
- let deps: ReturnType<typeof createDeps>;
480
-
481
- describe("workspace.execute", () => {
482
- it("delegates to workspace handler via x-workspace/task.execute", async () => {
483
- const handleWorkspaceExecute = vi.fn().mockResolvedValue(undefined);
484
- deps = createDeps({
485
- workspaceHandler: {
486
- handleWorkspaceExecute,
487
- isWorkspaceExecuteMessage: (msg) => msg.method === "x-workspace/task.execute",
488
- },
489
- });
490
- setupCoordinationHandlers(deps);
491
-
492
- const params = { request_id: "req-1", prompt: "Do thing", cwd: "/tmp" };
493
- await deps.mock.emitNotification("x-workspace/task.execute", params);
494
-
495
- expect(handleWorkspaceExecute).toHaveBeenCalledWith(params);
496
- });
497
-
498
- it("delegates legacy x-openhive/learning.workspace.execute", async () => {
499
- const handleWorkspaceExecute = vi.fn().mockResolvedValue(undefined);
500
- deps = createDeps({
501
- workspaceHandler: {
502
- handleWorkspaceExecute,
503
- isWorkspaceExecuteMessage: () => true,
504
- },
505
- });
506
- setupCoordinationHandlers(deps);
507
-
508
- const params = { request_id: "req-2", prompt: "Legacy task", cwd: "/tmp" };
509
- await deps.mock.emitNotification("x-openhive/learning.workspace.execute", params);
510
-
511
- expect(handleWorkspaceExecute).toHaveBeenCalledWith(params);
512
- });
513
-
514
- it("warns on workspace handler failure without throwing", async () => {
515
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
516
- const handleWorkspaceExecute = vi.fn().mockRejectedValue(new Error("spawn failed"));
517
- deps = createDeps({
518
- workspaceHandler: {
519
- handleWorkspaceExecute,
520
- isWorkspaceExecuteMessage: () => true,
521
- },
522
- });
523
- setupCoordinationHandlers(deps);
524
-
525
- await deps.mock.emitNotification("x-workspace/task.execute", {
526
- request_id: "req-3",
527
- prompt: "fail",
528
- cwd: "/tmp",
529
- });
530
-
531
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("spawn failed"));
532
- warnSpy.mockRestore();
533
- });
534
-
535
- it("does not register workspace handlers when no workspace handler provided", () => {
536
- deps = createDeps();
537
- setupCoordinationHandlers(deps);
538
-
539
- expect(deps.mock.notificationHandlers.has("x-workspace/task.execute")).toBe(false);
540
- expect(deps.mock.notificationHandlers.has("x-openhive/learning.workspace.execute")).toBe(false);
541
- });
542
- });
543
- });
544
-
545
- // =============================================================================
546
- // Tests — Handler registration and cleanup
547
- // =============================================================================
548
-
549
- describe("CoordinationHandler — cleanup", () => {
550
- it("removes all handlers on cleanup", () => {
551
- const deps = createDeps({
552
- workspaceHandler: {
553
- handleWorkspaceExecute: vi.fn(),
554
- isWorkspaceExecuteMessage: () => true,
555
- },
556
- });
557
- const cleanup = setupCoordinationHandlers(deps);
558
-
559
- // Verify handlers were registered
560
- expect(deps.mock.messageHandlers.size).toBe(1);
561
- expect(deps.mock.notificationHandlers.size).toBeGreaterThan(0);
562
-
563
- cleanup();
564
-
565
- // All handlers removed
566
- expect(deps.mock.messageHandlers.size).toBe(0);
567
- for (const [, handlers] of deps.mock.notificationHandlers) {
568
- expect(handlers.size).toBe(0);
569
- }
570
- });
571
-
572
- it("only registers message handler and workspace notifications", () => {
573
- const deps = createDeps({
574
- workspaceHandler: {
575
- handleWorkspaceExecute: vi.fn(),
576
- isWorkspaceExecuteMessage: () => true,
577
- },
578
- });
579
- setupCoordinationHandlers(deps);
580
-
581
- // One message handler for task events
582
- expect(deps.mock.messageHandlers.size).toBe(1);
583
-
584
- // Two notification handlers: x-workspace/task.execute + legacy
585
- const registeredMethods = [...deps.mock.notificationHandlers.keys()];
586
- expect(registeredMethods).toContain("x-workspace/task.execute");
587
- expect(registeredMethods).toContain("x-openhive/learning.workspace.execute");
588
- expect(registeredMethods).toHaveLength(2);
589
- });
590
-
591
- it("registers only message handler when no workspace handler", () => {
592
- const deps = createDeps();
593
- setupCoordinationHandlers(deps);
594
-
595
- expect(deps.mock.messageHandlers.size).toBe(1);
596
- expect(deps.mock.notificationHandlers.size).toBe(0);
597
- });
598
- });