macro-agent 0.1.12 → 0.2.1

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 (116) hide show
  1. package/dist/acp/macro-agent.d.ts.map +1 -1
  2. package/dist/acp/macro-agent.js +18 -40
  3. package/dist/acp/macro-agent.js.map +1 -1
  4. package/dist/agent/agent-manager-v2.d.ts.map +1 -1
  5. package/dist/agent/agent-manager-v2.js +241 -8
  6. package/dist/agent/agent-manager-v2.js.map +1 -1
  7. package/dist/agent/types.d.ts +47 -0
  8. package/dist/agent/types.d.ts.map +1 -1
  9. package/dist/agent/types.js.map +1 -1
  10. package/dist/boot-v2.d.ts +33 -0
  11. package/dist/boot-v2.d.ts.map +1 -1
  12. package/dist/boot-v2.js +144 -11
  13. package/dist/boot-v2.js.map +1 -1
  14. package/dist/cli/acp.js +0 -0
  15. package/dist/cli/inbox-mcp-proxy.d.ts +36 -0
  16. package/dist/cli/inbox-mcp-proxy.d.ts.map +1 -0
  17. package/dist/cli/inbox-mcp-proxy.js +51 -0
  18. package/dist/cli/inbox-mcp-proxy.js.map +1 -0
  19. package/dist/cli/index.js +0 -0
  20. package/dist/cli/mcp.js +0 -0
  21. package/dist/dispatch/loadout-translation.d.ts +100 -0
  22. package/dist/dispatch/loadout-translation.d.ts.map +1 -0
  23. package/dist/dispatch/loadout-translation.js +90 -0
  24. package/dist/dispatch/loadout-translation.js.map +1 -0
  25. package/dist/dispatch/mail-inbound-consumer.d.ts +136 -0
  26. package/dist/dispatch/mail-inbound-consumer.d.ts.map +1 -0
  27. package/dist/dispatch/mail-inbound-consumer.js +360 -0
  28. package/dist/dispatch/mail-inbound-consumer.js.map +1 -0
  29. package/dist/dispatch/mail-inbound-reuse-consumer.d.ts +75 -0
  30. package/dist/dispatch/mail-inbound-reuse-consumer.d.ts.map +1 -0
  31. package/dist/dispatch/mail-inbound-reuse-consumer.js +325 -0
  32. package/dist/dispatch/mail-inbound-reuse-consumer.js.map +1 -0
  33. package/dist/dispatch/permission-evaluator.d.ts +68 -0
  34. package/dist/dispatch/permission-evaluator.d.ts.map +1 -0
  35. package/dist/dispatch/permission-evaluator.js +159 -0
  36. package/dist/dispatch/permission-evaluator.js.map +1 -0
  37. package/dist/dispatch/permission-overlay.d.ts +64 -0
  38. package/dist/dispatch/permission-overlay.d.ts.map +1 -0
  39. package/dist/dispatch/permission-overlay.js +72 -0
  40. package/dist/dispatch/permission-overlay.js.map +1 -0
  41. package/dist/dispatch/permissions-handler.d.ts +71 -0
  42. package/dist/dispatch/permissions-handler.d.ts.map +1 -0
  43. package/dist/dispatch/permissions-handler.js +83 -0
  44. package/dist/dispatch/permissions-handler.js.map +1 -0
  45. package/dist/dispatch/spawn-agent-handler.d.ts +84 -0
  46. package/dist/dispatch/spawn-agent-handler.d.ts.map +1 -0
  47. package/dist/dispatch/spawn-agent-handler.js +85 -0
  48. package/dist/dispatch/spawn-agent-handler.js.map +1 -0
  49. package/dist/lifecycle/handlers-v2.d.ts +7 -0
  50. package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
  51. package/dist/lifecycle/handlers-v2.js +27 -0
  52. package/dist/lifecycle/handlers-v2.js.map +1 -1
  53. package/dist/map/lifecycle-bridge.d.ts +18 -0
  54. package/dist/map/lifecycle-bridge.d.ts.map +1 -1
  55. package/dist/map/lifecycle-bridge.js +23 -1
  56. package/dist/map/lifecycle-bridge.js.map +1 -1
  57. package/dist/map/mail-bridge.d.ts +55 -0
  58. package/dist/map/mail-bridge.d.ts.map +1 -0
  59. package/dist/map/mail-bridge.js +115 -0
  60. package/dist/map/mail-bridge.js.map +1 -0
  61. package/dist/map/repo-workspace.d.ts +46 -0
  62. package/dist/map/repo-workspace.d.ts.map +1 -0
  63. package/dist/map/repo-workspace.js +39 -0
  64. package/dist/map/repo-workspace.js.map +1 -0
  65. package/dist/map/server.d.ts.map +1 -1
  66. package/dist/map/server.js +1 -0
  67. package/dist/map/server.js.map +1 -1
  68. package/dist/map/sidecar.d.ts.map +1 -1
  69. package/dist/map/sidecar.js +308 -1
  70. package/dist/map/sidecar.js.map +1 -1
  71. package/dist/map/types.d.ts +29 -0
  72. package/dist/map/types.d.ts.map +1 -1
  73. package/dist/mcp/tools/done-v2.d.ts.map +1 -1
  74. package/dist/mcp/tools/done-v2.js +1 -0
  75. package/dist/mcp/tools/done-v2.js.map +1 -1
  76. package/dist/teams/team-loader.d.ts.map +1 -1
  77. package/dist/teams/team-loader.js.map +1 -1
  78. package/dist/teams/team-runtime-v2.d.ts.map +1 -1
  79. package/dist/teams/team-runtime-v2.js +2 -0
  80. package/dist/teams/team-runtime-v2.js.map +1 -1
  81. package/package.json +7 -5
  82. package/src/acp/macro-agent.ts +20 -42
  83. package/src/agent/__tests__/agent-manager-v2.permission-interception.test.ts +296 -0
  84. package/src/agent/__tests__/agent-manager-v2.permissions.test.ts +233 -0
  85. package/src/agent/agent-manager-v2.ts +269 -8
  86. package/src/agent/types.ts +51 -0
  87. package/src/boot-v2.ts +192 -12
  88. package/src/cli/inbox-mcp-proxy.ts +56 -0
  89. package/src/dispatch/CLAUDE.md +129 -0
  90. package/src/dispatch/__tests__/loadout-translation.test.ts +141 -0
  91. package/src/dispatch/__tests__/mail-inbound-consumer.integration.test.ts +519 -0
  92. package/src/dispatch/__tests__/mail-inbound-consumer.test.ts +800 -0
  93. package/src/dispatch/__tests__/mail-inbound-reuse-consumer.test.ts +575 -0
  94. package/src/dispatch/__tests__/permission-evaluator.test.ts +196 -0
  95. package/src/dispatch/__tests__/permission-overlay.test.ts +56 -0
  96. package/src/dispatch/__tests__/permissions-handler.test.ts +168 -0
  97. package/src/dispatch/__tests__/spawn-agent-handler.test.ts +282 -0
  98. package/src/dispatch/loadout-translation.ts +138 -0
  99. package/src/dispatch/mail-inbound-consumer.ts +560 -0
  100. package/src/dispatch/mail-inbound-reuse-consumer.ts +479 -0
  101. package/src/dispatch/permission-evaluator.ts +191 -0
  102. package/src/dispatch/permission-overlay.ts +89 -0
  103. package/src/dispatch/permissions-handler.ts +112 -0
  104. package/src/dispatch/spawn-agent-handler.ts +160 -0
  105. package/src/lifecycle/handlers-v2.ts +34 -0
  106. package/src/map/__tests__/lifecycle-bridge.test.ts +64 -0
  107. package/src/map/__tests__/mail-bridge.test.ts +196 -0
  108. package/src/map/lifecycle-bridge.ts +48 -2
  109. package/src/map/mail-bridge.ts +203 -0
  110. package/src/map/repo-workspace.ts +82 -0
  111. package/src/map/server.ts +1 -0
  112. package/src/map/sidecar.ts +431 -1
  113. package/src/map/types.ts +34 -0
  114. package/src/mcp/tools/done-v2.ts +1 -0
  115. package/src/teams/team-loader.ts +3 -1
  116. package/src/teams/team-runtime-v2.ts +2 -0
