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
@@ -37,7 +37,28 @@ export function createLifecycleBridge(
37
37
  scope: string,
38
38
  taskBridge?: TaskBridge,
39
39
  getLocalMapId?: (localAgentId: string) => string | undefined,
40
- ): { callback: AgentLifecycleCallback; cleanup: () => Promise<void> } {
40
+ ): {
41
+ callback: AgentLifecycleCallback;
42
+ cleanup: () => Promise<void>;
43
+ /**
44
+ * Resolve true once the named agent has completed `map/agents/register`
45
+ * with the hub (its entry.mapId is populated). Used by the dispatch
46
+ * spawn-agent handler to barrier-wait for hub-side registration before
47
+ * returning, so the orchestrator's subsequent `findAcpAgentInfo` lookup
48
+ * doesn't race the registration.
49
+ *
50
+ * Returns false if the timeout elapses before registration completes.
51
+ */
52
+ awaitRegistration: (agentId: string, timeoutMs?: number) => Promise<boolean>;
53
+ /**
54
+ * Reverse-lookup: hub-assigned MAP ULID → local agent id. Used by the
55
+ * `map/dispatch/message` handler in the sidecar to translate envelope
56
+ * recipients (which the hub addresses by MAP ULID) into local agent ids
57
+ * (which the inbox addresses messages by). Returns undefined when no
58
+ * registered agent matches.
59
+ */
60
+ findLocalAgentByMapId: (mapId: string) => string | undefined;
61
+ } {
41
62
  const registered = new Map<string, RegisteredAgent>();
42
63
 
43
64
  /**
@@ -186,5 +207,30 @@ export function createLifecycleBridge(
186
207
  registered.clear();
187
208
  };
188
209
 
189
- return { callback, cleanup };
210
+ /**
211
+ * Block until the named agent's hub-side registration completes (entry
212
+ * has been assigned a mapId by the `map/agents/register` response) or
213
+ * the timeout elapses. Polls the local `registered` map.
214
+ */
215
+ const awaitRegistration = async (
216
+ agentId: string,
217
+ timeoutMs = 5000,
218
+ ): Promise<boolean> => {
219
+ const deadline = Date.now() + timeoutMs;
220
+ while (Date.now() < deadline) {
221
+ const entry = registered.get(agentId);
222
+ if (entry?.mapId) return true;
223
+ await new Promise((r) => setTimeout(r, 50));
224
+ }
225
+ return Boolean(registered.get(agentId)?.mapId);
226
+ };
227
+
228
+ const findLocalAgentByMapId = (mapId: string): string | undefined => {
229
+ for (const [localId, entry] of registered) {
230
+ if (entry.mapId === mapId) return localId;
231
+ }
232
+ return undefined;
233
+ };
234
+
235
+ return { callback, cleanup, awaitRegistration, findLocalAgentByMapId };
190
236
  }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Mail Bridge — connects OpenHive's hub-side mail to macro-agent's local inbox.
