macro-agent 0.1.7 → 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 (259) hide show
  1. package/CLAUDE.md +179 -38
  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 -71
  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 +34 -37
  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 -8
  55. package/dist/map/lifecycle-bridge.d.ts.map +1 -1
  56. package/dist/map/lifecycle-bridge.js +76 -22
  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 -4
  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 +71 -11
  197. package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
  198. package/src/agent/agent-manager-v2.ts +293 -77
  199. package/src/agent/agent-manager.ts +14 -0
  200. package/src/agent/types.ts +16 -2
  201. package/src/boot-v2.ts +87 -36
  202. package/src/cli/index.ts +61 -0
  203. package/src/cognitive/__tests__/macro-agent-backend.test.ts +47 -5
  204. package/src/cognitive/macro-agent-backend.ts +45 -29
  205. package/src/integrations/skilltree.ts +1 -0
  206. package/src/lifecycle/cleanup.ts +52 -3
  207. package/src/lifecycle/handlers-v2.ts +40 -3
  208. package/src/lifecycle/types.ts +12 -0
  209. package/src/map/__tests__/cascade-bridge.test.ts +229 -0
  210. package/src/map/__tests__/lifecycle-bridge.test.ts +165 -22
  211. package/src/map/acp-bridge.ts +26 -3
  212. package/src/map/cascade-bridge.ts +301 -0
  213. package/src/map/lifecycle-bridge.ts +77 -27
  214. package/src/map/server.ts +47 -6
  215. package/src/map/sidecar.ts +31 -3
  216. package/src/map/types.ts +20 -0
  217. package/src/mcp/tools/done-v2.ts +9 -0
  218. package/src/teams/team-manager-v2.ts +37 -0
  219. package/src/teams/team-runtime-v2.ts +23 -3
  220. package/src/workspace/__tests__/{dataplane-adapter.test.ts → git-cascade-adapter.test.ts} +209 -14
  221. package/src/workspace/__tests__/self-driving-yaml.test.ts +114 -0
  222. package/src/workspace/__tests__/shared-worktree-refcount.test.ts +154 -0
  223. package/src/workspace/__tests__/standalone-mode.test.ts +118 -0
  224. package/src/workspace/__tests__/workspace-manager-v3.test.ts +245 -0
  225. package/src/workspace/__tests__/yaml-schema.test.ts +210 -0
  226. package/src/workspace/config.ts +11 -11
  227. package/src/workspace/git-cascade-adapter.ts +1186 -0
  228. package/src/workspace/index.ts +11 -11
  229. package/src/workspace/landing/__tests__/strategies.test.ts +142 -0
  230. package/src/workspace/landing/direct-push.ts +91 -0
  231. package/src/workspace/landing/index.ts +40 -0
  232. package/src/workspace/landing/merge-to-parent.ts +228 -0
  233. package/src/workspace/landing/optimistic-push.ts +36 -0
  234. package/src/workspace/landing/queue-to-branch.ts +108 -0
  235. package/src/workspace/merge-queue/merge-queue.ts +10 -0
  236. package/src/workspace/merge-queue/types.ts +16 -2
  237. package/src/workspace/pool/__tests__/worktree-pool.integration.test.ts +5 -5
  238. package/src/workspace/pool/types.ts +1 -0
  239. package/src/workspace/pool/worktree-pool.ts +1 -0
  240. package/src/workspace/recovery/__tests__/auto-resolve-integration.test.ts +127 -0
  241. package/src/workspace/recovery/__tests__/spawn-resolver.test.ts +139 -0
  242. package/src/workspace/recovery/__tests__/strategies.test.ts +145 -0
  243. package/src/workspace/recovery/abandon.ts +51 -0
  244. package/src/workspace/recovery/auto-resolve.ts +119 -0
  245. package/src/workspace/recovery/defer.ts +23 -0
  246. package/src/workspace/recovery/escalate.ts +30 -0
  247. package/src/workspace/recovery/index.ts +58 -0
  248. package/src/workspace/recovery/spawn-resolver.ts +145 -0
  249. package/src/workspace/recovery/types.ts +54 -0
  250. package/src/workspace/topology/__tests__/yaml-driven.test.ts +345 -0
  251. package/src/workspace/topology/index.ts +18 -0
  252. package/src/workspace/topology/no-workspace.ts +39 -0
  253. package/src/workspace/topology/types.ts +116 -0
  254. package/src/workspace/topology/yaml-driven.ts +316 -0
  255. package/src/workspace/types-v3.ts +155 -0
  256. package/src/workspace/types.ts +191 -20
  257. package/src/workspace/workspace-manager.ts +474 -19
  258. package/src/workspace/yaml-schema.ts +216 -0
  259. package/src/workspace/dataplane-adapter.ts +0 -546