@@ -23,6 +23,13 @@ import type {
23
23
  TaskBridge,
24
24
  } from "./types.js";
25
25
  import type { AgentLifecycleCallback } from "../agent/types.js";
26
+ import {
27
+ REPO_PROTOCOL_VERSION,
28
+ RepoClient,
29
+ RepoManager,
30
+ type RepoClientTransport,
31
+ type WorkspaceCapability,
32
+ } from "./repo-workspace.js";
26
33
 
27
34
  /**
28
35
  * Create a MAP sidecar that connects macro-agent to an OpenHive MAP hub.
@@ -35,7 +42,7 @@ export function createMAPSidecar(
35
42
  deps: MAPSidecarDeps,
36
43
  config: MAPSidecarConfig,
37
44
  ): MAPSidecar {
38
- const { agentManager, agentStore, inboxAdapter, tasksAdapter, getLocalMapId, gitCascadeAdapter } = deps;
45
+ const { agentManager, agentStore, inboxAdapter, tasksAdapter, getLocalMapId, gitCascadeAdapter, dispatcherAgentId } = deps;
39
46
  const scope = config.scope ?? "swarm:macro-agent";
40
47
  const agentName = config.agentName ?? "macro-agent-sidecar";
41
48
 
@@ -51,8 +58,29 @@ export function createMAPSidecar(
51
58
  let taskBridge: TaskBridge | null = null;
52
59
  let coordinationCleanup: (() => void) | null = null;
53
60
  let cascadeBridgeCleanup: (() => void) | null = null;
61
+ let mailBridgeCleanup: (() => void) | null = null;
62
+ let dispatchSpawnHandlerCleanup: (() => void) | null = null;
63
+ let dispatchMessageHandlerCleanup: (() => void) | null = null;
64
+ let dispatchPermissionsHandlerCleanup: (() => void) | null = null;
65
+ let workspaceManager: RepoManager | null = null;
66
+ let workspaceTransport: RepoClientTransport | null = null;
54
67
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
55
68
 
69
+ // Resolve the workspace capability from env vars. Setting OPENHIVE_WORKSPACE_DECLARE=off
70
+ // disables both explicit declare AND trajectory-handler bootstrap on the hub side.
71
+ const workspaceCapability: WorkspaceCapability = {
72
+ protocolVersion: REPO_PROTOCOL_VERSION,
73
+ declare: {
74
+ enabled: process.env.OPENHIVE_WORKSPACE_DECLARE !== "off",
75
+ defaultVisibility:
76
+ (process.env.OPENHIVE_WORKSPACE_VISIBILITY as
77
+ | "private"
78
+ | "hub_local"
79
+ | "federated") ?? "hub_local",
80
+ },
81
+ list: { enabled: true },
82
+ };
83
+
56
84
  /**
57
85
  * Build the MAP connection URL with auth token.
58
86
  */
