macro-agent 0.1.8 → 0.1.10

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 (258) hide show
  1. package/CLAUDE.md +166 -33
  2. package/README.md +781 -131
  3. package/dist/acp/claude-code-replay.d.ts +11 -0
  4. package/dist/acp/claude-code-replay.d.ts.map +1 -0
  5. package/dist/acp/claude-code-replay.js +190 -0
  6. package/dist/acp/claude-code-replay.js.map +1 -0
  7. package/dist/acp/macro-agent.d.ts.map +1 -1
  8. package/dist/acp/macro-agent.js +155 -6
  9. package/dist/acp/macro-agent.js.map +1 -1
  10. package/dist/acp/types.d.ts +9 -0
  11. package/dist/acp/types.d.ts.map +1 -1
  12. package/dist/acp/types.js.map +1 -1
  13. package/dist/agent/agent-manager-v2.d.ts +21 -0
  14. package/dist/agent/agent-manager-v2.d.ts.map +1 -1
  15. package/dist/agent/agent-manager-v2.js +234 -43
  16. package/dist/agent/agent-manager-v2.js.map +1 -1
  17. package/dist/agent/agent-manager.d.ts +12 -0
  18. package/dist/agent/agent-manager.d.ts.map +1 -1
  19. package/dist/agent/agent-manager.js.map +1 -1
  20. package/dist/agent/types.d.ts +15 -2
  21. package/dist/agent/types.d.ts.map +1 -1
  22. package/dist/agent/types.js.map +1 -1
  23. package/dist/boot-v2.d.ts +41 -0
  24. package/dist/boot-v2.d.ts.map +1 -1
  25. package/dist/boot-v2.js +16 -1
  26. package/dist/boot-v2.js.map +1 -1
  27. package/dist/cli/index.js +56 -0
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/cognitive/macro-agent-backend.d.ts.map +1 -1
  30. package/dist/cognitive/macro-agent-backend.js +40 -22
  31. package/dist/cognitive/macro-agent-backend.js.map +1 -1
  32. package/dist/integrations/skilltree.d.ts.map +1 -1
  33. package/dist/integrations/skilltree.js +1 -0
  34. package/dist/integrations/skilltree.js.map +1 -1
  35. package/dist/lifecycle/cleanup.d.ts +33 -2
  36. package/dist/lifecycle/cleanup.d.ts.map +1 -1
  37. package/dist/lifecycle/cleanup.js +28 -6
  38. package/dist/lifecycle/cleanup.js.map +1 -1
  39. package/dist/lifecycle/handlers-v2.d.ts +7 -0
  40. package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
  41. package/dist/lifecycle/handlers-v2.js +28 -2
  42. package/dist/lifecycle/handlers-v2.js.map +1 -1
  43. package/dist/lifecycle/types.d.ts +11 -0
  44. package/dist/lifecycle/types.d.ts.map +1 -1
  45. package/dist/lifecycle/types.js.map +1 -1
  46. package/dist/map/acp-bridge.d.ts +9 -0
  47. package/dist/map/acp-bridge.d.ts.map +1 -1
  48. package/dist/map/acp-bridge.js +15 -2
  49. package/dist/map/acp-bridge.js.map +1 -1
  50. package/dist/map/cascade-bridge.d.ts +44 -0
  51. package/dist/map/cascade-bridge.d.ts.map +1 -0
  52. package/dist/map/cascade-bridge.js +257 -0
  53. package/dist/map/cascade-bridge.js.map +1 -0
  54. package/dist/map/lifecycle-bridge.d.ts +1 -1
  55. package/dist/map/lifecycle-bridge.d.ts.map +1 -1
  56. package/dist/map/lifecycle-bridge.js +58 -23
  57. package/dist/map/lifecycle-bridge.js.map +1 -1
  58. package/dist/map/server.d.ts.map +1 -1
  59. package/dist/map/server.js +47 -6
  60. package/dist/map/server.js.map +1 -1
  61. package/dist/map/sidecar.d.ts.map +1 -1
  62. package/dist/map/sidecar.js +33 -2
  63. package/dist/map/sidecar.js.map +1 -1
  64. package/dist/map/types.d.ts +20 -0
  65. package/dist/map/types.d.ts.map +1 -1
  66. package/dist/mcp/tools/done-v2.d.ts.map +1 -1
  67. package/dist/mcp/tools/done-v2.js +8 -0
  68. package/dist/mcp/tools/done-v2.js.map +1 -1
  69. package/dist/teams/team-manager-v2.d.ts.map +1 -1
  70. package/dist/teams/team-manager-v2.js +26 -0
  71. package/dist/teams/team-manager-v2.js.map +1 -1
  72. package/dist/teams/team-runtime-v2.d.ts.map +1 -1
  73. package/dist/teams/team-runtime-v2.js +16 -3
  74. package/dist/teams/team-runtime-v2.js.map +1 -1
  75. package/dist/workspace/config.d.ts +10 -10
  76. package/dist/workspace/config.d.ts.map +1 -1
  77. package/dist/workspace/config.js +4 -4
  78. package/dist/workspace/config.js.map +1 -1
  79. package/dist/workspace/git-cascade-adapter.d.ts +510 -0
  80. package/dist/workspace/git-cascade-adapter.d.ts.map +1 -0
  81. package/dist/workspace/git-cascade-adapter.js +908 -0
  82. package/dist/workspace/git-cascade-adapter.js.map +1 -0
  83. package/dist/workspace/index.d.ts +3 -3
  84. package/dist/workspace/index.d.ts.map +1 -1
  85. package/dist/workspace/index.js +4 -4
  86. package/dist/workspace/index.js.map +1 -1
  87. package/dist/workspace/landing/direct-push.d.ts +20 -0
  88. package/dist/workspace/landing/direct-push.d.ts.map +1 -0
  89. package/dist/workspace/landing/direct-push.js +74 -0
  90. package/dist/workspace/landing/direct-push.js.map +1 -0
  91. package/dist/workspace/landing/index.d.ts +29 -0
  92. package/dist/workspace/landing/index.d.ts.map +1 -0
  93. package/dist/workspace/landing/index.js +37 -0
  94. package/dist/workspace/landing/index.js.map +1 -0
  95. package/dist/workspace/landing/merge-to-parent.d.ts +41 -0
  96. package/dist/workspace/landing/merge-to-parent.d.ts.map +1 -0
  97. package/dist/workspace/landing/merge-to-parent.js +185 -0
  98. package/dist/workspace/landing/merge-to-parent.js.map +1 -0
  99. package/dist/workspace/landing/optimistic-push.d.ts +16 -0
  100. package/dist/workspace/landing/optimistic-push.d.ts.map +1 -0
  101. package/dist/workspace/landing/optimistic-push.js +27 -0
  102. package/dist/workspace/landing/optimistic-push.js.map +1 -0
  103. package/dist/workspace/landing/queue-to-branch.d.ts +24 -0
  104. package/dist/workspace/landing/queue-to-branch.d.ts.map +1 -0
  105. package/dist/workspace/landing/queue-to-branch.js +79 -0
  106. package/dist/workspace/landing/queue-to-branch.js.map +1 -0
  107. package/dist/workspace/merge-queue/merge-queue.d.ts +10 -0
  108. package/dist/workspace/merge-queue/merge-queue.d.ts.map +1 -1
  109. package/dist/workspace/merge-queue/merge-queue.js +10 -0
  110. package/dist/workspace/merge-queue/merge-queue.js.map +1 -1
  111. package/dist/workspace/merge-queue/types.d.ts +16 -2
  112. package/dist/workspace/merge-queue/types.d.ts.map +1 -1
  113. package/dist/workspace/merge-queue/types.js +9 -0
  114. package/dist/workspace/merge-queue/types.js.map +1 -1
  115. package/dist/workspace/pool/types.d.ts +1 -0
  116. package/dist/workspace/pool/types.d.ts.map +1 -1
  117. package/dist/workspace/pool/worktree-pool.d.ts.map +1 -1
  118. package/dist/workspace/pool/worktree-pool.js +1 -0
  119. package/dist/workspace/pool/worktree-pool.js.map +1 -1
  120. package/dist/workspace/recovery/abandon.d.ts +15 -0
  121. package/dist/workspace/recovery/abandon.d.ts.map +1 -0
  122. package/dist/workspace/recovery/abandon.js +45 -0
  123. package/dist/workspace/recovery/abandon.js.map +1 -0
  124. package/dist/workspace/recovery/auto-resolve.d.ts +27 -0
  125. package/dist/workspace/recovery/auto-resolve.d.ts.map +1 -0
  126. package/dist/workspace/recovery/auto-resolve.js +99 -0
  127. package/dist/workspace/recovery/auto-resolve.js.map +1 -0
  128. package/dist/workspace/recovery/defer.d.ts +15 -0
  129. package/dist/workspace/recovery/defer.d.ts.map +1 -0
  130. package/dist/workspace/recovery/defer.js +16 -0
  131. package/dist/workspace/recovery/defer.js.map +1 -0
  132. package/dist/workspace/recovery/escalate.d.ts +16 -0
  133. package/dist/workspace/recovery/escalate.d.ts.map +1 -0
  134. package/dist/workspace/recovery/escalate.js +24 -0
  135. package/dist/workspace/recovery/escalate.js.map +1 -0
  136. package/dist/workspace/recovery/index.d.ts +32 -0
  137. package/dist/workspace/recovery/index.d.ts.map +1 -0
  138. package/dist/workspace/recovery/index.js +45 -0
  139. package/dist/workspace/recovery/index.js.map +1 -0
  140. package/dist/workspace/recovery/spawn-resolver.d.ts +45 -0
  141. package/dist/workspace/recovery/spawn-resolver.d.ts.map +1 -0
  142. package/dist/workspace/recovery/spawn-resolver.js +111 -0
  143. package/dist/workspace/recovery/spawn-resolver.js.map +1 -0
  144. package/dist/workspace/recovery/types.d.ts +63 -0
  145. package/dist/workspace/recovery/types.d.ts.map +1 -0
  146. package/dist/workspace/recovery/types.js +12 -0
  147. package/dist/workspace/recovery/types.js.map +1 -0
  148. package/dist/workspace/topology/index.d.ts +9 -0
  149. package/dist/workspace/topology/index.d.ts.map +1 -0
  150. package/dist/workspace/topology/index.js +8 -0
  151. package/dist/workspace/topology/index.js.map +1 -0
  152. package/dist/workspace/topology/no-workspace.d.ts +18 -0
  153. package/dist/workspace/topology/no-workspace.d.ts.map +1 -0
  154. package/dist/workspace/topology/no-workspace.js +25 -0
  155. package/dist/workspace/topology/no-workspace.js.map +1 -0
  156. package/dist/workspace/topology/types.d.ts +97 -0
  157. package/dist/workspace/topology/types.d.ts.map +1 -0
  158. package/dist/workspace/topology/types.js +20 -0
  159. package/dist/workspace/topology/types.js.map +1 -0
  160. package/dist/workspace/topology/yaml-driven.d.ts +69 -0
  161. package/dist/workspace/topology/yaml-driven.d.ts.map +1 -0
  162. package/dist/workspace/topology/yaml-driven.js +273 -0
  163. package/dist/workspace/topology/yaml-driven.js.map +1 -0
  164. package/dist/workspace/types-v3.d.ts +110 -0
  165. package/dist/workspace/types-v3.d.ts.map +1 -0
  166. package/dist/workspace/types-v3.js +20 -0
  167. package/dist/workspace/types-v3.js.map +1 -0
  168. package/dist/workspace/types.d.ts +145 -17
  169. package/dist/workspace/types.d.ts.map +1 -1
  170. package/dist/workspace/workspace-manager.d.ts +92 -13
  171. package/dist/workspace/workspace-manager.d.ts.map +1 -1
  172. package/dist/workspace/workspace-manager.js +373 -13
  173. package/dist/workspace/workspace-manager.js.map +1 -1
  174. package/dist/workspace/yaml-schema.d.ts +254 -0
  175. package/dist/workspace/yaml-schema.d.ts.map +1 -0
  176. package/dist/workspace/yaml-schema.js +170 -0
  177. package/dist/workspace/yaml-schema.js.map +1 -0
  178. package/docs/conflict-recovery.md +472 -0
  179. package/docs/git-cascade-integration-gaps.md +678 -0
  180. package/docs/workspace-interfaces.md +731 -0
  181. package/docs/workspace-redesign-plan.md +302 -0
  182. package/package.json +4 -4
  183. package/src/__tests__/e2e/auto-sync.e2e.test.ts +257 -0
  184. package/src/__tests__/e2e/cascade-rebase.e2e.test.ts +254 -0
  185. package/src/__tests__/e2e/cli-run.e2e.test.ts +167 -0
  186. package/src/__tests__/e2e/self-driving-v3.e2e.test.ts +197 -0
  187. package/src/__tests__/e2e/spawn-resolver.e2e.test.ts +200 -0
  188. package/src/__tests__/e2e/workspace-lifecycle.e2e.test.ts +30 -22
  189. package/src/__tests__/e2e/workspace-v3.e2e.test.ts +413 -0
  190. package/src/acp/__tests__/claude-code-replay.test.ts +225 -0
  191. package/src/acp/__tests__/macro-agent.test.ts +39 -1
  192. package/src/acp/claude-code-replay.ts +208 -0
  193. package/src/acp/macro-agent.ts +167 -9
  194. package/src/acp/types.ts +10 -0
  195. package/src/agent/__tests__/agent-manager-topology.test.ts +73 -0
  196. package/src/agent/__tests__/agent-manager-v2.test.ts +66 -0
  197. package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
  198. package/src/agent/agent-manager-v2.ts +293 -48
  199. package/src/agent/agent-manager.ts +14 -0
  200. package/src/agent/types.ts +16 -2
  201. package/src/boot-v2.ts +68 -1
  202. package/src/cli/index.ts +61 -0
  203. package/src/cognitive/macro-agent-backend.ts +45 -29
  204. package/src/integrations/skilltree.ts +1 -0
  205. package/src/lifecycle/cleanup.ts +52 -3
  206. package/src/lifecycle/handlers-v2.ts +40 -3
  207. package/src/lifecycle/types.ts +12 -0
  208. package/src/map/__tests__/cascade-bridge.test.ts +229 -0
  209. package/src/map/__tests__/lifecycle-bridge.test.ts +86 -10
  210. package/src/map/acp-bridge.ts +26 -3
  211. package/src/map/cascade-bridge.ts +301 -0
  212. package/src/map/lifecycle-bridge.ts +52 -17
  213. package/src/map/server.ts +47 -6
  214. package/src/map/sidecar.ts +31 -1
  215. package/src/map/types.ts +20 -0
  216. package/src/mcp/tools/done-v2.ts +9 -0
  217. package/src/teams/team-manager-v2.ts +37 -0
  218. package/src/teams/team-runtime-v2.ts +23 -3
  219. package/src/workspace/__tests__/{dataplane-adapter.test.ts → git-cascade-adapter.test.ts} +209 -14
  220. package/src/workspace/__tests__/self-driving-yaml.test.ts +114 -0
  221. package/src/workspace/__tests__/shared-worktree-refcount.test.ts +154 -0
  222. package/src/workspace/__tests__/standalone-mode.test.ts +118 -0
  223. package/src/workspace/__tests__/workspace-manager-v3.test.ts +245 -0
  224. package/src/workspace/__tests__/yaml-schema.test.ts +210 -0
  225. package/src/workspace/config.ts +11 -11
  226. package/src/workspace/git-cascade-adapter.ts +1186 -0
  227. package/src/workspace/index.ts +11 -11
  228. package/src/workspace/landing/__tests__/strategies.test.ts +142 -0
  229. package/src/workspace/landing/direct-push.ts +91 -0
  230. package/src/workspace/landing/index.ts +40 -0
  231. package/src/workspace/landing/merge-to-parent.ts +228 -0
  232. package/src/workspace/landing/optimistic-push.ts +36 -0
  233. package/src/workspace/landing/queue-to-branch.ts +108 -0
  234. package/src/workspace/merge-queue/merge-queue.ts +10 -0
  235. package/src/workspace/merge-queue/types.ts +16 -2
  236. package/src/workspace/pool/__tests__/worktree-pool.integration.test.ts +5 -5
  237. package/src/workspace/pool/types.ts +1 -0
  238. package/src/workspace/pool/worktree-pool.ts +1 -0
  239. package/src/workspace/recovery/__tests__/auto-resolve-integration.test.ts +127 -0
  240. package/src/workspace/recovery/__tests__/spawn-resolver.test.ts +139 -0
  241. package/src/workspace/recovery/__tests__/strategies.test.ts +145 -0
  242. package/src/workspace/recovery/abandon.ts +51 -0
  243. package/src/workspace/recovery/auto-resolve.ts +119 -0
  244. package/src/workspace/recovery/defer.ts +23 -0
  245. package/src/workspace/recovery/escalate.ts +30 -0
  246. package/src/workspace/recovery/index.ts +58 -0
  247. package/src/workspace/recovery/spawn-resolver.ts +145 -0
  248. package/src/workspace/recovery/types.ts +54 -0
  249. package/src/workspace/topology/__tests__/yaml-driven.test.ts +345 -0
  250. package/src/workspace/topology/index.ts +18 -0
  251. package/src/workspace/topology/no-workspace.ts +39 -0
  252. package/src/workspace/topology/types.ts +116 -0
  253. package/src/workspace/topology/yaml-driven.ts +316 -0
  254. package/src/workspace/types-v3.ts +155 -0
  255. package/src/workspace/types.ts +191 -20
  256. package/src/workspace/workspace-manager.ts +474 -19
  257. package/src/workspace/yaml-schema.ts +216 -0
  258. package/src/workspace/dataplane-adapter.ts +0 -546
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Tests for AgentManagerV2's three-level TaskRef resolution:
3
+ * 1. Explicit SpawnAgentOptions.taskRef wins.
4
+ * 2. resolveTaskRef(opts) runs when no explicit ref.
5
+ * 3. Fallback to taskResourceId + options.task_id when both above yield nothing.
6
+ *
7
+ * Verifies the G3/G14 closure: single-graph default via boot config + multi-graph
8
+ * resolver for deployments that touch more than one opentasks graph.
9
+ */
10
+
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
12
+ import { createAgentManagerV2 } from "../agent-manager-v2.js";
13
+ import { AgentStore } from "../agent-store.js";
14
+ import type { AgentManager } from "../agent-manager.js";
15
+ import type { InboxAdapter, TasksAdapter } from "../../adapters/types.js";
16
+ import type { SpawnAgentOptions } from "../types.js";
17
+ import type { TaskRef } from "git-cascade/events";
18
+
19
+ vi.mock("acp-factory", () => ({
20
+ AgentFactory: {
21
+ spawn: vi.fn().mockResolvedValue({
22
+ createSession: vi.fn().mockResolvedValue({
23
+ id: "provider-session-1",
24
+ prompt: vi.fn().mockReturnValue({
25
+ [Symbol.asyncIterator]: () => ({
26
+ next: () => Promise.resolve({ done: true, value: undefined }),
27
+ }),
28
+ }),
29
+ forkWithFlush: vi.fn().mockResolvedValue({ id: "forked-session-1" }),
30
+ }),
31
+ loadSession: vi.fn(),
32
+ close: vi.fn().mockResolvedValue(undefined),
33
+ isRunning: vi.fn().mockReturnValue(true),
34
+ }),
35
+ },
36
+ }));
37
+
38
+ function mockInbox(): InboxAdapter {
39
+ return {
40
+ registerAgent: vi.fn().mockResolvedValue(undefined),
41
+ deregisterAgent: vi.fn().mockResolvedValue(undefined),
42
+ send: vi.fn().mockResolvedValue("msg-1"),
43
+ onDelivery: vi.fn(),
44
+ offDelivery: vi.fn(),
45
+ checkInbox: vi.fn().mockResolvedValue([]),
46
+ readThread: vi.fn().mockResolvedValue([]),
47
+ setSignalFilter: vi.fn(),
48
+ setEmissionValidator: vi.fn(),
49
+ socketPath: "/tmp/test-inbox.sock",
50
+ stop: vi.fn().mockResolvedValue(undefined),
51
+ } as unknown as InboxAdapter;
52
+ }
53
+
54
+ function mockTasks(): TasksAdapter {
55
+ return {
56
+ createTask: vi.fn().mockResolvedValue("ot-task-1"),
57
+ assignTask: vi.fn().mockResolvedValue(undefined),
58
+ transitionTask: vi.fn().mockResolvedValue(undefined),
59
+ getTask: vi.fn().mockResolvedValue({ id: "t-1", title: "x", status: "open" }),
60
+ queryReady: vi.fn().mockResolvedValue([]),
61
+ listTasks: vi.fn().mockResolvedValue([]),
62
+ addBlocker: vi.fn().mockResolvedValue(undefined),
63
+ removeBlocker: vi.fn().mockResolvedValue(undefined),
64
+ claimTask: vi.fn().mockResolvedValue(null),
65
+ unclaimTask: vi.fn().mockResolvedValue(undefined),
66
+ listClaimable: vi.fn().mockResolvedValue([]),
67
+ connect: vi.fn().mockResolvedValue(undefined),
68
+ disconnect: vi.fn(),
69
+ connected: true,
70
+ } as unknown as TasksAdapter;
71
+ }
72
+
73
+ describe("AgentManagerV2 taskRef resolution", () => {
74
+ let agentStore: AgentStore;
75
+ let inbox: InboxAdapter;
76
+ let tasks: TasksAdapter;
77
+ let manager: AgentManager;
78
+
79
+ afterEach(async () => {
80
+ await manager?.close();
81
+ agentStore?.close();
82
+ });
83
+
84
+ beforeEach(() => {
85
+ agentStore = new AgentStore(":memory:");
86
+ inbox = mockInbox();
87
+ tasks = mockTasks();
88
+ });
89
+
90
+ function getStoredTaskRef(agentId: string): TaskRef | undefined {
91
+ const record = agentStore.getAgent(agentId);
92
+ const meta = record?.metadata as Record<string, unknown> | undefined;
93
+ return meta?.task_ref as TaskRef | undefined;
94
+ }
95
+
96
+ it("uses taskResourceId + task_id when no other source resolves a ref", async () => {
97
+ manager = createAgentManagerV2(agentStore, inbox, tasks, {
98
+ defaultCwd: "/tmp/t",
99
+ taskResourceId: "resource-default",
100
+ });
101
+
102
+ const result = await manager.spawn({
103
+ task: "work",
104
+ task_id: "node-42" as unknown as SpawnAgentOptions["task_id"],
105
+ role: "worker",
106
+ });
107
+
108
+ expect(getStoredTaskRef(result.id)).toEqual({
109
+ resource_id: "resource-default",
110
+ node_id: "node-42",
111
+ });
112
+ });
113
+
114
+ it("falls through to no taskRef when neither resolver nor taskResourceId apply", async () => {
115
+ manager = createAgentManagerV2(agentStore, inbox, tasks, {
116
+ defaultCwd: "/tmp/t",
117
+ // No taskResourceId, no resolveTaskRef.
118
+ });
119
+
120
+ const result = await manager.spawn({ task: "work", role: "worker" });
121
+
122
+ expect(getStoredTaskRef(result.id)).toBeUndefined();
123
+ });
124
+
125
+ it("resolveTaskRef wins over the taskResourceId fallback", async () => {
126
+ const resolveTaskRef = vi
127
+ .fn()
128
+ .mockReturnValue({ resource_id: "resource-from-resolver", node_id: "n1" });
129
+
130
+ manager = createAgentManagerV2(agentStore, inbox, tasks, {
131
+ defaultCwd: "/tmp/t",
132
+ taskResourceId: "resource-default",
133
+ resolveTaskRef,
134
+ });
135
+
136
+ const result = await manager.spawn({ task: "work", role: "worker" });
137
+
138
+ expect(resolveTaskRef).toHaveBeenCalledOnce();
139
+ expect(getStoredTaskRef(result.id)).toEqual({
140
+ resource_id: "resource-from-resolver",
141
+ node_id: "n1",
142
+ });
143
+ });
144
+
145
+ it("resolveTaskRef returning undefined falls through to taskResourceId", async () => {
146
+ const resolveTaskRef = vi.fn().mockReturnValue(undefined);
147
+
148
+ manager = createAgentManagerV2(agentStore, inbox, tasks, {
149
+ defaultCwd: "/tmp/t",
150
+ taskResourceId: "resource-default",
151
+ resolveTaskRef,
152
+ });
153
+
154
+ const result = await manager.spawn({
155
+ task: "work",
156
+ task_id: "node-9" as unknown as SpawnAgentOptions["task_id"],
157
+ role: "worker",
158
+ });
159
+
160
+ expect(resolveTaskRef).toHaveBeenCalledOnce();
161
+ expect(getStoredTaskRef(result.id)).toEqual({
162
+ resource_id: "resource-default",
163
+ node_id: "node-9",
164
+ });
165
+ });
166
+
167
+ it("explicit SpawnAgentOptions.taskRef wins over resolver AND taskResourceId", async () => {
168
+ const resolveTaskRef = vi
169
+ .fn()
170
+ .mockReturnValue({ resource_id: "resource-from-resolver", node_id: "r1" });
171
+
172
+ manager = createAgentManagerV2(agentStore, inbox, tasks, {
173
+ defaultCwd: "/tmp/t",
174
+ taskResourceId: "resource-default",
175
+ resolveTaskRef,
176
+ });
177
+
178
+ const explicit: TaskRef = { resource_id: "resource-explicit", node_id: "e1" };
179
+ const result = await manager.spawn({
180
+ task: "work",
181
+ role: "worker",
182
+ taskRef: explicit,
183
+ });
184
+
185
+ // Resolver should NOT be called when caller already supplied taskRef.
186
+ expect(resolveTaskRef).not.toHaveBeenCalled();
187
+ expect(getStoredTaskRef(result.id)).toEqual(explicit);
188
+ });
189
+
190
+ it("resolver receives the intercepted SpawnAgentOptions (post-interceptor)", async () => {
191
+ const resolveTaskRef = vi.fn().mockReturnValue(undefined);
192
+
193
+ manager = createAgentManagerV2(agentStore, inbox, tasks, {
194
+ defaultCwd: "/tmp/t",
195
+ resolveTaskRef,
196
+ });
197
+ manager.setSpawnInterceptor((opts) => ({ ...opts, cwd: "/overridden/cwd" }));
198
+
199
+ await manager.spawn({ task: "x", role: "worker" });
200
+
201
+ expect(resolveTaskRef).toHaveBeenCalledOnce();
202
+ const passed = resolveTaskRef.mock.calls[0][0];
203
+ expect(passed.cwd).toBe("/overridden/cwd");
204
+ });
205
+
206
+ it("resolver throwing does not block spawn; falls through to taskResourceId", async () => {
207
+ const resolveTaskRef = vi.fn().mockImplementation(() => {
208
+ throw new Error("resolver exploded");
209
+ });
210
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
211
+
212
+ manager = createAgentManagerV2(agentStore, inbox, tasks, {
213
+ defaultCwd: "/tmp/t",
214
+ taskResourceId: "resource-safe",
215
+ resolveTaskRef,
216
+ });
217
+
218
+ const result = await manager.spawn({
219
+ task: "x",
220
+ task_id: "safe-node" as unknown as SpawnAgentOptions["task_id"],
221
+ role: "worker",
222
+ });
223
+
224
+ expect(warnSpy).toHaveBeenCalled();
225
+ expect(getStoredTaskRef(result.id)).toEqual({
226
+ resource_id: "resource-safe",
227
+ node_id: "safe-node",
228
+ });
229
+ warnSpy.mockRestore();
230
+ });
231
+ });
@@ -116,6 +116,29 @@ export interface AgentManagerV2Config {
116
116
  serverToken?: string;
117
117
  /** Control socket path for MCP subprocess lifecycle RPC */
118
118
  controlSocketPath?: string;
119
+ /**
120
+ * Default opentasks resource ID hosted on the OpenHive hub. When set,
121
+ * spawn paths build `taskRef = { resource_id: <this>, node_id: task_id }`
122
+ * automatically from `SpawnAgentOptions.task_id` (for any spawn where
123
+ * `resolveTaskRef` returned undefined AND the caller didn't supply an
124
+ * explicit `taskRef`).
125
+ *
126
+ * Operators set this once at swarm registration for the common
127
+ * single-graph case. Multi-graph deployments should use `resolveTaskRef`
128
+ * instead.
129
+ */
130
+ taskResourceId?: string;
131
+
132
+ /**
133
+ * Multi-graph resolver. Called at every spawn; return a `TaskRef` to set
134
+ * the binding or `undefined` to fall through to the `taskResourceId`
135
+ * default. Explicit `SpawnAgentOptions.taskRef` always wins over both.
136
+ *
137
+ * Keep cheap — runs per-spawn.
138
+ */
139
+ resolveTaskRef?: (
140
+ spawnOptions: SpawnAgentOptions
141
+ ) => import("git-cascade/events").TaskRef | undefined;
119
142
  }