3
+ *
4
+ * OpenHive's mail module (src/mail/index.ts) calls forwardTurnToSwarms() when
5
+ * a new mail turn lands in a conversation a swarm participates in. The hub
6
+ * sends a `mail/turn.received` JSON-RPC notification to the connected swarm
7
+ * sidecar via MAP. macro-agent's sidecar (this module) receives the
8
+ * notification, parses the turn content, and forwards it into the local
9
+ * agent-inbox so:
10
+ *
11
+ * 1. swarm-dispatch's MessagePort (boot-v2 wires `createAgentInboxPort`)
12
+ * sees the inbox delivery event and runs its classifier on the message.
13
+ * 2. The classifier recognizes `x-dispatch/work` schema and routes the
14
+ * prompt to a worker agent (existing or freshly spawned).
15
+ *
16
+ * Without this bridge, hub-side mail turns never reach macro-agent's
17
+ * dispatcher. The MessagePort is wired to local inbox events only.
18
+ */
19
+ import type { InboxAdapter } from "../adapters/types.js";
20
+
21
+ /**
22
+ * Subset of MAPClient surface this module needs. Mirrors the shape used by
23
+ * trajectory-reporter / coordination-handler — keeps the bridge testable
24
+ * without dragging in the full SDK type.
25
+ */
26
+ export interface MailBridgeConnection {
27
+ onNotification(
28
+ method: string,
29
+ handler: (params: unknown) => void | Promise<void>,
30
+ ): void;
31
+ offNotification(
32
+ method: string,
33
+ handler: (params: unknown) => void | Promise<void>,
34
+ ): void;
35
+ }
36
+
37
+ /**
38
+ * Synthetic recipient registered on the local inbox to receive bridged
39
+ * turns. Choosing a stable name lets the macro-agent dispatcher (or any
40
+ * other inbox observer) filter on `to: BRIDGE_RECIPIENT_ID` if it cares
41
+ * about the source — but normally it doesn't, since the dispatcher's
42
+ * MessagePort matches on the `x-dispatch/work` schema in the payload, not
43
+ * on the recipient id.
44
+ */
45
+ const BRIDGE_RECIPIENT_ID = "openhive-mail-bridge";
46
+
47
+ interface MailTurnReceivedParams {
48
+ conversation_id?: string;
49
+ turn_id?: string;
50
+ participant_id?: string;
51
+ content_type?: string;
52
+ content?: unknown;
53
+ thread_id?: string;
54
+ created_at?: string;
55
+ }
56
+
57
+ /**
58
+ * Parse the turn content into a JS object suitable for inbox routing.
59
+ * Returns null when the content is not parseable as JSON or doesn't look
60
+ * like an object payload (e.g., plain text mail turns).
61
+ */
62
+ function parseTurnContent(
63
+ content: unknown,
64
+ contentType: string | undefined,
65
+ ): Record<string, unknown> | null {
66
+ if (typeof content === "object" && content !== null) {
67
+ return content as Record<string, unknown>;
68
+ }
69
+ if (typeof content !== "string") return null;
70
+ // The hub serializes envelope objects via JSON.stringify and tags them
71
+ // application/json. We accept either explicit content type or a leading
72
+ // `{` to handle clients that omit the header.
73
+ if (
74
+ contentType === "application/json" ||
75
+ content.startsWith("{") ||
76
+ content.startsWith("[")
77
+ ) {
78
+ try {
79
+ const parsed = JSON.parse(content);
80
+ return typeof parsed === "object" && parsed !== null
81
+ ? (parsed as Record<string, unknown>)
82
+ : null;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+
90
+ export interface MailBridgeOptions {
91
+ connection: MailBridgeConnection;
92
+ inboxAdapter: InboxAdapter;
93
+ /**
94
+ * The dispatcher agent ID used by swarm-dispatch's createAgentInboxPort.
95
+ * Messages must be delivered TO this ID so the MessagePort's onIncoming
96
+ * filter (event.agentId === dispatcherAgentId) fires correctly.
97
+ * When omitted the bridge falls back to BRIDGE_RECIPIENT_ID (inert —
98
+ * the MessagePort will never see the message).
99
+ */
100
+ dispatcherAgentId?: string;
101
+ /**
102
+ * Optional logger for diagnostic output. The sidecar currently logs via
103
+ * console; swap if you want structured output.
104
+ */
105
+ log?: (msg: string) => void;
106
+ }
107
+
108
+ /**
109
+ * Wire the mail bridge. Returns a cleanup function that detaches the
110
+ * notification handler.
111
+ *
112
+ * Idempotent registration: register is called exactly once; the handler
113
+ * itself is best-effort (errors are caught and logged so a malformed turn
114
+ * doesn't break subsequent ones).
115
+ */
116
+ export async function setupMailBridge(
117
+ opts: MailBridgeOptions,
118
+ ): Promise<() => void> {
119
+ const { connection, inboxAdapter, dispatcherAgentId, log = () => {} } = opts;
120
+
121
+ // Determine the inbox recipient. When a dispatcherAgentId is provided we
122
+ // deliver directly to the dispatcher so createAgentInboxPort.onIncoming
123
+ // (which filters event.agentId === dispatcherAgentId) picks it up.
124
+ const recipientId = dispatcherAgentId ?? BRIDGE_RECIPIENT_ID;
125
+
126
+ // Register the recipient on the local inbox so routeMessage accepts the
127
+ // forwarded turn. Idempotent — registerAgent putAgent is an upsert.
128
+ await inboxAdapter.registerAgent(recipientId, {
129
+ name: dispatcherAgentId ? "OpenHive Dispatcher (bridged)" : "OpenHive Mail Bridge",
130
+ role: dispatcherAgentId ? "dispatcher" : "mail-bridge",
131
+ scope: "default",
132
+ metadata: { source: "openhive-mail-forward" },
133
+ });
134
+
135
+ const handler = async (params: unknown): Promise<void> => {
136
+ const turn = (params ?? {}) as MailTurnReceivedParams;
137
+ const raw = parseTurnContent(turn.content, turn.content_type);
138
+ if (!raw) {
139
+ log(
140
+ `[mail-bridge] Dropping non-JSON turn (conv=${turn.conversation_id ?? "?"} ` +
141
+ `participant=${turn.participant_id ?? "?"})`,
142
+ );
143
+ return;
144
+ }
145
+
146
+ // Translate hub envelope shape { type, body } → canonical inbox shape
147
+ // { schema, data } that classifyMessage in boot-v2 expects.
148
+ // Hub sends: { type: "x-dispatch/work", body: { prompt, taskId, role } }
149
+ // Classifier expects: { schema: "x-dispatch/work", data: { prompt, taskId, role } }
150
+ const hubType = (raw as { type?: string }).type;
151
+ const hubBody = (raw as { body?: Record<string, unknown> }).body;
152
+ const content: Record<string, unknown> =
153
+ hubType && hubBody
154
+ ? { schema: hubType, data: hubBody }
155
+ : raw;
156
+
157
+ // Attach conversation_id as a top-level field in the payload so
158
+ // the mail-inbound consumer can thread it into the worker spawn.
159
+ // This lets the reply bridge look up which hub conversation to post
160
+ // the worker's output back to.
161
+ //
162
+ // IMPORTANT: include `type: "data"` so agent-inbox's normalizeContent()
163
+ // passes the object through unchanged. Without it, an object lacking
164
+ // a `type` string is re-wrapped as `{ type:"data", data: original }`,
165
+ // burying `schema` one level deeper and breaking the consumer's filter.
166
+ const contentWithConvId: Record<string, unknown> = {
167
+ type: "data",
168
+ ...content,
169
+ ...(turn.conversation_id ? { _conversationId: turn.conversation_id } : {}),
170
+ };
171
+
172
+ try {
173
+ await inboxAdapter.send(
174
+ turn.participant_id ?? "openhive-hub",
175
+ recipientId,
176
+ contentWithConvId as never,
177
+ {
178
+ threadTag: turn.thread_id,
179
+ importance: "normal",
180
+ },
181
+ );
182
+ log(
183
+ `[mail-bridge] Forwarded turn ${turn.turn_id ?? "?"} into local inbox ` +
184
+ `(schema=${(content as { schema?: string }).schema ?? "n/a"})`,
185
+ );
186
+ } catch (err) {
187
+ log(
188
+ `[mail-bridge] Forward failed for turn ${turn.turn_id ?? "?"}: ` +
189
+ `${(err as Error).message}`,
190
+ );
191
+ }
192
+ };
193
+
194
+ connection.onNotification("mail/turn.received", handler);
195
+
196
+ return () => {
197
+ try {
198
+ connection.offNotification("mail/turn.received", handler);
199
+ } catch {
200
+ // best effort
201
+ }
202
+ };
203
+ }
@@ -0,0 +1,82 @@
1
+ export const REPO_PROTOCOL_VERSION = "0.1.0";
2
+
3
+ export interface WorkspaceCapability {
4
+ protocolVersion: string;
5
+ declare: {
6
+ enabled: boolean;
7
+ defaultVisibility: "private" | "hub_local" | "federated";
8
+ };
9
+ list: {
10
+ enabled: boolean;
11
+ };
12
+ }
13
+
14
+ export interface RepoClientTransport {
15
+ notify(method: string, params: unknown): Promise<void>;
16
+ request(method: string, params: unknown): Promise<unknown>;
17
+ }
18
+
19
+ export interface RepoAttachConfig {
20
+ remoteUrl: string;
21
+ localPath: string;
22
+ currentBranch?: string;
23
+ }
24
+
25
+ export interface RepoHandle {
26
+ identity: {
27
+ canonicalUrl: string;
28
+ };
29
+ localPath: string;
30
+ currentBranch?: string;
31
+ }
32
+
33
+ export interface RepoDeclaration {
34
+ bindings: Array<{
35
+ canonical_url: string;
36
+ local_path: string;
37
+ current_branch?: string;
38
+ }>;
39
+ }
40
+
41
+ export class RepoManager {
42
+ private readonly repos: RepoHandle[] = [];
43
+
44
+ async attach(config: RepoAttachConfig): Promise<RepoHandle> {
45
+ const existing = this.repos.find(
46
+ (repo) =>
47
+ repo.identity.canonicalUrl === config.remoteUrl ||
48
+ repo.localPath === config.localPath,
49
+ );
50
+ if (existing) return existing;
51
+
52
+ const handle: RepoHandle = {
53
+ identity: { canonicalUrl: config.remoteUrl },
54
+ localPath: config.localPath,
55
+ ...(config.currentBranch ? { currentBranch: config.currentBranch } : {}),
56
+ };
57
+ this.repos.push(handle);
58
+ return handle;
59
+ }
60
+
61
+ list(): RepoHandle[] {
62
+ return [...this.repos];
63
+ }
64
+ }
65
+
66
+ export class RepoClient {
67
+ constructor(private readonly transport: RepoClientTransport) {}
68
+
69
+ static snapshot(manager: RepoManager): RepoDeclaration {
70
+ return {
71
+ bindings: manager.list().map((repo) => ({
72
+ canonical_url: repo.identity.canonicalUrl,
73
+ local_path: repo.localPath,
74
+ ...(repo.currentBranch ? { current_branch: repo.currentBranch } : {}),
75
+ })),
76
+ };
77
+ }
78
+
79
+ async declare(declaration: RepoDeclaration): Promise<void> {
80
+ await this.transport.notify("x-workspace/repo.declare", declaration);
81
+ }
82
+ }
package/src/map/server.ts CHANGED
@@ -108,6 +108,7 @@ export function createMAPServerInstance(
108
108
  cwd: params.cwd,
109
109
  role: params.role ?? "worker",
110
110
  permissionMode: params.permissionMode,
111
+ askForAllTools: params.askForAllTools,
111
112
  agentType: params.agentType,
112
113
  customPrompt: params.customPrompt,
113
114
  topics: params.topics,