@@ -83,10 +111,26 @@ export function createMAPSidecar(
83
111
  coordinationCleanup();
84
112
  coordinationCleanup = null;
85
113
  }
114
+ if (mailBridgeCleanup) {
115
+ try { mailBridgeCleanup(); } catch { /* non-critical */ }
116
+ mailBridgeCleanup = null;
117
+ }
86
118
  if (cascadeBridgeCleanup) {
87
119
  try { cascadeBridgeCleanup(); } catch { /* non-critical */ }
88
120
  cascadeBridgeCleanup = null;
89
121
  }
122
+ if (dispatchMessageHandlerCleanup) {
123
+ try { dispatchMessageHandlerCleanup(); } catch { /* non-critical */ }
124
+ dispatchMessageHandlerCleanup = null;
125
+ }
126
+ if (dispatchSpawnHandlerCleanup) {
127
+ try { dispatchSpawnHandlerCleanup(); } catch { /* non-critical */ }
128
+ dispatchSpawnHandlerCleanup = null;
129
+ }
130
+ if (dispatchPermissionsHandlerCleanup) {
131
+ try { dispatchPermissionsHandlerCleanup(); } catch { /* non-critical */ }
132
+ dispatchPermissionsHandlerCleanup = null;
133
+ }
90
134
  if (trajectoryReporter) {
91
135
  trajectoryReporter.stop();
92
136
  trajectoryReporter = null;
@@ -101,6 +145,8 @@ export function createMAPSidecar(
101
145
  }
102
146
  lifecycleCallback = null;
103
147
  taskBridge = null;
148
+ workspaceManager = null;
149
+ workspaceTransport = null;
104
150
  }
105
151
 