120
143
 
121
144
  // ─────────────────────────────────────────────────────────────────
@@ -139,6 +162,8 @@ export function createAgentManagerV2(
139
162
  serverToken,
140
163
  agentTokenManager,
141
164
  controlSocketPath,
165
+ taskResourceId,
166
+ resolveTaskRef,
142
167
  } = config;
143
168
 
144
169
  // In-memory state
@@ -158,6 +183,12 @@ export function createAgentManagerV2(
158
183
  // MAP sidecar reference for trajectory reporting (set via setSidecar)
159
184
  let sidecarRef: { connected: boolean; reportCheckpoint(cp: any): Promise<any> } | null = null;
160
185
 
186
+ // TopologyPolicy for workspace allocation (Phase 3+); set via setTopologyPolicy.
187
+ // When null, createWorkspaceForRole falls back to legacy role-name dispatch.
188
+ let topologyPolicy:
189
+ | import('../workspace/topology/types.js').TopologyPolicy
190
+ | null = null;
191
+
161
192
  // ── Helpers ──────────────────────────────────────────────────
162
193
 
163
194
  function notifyLifecycle(event: AgentLifecycleEvent): void {
@@ -247,6 +278,109 @@ export function createAgentManagerV2(
247
278
 
248
279
  // ── Workspace Helper ─────────────────────────────────────────
249
280
 
281
+ /**
282
+ * Execute a TopologyPolicy decision against the WorkspaceManager.
283
+ *
284
+ * Translates declarative `WorkspaceDecision` into concrete workspace
285
+ * allocations. Returns a `Workspace` compatible with the legacy shape
286
+ * so the rest of AgentManagerV2 doesn't need to change.
287
+ */
288
+ async function executeWorkspaceDecision(
289
+ agentId: AgentId,
290
+ decision: import('../workspace/topology/types.js').WorkspaceDecision,
291
+ role?: string,
292
+ spawnOptions?: SpawnAgentOptions
293
+ ): Promise<Workspace | undefined> {
294
+ if (!workspaceManager) return undefined;
295
+
296
+ switch (decision.kind) {
297
+ case 'none':
298
+ case 'share-parent-cwd':
299
+ return undefined;
300
+
301
+ case 'share-with-agent': {
302
+ const worktree = workspaceManager.allocateWorktree({
303
+ agentId,
304
+ sharedWithAgent: decision.agentId,
305
+ });
306
+ return {
307
+ agentId,
308
+ path: worktree.path,
309
+ branch: worktree.currentStream
310
+ ? `stream/${worktree.currentStream}`
311
+ : 'unknown',
312
+ streamId: worktree.currentStream ?? '',
313
+ role: 'v3', // V3 path — bypass legacy worker task/merge-queue flows
314
+ createdAt: worktree.createdAt,
315
+ };
316
+ }
317
+
318
+ case 'attach-to-stream': {
319
+ // Record even if no worktree — the topology needs the stream↔role
320
+ // mapping for event-driven features like on_parent_advanced.
321
+ const attachPolicy = topologyPolicy as unknown as {
322
+ recordAgentStream?: (a: string, s: string, role?: string) => void;
323
+ };
324
+ attachPolicy.recordAgentStream?.(agentId, decision.streamId, role);
325
+
326
+ if (!decision.allocateWorktree) {
327
+ return undefined;
328
+ }
329
+ const worktree = workspaceManager.allocateWorktree({
330
+ agentId,
331
+ streamId: decision.streamId,
332
+ });
333
+ return {
334
+ agentId,
335
+ path: worktree.path,
336
+ branch: `stream/${decision.streamId}`,
337
+ streamId: decision.streamId,
338
+ role: 'v3', // V3 path — attach-to-team-root
339
+ createdAt: worktree.createdAt,
340
+ };
341
+ }
342
+
343
+ case 'new-stream': {
344
+ // If the spawning agent has a taskRef and the streamSpec doesn't
345
+ // already carry one, weave it into metadata so the resulting stream
346
+ // binds to the OpenTasks node. Explicit streamSpec.metadata.task_ref
347
+ // wins.
348
+ const taskRef = spawnOptions?.taskRef;
349
+ const existingMeta = decision.streamSpec.metadata as
350
+ | Record<string, unknown>
351
+ | undefined;
352
+ const streamSpec = taskRef && !existingMeta?.task_ref
353
+ ? {
354
+ ...decision.streamSpec,
355
+ metadata: { ...(existingMeta ?? {}), task_ref: taskRef },
356
+ }
357
+ : decision.streamSpec;
358
+ const streamId = workspaceManager.createStreamV3(streamSpec);
359
+ // Record the mapping in the topology if it supports it (for share-with lookup).
360
+ const policy = topologyPolicy as unknown as {
361
+ recordAgentStream?: (a: string, s: string, role?: string) => void;
362
+ };
363
+ policy.recordAgentStream?.(agentId, streamId, role);
364
+
365
+ if (!decision.allocateWorktree) {
366
+ return undefined;
367
+ }
368
+ const worktree = workspaceManager.allocateWorktree({
369
+ agentId,
370
+ streamId,
371
+ });
372
+ return {
373
+ agentId,
374
+ path: worktree.path,
375
+ branch: `stream/${streamId}`,
376
+ streamId,
377
+ role: 'v3', // V3 path — new-stream
378
+ createdAt: worktree.createdAt,
379
+ };
380
+ }
381
+ }
382
+ }
383
+
250
384
  async function createWorkspaceForRole(
251
385
  agentId: AgentId,
252
386
  role: string,
@@ -254,56 +388,92 @@ export function createAgentManagerV2(
254
388
  ): Promise<Workspace | undefined> {
255
389
  if (!workspaceManager) return undefined;
256
390
 
391
+ // V3 path — TopologyPolicy-driven. Set by boot-v2 when team YAML has
392
+ // `macro_agent.workspace`. When set, this takes precedence over the legacy
393
+ // capability/role-name dispatch below.
394
+ if (topologyPolicy) {
395
+ const decision = await topologyPolicy.onAgentSpawn({
396
+ agentId,
397
+ role,
398
+ parentAgentId: options.parent ?? undefined,
399
+ parentStreamId: options.streamId,
400
+ teamStreamId: (() => {
401
+ const stream = (
402
+ topologyPolicy as { getAgentStream?: (a: AgentId) => string | null }
403
+ ).getAgentStream?.(agentId);
404
+ return stream ?? undefined;
405
+ })(),
406
+ workspaceManager,
407
+ getAgentByRole: (r: string) => {
408
+ for (const [aid, ws] of agentWorkspaces) {
409
+ const rec = agentStore.getAgent(aid);
410
+ if (rec?.role === r) return aid;
411
+ }
412
+ return null;
413
+ },
414
+ });
415
+ return executeWorkspaceDecision(agentId, decision, role, options);
416
+ }
417
+
418
+ // Capability-based dispatch for programmatic callers that don't use
419
+ // team YAML. This is the supported path for libraries that construct
420
+ // WorkspaceManager + GitCascadeAdapter directly and spawn agents with
421
+ // explicit `capabilities` + `streamId` arguments. It coexists with the
422
+ // V3 topology path above.
423
+ return capabilityBasedDispatch(agentId, options, workspaceManager);
424
+ }
425
+
426
+ /**
427
+ * Capability-based workspace allocation for programmatic callers.
428
+ *
429
+ * Matches on `workspace.stream` / `workspace.integrate` / `workspace.worktree`
430
+ * capabilities + corresponding streamId/streamConfig args. Delegates to the
431
+ * role-shaped WorkspaceManager methods (createWorkerWorkspace,
432
+ * createIntegratorWorkspace, createCoordinatorWorkspace).
433
+ *
434
+ * Not used by team-YAML-driven teams — those go through TopologyPolicy above.
435
+ */
436
+ async function capabilityBasedDispatch(
437
+ agentId: AgentId,
438
+ options: SpawnAgentOptions,
439
+ ws: WorkspaceManager
440
+ ): Promise<Workspace | undefined> {
257
441
  const capabilities = options.capabilities ?? [];
258
442
  const streamId = options.streamId;
259
- const streamConfig = options.streamConfig;
260
- const dataplaneTaskId = options.dataplaneTaskId;
443
+ // Merge taskRef (if set at spawn time) into streamConfig.metadata so that
444
+ // adapter.createStream x-cascade/stream.opened carries the binding to
445
+ // OpenTasks. Explicit streamConfig.metadata.task_ref wins if already set.
446
+ const streamConfig = options.streamConfig
447
+ ? options.taskRef &&
448
+ !(options.streamConfig.metadata &&
449
+ (options.streamConfig.metadata as Record<string, unknown>).task_ref)
450
+ ? {
451
+ ...options.streamConfig,
452
+ metadata: {
453
+ ...(options.streamConfig.metadata ?? {}),
454
+ task_ref: options.taskRef,
455
+ },
456
+ }
457
+ : options.streamConfig
458
+ : undefined;
459
+ const gitCascadeTaskId = options.gitCascadeTaskId;
261
460
 
262
- // Capability-based dispatch
263
461
  if (capabilities.includes("workspace.stream") && streamConfig) {
264
- const newStreamId = workspaceManager.createIntegrationStream(
265
- agentId,
266
- streamConfig
267
- );
268
- return workspaceManager.createCoordinatorWorkspace(agentId, newStreamId);
462
+ const newStreamId = ws.createIntegrationStream(agentId, streamConfig);
463
+ return ws.createCoordinatorWorkspace(agentId, newStreamId);
269
464
  }
270
465
 
271
466
  if (capabilities.includes("workspace.integrate") && streamId) {
272
- return workspaceManager.createIntegratorWorkspace(agentId, streamId);
467
+ return ws.createIntegratorWorkspace(agentId, streamId);
273
468
  }
274
469
 
275
470
  if (capabilities.includes("workspace.worktree") && streamId) {
276
- const taskId = dataplaneTaskId ?? agentId;
277
- return workspaceManager.createWorkerWorkspace(agentId, taskId, streamId);
471
+ const taskId = gitCascadeTaskId ?? agentId;
472
+ return ws.createWorkerWorkspace(agentId, taskId, streamId);
278
473
  }
279
474
 
280
- // Role-name fallback
281
- switch (role) {
282
- case "coordinator":
283
- if (streamConfig) {
284
- const sid = workspaceManager.createIntegrationStream(
285
- agentId,
286
- streamConfig
287
- );
288
- return workspaceManager.createCoordinatorWorkspace(agentId, sid);
289
- }
290
- return undefined;
291
- case "integrator":
292
- if (streamId) {
293
- return workspaceManager.createIntegratorWorkspace(agentId, streamId);
294
- }
295
- return undefined;
296
- case "worker":
297
- case "worker.resolver": {
298
- if (streamId) {
299
- const tid = dataplaneTaskId ?? agentId;
300
- return workspaceManager.createWorkerWorkspace(agentId, tid, streamId);
301
- }
302
- return undefined;
303
- }
304
- default:
305
- return undefined;
306
- }
475
+ // No matching capability — agent inherits parent cwd (no workspace)
476
+ return undefined;
307
477
  }
308
478
 
309
479
  // ── Core Lifecycle ───────────────────────────────────────────
@@ -317,10 +487,40 @@ export function createAgentManagerV2(
317
487
  }
318
488
 
319
489
  // Apply spawn interceptor (set by TeamRuntime)
320
- const options = spawnInterceptor
490
+ const interceptedOptions = spawnInterceptor
321
491
  ? await spawnInterceptor(rawOptions)
322
492
  : rawOptions;
323
493
 
494
+ // Resolve taskRef with three-level precedence:
495
+ // 1. Explicit `options.taskRef` (caller knows exactly what graph).
496
+ // 2. `resolveTaskRef(opts)` (multi-graph deployments decide per spawn).
497
+ // 3. `taskResourceId` + `options.task_id` (single-graph default).
498
+ // If none resolves, spawn proceeds with no taskRef — cascade events
499
+ // land without a task binding (hub back-fills from first commit that
500
+ // carries one, if any).
501
+ let resolvedTaskRef = interceptedOptions.taskRef;
502
+ if (!resolvedTaskRef && resolveTaskRef) {
503
+ try {
504
+ resolvedTaskRef = resolveTaskRef(interceptedOptions);
505
+ } catch (err) {
506
+ // Resolver failures must not block spawn. Log + fall through.
507
+ // eslint-disable-next-line no-console
508
+ console.warn(
509
+ "[agent-manager-v2] resolveTaskRef threw; falling back to taskResourceId default:",
510
+ err instanceof Error ? err.message : err
511
+ );
512
+ }
513
+ }
514
+ if (!resolvedTaskRef && taskResourceId && interceptedOptions.task_id) {
515
+ resolvedTaskRef = {
516
+ resource_id: taskResourceId,
517
+ node_id: String(interceptedOptions.task_id),
518
+ };
519
+ }
520
+ const options = resolvedTaskRef === interceptedOptions.taskRef
521
+ ? interceptedOptions
522
+ : { ...interceptedOptions, taskRef: resolvedTaskRef };
523
+
324
524
  const {
325
525
  task,
326
526
  task_id,
@@ -404,7 +604,9 @@ export function createAgentManagerV2(
404
604
  systemPrompt += `\n\n${interactionPatterns.join("\n\n")}`;
405
605
  }
406
606
 
407
- // Persist agent in store
607
+ // Persist agent in store. Stash taskRef in metadata so done()'s
608
+ // lifecycle context can read it without separate plumbing — this is the
609
+ // path that makes per-commit task_ref binding work end-to-end.
408
610
  const now = Date.now() as Timestamp;
409
611
  const agentRecord: AgentRecord = {
410
612
  id: agentId,
@@ -422,7 +624,7 @@ export function createAgentManagerV2(
422
624
  created_at: now,
423
625
  started_at: now,
424
626
  config: agentConfig as Record<string, unknown>,
425
- metadata: {},
627
+ metadata: options.taskRef ? { task_ref: options.taskRef } : {},
426
628
  };
427
629
  agentStore.putAgent(agentRecord);
428
630
 
@@ -456,13 +658,13 @@ export function createAgentManagerV2(
456
658
  if (workspace) {
457
659
  agentWorkspaces.set(agentId, workspace);
458
660
 
459
- // Create and claim dataplane task for workers
661
+ // Create and claim git-cascade task for workers
460
662
  if (
461
663
  workspace.role === "worker" &&
462
664
  workspace.streamId &&
463
665
  workspaceManager
464
666
  ) {
465
- const dpTaskId = options.dataplaneTaskId ?? agentId;
667
+ const dpTaskId = options.gitCascadeTaskId ?? agentId;
466
668
  workspaceManager.createTask(workspace.streamId, {
467
669
  title: task ?? `Task for ${agentId}`,
468
670
  });
@@ -588,9 +790,11 @@ export function createAgentManagerV2(
588
790
  created_at: now,
589
791
  });
590
792
 
591
- // Update agent with provider session ID
793
+ // Update agent with provider session ID. Merge with existing metadata
794
+ // so fields set at spawn time (e.g. task_ref) aren't clobbered.
795
+ const existingMeta = agentStore.getAgent(agentId)?.metadata ?? {};
592
796
  agentStore.updateAgent(agentId, {
593
- metadata: { provider_session_id: session.id },
797
+ metadata: { ...existingMeta, provider_session_id: session.id },
594
798
  });
595
799
 
596
800
  // Register agent in inbox
@@ -1134,12 +1338,21 @@ export function createAgentManagerV2(
1134
1338
  async function getOrCreateHeadManager(
1135
1339
  options: HeadManagerOptions
1136
1340
  ): Promise<SpawnedAgent> {
1137
- // Check for existing head manager
1341
+ // Check for an existing head manager matching this cwd that ALSO has a
1342
+ // live session in this process. The activeSessions check has to be inside
1343
+ // the predicate (not after .find) — the agentStore is persistent across
1344
+ // process restarts, so without this filter we'd match stale "running"
1345
+ // records from previous processes whose sessions are gone, then fall
1346
+ // through to spawn() and create a duplicate coordinator.
1138
1347
  const existing = agentStore
1139
1348
  .listAgents({ parent_id: null, state: "running" })
1140
- .find((a) => a.cwd === options.cwd);
1349
+ .find(
1350
+ (a) =>
1351
+ a.cwd === options.cwd &&
1352
+ activeSessions.has(a.id as AgentId),
1353
+ );
1141
1354
 
1142
- if (existing && activeSessions.has(existing.id as AgentId)) {
1355
+ if (existing) {
1143
1356
  const sessionEntry = activeSessions.get(existing.id as AgentId)!;
1144
1357
  const storedSession = agentStore.getSession(existing.id as AgentId);
1145
1358
  return {
@@ -1167,6 +1380,30 @@ export function createAgentManagerV2(
1167
1380
  .map(agentRecordToAgent);
1168
1381
  }
1169
1382
 
1383
+ /**
1384
+ * Look up the spawned-agent shape for any agent that's still alive in this
1385
+ * process (any role, not just coordinators). Returns null if the agent
1386
+ * doesn't exist, isn't running, or has no live session in `activeSessions`.
1387
+ *
1388
+ * Used by the ACP layer to bind a session to a specific agent when the MAP
1389
+ * stream targets one explicitly — preserving the routing intent that
1390
+ * cwd-based head-manager lookup would otherwise lose in multi-coordinator
1391
+ * scenarios.
1392
+ */
1393
+ function getActiveAgentSession(agentId: AgentId): SpawnedAgent | null {
1394
+ if (!activeSessions.has(agentId)) return null;
1395
+ const record = agentStore.getAgent(agentId);
1396
+ if (!record || record.state !== "running") return null;
1397
+ const sessionEntry = activeSessions.get(agentId)!;
1398
+ const storedSession = agentStore.getSession(agentId);
1399
+ return {
1400
+ id: agentId,
1401
+ session_id: storedSession?.session_id ?? sessionEntry.session.id ?? "",
1402
+ agent: agentRecordToAgent(record),
1403
+ session: sessionEntry.session,
1404
+ };
1405
+ }
1406
+
1170
1407
  // ── Session Interaction ──────────────────────────────────────
1171
1408
 
1172
1409
  async function* prompt(
@@ -1380,6 +1617,12 @@ export function createAgentManagerV2(
1380
1617
  sidecarRef = sidecar;
1381
1618
  }
1382
1619
 
1620
+ function setTopologyPolicyFn(
1621
+ policy: import('../workspace/topology/types.js').TopologyPolicy | null
1622
+ ): void {
1623
+ topologyPolicy = policy;
1624
+ }
1625
+
1383
1626
  function setMailServices(): void {
1384
1627
  // No-op: agent-inbox handles conversation tracking
1385
1628
  }
@@ -1422,6 +1665,7 @@ export function createAgentManagerV2(
1422
1665
  getHierarchy,
1423
1666
  getOrCreateHeadManager,
1424
1667
  listHeadManagers,
1668
+ getActiveAgentSession,
1425
1669
  prompt,
1426
1670
  promptUntilDone,
1427
1671
  getSession,
@@ -1441,6 +1685,7 @@ export function createAgentManagerV2(
1441
1685
  setIntegrationConfigs,
1442
1686
  setSkillLoadout,
1443
1687
  setSidecar,
1688
+ setTopologyPolicy: setTopologyPolicyFn,
1444
1689
  setMailServices,
1445
1690
  close,
1446
1691
  } as AgentManager;