macro-agent 0.2.0 → 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 (42) 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 +1 -1
  6. package/dist/agent/agent-manager-v2.js.map +1 -1
  7. package/dist/boot-v2.d.ts.map +1 -1
  8. package/dist/boot-v2.js +2 -0
  9. package/dist/boot-v2.js.map +1 -1
  10. package/dist/cli/acp.js +0 -0
  11. package/dist/cli/index.js +0 -0
  12. package/dist/cli/mcp.js +0 -0
  13. package/dist/dispatch/mail-inbound-consumer.d.ts +47 -0
  14. package/dist/dispatch/mail-inbound-consumer.d.ts.map +1 -1
  15. package/dist/dispatch/mail-inbound-consumer.js +117 -18
  16. package/dist/dispatch/mail-inbound-consumer.js.map +1 -1
  17. package/dist/map/repo-workspace.d.ts +46 -0
  18. package/dist/map/repo-workspace.d.ts.map +1 -0
  19. package/dist/map/repo-workspace.js +39 -0
  20. package/dist/map/repo-workspace.js.map +1 -0
  21. package/dist/map/server.d.ts.map +1 -1
  22. package/dist/map/server.js +1 -0
  23. package/dist/map/server.js.map +1 -1
  24. package/dist/map/sidecar.d.ts.map +1 -1
  25. package/dist/map/sidecar.js +63 -0
  26. package/dist/map/sidecar.js.map +1 -1
  27. package/dist/map/types.d.ts +14 -0
  28. package/dist/map/types.d.ts.map +1 -1
  29. package/dist/workspace/dataplane-adapter.d.ts +260 -0
  30. package/dist/workspace/dataplane-adapter.d.ts.map +1 -0
  31. package/dist/workspace/dataplane-adapter.js +416 -0
  32. package/dist/workspace/dataplane-adapter.js.map +1 -0
  33. package/package.json +2 -1
  34. package/src/acp/macro-agent.ts +20 -42
  35. package/src/agent/agent-manager-v2.ts +1 -0
  36. package/src/boot-v2.ts +2 -0
  37. package/src/dispatch/__tests__/mail-inbound-consumer.test.ts +211 -0
  38. package/src/dispatch/mail-inbound-consumer.ts +195 -32
  39. package/src/map/repo-workspace.ts +82 -0
  40. package/src/map/server.ts +1 -0
  41. package/src/map/sidecar.ts +85 -0
  42. package/src/map/types.ts +13 -0
@@ -586,4 +586,215 @@ describe("createMailInboundConsumer", () => {
586
586
  expect(consumer.stats().seenTaskIds).toBe(1);
587
587
  expect(consumer.stats().droppedMalformed).toBe(2);
588
588
  });