106
152
  /**
@@ -127,6 +173,7 @@ export function createMAPSidecar(
127
173
  canUpdate: true,
128
174
  canList: true,
129
175
  },
176
+ workspace: workspaceCapability,
130
177
  },
131
178
  metadata: {
132
179
  systemId: config.systemId ?? "macro-agent",
@@ -278,6 +325,8 @@ export function createMAPSidecar(
278
325
  );
279
326
  lifecycleCallback = bridge.callback;
280
327
  lifecycleCleanup = bridge.cleanup;
328
+ const awaitAcpRegistration = bridge.awaitRegistration;
329
+ const findLocalAgentByMapId = bridge.findLocalAgentByMapId;
281
330
  lifecycleUnsubscribe = agentManager.onLifecycleEvent(lifecycleCallback);
282
331
 
283
332
  // 3. Trajectory Reporter
@@ -294,6 +343,300 @@ export function createMAPSidecar(
294
343
  trajectoryReporter,
295
344
  });
296
345
 
346
+ // 4b. Mail Bridge — forwards `mail/turn.received` notifications from the
347
+ // hub into the local agent-inbox so swarm-dispatch's MessagePort can
348
+ // pick them up via its `inbox.events` subscription. Without this,
349
+ // hub-side mail never reaches the dispatcher.
350
+ const { setupMailBridge } = await import("./mail-bridge.js");
351
+ mailBridgeCleanup = await setupMailBridge({
352
+ connection,
353
+ inboxAdapter,
354
+ dispatcherAgentId,
355
+ log: (msg) => console.log(msg),
356
+ });
357
+
358
+ // 4c. x-dispatch/spawn-agent handler — notification-pair pattern.
359
+ //
360
+ // The MAP SDK's AgentConnection doesn't expose setRequestHandler, so
361
+ // the hub→swarm spawn-agent "request" is sent as a notification
362
+ // with a correlation_id. We process and reply with a `.response`
363
+ // notification carrying the same correlation_id (or an error).
364
+ //
365
+ // Dual-listen: subscribe to BOTH the canonical
366
+ // `x-dispatch/spawn-agent.request` (Tier 2+, owned by swarm-dispatch)
367
+ // AND the legacy `dispatch/spawn-agent.request` for one release
368
+ // window. Reply on the matching channel — the hub's response
369
+ // dispatcher accepts both.
370
+ const { handleDispatchSpawnAgent } = await import(
371
+ "../dispatch/spawn-agent-handler.js"
372
+ );
373
+ const {
374
+ handleSpawnAgentRequest,
375
+ X_DISPATCH_METHODS: SPAWN_METHODS,
376
+ LEGACY_DISPATCH_SPAWN_AGENT_REQUEST,
377
+ LEGACY_DISPATCH_SPAWN_AGENT_RESPONSE,
378
+ } = await import("swarm-dispatch/client");
379
+
380
+ const makeSpawnHandler = (
381
+ responseMethod: string,
382
+ ): ((params: unknown) => Promise<void>) =>
383
+ async (params) => {
384
+ await handleSpawnAgentRequest({
385
+ params,
386
+ runtime: {
387
+ async spawn(req) {
388
+ return handleDispatchSpawnAgent(
389
+ req as unknown as Parameters<typeof handleDispatchSpawnAgent>[0],
390
+ {
391
+ agentManager,
392
+ // Wait barrier: lifecycle-bridge resolves once
393
+ // `map/agents/register` completes, so the orchestrator's
394
+ // subsequent `findAcpAgentInfo` lookup doesn't race.
395
+ waitForAcpRegistration: awaitAcpRegistration,
396
+ log: (msg) => console.log(msg),
397
+ },
398
+ );
399
+ },
400
+ },
401
+ sendResponse: async (responseParams) => {
402
+ await connection.sendNotification(responseMethod, responseParams);
403
+ },
404
+ log: (msg) => console.log(msg),
405
+ });
406
+ };
407
+
408
+ const canonicalSpawnHandler = makeSpawnHandler(
409
+ SPAWN_METHODS.SPAWN_AGENT_RESPONSE,
410
+ );
411
+ const legacySpawnHandler = makeSpawnHandler(
412
+ LEGACY_DISPATCH_SPAWN_AGENT_RESPONSE,
413
+ );
414
+
415
+ connection.onNotification(
416
+ SPAWN_METHODS.SPAWN_AGENT_REQUEST,
417
+ canonicalSpawnHandler,
418
+ );
419
+ connection.onNotification(
420
+ LEGACY_DISPATCH_SPAWN_AGENT_REQUEST,
421
+ legacySpawnHandler,
422
+ );
423
+ dispatchSpawnHandlerCleanup = () => {
424
+ try {
425
+ if (typeof connection.offNotification === "function") {
426
+ connection.offNotification(
427
+ SPAWN_METHODS.SPAWN_AGENT_REQUEST,
428
+ canonicalSpawnHandler,
429
+ );
430
+ connection.offNotification(
431
+ LEGACY_DISPATCH_SPAWN_AGENT_REQUEST,
432
+ legacySpawnHandler,
433
+ );
434
+ }
435
+ } catch {
436
+ /* connection already torn down */
437
+ }
438
+ };
439
+
440
+ // 4c'. x-dispatch/permissions.{set,clear} handlers — notification-pair
441
+ // pattern, mirrors spawn-agent's shape. Used by hubs (e.g. OpenHive's
442
+ // ACP+reuse dispatch path) to apply per-dispatch loadout deny/allow
443
+ // rules to a long-lived agent's session at runtime via the
444
+ // permission-overlay registry. The prompt iterator in
445
+ // `agent-manager-v2.ts` enforces the overlay against
446
+ // `permission_request` ACP session updates. Pairs with the mail+reuse
447
+ // path's overlay set/clear in `mail-inbound-reuse-consumer.ts` —
448
+ // same registry, different transport.
449
+ const {
450
+ handlePermissionsSet,
451
+ handlePermissionsClear,
452
+ X_DISPATCH_PERMISSIONS_METHODS,
453
+ } = await import("../dispatch/permissions-handler.js");
454
+
455
+ // Response shape per swarm-dispatch's notification-rpc registry:
456
+ // { correlation_id, result } → resolve(result)
457
+ // { correlation_id, error: { code?, message? } } → reject
458
+ // The handler's `{ ok, error?: string }` is wrapped accordingly.
459
+ const sendPermissionsResponse = async (
460
+ method: string,
461
+ correlationId: string | undefined,
462
+ result: { ok: true } | { ok: false; error: string },
463
+ ): Promise<void> => {
464
+ const body: Record<string, unknown> = {};
465
+ if (correlationId) body.correlation_id = correlationId;
466
+ if (result.ok) {
467
+ body.result = result;
468
+ } else {
469
+ body.error = { message: result.error };
470
+ }
471
+ try {
472
+ await connection.sendNotification(method, body);
473
+ } catch (err) {
474
+ console.warn(
475
+ `[${method}] response send failed: ${(err as Error).message}`,
476
+ );
477
+ }
478
+ };
479
+
480
+ const permissionsSetHandler = async (params: unknown): Promise<void> => {
481
+ const correlationId =
482
+ (params as { correlation_id?: string })?.correlation_id;
483
+ const result = handlePermissionsSet(
484
+ params as Parameters<typeof handlePermissionsSet>[0],
485
+ (msg) => console.log(msg),
486
+ );
487
+ await sendPermissionsResponse(
488
+ X_DISPATCH_PERMISSIONS_METHODS.SET_RESPONSE,
489
+ correlationId,
490
+ result,
491
+ );
492
+ };
493
+
494
+ const permissionsClearHandler = async (params: unknown): Promise<void> => {
495
+ const correlationId =
496
+ (params as { correlation_id?: string })?.correlation_id;
497
+ const result = handlePermissionsClear(
498
+ params as Parameters<typeof handlePermissionsClear>[0],
499
+ (msg) => console.log(msg),
500
+ );
501
+ await sendPermissionsResponse(
502
+ X_DISPATCH_PERMISSIONS_METHODS.CLEAR_RESPONSE,
503
+ correlationId,
504
+ result,
505
+ );
506
+ };
507
+
508
+ connection.onNotification(
509
+ X_DISPATCH_PERMISSIONS_METHODS.SET_REQUEST,
510
+ permissionsSetHandler,
511
+ );
512
+ connection.onNotification(
513
+ X_DISPATCH_PERMISSIONS_METHODS.CLEAR_REQUEST,
514
+ permissionsClearHandler,
515
+ );
516
+ dispatchPermissionsHandlerCleanup = () => {
517
+ try {
518
+ if (typeof connection.offNotification === "function") {
519
+ connection.offNotification(
520
+ X_DISPATCH_PERMISSIONS_METHODS.SET_REQUEST,
521
+ permissionsSetHandler,
522
+ );
523
+ connection.offNotification(
524
+ X_DISPATCH_PERMISSIONS_METHODS.CLEAR_REQUEST,
525
+ permissionsClearHandler,
526
+ );
527
+ }
528
+ } catch {
529
+ /* connection already torn down */
530
+ }
531
+ };
532
+
533
+ // 4d. map/dispatch/message handler — receives hub-routed envelopes
534
+ // addressed to a specific agent on this swarm via MAP scope. The hub
535
+ // takes this path (not mail/turn) when the target agent declares
536
+ // `messaging.canReceive: true` per-agent but not `mail.canJoin`,
537
+ // which is the default for long-lived workers/coordinators registered
538
+ // by the lifecycle bridge. Without this handler, mail+reuse dispatches
539
+ // are silently dropped on the swarm side.
540
+ //
541
+ // Translate the hub-assigned MAP ULID (`to_agent_id`) → local agent
542
+ // id and forward the envelope into the local inbox so the new
543
+ // `mail-inbound-reuse-consumer` picks it up.
544
+ const dispatchMessageHandler = async (params: unknown): Promise<void> => {
545
+ const p = params as
546
+ | (Record<string, unknown> & {
547
+ to_agent_id?: string;
548
+ envelope?: unknown;
549
+ from_agent_id?: string;
550
+ })
551
+ | undefined;
552
+ const toAgentId = p?.to_agent_id;
553
+ const envelope = p?.envelope;
554
+ if (!toAgentId || !envelope) {
555
+ console.warn(
556
+ "[sidecar] map/dispatch/message missing to_agent_id or envelope; ignoring",
557
+ );
558
+ return;
559
+ }
560
+ const localAgentId = findLocalAgentByMapId(toAgentId);
561
+ if (!localAgentId) {
562
+ console.warn(
563
+ `[sidecar] map/dispatch/message recipient ${toAgentId} not registered locally; dropping`,
564
+ );
565
+ return;
566
+ }
567
+ // Translate envelope { type, body } → { schema, data } shape that the
568
+ // mail-inbound-reuse-consumer expects (mirrors mail-bridge's
569
+ // translation for `mail/turn.received`).
570
+ //
571
+ // Hub-side mail-transport injects `body._conversationId` when sending
572
+ // via MAP scope (sendViaMapScope) — extract it here and surface it
573
+ // on the top-level content (alongside `data`) so the
574
+ // mail-inbound-reuse-consumer's reply path can `postMailTurn` to
575
+ // the right conversation. Without this, the consumer drops the
576
+ // reply with "No conversationId".
577
+ const env = envelope as { type?: string; body?: Record<string, unknown> };
578
+ const conversationId =
579
+ env.body && typeof env.body._conversationId === "string"
580
+ ? (env.body._conversationId as string)
581
+ : undefined;
582
+ const content: Record<string, unknown> =
583
+ env.type && env.body
584
+ ? { schema: env.type, data: env.body }
585
+ : (envelope as Record<string, unknown>);
586
+ const contentWithMarker: Record<string, unknown> = {
587
+ type: "data",
588
+ ...content,
589
+ ...(conversationId ? { _conversationId: conversationId } : {}),
590
+ };
591
+ try {
592
+ await inboxAdapter.send(
593
+ (p?.from_agent_id as string | undefined) ?? "openhive-hub",
594
+ localAgentId,
595
+ contentWithMarker as never,
596
+ { importance: "normal" },
597
+ );
598
+ } catch (err) {
599
+ console.warn(
600
+ `[sidecar] map/dispatch/message inbox.send failed for ${localAgentId}: ` +
601
+ `${(err as Error).message}`,
602
+ );
603
+ }
604
+ };
605
+ // Dual-listen: subscribe to BOTH the canonical `x-dispatch/message`
606
+ // (the new method name owned by swarm-dispatch's protocol-constants
607
+ // module) AND the legacy `map/dispatch/message` alias for one
608
+ // release window. Older hub builds send under the legacy name; new
609
+ // builds send under the canonical name. Once the dual-listen window
610
+ // closes, drop the legacy registration.
611
+ const {
612
+ X_DISPATCH_METHODS,
613
+ LEGACY_MAP_DISPATCH_MESSAGE_METHOD,
614
+ } = await import("swarm-dispatch/client");
615
+ connection.onNotification(
616
+ X_DISPATCH_METHODS.MESSAGE,
617
+ dispatchMessageHandler,
618
+ );
619
+ connection.onNotification(
620
+ LEGACY_MAP_DISPATCH_MESSAGE_METHOD,
621
+ dispatchMessageHandler,
622
+ );
623
+ dispatchMessageHandlerCleanup = () => {
624
+ try {
625
+ if (typeof connection.offNotification === "function") {
626
+ connection.offNotification(
627
+ X_DISPATCH_METHODS.MESSAGE,
628
+ dispatchMessageHandler,
629
+ );
630
+ connection.offNotification(
631
+ LEGACY_MAP_DISPATCH_MESSAGE_METHOD,
632
+ dispatchMessageHandler,
633
+ );
634
+ }
635
+ } catch {
636
+ /* connection already torn down */
637
+ }
638
+ };
639
+
297
640
  // 5. Cascade Bridge + Action Handler (optional — only when a GitCascadeAdapter is available)
