macro-agent 0.2.0 → 0.2.2
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.
- package/dist/acp/macro-agent.d.ts.map +1 -1
- package/dist/acp/macro-agent.js +18 -40
- package/dist/acp/macro-agent.js.map +1 -1
- package/dist/agent/agent-manager-v2.d.ts.map +1 -1
- package/dist/agent/agent-manager-v2.js +1 -1
- package/dist/agent/agent-manager-v2.js.map +1 -1
- package/dist/boot-v2.d.ts.map +1 -1
- package/dist/boot-v2.js +2 -0
- package/dist/boot-v2.js.map +1 -1
- package/dist/dispatch/mail-inbound-consumer.d.ts +47 -0
- package/dist/dispatch/mail-inbound-consumer.d.ts.map +1 -1
- package/dist/dispatch/mail-inbound-consumer.js +117 -18
- package/dist/dispatch/mail-inbound-consumer.js.map +1 -1
- package/dist/map/mail-bridge.d.ts.map +1 -1
- package/dist/map/mail-bridge.js +8 -1
- package/dist/map/mail-bridge.js.map +1 -1
- package/dist/map/repo-workspace.d.ts +46 -0
- package/dist/map/repo-workspace.d.ts.map +1 -0
- package/dist/map/repo-workspace.js +39 -0
- package/dist/map/repo-workspace.js.map +1 -0
- package/dist/map/server.d.ts.map +1 -1
- package/dist/map/server.js +1 -0
- package/dist/map/server.js.map +1 -1
- package/dist/map/sidecar.d.ts.map +1 -1
- package/dist/map/sidecar.js +63 -0
- package/dist/map/sidecar.js.map +1 -1
- package/dist/map/types.d.ts +14 -0
- package/dist/map/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/acp/macro-agent.ts +20 -42
- package/src/agent/agent-manager-v2.ts +1 -0
- package/src/boot-v2.ts +2 -0
- package/src/dispatch/__tests__/mail-inbound-consumer.test.ts +211 -0
- package/src/dispatch/mail-inbound-consumer.ts +195 -32
- package/src/map/__tests__/mail-bridge.test.ts +88 -0
- package/src/map/mail-bridge.ts +13 -1
- package/src/map/repo-workspace.ts +82 -0
- package/src/map/server.ts +1 -0
- package/src/map/sidecar.ts +85 -0
- package/src/map/types.ts +13 -0
|
@@ -61,6 +61,38 @@ export interface MailInboundSidecar {
|
|
|
61
61
|
): Promise<void>;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Repo metadata surfaced by the hub's enrichWithRepo → mail port injection. */
|
|
65
|
+
export interface DispatchRepoMetadata {
|
|
66
|
+
repo_id?: string;
|
|
67
|
+
canonical_url?: string;
|
|
68
|
+
branch?: string;
|
|
69
|
+
commit_sha?: string;
|
|
70
|
+
clone_policy?: string;
|
|
71
|
+
clone_path?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Narrow interface for the sidecar's RepoManager — keeps the consumer
|
|
76
|
+
* testable without importing the full agent-workspace concrete type.
|
|
77
|
+
*/
|
|
78
|
+
export interface RepoManagerLike {
|
|
79
|
+
list(): Array<{ identity: { canonicalUrl: string }; localPath: string }>;
|
|
80
|
+
attach(config: {
|
|
81
|
+
remoteUrl: string;
|
|
82
|
+
localPath: string;
|
|
83
|
+
currentBranch?: string;
|
|
84
|
+
}): Promise<{ localPath: string }>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Narrow interface for the sidecar's RepoClient transport — used to
|
|
89
|
+
* declare newly-attached repos to the hub after clone.
|
|
90
|
+
*/
|
|
91
|
+
export interface RepoClientTransportLike {
|
|
92
|
+
notify(method: string, params: unknown): Promise<void>;
|
|
93
|
+
request(method: string, params: unknown): Promise<unknown>;
|
|
94
|
+
}
|
|
95
|
+
|
|
64
96
|
export interface MailInboundConsumerOptions {
|
|
65
97
|
/**
|
|
66
98
|
* The inbox agent ID that mail-bridge delivers envelopes to.
|
|
@@ -85,6 +117,19 @@ export interface MailInboundConsumerOptions {
|
|
|
85
117
|
*/
|
|
86
118
|
getSidecar: () => MailInboundSidecar | null | undefined;
|
|
87
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Optional repo manager for pre-spawn mount. When provided, the consumer
|
|
122
|
+
* can clone/attach repos before spawning workers and set the worker's cwd
|
|
123
|
+
* to the repo path. Populated lazily from the sidecar's workspace manager.
|
|
124
|
+
*/
|
|
125
|
+
getRepoManager?: () => RepoManagerLike | null | undefined;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Optional repo client transport for declaring newly-cloned repos to the
|
|
129
|
+
* hub after a pre-spawn clone. Uses the sidecar's MAP connection transport.
|
|
130
|
+
*/
|
|
131
|
+
getRepoTransport?: () => RepoClientTransportLike | null | undefined;
|
|
132
|
+
|
|
88
133
|
/** Optional logger (default: console.log). */
|
|
89
134
|
log?: (msg: string) => void;
|
|
90
135
|
}
|
|
@@ -121,6 +166,8 @@ export function createMailInboundConsumer(
|
|
|
121
166
|
agentManager,
|
|
122
167
|
agentStore,
|
|
123
168
|
getSidecar,
|
|
169
|
+
getRepoManager,
|
|
170
|
+
getRepoTransport,
|
|
124
171
|
log = (msg: string) => console.log(msg),
|
|
125
172
|
} = opts;
|
|
126
173
|
|
|
@@ -158,6 +205,102 @@ export function createMailInboundConsumer(
|
|
|
158
205
|
`(recipient=${dispatcherAgentId})`,
|
|
159
206
|
);
|
|
160
207
|
|
|
208
|
+
// ── Pre-spawn repo mount ─────────────────────────────────────
|
|
209
|
+
// Resolves the worker's cwd from the dispatch envelope's repo metadata.
|
|
210
|
+
// When clone_policy is 'allowed' and the repo isn't already attached,
|
|
211
|
+
// clones to clone_path (or a default under cwd) then attaches+declares.
|
|
212
|
+
// Best-effort: failures log a warning and return undefined (worker
|
|
213
|
+
// spawns without a repo-specific cwd).
|
|
214
|
+
async function resolveRepoCwd(
|
|
215
|
+
repoMeta: DispatchRepoMetadata,
|
|
216
|
+
taskId: string,
|
|
217
|
+
): Promise<string | undefined> {
|
|
218
|
+
const manager = getRepoManager?.();
|
|
219
|
+
if (!manager) return undefined;
|
|
220
|
+
|
|
221
|
+
const canonicalUrl = repoMeta.canonical_url;
|
|
222
|
+
if (!canonicalUrl) return undefined;
|
|
223
|
+
|
|
224
|
+
// Check if the repo is already attached (by canonical URL match).
|
|
225
|
+
const existing = manager.list().find(
|
|
226
|
+
(h) => h.identity.canonicalUrl === canonicalUrl,
|
|
227
|
+
);
|
|
228
|
+
if (existing) {
|
|
229
|
+
log(`[mail-inbound] Repo already attached at ${existing.localPath} for taskId=${taskId}`);
|
|
230
|
+
return existing.localPath;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Not attached — clone only if explicitly allowed.
|
|
234
|
+
if (repoMeta.clone_policy !== 'allowed') {
|
|
235
|
+
log(
|
|
236
|
+
`[mail-inbound] Repo ${canonicalUrl} not attached and clone_policy=${repoMeta.clone_policy ?? 'none'} — ` +
|
|
237
|
+
`skipping mount for taskId=${taskId}`,
|
|
238
|
+
);
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const clonePath = repoMeta.clone_path ?? `/tmp/openhive-repos/${repoMeta.repo_id}`;
|
|
243
|
+
try {
|
|
244
|
+
const { execSync } = await import("node:child_process");
|
|
245
|
+
|
|
246
|
+
// Clone if the directory doesn't exist yet.
|
|
247
|
+
const fs = await import("node:fs");
|
|
248
|
+
if (!fs.existsSync(clonePath)) {
|
|
249
|
+
log(`[mail-inbound] Cloning ${canonicalUrl} → ${clonePath} for taskId=${taskId}`);
|
|
250
|
+
execSync(`git clone --depth 1 ${canonicalUrl} ${clonePath}`, {
|
|
251
|
+
stdio: "pipe",
|
|
252
|
+
timeout: 120_000,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Checkout target branch if specified.
|
|
257
|
+
if (repoMeta.branch) {
|
|
258
|
+
try {
|
|
259
|
+
execSync(`git -C ${clonePath} fetch origin ${repoMeta.branch} --depth 1`, {
|
|
260
|
+
stdio: "pipe",
|
|
261
|
+
timeout: 60_000,
|
|
262
|
+
});
|
|
263
|
+
execSync(`git -C ${clonePath} checkout ${repoMeta.branch}`, {
|
|
264
|
+
stdio: "pipe",
|
|
265
|
+
timeout: 30_000,
|
|
266
|
+
});
|
|
267
|
+
} catch {
|
|
268
|
+
log(`[mail-inbound] Branch checkout failed for ${repoMeta.branch} — continuing on default branch`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Attach to the repo manager so future dispatches find it.
|
|
273
|
+
const handle = await manager.attach({
|
|
274
|
+
remoteUrl: canonicalUrl,
|
|
275
|
+
localPath: clonePath,
|
|
276
|
+
currentBranch: repoMeta.branch,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Declare the new workspace to the hub (best-effort).
|
|
280
|
+
const transport = getRepoTransport?.();
|
|
281
|
+
if (transport) {
|
|
282
|
+
try {
|
|
283
|
+
const bindings = manager.list().map((h) => ({
|
|
284
|
+
canonical_url: h.identity.canonicalUrl,
|
|
285
|
+
local_path: h.localPath,
|
|
286
|
+
}));
|
|
287
|
+
await transport.notify("x-workspace/repo.declare", { bindings });
|
|
288
|
+
} catch {
|
|
289
|
+
// Non-fatal — the hub may not support workspace declarations.
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
log(`[mail-inbound] Mounted repo at ${handle.localPath} for taskId=${taskId}`);
|
|
294
|
+
return handle.localPath;
|
|
295
|
+
} catch (err) {
|
|
296
|
+
log(
|
|
297
|
+
`[mail-inbound] Pre-spawn repo mount failed for taskId=${taskId}: ` +
|
|
298
|
+
`${(err as Error).message ?? String(err)}`,
|
|
299
|
+
);
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
161
304
|
// ── Inbox message listener ───────────────────────────────────
|
|
162
305
|
const onMessage = (event: InboxMessageEvent): void => {
|
|
163
306
|
// Only handle messages delivered to our dispatcher recipient.
|
|
@@ -258,17 +401,36 @@ export function createMailInboundConsumer(
|
|
|
258
401
|
fullAutonomous: true,
|
|
259
402
|
});
|
|
260
403
|
|
|
404
|
+
// Extract repo metadata from the envelope for pre-spawn mount.
|
|
405
|
+
const repoMeta: DispatchRepoMetadata = {
|
|
406
|
+
repo_id: data.metadata?.repo_id as string | undefined,
|
|
407
|
+
canonical_url: data.metadata?.canonical_url as string | undefined,
|
|
408
|
+
branch: data.metadata?.branch as string | undefined,
|
|
409
|
+
commit_sha: data.metadata?.commit_sha as string | undefined,
|
|
410
|
+
clone_policy: data.metadata?.clone_policy as string | undefined,
|
|
411
|
+
clone_path: data.metadata?.clone_path as string | undefined,
|
|
412
|
+
};
|
|
413
|
+
|
|
261
414
|
log(
|
|
262
415
|
`[mail-inbound] Received x-dispatch/work taskId=${taskId} ` +
|
|
263
416
|
`conv=${conversationId ?? "(none)"} role=${role}` +
|
|
417
|
+
(repoMeta.repo_id ? ` repo=${repoMeta.repo_id}` : "") +
|
|
264
418
|
(spawnLoadoutOpts.permissions
|
|
265
419
|
? ` permissions=${JSON.stringify(spawnLoadoutOpts.permissions)}`
|
|
266
420
|
: ""),
|
|
267
421
|
);
|
|
268
422
|
|
|
269
423
|
// Spawn is async — fire and forget. Errors are logged, not thrown.
|
|
270
|
-
|
|
271
|
-
|
|
424
|
+
// Pre-spawn mount resolves the worker's cwd from the repo metadata
|
|
425
|
+
// before spawning. Best-effort: mount failures proceed without a
|
|
426
|
+
// repo-specific cwd.
|
|
427
|
+
(async () => {
|
|
428
|
+
let repoCwd: string | undefined;
|
|
429
|
+
if (repoMeta.repo_id) {
|
|
430
|
+
repoCwd = await resolveRepoCwd(repoMeta, taskId);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const spawned = await agentManager.spawn({
|
|
272
434
|
task: prompt,
|
|
273
435
|
task_id: taskId,
|
|
274
436
|
role,
|
|
@@ -278,40 +440,41 @@ export function createMailInboundConsumer(
|
|
|
278
440
|
// (claude-code-swarm, oh-my-claudecode, …) don't auto-load and hang
|
|
279
441
|
// session/new on environments where the host services aren't reachable.
|
|
280
442
|
isolatedSettings: true,
|
|
443
|
+
...(repoCwd ? { cwd: repoCwd } : {}),
|
|
281
444
|
...spawnLoadoutOpts,
|
|
282
|
-
})
|
|
283
|
-
.then(async (spawned) => {
|
|
284
|
-
log(
|
|
285
|
-
`[mail-inbound] Spawned worker agentId=${spawned.id} for taskId=${taskId}`,
|
|
286
|
-
);
|
|
287
|
-
if (conversationId) {
|
|
288
|
-
agentConversationMap.set(spawned.id, conversationId);
|
|
289
|
-
}
|
|
445
|
+
});
|
|
290
446
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
447
|
+
log(
|
|
448
|
+
`[mail-inbound] Spawned worker agentId=${spawned.id} for taskId=${taskId}` +
|
|
449
|
+
(repoCwd ? ` cwd=${repoCwd}` : ""),
|
|
450
|
+
);
|
|
451
|
+
if (conversationId) {
|
|
452
|
+
agentConversationMap.set(spawned.id, conversationId);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Spawn only creates an idle ACP session — the task lives in the
|
|
456
|
+
// system prompt as instructions. To get the model to actually do
|
|
457
|
+
// the work, send the prompt as a user message via promptUntilDone.
|
|
458
|
+
// This drives the worker to completion (done() called) so the
|
|
459
|
+
// lifecycle stopped listener below fires and posts the reply
|
|
460
|
+
// back to the hub. Fire-and-forget; errors are logged.
|
|
461
|
+
try {
|
|
462
|
+
await agentManager.promptUntilDone(spawned.id, prompt, {
|
|
463
|
+
maxFollowUps: 0,
|
|
464
|
+
});
|
|
465
|
+
} catch (err) {
|
|
309
466
|
log(
|
|
310
|
-
`[mail-inbound]
|
|
311
|
-
(err as Error).message ?? String(err)
|
|
312
|
-
}`,
|
|
467
|
+
`[mail-inbound] promptUntilDone failed for agentId=${spawned.id}: ` +
|
|
468
|
+
`${(err as Error).message ?? String(err)}`,
|
|
313
469
|
);
|
|
314
|
-
}
|
|
470
|
+
}
|
|
471
|
+
})().catch((err: unknown) => {
|
|
472
|
+
log(
|
|
473
|
+
`[mail-inbound] Spawn failed for taskId=${taskId}: ${
|
|
474
|
+
(err as Error).message ?? String(err)
|
|
475
|
+
}`,
|
|
476
|
+
);
|
|
477
|
+
});
|
|
315
478
|
};
|
|
316
479
|
|
|
317
480
|
inboxEvents.on("inbox.message", onMessage);
|
|
@@ -173,6 +173,94 @@ describe("setupMailBridge", () => {
|
|
|
173
173
|
});
|
|
174
174
|
});
|
|
175
175
|
|
|
176
|
+
describe("importance derivation", () => {
|
|
177
|
+
const DISPATCHER_ID = "dispatcher:host:1234:abc";
|
|
178
|
+
|
|
179
|
+
it("passes through importance from hub notification params", async () => {
|
|
180
|
+
await setupMailBridge({
|
|
181
|
+
connection: conn,
|
|
182
|
+
inboxAdapter: inbox as any,
|
|
183
|
+
dispatcherAgentId: DISPATCHER_ID,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await conn._fire({
|
|
187
|
+
conversation_id: "conv-imp-1",
|
|
188
|
+
turn_id: "turn-imp-1",
|
|
189
|
+
participant_id: "user:admin",
|
|
190
|
+
content_type: "application/json",
|
|
191
|
+
content: JSON.stringify({ schema: "x-dispatch/work", data: { taskId: "t-1" } }),
|
|
192
|
+
importance: "high",
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(inbox.send).toHaveBeenCalledOnce();
|
|
196
|
+
const [, , , opts] = inbox.send.mock.calls[0];
|
|
197
|
+
expect(opts?.importance).toBe("high");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("passes through 'urgent' importance for orchestrator recall", async () => {
|
|
201
|
+
await setupMailBridge({
|
|
202
|
+
connection: conn,
|
|
203
|
+
inboxAdapter: inbox as any,
|
|
204
|
+
dispatcherAgentId: DISPATCHER_ID,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await conn._fire({
|
|
208
|
+
conversation_id: "conv-imp-2",
|
|
209
|
+
turn_id: "turn-imp-2",
|
|
210
|
+
participant_id: "system:dispatch-orchestrator",
|
|
211
|
+
content_type: "application/json",
|
|
212
|
+
content: JSON.stringify({ schema: "x-dispatch/work", data: { taskId: "t-2" } }),
|
|
213
|
+
importance: "urgent",
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(inbox.send).toHaveBeenCalledOnce();
|
|
217
|
+
const [, , , opts] = inbox.send.mock.calls[0];
|
|
218
|
+
expect(opts?.importance).toBe("urgent");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("defaults to 'normal' when importance is missing", async () => {
|
|
222
|
+
await setupMailBridge({
|
|
223
|
+
connection: conn,
|
|
224
|
+
inboxAdapter: inbox as any,
|
|
225
|
+
dispatcherAgentId: DISPATCHER_ID,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await conn._fire({
|
|
229
|
+
conversation_id: "conv-imp-3",
|
|
230
|
+
turn_id: "turn-imp-3",
|
|
231
|
+
participant_id: "user:admin",
|
|
232
|
+
content_type: "application/json",
|
|
233
|
+
content: JSON.stringify({ schema: "x-dispatch/work", data: { taskId: "t-3" } }),
|
|
234
|
+
// no importance field
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(inbox.send).toHaveBeenCalledOnce();
|
|
238
|
+
const [, , , opts] = inbox.send.mock.calls[0];
|
|
239
|
+
expect(opts?.importance).toBe("normal");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("ignores invalid importance values and falls back to 'normal'", async () => {
|
|
243
|
+
await setupMailBridge({
|
|
244
|
+
connection: conn,
|
|
245
|
+
inboxAdapter: inbox as any,
|
|
246
|
+
dispatcherAgentId: DISPATCHER_ID,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await conn._fire({
|
|
250
|
+
conversation_id: "conv-imp-4",
|
|
251
|
+
turn_id: "turn-imp-4",
|
|
252
|
+
participant_id: "user:admin",
|
|
253
|
+
content_type: "application/json",
|
|
254
|
+
content: JSON.stringify({ schema: "x-dispatch/work", data: { taskId: "t-4" } }),
|
|
255
|
+
importance: "critical", // invalid value
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(inbox.send).toHaveBeenCalledOnce();
|
|
259
|
+
const [, , , opts] = inbox.send.mock.calls[0];
|
|
260
|
+
expect(opts?.importance).toBe("normal");
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
176
264
|
describe("without dispatcherAgentId (fallback mode)", () => {
|
|
177
265
|
it("delivers to BRIDGE_RECIPIENT_ID", async () => {
|
|
178
266
|
await setupMailBridge({
|
package/src/map/mail-bridge.ts
CHANGED
|
@@ -52,6 +52,9 @@ interface MailTurnReceivedParams {
|
|
|
52
52
|
content?: unknown;
|
|
53
53
|
thread_id?: string;
|
|
54
54
|
created_at?: string;
|
|
55
|
+
/** Importance hint from the hub. When present, drives wake/interrupt
|
|
56
|
+
* decisions via TriggerSystemV2's mapImportanceToWakeAction. */
|
|
57
|
+
importance?: string;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
/**
|
|
@@ -169,6 +172,15 @@ export async function setupMailBridge(
|
|
|
169
172
|
...(turn.conversation_id ? { _conversationId: turn.conversation_id } : {}),
|
|
170
173
|
};
|
|
171
174
|
|
|
175
|
+
// Derive importance from the hub's wire params. Default to "normal"
|
|
176
|
+
// when the hub doesn't tag the turn (backward compat).
|
|
177
|
+
const VALID_IMPORTANCE = ["low", "normal", "high", "urgent"];
|
|
178
|
+
const wireImportance =
|
|
179
|
+
typeof turn.importance === "string" &&
|
|
180
|
+
VALID_IMPORTANCE.includes(turn.importance)
|
|
181
|
+
? (turn.importance as "low" | "normal" | "high" | "urgent")
|
|
182
|
+
: "normal";
|
|
183
|
+
|
|
172
184
|
try {
|
|
173
185
|
await inboxAdapter.send(
|
|
174
186
|
turn.participant_id ?? "openhive-hub",
|
|
@@ -176,7 +188,7 @@ export async function setupMailBridge(
|
|
|
176
188
|
contentWithConvId as never,
|
|
177
189
|
{
|
|
178
190
|
threadTag: turn.thread_id,
|
|
179
|
-
importance:
|
|
191
|
+
importance: wireImportance,
|
|
180
192
|
},
|
|
181
193
|
);
|
|
182
194
|
log(
|
|
@@ -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,
|
package/src/map/sidecar.ts
CHANGED
|
@@ -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.
|
|
@@ -55,8 +62,25 @@ export function createMAPSidecar(
|
|
|
55
62
|
let dispatchSpawnHandlerCleanup: (() => void) | null = null;
|
|
56
63
|
let dispatchMessageHandlerCleanup: (() => void) | null = null;
|
|
57
64
|
let dispatchPermissionsHandlerCleanup: (() => void) | null = null;
|
|
65
|
+
let workspaceManager: RepoManager | null = null;
|
|
66
|
+
let workspaceTransport: RepoClientTransport | null = null;
|
|
58
67
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
59
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
|
+
|
|
60
84
|
/**
|
|
61
85
|
* Build the MAP connection URL with auth token.
|
|
62
86
|
*/
|
|
@@ -121,6 +145,8 @@ export function createMAPSidecar(
|
|
|
121
145
|
}
|
|
122
146
|
lifecycleCallback = null;
|
|
123
147
|
taskBridge = null;
|
|
148
|
+
workspaceManager = null;
|
|
149
|
+
workspaceTransport = null;
|
|
124
150
|
}
|
|
125
151
|
|
|
126
152
|
/**
|
|
@@ -147,6 +173,7 @@ export function createMAPSidecar(
|
|
|
147
173
|
canUpdate: true,
|
|
148
174
|
canList: true,
|
|
149
175
|
},
|
|
176
|
+
workspace: workspaceCapability,
|
|
150
177
|
},
|
|
151
178
|
metadata: {
|
|
152
179
|
systemId: config.systemId ?? "macro-agent",
|
|
@@ -624,6 +651,56 @@ export function createMAPSidecar(
|
|
|
624
651
|
actionCleanup();
|
|
625
652
|
};
|
|
626
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
|
+
}
|
|
627
704
|
}
|
|
628
705
|
|
|
629
706
|
return {
|
|
@@ -708,5 +785,13 @@ export function createMAPSidecar(
|
|
|
708
785
|
);
|
|
709
786
|
}
|
|
710
787
|
},
|
|
788
|
+
|
|
789
|
+
getWorkspaceManager() {
|
|
790
|
+
return workspaceManager;
|
|
791
|
+
},
|
|
792
|
+
|
|
793
|
+
getRepoTransport() {
|
|
794
|
+
return workspaceTransport;
|
|
795
|
+
},
|
|
711
796
|
};
|
|
712
797
|
}
|
package/src/map/types.ts
CHANGED
|
@@ -127,6 +127,19 @@ export interface MAPSidecar {
|
|
|
127
127
|
participantId: string,
|
|
128
128
|
content: string,
|
|
129
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;
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
// =============================================================================
|