macro-agent 0.1.8 → 0.1.11

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 (306) hide show
  1. package/CLAUDE.md +263 -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 +192 -7
  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/adapters/tasks-adapter.d.ts.map +1 -1
  14. package/dist/adapters/tasks-adapter.js +3 -0
  15. package/dist/adapters/tasks-adapter.js.map +1 -1
  16. package/dist/adapters/types.d.ts +1 -0
  17. package/dist/adapters/types.d.ts.map +1 -1
  18. package/dist/agent/agent-manager-v2.d.ts +21 -0
  19. package/dist/agent/agent-manager-v2.d.ts.map +1 -1
  20. package/dist/agent/agent-manager-v2.js +308 -54
  21. package/dist/agent/agent-manager-v2.js.map +1 -1
  22. package/dist/agent/agent-manager.d.ts +12 -0
  23. package/dist/agent/agent-manager.d.ts.map +1 -1
  24. package/dist/agent/agent-manager.js.map +1 -1
  25. package/dist/agent/agent-store.d.ts +10 -0
  26. package/dist/agent/agent-store.d.ts.map +1 -1
  27. package/dist/agent/agent-store.js +22 -0
  28. package/dist/agent/agent-store.js.map +1 -1
  29. package/dist/agent/types.d.ts +15 -2
  30. package/dist/agent/types.d.ts.map +1 -1
  31. package/dist/agent/types.js.map +1 -1
  32. package/dist/boot-v2.d.ts +129 -1
  33. package/dist/boot-v2.d.ts.map +1 -1
  34. package/dist/boot-v2.js +359 -8
  35. package/dist/boot-v2.js.map +1 -1
  36. package/dist/cli/acp.js +4 -0
  37. package/dist/cli/acp.js.map +1 -1
  38. package/dist/cli/index.js +56 -0
  39. package/dist/cli/index.js.map +1 -1
  40. package/dist/cognitive/macro-agent-backend.d.ts.map +1 -1
  41. package/dist/cognitive/macro-agent-backend.js +40 -22
  42. package/dist/cognitive/macro-agent-backend.js.map +1 -1
  43. package/dist/integrations/skilltree.d.ts.map +1 -1
  44. package/dist/integrations/skilltree.js +1 -0
  45. package/dist/integrations/skilltree.js.map +1 -1
  46. package/dist/lifecycle/cascade.d.ts +25 -2
  47. package/dist/lifecycle/cascade.d.ts.map +1 -1
  48. package/dist/lifecycle/cascade.js +70 -2
  49. package/dist/lifecycle/cascade.js.map +1 -1
  50. package/dist/lifecycle/cleanup.d.ts +33 -2
  51. package/dist/lifecycle/cleanup.d.ts.map +1 -1
  52. package/dist/lifecycle/cleanup.js +28 -6
  53. package/dist/lifecycle/cleanup.js.map +1 -1
  54. package/dist/lifecycle/handlers-v2.d.ts +7 -0
  55. package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
  56. package/dist/lifecycle/handlers-v2.js +28 -2
  57. package/dist/lifecycle/handlers-v2.js.map +1 -1
  58. package/dist/lifecycle/types.d.ts +11 -0
  59. package/dist/lifecycle/types.d.ts.map +1 -1
  60. package/dist/lifecycle/types.js.map +1 -1
  61. package/dist/map/acp-bridge.d.ts +9 -0
  62. package/dist/map/acp-bridge.d.ts.map +1 -1
  63. package/dist/map/acp-bridge.js +15 -2
  64. package/dist/map/acp-bridge.js.map +1 -1
  65. package/dist/map/cascade-action-handler.d.ts +24 -0
  66. package/dist/map/cascade-action-handler.d.ts.map +1 -0
  67. package/dist/map/cascade-action-handler.js +170 -0
  68. package/dist/map/cascade-action-handler.js.map +1 -0
  69. package/dist/map/cascade-bridge.d.ts +44 -0
  70. package/dist/map/cascade-bridge.d.ts.map +1 -0
  71. package/dist/map/cascade-bridge.js +294 -0
  72. package/dist/map/cascade-bridge.js.map +1 -0
  73. package/dist/map/coordination-handler.d.ts.map +1 -1
  74. package/dist/map/coordination-handler.js +12 -1
  75. package/dist/map/coordination-handler.js.map +1 -1
  76. package/dist/map/lifecycle-bridge.d.ts +1 -1
  77. package/dist/map/lifecycle-bridge.d.ts.map +1 -1
  78. package/dist/map/lifecycle-bridge.js +58 -23
  79. package/dist/map/lifecycle-bridge.js.map +1 -1
  80. package/dist/map/server.d.ts.map +1 -1
  81. package/dist/map/server.js +219 -7
  82. package/dist/map/server.js.map +1 -1
  83. package/dist/map/sidecar.d.ts.map +1 -1
  84. package/dist/map/sidecar.js +49 -2
  85. package/dist/map/sidecar.js.map +1 -1
  86. package/dist/map/types.d.ts +22 -0
  87. package/dist/map/types.d.ts.map +1 -1
  88. package/dist/mcp/tools/done-v2.d.ts.map +1 -1
  89. package/dist/mcp/tools/done-v2.js +8 -0
  90. package/dist/mcp/tools/done-v2.js.map +1 -1
  91. package/dist/teams/team-manager-v2.d.ts.map +1 -1
  92. package/dist/teams/team-manager-v2.js +26 -0
  93. package/dist/teams/team-manager-v2.js.map +1 -1
  94. package/dist/teams/team-runtime-v2.d.ts.map +1 -1
  95. package/dist/teams/team-runtime-v2.js +16 -3
  96. package/dist/teams/team-runtime-v2.js.map +1 -1
  97. package/dist/workspace/config.d.ts +10 -10
  98. package/dist/workspace/config.d.ts.map +1 -1
  99. package/dist/workspace/config.js +4 -4
  100. package/dist/workspace/config.js.map +1 -1
  101. package/dist/workspace/git-cascade-adapter.d.ts +510 -0
  102. package/dist/workspace/git-cascade-adapter.d.ts.map +1 -0
  103. package/dist/workspace/git-cascade-adapter.js +934 -0
  104. package/dist/workspace/git-cascade-adapter.js.map +1 -0
  105. package/dist/workspace/index.d.ts +3 -3
  106. package/dist/workspace/index.d.ts.map +1 -1
  107. package/dist/workspace/index.js +4 -4
  108. package/dist/workspace/index.js.map +1 -1
  109. package/dist/workspace/landing/direct-push.d.ts +20 -0
  110. package/dist/workspace/landing/direct-push.d.ts.map +1 -0
  111. package/dist/workspace/landing/direct-push.js +74 -0
  112. package/dist/workspace/landing/direct-push.js.map +1 -0
  113. package/dist/workspace/landing/index.d.ts +29 -0
  114. package/dist/workspace/landing/index.d.ts.map +1 -0
  115. package/dist/workspace/landing/index.js +37 -0
  116. package/dist/workspace/landing/index.js.map +1 -0
  117. package/dist/workspace/landing/merge-to-parent.d.ts +41 -0
  118. package/dist/workspace/landing/merge-to-parent.d.ts.map +1 -0
  119. package/dist/workspace/landing/merge-to-parent.js +186 -0
  120. package/dist/workspace/landing/merge-to-parent.js.map +1 -0
  121. package/dist/workspace/landing/optimistic-push.d.ts +16 -0
  122. package/dist/workspace/landing/optimistic-push.d.ts.map +1 -0
  123. package/dist/workspace/landing/optimistic-push.js +27 -0
  124. package/dist/workspace/landing/optimistic-push.js.map +1 -0
  125. package/dist/workspace/landing/queue-to-branch.d.ts +24 -0
  126. package/dist/workspace/landing/queue-to-branch.d.ts.map +1 -0
  127. package/dist/workspace/landing/queue-to-branch.js +79 -0
  128. package/dist/workspace/landing/queue-to-branch.js.map +1 -0
  129. package/dist/workspace/merge-queue/merge-queue.d.ts +10 -0
  130. package/dist/workspace/merge-queue/merge-queue.d.ts.map +1 -1
  131. package/dist/workspace/merge-queue/merge-queue.js +10 -0
  132. package/dist/workspace/merge-queue/merge-queue.js.map +1 -1
  133. package/dist/workspace/merge-queue/types.d.ts +16 -2
  134. package/dist/workspace/merge-queue/types.d.ts.map +1 -1
  135. package/dist/workspace/merge-queue/types.js +9 -0
  136. package/dist/workspace/merge-queue/types.js.map +1 -1
  137. package/dist/workspace/pool/types.d.ts +1 -0
  138. package/dist/workspace/pool/types.d.ts.map +1 -1
  139. package/dist/workspace/pool/worktree-pool.d.ts.map +1 -1
  140. package/dist/workspace/pool/worktree-pool.js +1 -0
  141. package/dist/workspace/pool/worktree-pool.js.map +1 -1
  142. package/dist/workspace/recovery/abandon.d.ts +15 -0
  143. package/dist/workspace/recovery/abandon.d.ts.map +1 -0
  144. package/dist/workspace/recovery/abandon.js +45 -0
  145. package/dist/workspace/recovery/abandon.js.map +1 -0
  146. package/dist/workspace/recovery/auto-resolve.d.ts +27 -0
  147. package/dist/workspace/recovery/auto-resolve.d.ts.map +1 -0
  148. package/dist/workspace/recovery/auto-resolve.js +99 -0
  149. package/dist/workspace/recovery/auto-resolve.js.map +1 -0
  150. package/dist/workspace/recovery/defer.d.ts +15 -0
  151. package/dist/workspace/recovery/defer.d.ts.map +1 -0
  152. package/dist/workspace/recovery/defer.js +16 -0
  153. package/dist/workspace/recovery/defer.js.map +1 -0
  154. package/dist/workspace/recovery/escalate.d.ts +16 -0
  155. package/dist/workspace/recovery/escalate.d.ts.map +1 -0
  156. package/dist/workspace/recovery/escalate.js +24 -0
  157. package/dist/workspace/recovery/escalate.js.map +1 -0
  158. package/dist/workspace/recovery/index.d.ts +32 -0
  159. package/dist/workspace/recovery/index.d.ts.map +1 -0
  160. package/dist/workspace/recovery/index.js +45 -0
  161. package/dist/workspace/recovery/index.js.map +1 -0
  162. package/dist/workspace/recovery/spawn-resolver.d.ts +45 -0
  163. package/dist/workspace/recovery/spawn-resolver.d.ts.map +1 -0
  164. package/dist/workspace/recovery/spawn-resolver.js +118 -0
  165. package/dist/workspace/recovery/spawn-resolver.js.map +1 -0
  166. package/dist/workspace/recovery/types.d.ts +63 -0
  167. package/dist/workspace/recovery/types.d.ts.map +1 -0
  168. package/dist/workspace/recovery/types.js +12 -0
  169. package/dist/workspace/recovery/types.js.map +1 -0
  170. package/dist/workspace/topology/index.d.ts +9 -0
  171. package/dist/workspace/topology/index.d.ts.map +1 -0
  172. package/dist/workspace/topology/index.js +8 -0
  173. package/dist/workspace/topology/index.js.map +1 -0
  174. package/dist/workspace/topology/no-workspace.d.ts +18 -0
  175. package/dist/workspace/topology/no-workspace.d.ts.map +1 -0
  176. package/dist/workspace/topology/no-workspace.js +25 -0
  177. package/dist/workspace/topology/no-workspace.js.map +1 -0
  178. package/dist/workspace/topology/types.d.ts +97 -0
  179. package/dist/workspace/topology/types.d.ts.map +1 -0
  180. package/dist/workspace/topology/types.js +20 -0
  181. package/dist/workspace/topology/types.js.map +1 -0
  182. package/dist/workspace/topology/yaml-driven.d.ts +69 -0
  183. package/dist/workspace/topology/yaml-driven.d.ts.map +1 -0
  184. package/dist/workspace/topology/yaml-driven.js +273 -0
  185. package/dist/workspace/topology/yaml-driven.js.map +1 -0
  186. package/dist/workspace/types-v3.d.ts +117 -0
  187. package/dist/workspace/types-v3.d.ts.map +1 -0
  188. package/dist/workspace/types-v3.js +20 -0
  189. package/dist/workspace/types-v3.js.map +1 -0
  190. package/dist/workspace/types.d.ts +162 -17
  191. package/dist/workspace/types.d.ts.map +1 -1
  192. package/dist/workspace/workspace-manager.d.ts +101 -13
  193. package/dist/workspace/workspace-manager.d.ts.map +1 -1
  194. package/dist/workspace/workspace-manager.js +416 -13
  195. package/dist/workspace/workspace-manager.js.map +1 -1
  196. package/dist/workspace/yaml-schema.d.ts +254 -0
  197. package/dist/workspace/yaml-schema.d.ts.map +1 -0
  198. package/dist/workspace/yaml-schema.js +170 -0
  199. package/dist/workspace/yaml-schema.js.map +1 -0
  200. package/docs/conflict-recovery.md +472 -0
  201. package/docs/design/task-dispatcher.md +880 -0
  202. package/docs/git-cascade-integration-gaps.md +678 -0
  203. package/docs/workspace-interfaces.md +731 -0
  204. package/docs/workspace-redesign-plan.md +302 -0
  205. package/package.json +6 -5
  206. package/src/__tests__/boot-v2.test.ts +435 -0
  207. package/src/__tests__/e2e/acp-over-map.e2e.test.ts +92 -0
  208. package/src/__tests__/e2e/auto-sync.e2e.test.ts +257 -0
  209. package/src/__tests__/e2e/bootstrap.e2e.test.ts +319 -0
  210. package/src/__tests__/e2e/cascade-rebase.e2e.test.ts +254 -0
  211. package/src/__tests__/e2e/cli-run.e2e.test.ts +167 -0
  212. package/src/__tests__/e2e/dispatch-coordination.e2e.test.ts +495 -0
  213. package/src/__tests__/e2e/dispatch-live.e2e.test.ts +564 -0
  214. package/src/__tests__/e2e/dispatch-opentasks.e2e.test.ts +496 -0
  215. package/src/__tests__/e2e/dispatch-phase2-live.e2e.test.ts +456 -0
  216. package/src/__tests__/e2e/dispatch-phase2.e2e.test.ts +386 -0
  217. package/src/__tests__/e2e/dispatch.e2e.test.ts +376 -0
  218. package/src/__tests__/e2e/self-driving-v3.e2e.test.ts +197 -0
  219. package/src/__tests__/e2e/spawn-resolver.e2e.test.ts +200 -0
  220. package/src/__tests__/e2e/workspace-lifecycle.e2e.test.ts +30 -22
  221. package/src/__tests__/e2e/workspace-v3.e2e.test.ts +413 -0
  222. package/src/acp/__tests__/claude-code-replay.test.ts +225 -0
  223. package/src/acp/__tests__/macro-agent.test.ts +39 -1
  224. package/src/acp/claude-code-replay.ts +208 -0
  225. package/src/acp/macro-agent.ts +203 -10
  226. package/src/acp/types.ts +10 -0
  227. package/src/adapters/__tests__/tasks-adapter.test.ts +1 -0
  228. package/src/adapters/tasks-adapter.ts +3 -0
  229. package/src/adapters/types.ts +1 -0
  230. package/src/agent/__tests__/agent-manager-topology.test.ts +73 -0
  231. package/src/agent/__tests__/agent-manager-v2.test.ts +66 -0
  232. package/src/agent/__tests__/agent-store.test.ts +52 -0
  233. package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
  234. package/src/agent/agent-manager-v2.ts +372 -59
  235. package/src/agent/agent-manager.ts +14 -0
  236. package/src/agent/agent-store.ts +24 -0
  237. package/src/agent/types.ts +16 -2
  238. package/src/boot-v2.ts +589 -35
  239. package/src/cli/acp.ts +4 -0
  240. package/src/cli/index.ts +61 -0
  241. package/src/cognitive/macro-agent-backend.ts +45 -29
  242. package/src/integrations/skilltree.ts +1 -0
  243. package/src/lifecycle/__tests__/cascade-consolidation.test.ts +240 -0
  244. package/src/lifecycle/cascade.ts +77 -2
  245. package/src/lifecycle/cleanup.ts +52 -3
  246. package/src/lifecycle/handlers-v2.ts +40 -3
  247. package/src/lifecycle/types.ts +12 -0
  248. package/src/map/__tests__/cascade-bridge.test.ts +229 -0
  249. package/src/map/__tests__/emit-event.test.ts +71 -0
  250. package/src/map/__tests__/lifecycle-bridge.test.ts +86 -10
  251. package/src/map/acp-bridge.ts +26 -3
  252. package/src/map/cascade-action-handler.ts +205 -0
  253. package/src/map/cascade-bridge.ts +339 -0
  254. package/src/map/coordination-handler.ts +13 -1
  255. package/src/map/lifecycle-bridge.ts +52 -17
  256. package/src/map/server.ts +225 -7
  257. package/src/map/sidecar.ts +48 -1
  258. package/src/map/types.ts +23 -0
  259. package/src/mcp/tools/done-v2.ts +9 -0
  260. package/src/teams/team-manager-v2.ts +37 -0
  261. package/src/teams/team-runtime-v2.ts +23 -3
  262. package/src/workspace/__tests__/{dataplane-adapter.test.ts → git-cascade-adapter.test.ts} +209 -14
  263. package/src/workspace/__tests__/land-dispatch.test.ts +214 -0
  264. package/src/workspace/__tests__/self-driving-yaml.test.ts +114 -0
  265. package/src/workspace/__tests__/shared-worktree-refcount.test.ts +154 -0
  266. package/src/workspace/__tests__/standalone-mode.test.ts +118 -0
  267. package/src/workspace/__tests__/workspace-manager-v3.test.ts +245 -0
  268. package/src/workspace/__tests__/yaml-schema.test.ts +210 -0
  269. package/src/workspace/config.ts +11 -11
  270. package/src/workspace/git-cascade-adapter.ts +1213 -0
  271. package/src/workspace/index.ts +11 -11
  272. package/src/workspace/landing/__tests__/strategies.test.ts +184 -0
  273. package/src/workspace/landing/direct-push.ts +91 -0
  274. package/src/workspace/landing/index.ts +40 -0
  275. package/src/workspace/landing/merge-to-parent.ts +229 -0
  276. package/src/workspace/landing/optimistic-push.ts +36 -0
  277. package/src/workspace/landing/queue-to-branch.ts +108 -0
  278. package/src/workspace/merge-queue/merge-queue.ts +10 -0
  279. package/src/workspace/merge-queue/types.ts +16 -2
  280. package/src/workspace/pool/__tests__/worktree-pool.integration.test.ts +5 -5
  281. package/src/workspace/pool/types.ts +1 -0
  282. package/src/workspace/pool/worktree-pool.ts +1 -0
  283. package/src/workspace/recovery/__tests__/auto-resolve-integration.test.ts +127 -0
  284. package/src/workspace/recovery/__tests__/spawn-resolver.test.ts +139 -0
  285. package/src/workspace/recovery/__tests__/strategies.test.ts +145 -0
  286. package/src/workspace/recovery/abandon.ts +51 -0
  287. package/src/workspace/recovery/auto-resolve.ts +119 -0
  288. package/src/workspace/recovery/defer.ts +23 -0
  289. package/src/workspace/recovery/escalate.ts +30 -0
  290. package/src/workspace/recovery/index.ts +58 -0
  291. package/src/workspace/recovery/spawn-resolver.ts +152 -0
  292. package/src/workspace/recovery/types.ts +54 -0
  293. package/src/workspace/topology/__tests__/yaml-driven.test.ts +345 -0
  294. package/src/workspace/topology/index.ts +18 -0
  295. package/src/workspace/topology/no-workspace.ts +39 -0
  296. package/src/workspace/topology/types.ts +116 -0
  297. package/src/workspace/topology/yaml-driven.ts +316 -0
  298. package/src/workspace/types-v3.ts +162 -0
  299. package/src/workspace/types.ts +211 -20
  300. package/src/workspace/workspace-manager.ts +533 -19
  301. package/src/workspace/yaml-schema.ts +216 -0
  302. package/dist/workspace/dataplane-adapter.d.ts +0 -260
  303. package/dist/workspace/dataplane-adapter.d.ts.map +0 -1
  304. package/dist/workspace/dataplane-adapter.js +0 -416
  305. package/dist/workspace/dataplane-adapter.js.map +0 -1
  306. package/src/workspace/dataplane-adapter.ts +0 -546