298
641
  if (gitCascadeAdapter) {
299
642
  const { createCascadeBridge } = await import("./cascade-bridge.js");
@@ -308,6 +651,56 @@ export function createMAPSidecar(
308
651
  actionCleanup();
309
652
  };
310
653
  }
654
+
655
+ // 6. Workspace (kinds/repo) — declare attached repos to the hub.
656
+ // Discovers repos from WORKSPACE_* env vars (set by openhive's swarm-spawn
657
+ // flow when spawning with a `repo_id`) plus OPENHIVE_WORKSPACE_REPOS for
658
+ // multi-repo declarations. Skipped entirely when capability.declare is off.
659
+ if (workspaceCapability.declare.enabled) {
660
+ try {
661
+ // OpenHive's MAP server registers x-workspace/repo.* as request handlers
662
+ // (additionalHandlers), not notification handlers — so route notify
663
+ // through callExtension and ignore the (void) response.
664
+ const repoTransport: RepoClientTransport = {
665
+ notify: async (method: string, params: unknown) => {
666
+ await connection.callExtension(method, params);
667
+ },
668
+ request: (method: string, params: unknown) =>
669
+ connection.callExtension(method, params),
670
+ };
671
+ const manager = new RepoManager();
672
+ const single =
673
+ process.env.WORKSPACE_REPO_URL && process.env.WORKSPACE_LOCAL_PATH
674
+ ? [{
675
+ remoteUrl: process.env.WORKSPACE_REPO_URL,
676
+ localPath: process.env.WORKSPACE_LOCAL_PATH,
677
+ }]
678
+ : [];
679
+ const multi = process.env.OPENHIVE_WORKSPACE_REPOS
680
+ ? (JSON.parse(process.env.OPENHIVE_WORKSPACE_REPOS) as Array<{
681
+ remoteUrl: string;
682
+ localPath: string;
683
+ }>)
684
+ : [];
685
+ for (const cfg of [...single, ...multi]) {
686
+ await manager.attach(cfg);
687
+ }
688
+ if (manager.list().length > 0) {
689
+ const client = new RepoClient(repoTransport);
690
+ await client.declare(RepoClient.snapshot(manager));
691
+ workspaceManager = manager;
692
+ workspaceTransport = repoTransport;
693
+ console.log(
694
+ `[map-sidecar] Declared ${manager.list().length} workspace(s) to hub`,
695
+ );
696
+ }
697
+ } catch (err) {
698
+ // Non-fatal — sidecar continues without workspace declarations
699
+ console.warn(
700
+ `[map-sidecar] Workspace declare failed: ${(err as Error).message}`,
701
+ );
702
+ }
703
+ }
311
704
  }