@@ -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
@@ -600,20 +804,6 @@ export function createAgentManagerV2(
600
804
  scope: team_instance ?? "default",
601
805
  });
602
806
 
603
- // Create task in opentasks
604
- if (tasksAdapter.connected) {
605
- try {
606
- const otTaskId = await tasksAdapter.createTask({
607
- title: task ?? `Task for ${agentId}`,
608
- assignee: agentId,
609
- tags: role ? [role] : [],
610
- });
611
- agentStore.updateAgent(agentId, { task_id: otTaskId });
612
- } catch {
613
- // Non-fatal — opentasks may not be available
614
- }
615
- }
616
-
617
807
  // Track active session
618
808
  const activeSession: ActiveSession = {
619
809
  agentId,
@@ -728,21 +918,6 @@ export function createAgentManagerV2(
728
918
  agentTokenManager.revokeToken(agentId);
729
919
  }
730
920
 
731
- // Transition task in opentasks
732
- if (record.task_id && tasksAdapter.connected) {
733
- try {
734
- const action =
735
- reason === "completed"
736
- ? "complete"
737
- : reason === "failed"
738
- ? "fail"
739
- : "block";
740
- await tasksAdapter.transitionTask(record.task_id, action as any);
741
- } catch {
742
- // Non-fatal task transition failure
743
- }
744
- }
745
-
746
921
  // Notify parent via inbox
747
922
  if (record.parent_id) {
748
923
  try {
@@ -1163,12 +1338,21 @@ export function createAgentManagerV2(
1163
1338
  async function getOrCreateHeadManager(
1164
1339
  options: HeadManagerOptions
1165
1340
  ): Promise<SpawnedAgent> {
1166
- // 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.
1167
1347
  const existing = agentStore
1168
1348
  .listAgents({ parent_id: null, state: "running" })
1169
- .find((a) => a.cwd === options.cwd);
1349
+ .find(
1350
+ (a) =>
1351
+ a.cwd === options.cwd &&
1352
+ activeSessions.has(a.id as AgentId),
1353
+ );
1170
1354
 
1171
- if (existing && activeSessions.has(existing.id as AgentId)) {
1355
+ if (existing) {
1172
1356
  const sessionEntry = activeSessions.get(existing.id as AgentId)!;
1173
1357
  const storedSession = agentStore.getSession(existing.id as AgentId);
1174
1358
  return {
@@ -1196,6 +1380,30 @@ export function createAgentManagerV2(
1196
1380
  .map(agentRecordToAgent);
1197
1381
  }
1198
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
+
1199
1407
  // ── Session Interaction ──────────────────────────────────────
1200
1408
 
1201
1409
  async function* prompt(
@@ -1409,6 +1617,12 @@ export function createAgentManagerV2(
1409
1617
  sidecarRef = sidecar;
1410
1618
  }
1411
1619
 
1620
+ function setTopologyPolicyFn(
1621
+ policy: import('../workspace/topology/types.js').TopologyPolicy | null
1622
+ ): void {
1623
+ topologyPolicy = policy;
1624
+ }
1625
+
1412
1626
  function setMailServices(): void {
1413
1627
  // No-op: agent-inbox handles conversation tracking
1414
1628
  }
@@ -1451,6 +1665,7 @@ export function createAgentManagerV2(
1451
1665
  getHierarchy,
1452
1666
  getOrCreateHeadManager,
1453
1667
  listHeadManagers,
1668
+ getActiveAgentSession,
1454
1669
  prompt,
1455
1670
  promptUntilDone,
1456
1671
  getSession,
@@ -1470,6 +1685,7 @@ export function createAgentManagerV2(
1470
1685
  setIntegrationConfigs,
1471
1686
  setSkillLoadout,
1472
1687
  setSidecar,
1688
+ setTopologyPolicy: setTopologyPolicyFn,
1473
1689
  setMailServices,
1474
1690
  close,
1475
1691
  } as AgentManager;
@@ -131,6 +131,12 @@ export interface AgentManager {
131
131
  */
132
132
  listHeadManagers(): Agent[];
133
133
 
134
+ /**
135
+ * Look up the SpawnedAgent shape for any agent (any role) that's running
136
+ * AND has a live session in this process. Returns null otherwise.
137
+ */
138
+ getActiveAgentSession(agentId: AgentId): SpawnedAgent | null;
139
+
134
140
  // ── Session Interaction ────────────────────────────────────────
135
141
 
136
142
  /**
@@ -296,6 +302,14 @@ export interface AgentManager {
296
302
  */
297
303
  setSidecar(sidecar: { connected: boolean; reportCheckpoint(cp: any): Promise<any> } | null): void;
298
304
 
305
+ /**
306
+ * Set a TopologyPolicy for workspace allocation decisions.
307
+ * When set, AgentManagerV2 delegates `createWorkspaceForRole` to the policy
308
+ * before falling back to role-name dispatch. Set by boot-v2 when team YAML
309
+ * contains `macro_agent.workspace`.
310
+ */
311
+ setTopologyPolicy(policy: import('../workspace/topology/types.js').TopologyPolicy | null): void;
312
+
299
313
  // ── Cleanup ────────────────────────────────────────────────────
300
314
 
301
315
  /**
@@ -15,6 +15,7 @@ import type {
15
15
  StreamConfig,
16
16
  Workspace,
17
17
  } from "../workspace/types.js";
18
+ import type { TaskRef } from "git-cascade/events";
18
19
 
19
20
  // ─────────────────────────────────────────────────────────────────
20
21
  // Spawn Options
@@ -90,16 +91,29 @@ export interface SpawnAgentOptions {
90
91
  streamConfig?: StreamConfig;
91
92
 
92
93
  /**
93
- * Dataplane task ID to claim (for workers).
94
+ * git-cascade task ID to claim (for workers).
94
95
  * If provided, the worker will claim this task and work on it.
95
96
  */
96
- dataplaneTaskId?: string;
97
+ gitCascadeTaskId?: string;
97
98
 
98
99
  /**
99
100
  * Resolved capabilities for this agent's role.
100
101
  * Injected by TeamRuntime spawn interceptor for capability-based workspace dispatch.
101
102
  */
102
103
  capabilities?: string[];
104
+
105
+ /**
106
+ * Optional reference to an external task this agent is working on. When
107
+ * set, the reference is woven into cascade event metadata so OpenHive (or
108
+ * any other observer) can bind the agent's commits, merges, and streams
109
+ * to the OpenTasks node. Propagates through:
110
+ * - stream creation: `adapter.createStream({ metadata: { task_ref } })`
111
+ * - commits: `adapter.commitChanges({ metadata: { task_ref } })` (pass
112
+ * per-commit; each commit can carry a distinct sub-task binding)
113
+ * Can also arrive late via pull-mode `claim_task` — workers that claim a
114
+ * task mid-session should include the ref on subsequent commits.
115
+ */
116
+ taskRef?: TaskRef;
103
117
  }
104
118
 
105
119
  /**