package/src/map/server.ts CHANGED
@@ -74,6 +74,14 @@ export function createMAPServerInstance(
74
74
  const clientWebSockets = new Map<string, WebSocket>(); // participant/agent ID → WebSocket
75
75
  // Track subscription IDs by client agent ID for ACP response delivery
76
76
  const clientSubscriptions = new Map<string, string[]>(); // agent ID → subscription IDs
77
+ /**
78
+ * Per-subscription monotonic event counter. The MAP SDK's Subscription
79
+ * checks `sequenceNumber !== lastSequenceNumber + 1` and warns on gaps —
80
+ * using `Date.now()` (millisecond timestamp) breaks that assumption since
81
+ * each event becomes a "gap". Track a per-subscription counter starting
82
+ * at 1 and increment per event.
83
+ */
84
+ const subscriptionSequence = new Map<string, number>(); // subscription ID → next sequence number
77
85
  // Track original ws.send for each WebSocket (before interception)
78
86
  const originalSends = new Map<WebSocket, Function>();
79
87
 
@@ -90,11 +98,21 @@ export function createMAPServerInstance(
90
98
 
91
99
  // ── Agent extensions ──────────────────────────────────────────
92
100
  handlers["_macro/spawnAgent"] = async (params, ctx) => {
101
+ // Forward the full SpawnAgentOptions surface so callers can set
102
+ // permission mode, agent type, custom prompt, model config, etc.
103
+ // Previously this handler dropped everything except role/cwd/task,
104
+ // making _macro/spawnAgent useless for any non-default agent.
93
105
  const spawned = await agentManager.spawn({
94
106
  task: params.task ?? "Spawned via MAP",
95
107
  parent: params.parent ?? null,
96
108
  cwd: params.cwd,
97
109
  role: params.role ?? "worker",
110
+ permissionMode: params.permissionMode,
111
+ agentType: params.agentType,
112
+ customPrompt: params.customPrompt,
113
+ topics: params.topics,
114
+ config: params.config,
115
+ taskRef: params.taskRef,
98
116
  });
99
117
 
100
118
  // Ensure agent is registered in MAPServer's registry.
@@ -107,7 +125,7 @@ export function createMAPServerInstance(
107
125
  role: params.role ?? "worker",
108
126
  state: "idle",
109
127
  sessionId: ctx?.session?.id,
110
- metadata: { localAgentId: spawned.id, task: params.task },
128
+ metadata: { peerAgentId: spawned.id, task: params.task },
111
129
  });
112
130
  if (registered?.id) {
113
131
  mapIdToLocalId.set(registered.id, spawned.id);
@@ -145,6 +163,160 @@ export function createMAPServerInstance(
145
163
  return { agent: { id: spawned.id } };
146
164
  };
147
165
 
166
+ /**
167
+ * Resume an agent session with full routing + session info returned.
168
+ *
169
+ * Session-first resolution: given a Claude Code `providerSessionId` (the
170
+ * session UUID persisted on the session record), reverse-look-up the
171
+ * owning agent. Falls back to `agentId` (either the MAP ULID or the
172
+ * local store id) when no providerSessionId is given or the reverse
173
+ * lookup misses.
174
+ *
175
+ * Behavior:
176
+ * 1. Resolve the local agent id.
177
+ * 2. Call `agentManager.resume(localId)` — idempotent; the manager
178
+ * re-spawns the coordinator/head-manager if its process isn't live,
179
+ * otherwise returns the existing handle.
180
+ * 3. Ensure the agent is registered in the MAPServer's registry so
181
+ * ACP streams can target it via the returned peerMapId.
182
+ * 4. Return `{ agent: { id: peerMapId, localId, name }, acpSessionId,
183
+ * providerSessionId }` — the caller needs peerMapId to open the
184
+ * ACP stream and providerSessionId to pass into `session/load` so
185
+ * Claude Code replays its on-disk transcript.
186
+ *
187
+ * Used by OpenHive's POST /sessions/:id/resume to revive a session whose
188
+ * swarm has been offline for longer than the hub's stale-grace window.
189
+ */
190
+ handlers["_macro/resumeAgent"] = async (params, ctx) => {
191
+ const providerSessionIdParam = params.providerSessionId as string | undefined;
192
+ const agentIdParam = params.agentId as string | undefined;
193
+
194
+ let localId: string | undefined;
195
+ let providerSessionId: string | undefined;
196
+
197
+ if (providerSessionIdParam) {
198
+ const session = agentStore.findSessionByProviderSessionId(providerSessionIdParam);
199
+ if (session) {
200
+ localId = session.agent_id;
201
+ providerSessionId = session.provider_session_id;
202
+ }
203
+ }
204
+
205
+ if (!localId && agentIdParam) {
206
+ localId = mapIdToLocalId.get(agentIdParam) ?? agentIdParam;
207
+ const session = agentStore.getSession(localId);
208
+ providerSessionId = session?.provider_session_id;
209
+ }
210
+
211
+ if (!localId) {
212
+ return {
213
+ success: false,
214
+ error: "providerSessionId or agentId required",
215
+ };
216
+ }
217
+
218
+ const agentRec = agentStore.getAgent(localId);
219
+ if (!agentRec) {
220
+ return { success: false, error: `Agent not found: ${localId}` };
221
+ }
222
+
223
+ // Already-running case: skip resume() (which rejects with ALREADY_RUNNING)
224
+ // and return the live agent's session info straight from the store.
225
+ // This makes the call idempotent — callers don't need to pre-check.
226
+ let resumedId: string;
227
+ let resumedSessionId: string;
228
+ let resumedName: string | undefined;
229
+ if (agentManager.hasActiveSession(localId as any)) {
230
+ resumedId = localId;
231
+ resumedName = agentRec.name;
232
+ const liveSession = agentStore.getSession(localId);
233
+ if (!liveSession) {
234
+ return {
235
+ success: false,
236
+ error: `Agent ${localId} is active but has no session record`,
237
+ };
238
+ }
239
+ resumedSessionId = liveSession.session_id;
240
+ } else {
241
+ try {
242
+ const resumed = await agentManager.resume(localId as any);
243
+ resumedId = resumed.id;
244
+ resumedSessionId = resumed.session_id;
245
+ resumedName = (resumed as any).name;
246
+ } catch (err) {
247
+ return { success: false, error: (err as Error).message };
248
+ }
249
+ }
250
+
251
+ // Ensure agent is registered in MAPServer's registry. resume() fires
252
+ // the spawned lifecycle event, which the lifecycle bridge handles —
253
+ // but we also register here for subscription routing context on the
254
+ // current MAP session (mirrors _macro/spawnAgent).
255
+ if (mapServer && !localIdToMapId.has(resumedId)) {
256
+ try {
257
+ const registered = mapServer.agents.register({
258
+ name: resumedName ?? resumedId,
259
+ role: agentRec.role,
260
+ state: "idle",
261
+ sessionId: ctx?.session?.id,
262
+ metadata: { peerAgentId: resumedId },
263
+ });
264
+ if (registered?.id) {
265
+ mapIdToLocalId.set(registered.id, resumedId);
266
+ localIdToMapId.set(resumedId, registered.id);
267
+ }
268
+ } catch {
269
+ // Best effort; lifecycle bridge will register on spawned event
270
+ }
271
+ }
272
+
273
+ const peerMapId = localIdToMapId.get(resumedId) ?? resumedId;
274
+
275
+ return {
276
+ success: true,
277
+ agent: {
278
+ id: peerMapId,
279
+ localId: resumedId,
280
+ name: resumedName,
281
+ role: agentRec.role,
282
+ },
283
+ acpSessionId: resumedSessionId,
284
+ providerSessionId,
285
+ };
286
+ };
287
+
288
+ /**
289
+ * Terminate a running agent. Accepts either the agent's local ID or the
290
+ * MAP-assigned ULID (we resolve back to local via mapIdToLocalId).
291
+ * Reason defaults to "stopped"; use "cancelled" for user-initiated stops.
292
+ */
293
+ handlers["_macro/terminateAgent"] = async (params) => {
294
+ const agentIdParam = params.agentId as string | undefined;
295
+ const reason = (params.reason as string) ?? "cancelled";
296
+ if (!agentIdParam) {
297
+ return { success: false, error: "agentId is required" };
298
+ }
299
+ // Resolve either a MAP ULID or a local agent ID to our internal ID.
300
+ const localId = mapIdToLocalId.get(agentIdParam) ?? agentIdParam;
301
+ try {
302
+ await agentManager.terminate(localId as any, reason as any);
303
+ return { success: true };
304
+ } catch (err) {
305
+ return { success: false, error: (err as Error).message };
306
+ }
307
+ };
308
+
309
+ /**
310
+ * Inspect ACP stream → peer agent bindings on this MAP server.
311
+ * Each stream carries the peer agent id (macro-agent's internal store id)
312
+ * it was opened against, set by the bridge from MAP routing. Useful for
313
+ * routing tests and debugging multi-coordinator scenarios.
314
+ */
315
+ handlers["_macro/getAcpStreamBindings"] = async () => {
316
+ if (!acpBridge) return { bindings: [] };
317
+ return { bindings: acpBridge.getStreamBindings() };
318
+ };
319
+
148
320
  // ── Task extensions ───────────────────────────────────────────
149
321
  handlers["_macro/task/list"] = async () => {
150
322
  if (!tasksAdapter.connected) return { tasks: [] };
@@ -256,15 +428,21 @@ export function createMAPServerInstance(
256
428
 
257
429
  // Send as subscription event notification (what ACPStreamConnection expects).
258
430
  // The _pushEvent method expects: { subscriptionId, sequenceNumber, eventId, timestamp, event }
431
+ //
432
+ // sequenceNumber must be a per-subscription monotonic counter that
433
+ // increments by exactly 1 — the SDK warns on any gap. Don't use
434
+ // Date.now() here (breaks the contract on every event).
259
435
  for (const subId of subIds) {
260
436
  const event = rawEvent.params?.event ?? rawEvent;
261
437
  const eventId = event.id ?? `acp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
438
+ const nextSeq = (subscriptionSequence.get(subId) ?? 0) + 1;
439
+ subscriptionSequence.set(subId, nextSeq);
262
440
  const notification = JSON.stringify({
263
441
  jsonrpc: "2.0",
264
442
  method: "map/event",
265
443
  params: {
266
444
  subscriptionId: subId,
267
- sequenceNumber: Date.now(),
445
+ sequenceNumber: nextSeq,
268
446
  eventId,
269
447
  timestamp: Date.now(),
270
448
  event,
@@ -381,6 +559,32 @@ export function createMAPServerInstance(
381
559
  return originalSend(data, ...args);
382
560
  } as any;
383
561
 
562
+ // Observe incoming messages so we drop subscription IDs from our
563
+ // routing array when the client unsubscribes. Without this, closed
564
+ // ACP streams keep receiving events ("MAP: Event for unknown
565
+ // subscription" warnings on the client). We don't intercept the
566
+ // SDK's processing — this listener runs alongside it.
567
+ ws.on("message", (data: any) => {
568
+ try {
569
+ const text = typeof data === "string"
570
+ ? data
571
+ : Buffer.isBuffer(data)
572
+ ? data.toString("utf-8")
573
+ : String(data);
574
+ const msg = JSON.parse(text);
575
+ if (msg?.method === "map/unsubscribe") {
576
+ const subId = msg?.params?.subscriptionId;
577
+ if (typeof subId === "string") {
578
+ const idx = subscriptionIds.indexOf(subId);
579
+ if (idx >= 0) subscriptionIds.splice(idx, 1);
580
+ subscriptionSequence.delete(subId);
581
+ }
582
+ }
583
+ } catch {
584
+ // Non-JSON or parse failure — ignore
585
+ }
586
+ });
587
+
384
588
  const stream = websocketStream(ws as unknown as globalThis.WebSocket);
385
589
  const router = mapServer.accept(stream, {
386
590
  role: "client",
@@ -390,6 +594,11 @@ export function createMAPServerInstance(
390
594
 
391
595
  ws.on("close", () => {
392
596
  connectionCount--;
597
+ // Clear sequence counters for any subscriptions belonging to this
598
+ // connection. Use a copy of subscriptionIds since we don't mutate it.
599
+ for (const subId of subscriptionIds) {
600
+ subscriptionSequence.delete(subId);
601
+ }
393
602
  if (clientAgentId) {
394
603
  clientWebSockets.delete(clientAgentId);
395
604
  clientSubscriptions.delete(clientAgentId);
@@ -404,16 +613,21 @@ export function createMAPServerInstance(
404
613
  try {
405
614
  if (event.type === "spawned" || event.type === "started") {
406
615
  const agent = event.agent;
407
- // Register agent in MAPServer's registry so it's visible to clients.
408
- // We wrap in try/catch because the registry's event bus may throw
409
- // if subscription filters encounter unexpected state.
616
+ // Register agent ONCE. spawn() fires "spawned" immediately followed
617
+ // by "started", so without this guard the listener re-registers
618
+ // on the second event generating a fresh MAP ULID and overwriting
619
+ // localIdToMapId. Consumers racing against that overwrite (like the
620
+ // sidecar's lifecycle bridge, which snapshots peerMapId into hub
621
+ // metadata) end up disagreeing with _macro/spawnAgent's return
622
+ // value on which ULID refers to this agent.
623
+ if (localIdToMapId.has(agent.id)) return;
410
624
  try {
411
625
  const registered = mapServer.agents.register({
412
626
  name: agent.name ?? agent.id,
413
627
  role: agent.role ?? "worker",
414
628
  state: "idle",
415
629
  metadata: {
416
- localAgentId: agent.id, // Store local ID in metadata
630
+ peerAgentId: agent.id, // macro-agent's internal store id
417
631
  parent: (agent as any).parent ?? null,
418
632
  task: (agent as any).task ?? null,
419
633
  cwd: (agent as any).cwd ?? null,
@@ -454,7 +668,7 @@ export function createMAPServerInstance(
454
668
  role: agent.role ?? "worker",
455
669
  state: agent.state === "running" ? "busy" : "idle",
456
670
  metadata: {
457
- localAgentId: agent.id,
671
+ peerAgentId: agent.id,
458
672
  parent: agent.parent ?? null,
459
673
  task: agent.task ?? null,
460
674
  },
@@ -535,5 +749,9 @@ export function createMAPServerInstance(
535
749
  getConnectionCount(): number {
536
750
  return connectionCount;
537
751
  },
752
+
753
+ getLocalMapId(localAgentId: string): string | undefined {
754
+ return localIdToMapId.get(localAgentId);
755
+ },
538
756
  };
539
757
  }
@@ -35,7 +35,7 @@ export function createMAPSidecar(
35
35
  deps: MAPSidecarDeps,
36
36
  config: MAPSidecarConfig,
37
37
  ): MAPSidecar {
38
- const { agentManager, agentStore, inboxAdapter, tasksAdapter } = deps;
38
+ const { agentManager, agentStore, inboxAdapter, tasksAdapter, getLocalMapId, gitCascadeAdapter } = deps;
39
39
  const scope = config.scope ?? "swarm:macro-agent";
40
40
  const agentName = config.agentName ?? "macro-agent-sidecar";
41
41
 
@@ -50,6 +50,7 @@ export function createMAPSidecar(
50
50
  let trajectoryReporter: TrajectoryReporter | null = null;
51
51
  let taskBridge: TaskBridge | null = null;
52
52
  let coordinationCleanup: (() => void) | null = null;
53
+ let cascadeBridgeCleanup: (() => void) | null = null;
53
54
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
54
55
 
55
56
  /**
@@ -82,6 +83,10 @@ export function createMAPSidecar(
82
83
  coordinationCleanup();
83
84
  coordinationCleanup = null;
84
85
  }
86
+ if (cascadeBridgeCleanup) {
87
+ try { cascadeBridgeCleanup(); } catch { /* non-critical */ }
88
+ cascadeBridgeCleanup = null;
89
+ }
85
90
  if (trajectoryReporter) {
86
91
  trajectoryReporter.stop();
87
92
  trajectoryReporter = null;
@@ -126,6 +131,10 @@ export function createMAPSidecar(
126
131
  metadata: {
127
132
  systemId: config.systemId ?? "macro-agent",
128
133
  type: "macro-agent-sidecar",
134
+ // Signals that this swarm can spawn ACP-capable coordinators on demand,
135
+ // even before any coordinator has registered. The hub's /sessions/create-acp
136
+ // endpoint handles the spawn via _macro/spawnAgent when no ACP agent exists.
137
+ canHostAcp: true,
129
138
  },
130
139
  reconnection: {
131
140
  enabled: config.reconnection?.enabled ?? true,
@@ -175,6 +184,19 @@ export function createMAPSidecar(
175
184
  isConnected = true;
176
185
  } // end if (!isConnected)
177
186
 
187
+ // Publish sidecar metadata to the hub. The MAP SDK's connect()/register()
188
+ // does not propagate the `metadata` field from connect options — it only
189
+ // forwards name/role/capabilities/scopes. Call updateMetadata explicitly
190
+ // so the hub sees canHostAcp (and any other metadata the UI relies on).
191
+ try {
192
+ const metadata = (connectOpts.metadata as Record<string, unknown>) ?? {};
193
+ if (typeof connection.updateMetadata === "function") {
194
+ await connection.updateMetadata(metadata);
195
+ }
196
+ } catch {
197
+ // Non-fatal — metadata is advisory
198
+ }
199
+
178
200
  // Monitor connection state
179
201
  connection.onStateChange(
180
202
  (newState: string, _oldState: string) => {
@@ -252,6 +274,7 @@ export function createMAPSidecar(
252
274
  agentStore,
253
275
  scope,
254
276
  taskBridge,
277
+ getLocalMapId,
255
278
  );
256
279
  lifecycleCallback = bridge.callback;
257
280
  lifecycleCleanup = bridge.cleanup;
@@ -270,6 +293,21 @@ export function createMAPSidecar(
270
293
  tasksAdapter,
271
294
  trajectoryReporter,
272
295
  });
296
+
297
+ // 5. Cascade Bridge + Action Handler (optional — only when a GitCascadeAdapter is available)
298
+ if (gitCascadeAdapter) {
299
+ const { createCascadeBridge } = await import("./cascade-bridge.js");
300
+ const cascadeBridge = createCascadeBridge(connection, gitCascadeAdapter);
301
+
302
+ // 5b. Inbound action handler — receives x-cascade/request.* from hub
303
+ const { setupCascadeActionHandlers } = await import("./cascade-action-handler.js");
304
+ const actionCleanup = setupCascadeActionHandlers(connection, gitCascadeAdapter);
305
+
306
+ cascadeBridgeCleanup = () => {
307
+ cascadeBridge.dispose();
308
+ actionCleanup();
309
+ };
310
+ }
273
311
  }
274
312
 
275
313
  return {
@@ -316,5 +354,14 @@ export function createMAPSidecar(
316
354
  if (!trajectoryReporter) return null;
317
355
  return trajectoryReporter.reportCheckpoint(checkpoint);
318
356
  },
357
+
358
+ async emitEvent(event: Record<string, unknown>): Promise<void> {
359
+ if (!connection || !isConnected) return;
360
+ try {
361
+ await connection.send({ scope }, { ...event, _origin: "macro-agent" });
362
+ } catch {
363
+ // Best effort — MAP hub may be temporarily unavailable
364
+ }
365
+ },
319
366
  };
320
367
  }
package/src/map/types.ts CHANGED
@@ -68,6 +68,21 @@ export interface MAPSidecarDeps {
68
68
  agentStore: AgentStore;
69
69
  inboxAdapter: InboxAdapter;
70
70
  tasksAdapter: TasksAdapter;
71
+ /**
72
+ * Optional lookup for the local MAP server's ULID for a given local agent ID.
73
+ * When provided, the lifecycle bridge includes this ID in hub registration
74
+ * metadata so clients (e.g., SwarmCraft) can target the agent correctly on
75
+ * the macro-agent's own MAP server.
76
+ */
77
+ getLocalMapId?: (localAgentId: string) => string | undefined;
78
+ /**
79
+ * Optional GitCascadeAdapter. When provided, the sidecar wires a cascade
80
+ * bridge that forwards the adapter's event stream to the hub as
81
+ * `x-cascade/*` MAP notifications. Leave undefined to disable cascade
82
+ * event forwarding (macro-agent will still use cascade internally, just
83
+ * without hub observability).
84
+ */
85
+ gitCascadeAdapter?: import("../workspace/git-cascade-adapter.js").GitCascadeAdapter;
71
86
  }
72
87
 
73
88
  // =============================================================================
@@ -88,6 +103,9 @@ export interface MAPSidecar {
88
103
  reportCheckpoint(
89
104
  checkpoint: TrajectoryCheckpointPayload,
90
105
  ): Promise<TrajectoryCheckpointResult | null>;
106
+
107
+ /** Emit a custom event to the MAP hub scope (best-effort, no-op if disconnected) */
108
+ emitEvent?(event: Record<string, unknown>): Promise<void>;
91
109
  }
92
110
 
93
111
  // =============================================================================
@@ -244,4 +262,9 @@ export interface MAPServerInstance {
244
262
  getUrl(): string;
245
263
  /** Get number of active connections */
246
264
  getConnectionCount(): number;
265
+ /**
266
+ * Resolve a local agent ID (macro-agent internal) to its MAP server-assigned ULID.
267
+ * Returns undefined if the agent is not registered on the MAP server yet.
268
+ */
269
+ getLocalMapId(localAgentId: string): string | undefined;
247
270
  }
@@ -106,6 +106,15 @@ function buildLifecycleContext(
106
106
  streamId: record?.workspace_stream_id ?? process.env.MACRO_STREAM_ID,
107
107
  };
108
108
 
109
+ // Pull task_ref out of agent metadata if it was stashed there at spawn
110
+ // time. Validates the shape — bad data is silently dropped rather than
111
+ // pushed downstream into cascade payloads.
112
+ const meta = record?.metadata as Record<string, unknown> | undefined;
113
+ const tr = meta?.task_ref as { resource_id?: unknown; node_id?: unknown } | undefined;
114
+ if (tr && typeof tr.resource_id === "string" && typeof tr.node_id === "string") {
115
+ ctx.taskRef = { resource_id: tr.resource_id, node_id: tr.node_id };
116
+ }
117
+
109
118
  // Resolve capabilities for dispatch
110
119
  if (roleRegistry) {
111
120
  try {
@@ -142,6 +142,43 @@ export class TeamManagerV2 {
142
142
  basePath ?? process.cwd()
143
143
  );
144
144
 
145
+ // V3: auto-wire TopologyPolicy when the team declares
146
+ // `macro_agent.workspace`. Requires a WorkspaceManager to be present.
147
+ if (workspaceManager) {
148
+ try {
149
+ const { extractWorkspaceConfig } = await import(
150
+ "../workspace/yaml-schema.js"
151
+ );
152
+ const workspaceConfig = extractWorkspaceConfig(
153
+ manifest as unknown as { macro_agent?: Record<string, unknown> }
154
+ );
155
+ if (workspaceConfig) {
156
+ const { YamlDrivenTopology } = await import(
157
+ "../workspace/topology/yaml-driven.js"
158
+ );
159
+ const policy = new YamlDrivenTopology(workspaceConfig);
160
+ agentManager.setTopologyPolicy(policy);
161
+
162
+ // Kick the topology's onTeamStart so team-root streams get
163
+ // created before any agents spawn.
164
+ await policy.onTeamStart({
165
+ teamName: name,
166
+ teamInstanceId: `${name}-${this.instanceCounter + 1}`,
167
+ workspaceConfig,
168
+ workspaceManager,
169
+ });
170
+ }
171
+ } catch (err) {
172
+ // Non-fatal: topology wiring is a progressive enhancement. Log and
173
+ // fall through to legacy capability-based dispatch.
174
+ console.warn(
175
+ `[TeamManagerV2] topology auto-wire skipped for team "${name}": ${
176
+ err instanceof Error ? err.message : String(err)
177
+ }`
178
+ );
179
+ }
180
+ }
181
+
145
182
  // Create runtime
146
183
  const runtimeServices: TeamServicesV2 = {
147
184
  agentManager,
@@ -506,12 +506,12 @@ export class TeamRuntimeV2 {
506
506
  const capabilities = resolved.capabilities;
507
507
  let streamId = options.streamId;
508
508
  let streamConfig = options.streamConfig;
509
- let dataplaneTaskId = options.dataplaneTaskId;
509
+ let gitCascadeTaskId = options.gitCascadeTaskId;
510
510
 
511
511
  if (this.teamStreamId && capabilities) {
512
512
  if (capabilities.includes(WORKSPACE_CAPABILITIES.WORKTREE)) {
513
513
  streamId = streamId ?? this.teamStreamId;
514
- dataplaneTaskId = dataplaneTaskId ?? `worker-${Date.now()}`;
514
+ gitCascadeTaskId = gitCascadeTaskId ?? `worker-${Date.now()}`;
515
515
  } else if (capabilities.includes(WORKSPACE_CAPABILITIES.INTEGRATE)) {
516
516
  streamId = streamId ?? this.teamStreamId;
517
517
  }
@@ -521,7 +521,7 @@ export class TeamRuntimeV2 {
521
521
  ...options,
522
522
  streamId,
523
523
  streamConfig,
524
- dataplaneTaskId,
524
+ gitCascadeTaskId,
525
525
  capabilities: capabilities ?? options.capabilities,
526
526
  // Set team scope on all team agents
527
527
  team_instance: options.team_instance ?? this.manifest.name,
@@ -846,6 +846,26 @@ Focus on correctness — your changes go live immediately.`);
846
846
  const { workspaceManager } = this.services;
847
847
  if (!workspaceManager || !this.integrationStrategy) return;
848
848
 
849
+ // V3 coexistence: if TeamManagerV2 has already wired a YamlDrivenTopology
850
+ // from `macro_agent.workspace`, that policy owns the team root stream.
851
+ // Don't create a second one via the legacy createIntegrationStream.
852
+ const hasV3Topology =
853
+ typeof (
854
+ this.services.agentManager as {
855
+ getTopologyPolicy?: () => unknown;
856
+ }
857
+ ).getTopologyPolicy === 'function';
858
+ // AgentManager doesn't expose a getter today, so detect indirectly: a
859
+ // V3-wired team has already created a stream owned by `team:<name>`.
860
+ const existingTeamRoot = workspaceManager
861
+ .listStreams({ ownerId: `team:${this.manifest.name}` } as never)
862
+ .find((s: { name: string }) => s.name === this.manifest.name);
863
+
864
+ if (existingTeamRoot) {
865
+ this.teamStreamId = existingTeamRoot.id;
866
+ return;
867
+ }
868
+
849
869
  try {
850
870
  this.teamStreamId = workspaceManager.createIntegrationStream(
851
871
  rootAgentId,