312
705
 
313
706
  return {
@@ -363,5 +756,42 @@ export function createMAPSidecar(
363
756
  // Best effort — MAP hub may be temporarily unavailable
364
757
  }
365
758
  },
759
+
760
+ async postMailTurn(
761
+ conversationId: string,
762
+ participantId: string,
763
+ content: string,
764
+ ): Promise<void> {
765
+ if (!connection || !isConnected) {
766
+ console.warn(
767
+ `[map-sidecar] postMailTurn skipped (connection=${!!connection} ` +
768
+ `isConnected=${isConnected}) conv=${conversationId}`,
769
+ );
770
+ return;
771
+ }
772
+ try {
773
+ await connection.sendNotification("mail/turn", {
774
+ conversationId,
775
+ participantId,
776
+ contentType: "text/plain",
777
+ content,
778
+ });
779
+ } catch (err) {
780
+ // Best effort — hub may be temporarily unavailable. Log at warn
781
+ // so silent failures are visible during postmortem.
782
+ console.warn(
783
+ `[map-sidecar] postMailTurn failed for conv=${conversationId}: ` +
784
+ `${(err as Error).message ?? String(err)}`,
785
+ );
786
+ }
787
+ },
788
+
789
+ getWorkspaceManager() {
790
+ return workspaceManager;
791
+ },
792
+
793
+ getRepoTransport() {
794
+ return workspaceTransport;
795
+ },
366
796
  };