589
+
590
+ // ── Pre-spawn repo mount ──────────────────────────────────────────
591
+
592
+ describe("pre-spawn repo mount", () => {
593
+ function workEnvelopeWithRepo(
594
+ taskId: string,
595
+ prompt: string,
596
+ repoMeta: Record<string, unknown>,
597
+ conversationId?: string,
598
+ ): InboxMessageEvent {
599
+ return {
600
+ agentId: DISPATCHER_ID,
601
+ message: {
602
+ id: `msg-${taskId}`,
603
+ content: {
604
+ schema: "x-dispatch/work",
605
+ data: {
606
+ taskId,
607
+ prompt,
608
+ role: "worker",
609
+ metadata: repoMeta,
610
+ },
611
+ ...(conversationId ? { _conversationId: conversationId } : {}),
612
+ },
613
+ },
614
+ };
615
+ }
616
+
617
+ function makeRepoManager(existingRepos: Array<{ canonicalUrl: string; localPath: string }> = []) {
618
+ const attached: Array<{ remoteUrl: string; localPath: string }> = [];
619
+ return {
620
+ manager: {
621
+ list: () =>
622
+ [
623
+ ...existingRepos.map((r) => ({
624
+ identity: { canonicalUrl: r.canonicalUrl },
625
+ localPath: r.localPath,
626
+ })),
627
+ ...attached.map((r) => ({
628
+ identity: { canonicalUrl: r.remoteUrl },
629
+ localPath: r.localPath,
630
+ })),
631
+ ],
632
+ attach: vi.fn(async (config: { remoteUrl: string; localPath: string }) => {
633
+ attached.push(config);
634
+ return { localPath: config.localPath };
635
+ }),
636
+ },
637
+ attached,
638
+ };
639
+ }
640
+
641
+ it("passes cwd from already-attached repo to spawn", async () => {
642
+ const repo = makeRepoManager([
643
+ { canonicalUrl: "https://github.com/org/repo.git", localPath: "/repos/repo" },
644
+ ]);
645
+ const logs: string[] = [];
646
+
647
+ createMailInboundConsumer({
648
+ dispatcherAgentId: DISPATCHER_ID,
649
+ inboxEvents,
650
+ agentManager: am.manager as AgentManager,
651
+ agentStore: store as AgentStore,
652
+ getSidecar: () => sidecar,
653
+ getRepoManager: () => repo.manager,
654
+ log: (m) => logs.push(m),
655
+ });
656
+
657
+ inboxEvents.fire(
658
+ workEnvelopeWithRepo("task-repo-1", "work on repo", {
659
+ repo_id: "repo_abc",
660
+ canonical_url: "https://github.com/org/repo.git",
661
+ }),
662
+ );
663
+
664
+ await new Promise((r) => setTimeout(r, 30));
665
+
666
+ expect(am.spawnFn).toHaveBeenCalledOnce();
667
+ expect(am.spawnFn).toHaveBeenCalledWith(
668
+ expect.objectContaining({ cwd: "/repos/repo" }),
669
+ );
670
+ expect(logs.some((l) => l.includes("already attached"))).toBe(true);
671
+ });
672
+
673
+ it("spawns without cwd when clone_policy is not 'allowed' and repo not attached", async () => {
674
+ const repo = makeRepoManager([]);
675
+ const logs: string[] = [];
676
+
677
+ createMailInboundConsumer({
678
+ dispatcherAgentId: DISPATCHER_ID,
679
+ inboxEvents,
680
+ agentManager: am.manager as AgentManager,
681
+ agentStore: store as AgentStore,
682
+ getSidecar: () => sidecar,
683
+ getRepoManager: () => repo.manager,
684
+ log: (m) => logs.push(m),
685
+ });
686
+
687
+ inboxEvents.fire(
688
+ workEnvelopeWithRepo("task-repo-2", "work", {
689
+ repo_id: "repo_xyz",
690
+ canonical_url: "https://github.com/org/other.git",
691
+ }),
692
+ );
693
+
694
+ await new Promise((r) => setTimeout(r, 30));
695
+
696
+ expect(am.spawnFn).toHaveBeenCalledOnce();
697
+ // No cwd passed — clone_policy defaults to 'none'
698
+ const spawnArgs = am.spawnFn.mock.calls[0]![0] as Record<string, unknown>;
699
+ expect(spawnArgs.cwd).toBeUndefined();
700
+ expect(logs.some((l) => l.includes("skipping mount"))).toBe(true);
701
+ });
702
+
703
+ it("spawns without cwd when no repo metadata in envelope", async () => {
704
+ const repo = makeRepoManager([]);
705
+
706
+ createMailInboundConsumer({
707
+ dispatcherAgentId: DISPATCHER_ID,
708
+ inboxEvents,
709
+ agentManager: am.manager as AgentManager,
710
+ agentStore: store as AgentStore,
711
+ getSidecar: () => sidecar,
712
+ getRepoManager: () => repo.manager,
713
+ });
714
+
715
+ // Standard envelope without repo metadata
716
+ inboxEvents.fire(workEnvelope("task-no-repo", "plain work"));
717
+
718
+ await new Promise((r) => setTimeout(r, 30));
719
+
720
+ expect(am.spawnFn).toHaveBeenCalledOnce();
721
+ const spawnArgs = am.spawnFn.mock.calls[0]![0] as Record<string, unknown>;
722
+ expect(spawnArgs.cwd).toBeUndefined();
723
+ });
724
+
725
+ it("spawns without cwd when getRepoManager is not provided", async () => {
726
+ createMailInboundConsumer({
727
+ dispatcherAgentId: DISPATCHER_ID,
728
+ inboxEvents,
729
+ agentManager: am.manager as AgentManager,
730
+ agentStore: store as AgentStore,
731
+ getSidecar: () => sidecar,
732
+ // no getRepoManager
733
+ });
734
+
735
+ inboxEvents.fire(
736
+ workEnvelopeWithRepo("task-no-mgr", "work", {
737
+ repo_id: "repo_abc",
738
+ canonical_url: "https://github.com/org/repo.git",
739
+ clone_policy: "allowed",
740
+ }),
741
+ );
742
+
743
+ await new Promise((r) => setTimeout(r, 30));
744
+
745
+ expect(am.spawnFn).toHaveBeenCalledOnce();
746
+ const spawnArgs = am.spawnFn.mock.calls[0]![0] as Record<string, unknown>;
747
+ expect(spawnArgs.cwd).toBeUndefined();
748
+ });
749
+
750
+ it("spawns without cwd when canonical_url is missing from repo metadata", async () => {
751
+ const repo = makeRepoManager([]);
752
+
753
+ createMailInboundConsumer({
754
+ dispatcherAgentId: DISPATCHER_ID,
755
+ inboxEvents,
756
+ agentManager: am.manager as AgentManager,
757
+ agentStore: store as AgentStore,
758
+ getSidecar: () => sidecar,
759
+ getRepoManager: () => repo.manager,
760
+ });
761
+
762
+ inboxEvents.fire(
763
+ workEnvelopeWithRepo("task-no-url", "work", {
764
+ repo_id: "repo_abc",
765
+ // no canonical_url
766
+ }),
767
+ );
768
+
769
+ await new Promise((r) => setTimeout(r, 30));
770
+
771
+ expect(am.spawnFn).toHaveBeenCalledOnce();
772
+ const spawnArgs = am.spawnFn.mock.calls[0]![0] as Record<string, unknown>;
773
+ expect(spawnArgs.cwd).toBeUndefined();
774
+ });
775
+
776
+ it("logs repo_id in the received message log line", async () => {
777
+ const logs: string[] = [];
778
+
779
+ createMailInboundConsumer({
780
+ dispatcherAgentId: DISPATCHER_ID,
781
+ inboxEvents,
782
+ agentManager: am.manager as AgentManager,
783
+ agentStore: store as AgentStore,
784
+ getSidecar: () => sidecar,
785
+ log: (m) => logs.push(m),
786
+ });
787
+
788
+ inboxEvents.fire(
789
+ workEnvelopeWithRepo("task-log", "work", {
790
+ repo_id: "repo_visible",
791
+ canonical_url: "https://github.com/org/visible.git",
792
+ }),
793
+ );
794
+
795
+ await new Promise((r) => setTimeout(r, 20));
796
+
797
+ expect(logs.some((l) => l.includes("repo=repo_visible"))).toBe(true);
798
+ });
799
+ });
589
800
  });
@@ -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
- agentManager
271
- .spawn({
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
- // Spawn only creates an idle ACP session — the task lives in the
292
- // system prompt as instructions. To get the model to actually do
293
- // the work, send the prompt as a user message via promptUntilDone.
294
- // This drives the worker to completion (done() called) so the
295
- // lifecycle stopped listener below fires and posts the reply
296
- // back to the hub. Fire-and-forget; errors are logged.
297
- try {
298
- await agentManager.promptUntilDone(spawned.id, prompt, {
299
- maxFollowUps: 0,
300
- });
301
- } catch (err) {
302
- log(
303
- `[mail-inbound] promptUntilDone failed for agentId=${spawned.id}: ` +
304
- `${(err as Error).message ?? String(err)}`,
305
- );
306
- }
307
- })
308
- .catch((err: unknown) => {
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] Spawn failed for taskId=${taskId}: ${
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);
@@ -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,