367
797
  }
package/src/map/types.ts CHANGED
@@ -83,6 +83,15 @@ export interface MAPSidecarDeps {
83
83
  * without hub observability).
84
84
  */
85
85
  gitCascadeAdapter?: import("../workspace/git-cascade-adapter.js").GitCascadeAdapter;
86
+
87
+ /**
88
+ * The swarm-dispatch dispatcher agent ID (e.g. "dispatcher:<claimantId>").
89
+ * When provided, the mail bridge delivers hub-forwarded mail turns directly
90
+ * to this inbox recipient so createAgentInboxPort.onIncoming fires
91
+ * correctly. Without it, bridged turns land in BRIDGE_RECIPIENT_ID and the
92
+ * MessagePort never sees them.
93
+ */
94
+ dispatcherAgentId?: string;
86
95
  }
87
96
 
88
97
  // =============================================================================
@@ -106,6 +115,31 @@ export interface MAPSidecar {
106
115
 
107
116
  /** Emit a custom event to the MAP hub scope (best-effort, no-op if disconnected) */
108
117
  emitEvent?(event: Record<string, unknown>): Promise<void>;
118
+
119
+ /**
120
+ * Post a mail turn back to the hub via the `mail/turn` MAP notification.
121
+ * Used by the dispatch reply bridge to forward worker output into the hub's
122
+ * mail conversation after a mail-inbound task completes.
123
+ * No-op if disconnected.
124
+ */
125
+ postMailTurn?(
126
+ conversationId: string,
127
+ participantId: string,
128
+ content: string,
129
+ ): Promise<void>;
130
+
131
+ /**
132
+ * Access the sidecar's workspace RepoManager (if workspace declarations
133
+ * are enabled and the sidecar is connected). Used by mail-inbound-consumer
134
+ * for pre-spawn repo mount.
135
+ */
136
+ getWorkspaceManager?(): unknown;
137
+
138
+ /**
139
+ * Access the sidecar's repo client transport for declaring newly-attached
140
+ * repos to the hub. Used by mail-inbound-consumer after pre-spawn clone.
141
+ */
142
+ getRepoTransport?(): { notify(method: string, params: unknown): Promise<void>; request(method: string, params: unknown): Promise<unknown> } | null;
109
143
  }
110
144
 
111
145
  // =============================================================================
@@ -178,6 +178,7 @@ export function createDoneHandlerV2(
178
178
  inboxAdapter,
179
179
  tasksAdapter,
180
180
  agentManager,
181
+ agentStore,
181
182
  taskMode: deps.taskMode,
182
183
  mergeQueue: deps.mergeQueue,
183
184
  };
@@ -84,7 +84,9 @@ export async function loadTeam(
84
84
 
85
85
  const manifest = template.manifest;
86
86
  const communication = (manifest.communication ?? {}) as CommunicationConfig;
87
- const macroAgent = parseMacroAgentExtensions(manifest.macro_agent);
87
+ const macroAgent = parseMacroAgentExtensions(
88
+ manifest.macro_agent as Record<string, unknown> | undefined,
89
+ );
88
90
 
89
91
  // 2. Build enforcement-enriched roles
90
92
  const resolvedRoles = new Map<string, ResolvedTeamRole>();
@@ -63,6 +63,8 @@ function manifestToResolved(manifest: TeamManifest): MacroResolvedTemplate {
63
63
  roles: new Map(),
64
64
  prompts: new Map(),
65
65
  mcpServers: manifest._mcpServers,
66
+ mcpProviders: new Map(),
67
+ loadouts: new Map(),
66
68
  sourcePath: "",
67
69
  },
68
70
  resolvedRoles: manifest._resolvedRoles,