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
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Cascade Bridge — forwards GitCascadeAdapter events to the MAP hub as
3
+ * `x-cascade/*` notifications.
4
+ *
5
+ * macro-agent's GitCascadeAdapter maintains a structured `GitCascadeEvent`
6
+ * stream (translated from git-cascade's native `x-cascade/*` emissions + local
7
+ * adapter events). This bridge subscribes to that stream and translates
8
+ * **back** to `x-cascade/*` MAP method calls so the OpenHive hub receives the
9
+ * canonical event schema.
10
+ *
11
+ * The round-trip (git-cascade → adapter translation → bridge re-translation)
12
+ * is deliberate: it preserves the adapter's internal abstraction (other
13
+ * macro-agent code consumes `GitCascadeEvent`, not raw MAP params) while
14
+ * guaranteeing the hub sees the same schema git-cascade defines. The bridge
15
+ * is the only place that knows about both shapes.
16
+ *
17
+ * Standalone-safe: when `connection.isConnected` is false, events are
18
+ * dropped. macro-agent continues to work without an OpenHive hub.
19
+ *
20
+ * @module map/cascade-bridge
21
+ */
22
+
23
+ import { CASCADE_METHODS } from 'git-cascade/events';
24
+ import type { LifecycleBridgeConnection } from './lifecycle-bridge.js';
25
+ import type {
26
+ GitCascadeAdapter,
27
+ GitCascadeEvent,
28
+ } from '../workspace/git-cascade-adapter.js';
29
+
30
+ export interface CascadeBridgeDisposable {
31
+ /** Unsubscribe from the adapter event stream. Safe to call multiple times. */
32
+ dispose: () => void;
33
+ }
34
+
35
+ export interface CascadeBridgeOptions {
36
+ /** Enable debug logging when events fail to forward. Defaults to false. */
37
+ verbose?: boolean;
38
+ }
39
+
40
+ /**
41
+ * Create a cascade bridge.
42
+ *
43
+ * Subscribes to `adapter.onEvent()` and forwards a fixed subset of events
44
+ * (the ones that have a canonical `x-cascade/*` method name) as MAP
45
+ * notifications via `connection.callExtension()`. Events with no MAP
46
+ * counterpart (`stream:forked`, `worktree:*`, `task:*`, `mergeQueue:*`, etc.)
47
+ * are silently ignored.
48
+ *
49
+ * @returns A disposable that unsubscribes from the adapter stream.
50
+ */
51
+ export function createCascadeBridge(
52
+ connection: LifecycleBridgeConnection,
53
+ adapter: GitCascadeAdapter,
54
+ options: CascadeBridgeOptions = {}
55
+ ): CascadeBridgeDisposable {
56
+ const verbose = options.verbose ?? false;
57
+
58
+ const unsubscribe = adapter.onEvent((event: GitCascadeEvent) => {
59
+ if (!connection.isConnected) return;
60
+
61
+ const mapped = translate(event);
62
+ if (!mapped) return;
63
+
64
+ // Fire-and-forget: never block the adapter's event loop on a MAP RPC.
65
+ // Errors are swallowed to preserve standalone-safety; they indicate the
66
+ // hub is unreachable or the method isn't registered, neither of which
67
+ // should break local cascade operations.
68
+ void connection
69
+ .callExtension(mapped.method, mapped.params)
70
+ .catch((err) => {
71
+ if (verbose) {
72
+ // eslint-disable-next-line no-console
73
+ console.warn(
74
+ `[cascade-bridge] failed to forward ${event.type} as ${mapped.method}:`,
75
+ err instanceof Error ? err.message : err
76
+ );
77
+ }
78
+ });
79
+ });
80
+
81
+ return { dispose: unsubscribe };
82
+ }
83
+
84
+ // ─────────────────────────────────────────────────────────────────────────────
85
+ // GitCascadeEvent → x-cascade/* translation
86
+ // ─────────────────────────────────────────────────────────────────────────────
87
+
88
+ interface TranslatedCall {
89
+ method: string;
90
+ params: Record<string, unknown>;
91
+ }
92
+
93
+ /**
94
+ * Translate a `GitCascadeEvent` into a MAP method call.
95
+ *
96
+ * Covers the 7 event types that have canonical MAP method names. Returns
97
+ * `null` for events that are macro-agent-internal (worktree/task/mergeQueue
98
+ * lifecycle, local-only forks, etc.).
99
+ *
100
+ * Field names flip from camelCase (macro-agent internal) back to snake_case
101
+ * (MAP wire format). The bridge is intentionally conservative — it only
102
+ * emits fields present on the event, letting the hub back-fill/ignore as
103
+ * needed.
104
+ */
105
+ function translate(event: GitCascadeEvent): TranslatedCall | null {
106
+ const d = event.data;
107
+
108
+ switch (event.type) {
109
+ case 'stream:created':
110
+ return {
111
+ method: CASCADE_METHODS.STREAM_OPENED,
112
+ params: {
113
+ stream_id: d.streamId,
114
+ name: d.name,
115
+ agent_id: d.agentId,
116
+ base_commit: d.baseCommit,
117
+ parent_stream: d.parentStream,
118
+ branch_name: d.branchName,
119
+ metadata: d.metadata,
120
+ },
121
+ };
122
+
123
+ case 'stream:committed':
124
+ return {
125
+ method: CASCADE_METHODS.STREAM_COMMITTED,
126
+ params: {
127
+ stream_id: d.streamId,
128
+ commit_hash: d.commit,
129
+ change_id: d.changeId,
130
+ agent_id: d.agentId,
131
+ message_summary: d.messageSummary,
132
+ files_touched: d.filesTouched,
133
+ parent_commit: d.parentCommit,
134
+ metadata: d.metadata,
135
+ },
136
+ };
137
+
138
+ case 'stream:merged':
139
+ return {
140
+ method: CASCADE_METHODS.STREAM_MERGED,
141
+ params: {
142
+ source_stream_id: d.sourceStreamId,
143
+ target_stream_id: d.targetStreamId,
144
+ merge_commit: d.mergeCommit,
145
+ agent_id: d.agentId,
146
+ strategy: d.strategy,
147
+ source_commit: d.sourceCommit,
148
+ metadata: d.metadata,
149
+ },
150
+ };
151
+
152
+ case 'stream:conflicted':
153
+ return {
154
+ method: CASCADE_METHODS.STREAM_CONFLICTED,
155
+ params: {
156
+ stream_id: d.streamId,
157
+ conflict_id: d.conflictId,
158
+ conflicted_files: d.conflictedFiles,
159
+ agent_id: d.agentId,
160
+ conflicting_commit: d.conflictingCommit,
161
+ target_commit: d.targetCommit,
162
+ source: d.source,
163
+ metadata: d.metadata,
164
+ },
165
+ };
166
+
167
+ case 'conflict:resolved':
168
+ // The adapter emits this from two sources: workspace-manager's local
169
+ // resolveConflict path (carries resolvedBy + resolutionCommit) AND the
170
+ // forwarded git-cascade stream.conflict_resolved (carries the explicit
171
+ // resolution_method). Bridge only forwards events with conflict_id +
172
+ // stream_id present (the cascade-driven shape).
173
+ if (!d.streamId || !d.conflictId) return null;
174
+ return {
175
+ method: CASCADE_METHODS.STREAM_CONFLICT_RESOLVED,
176
+ params: {
177
+ stream_id: d.streamId,
178
+ conflict_id: d.conflictId,
179
+ resolution_method:
180
+ (d.resolutionMethod as string | undefined) ??
181
+ (d.resolvedBy ? 'agent' : 'manual'),
182
+ resolved_by: d.resolvedBy,
183
+ resolution_summary: d.resolutionSummary,
184
+ metadata: d.metadata,
185
+ },
186
+ };
187
+
188
+ case 'stream:abandoned':
189
+ return {
190
+ method: CASCADE_METHODS.STREAM_ABANDONED,
191
+ params: {
192
+ stream_id: d.streamId,
193
+ reason: d.reason,
194
+ cascade: d.cascade,
195
+ metadata: d.metadata,
196
+ },
197
+ };
198
+
199
+ case 'stream:pushed':
200
+ // Trunk-style push to a remote (direct-push / optimistic-push). Hub
201
+ // sees this as the "merged" equivalent for non-stream targets.
202
+ if (!d.streamId || !d.pushedCommit || !d.remoteRef) return null;
203
+ return {
204
+ method: CASCADE_METHODS.STREAM_PUSHED,
205
+ params: {
206
+ stream_id: d.streamId,
207
+ agent_id: d.agentId,
208
+ pushed_commit: d.pushedCommit,
209
+ remote: d.remote ?? 'origin',
210
+ remote_ref: d.remoteRef,
211
+ strategy: d.strategy,
212
+ metadata: d.metadata,
213
+ },
214
+ };
215
+
216
+ case 'cascade:rebased':
217
+ return {
218
+ method: CASCADE_METHODS.CASCADE_REBASED,
219
+ params: {
220
+ stream_id: d.streamId,
221
+ agent_id: d.agentId,
222
+ triggered_by_stream_id: d.triggeredByStreamId,
223
+ triggered_by_agent_id: d.triggeredByAgentId,
224
+ new_base_commit: d.newBaseCommit,
225
+ new_head: d.newHead,
226
+ new_commits: d.newCommits,
227
+ metadata: d.metadata,
228
+ },
229
+ };
230
+
231
+ case 'cascade:completed':
232
+ return {
233
+ method: CASCADE_METHODS.CASCADE_COMPLETED,
234
+ params: {
235
+ root_stream_id: d.rootStreamId,
236
+ agent_id: d.agentId,
237
+ strategy: d.strategy,
238
+ updated_streams: d.updatedStreams,
239
+ failed_streams: d.failedStreams,
240
+ skipped_streams: d.skippedStreams,
241
+ deferred_streams: d.deferredStreams,
242
+ metadata: d.metadata,
243
+ },
244
+ };
245
+
246
+ case 'mergeQueue:added':
247
+ if (!d.entryId || !d.streamId) return null;
248
+ return {
249
+ method: CASCADE_METHODS.QUEUE_ADDED,
250
+ params: {
251
+ entry_id: d.entryId,
252
+ stream_id: d.streamId,
253
+ target_branch: (d.targetBranch as string | undefined) ?? 'main',
254
+ metadata: d.metadata,
255
+ },
256
+ };
257
+
258
+ case 'mergeQueue:ready':
259
+ if (!d.entryId || !d.streamId) return null;
260
+ return {
261
+ method: CASCADE_METHODS.QUEUE_READY,
262
+ params: {
263
+ entry_id: d.entryId,
264
+ stream_id: d.streamId,
265
+ target_branch: (d.targetBranch as string | undefined) ?? 'main',
266
+ },
267
+ };
268
+
269
+ case 'mergeQueue:cancelled':
270
+ if (!d.entryId || !d.streamId) return null;
271
+ return {
272
+ method: CASCADE_METHODS.QUEUE_CANCELLED,
273
+ params: {
274
+ entry_id: d.entryId,
275
+ stream_id: d.streamId,
276
+ target_branch: (d.targetBranch as string | undefined) ?? 'main',
277
+ reason: d.reason,
278
+ },
279
+ };
280
+
281
+ case 'mergeQueue:removed':
282
+ if (!d.entryId || !d.streamId) return null;
283
+ return {
284
+ method: CASCADE_METHODS.QUEUE_REMOVED,
285
+ params: {
286
+ entry_id: d.entryId,
287
+ stream_id: d.streamId,
288
+ target_branch: (d.targetBranch as string | undefined) ?? 'main',
289
+ outcome: d.outcome,
290
+ },
291
+ };
292
+
293
+ // Local-only events with no MAP counterpart (Phase 1 scope).
294
+ // 'stream:updated', 'stream:forked', 'stream:paused', 'stream:resumed',
295
+ // 'worktree:*', 'task:*', 'change:*', 'conflict:*' (legacy local-only
296
+ // variant — cascade-bridge handles the cascade-driven 'conflict:resolved'
297
+ // separately above)
298
+ default:
299
+ return null;
300
+ }
301
+ }
@@ -13,13 +13,6 @@ import type { TaskBridge } from "./types.js";
13
13
 
14
14
  /** Minimal interface for the MAP connection methods we need */
15
15
  export interface LifecycleBridgeConnection {
16
- spawn(options: {
17
- agentId?: string | undefined;
18
- name?: string | undefined;
19
- role?: string | undefined;
20
- scopes?: string[];
21
- metadata?: Record<string, unknown>;
22
- }): Promise<unknown>;
23
16
  callExtension(method: string, params?: unknown): Promise<unknown>;
24
17
  get isConnected(): boolean;
25
18
  }
@@ -28,6 +21,8 @@ interface RegisteredAgent {
28
21
  id: string;
29
22
  name: string;
30
23
  role: string;
24
+ /** MAP-assigned agent ID (ULID) from the hub, used for unregistration */
25
+ mapId?: string;
31
26
  }
32
27
 
33
28
  /**
@@ -41,9 +36,27 @@ export function createLifecycleBridge(
41
36
  agentStore: AgentStore,
42
37
  scope: string,
43
38
  taskBridge?: TaskBridge,
39
+ getLocalMapId?: (localAgentId: string) => string | undefined,
44
40
  ): { callback: AgentLifecycleCallback; cleanup: () => Promise<void> } {
45
41
  const registered = new Map<string, RegisteredAgent>();
46
42
 
43
+ /**
44
+ * Poll for the local MAP server's assigned ID for an agent.
45
+ * The local MAP server and the lifecycle bridge both listen to the same
46
+ * lifecycle callback, so they may fire in any order. Poll briefly to handle
47
+ * the race.
48
+ */
49
+ async function waitForLocalMapId(localAgentId: string, timeoutMs = 500): Promise<string | undefined> {
50
+ if (!getLocalMapId) return undefined;
51
+ const deadline = Date.now() + timeoutMs;
52
+ while (Date.now() < deadline) {
53
+ const id = getLocalMapId(localAgentId);
54
+ if (id) return id;
55
+ await new Promise((r) => setTimeout(r, 20));
56
+ }
57
+ return getLocalMapId(localAgentId);
58
+ }
59
+
47
60
  const callback: AgentLifecycleCallback = (event) => {
48
61
  if (!connection.isConnected) return;
49
62
 
@@ -55,22 +68,57 @@ export function createLifecycleBridge(
55
68
  const entry: RegisteredAgent = { id: agent.id, name, role };
56
69
  registered.set(agent.id, entry);
57
70
 
58
- // Register agent with MAP hub
59
- connection
60
- .spawn({
61
- agentId: agent.id,
62
- name,
63
- role,
64
- scopes: [scope],
65
- metadata: {
66
- parent: (agent as any).parent_id ?? undefined,
67
- team: (agent as any).team ?? undefined,
68
- cwd: (agent as any).cwd ?? undefined,
69
- },
70
- })
71
- .catch(() => {
71
+ // Build per-agent capabilities.
72
+ // Coordinators (head managers) support ACP for interactive chat.
73
+ const capabilities: Record<string, unknown> = {
74
+ messaging: { canReceive: true },
75
+ };
76
+ if (role === "coordinator") {
77
+ capabilities.protocols = ["acp"];
78
+ capabilities.acp = { version: "2024-10-07" };
79
+ }
80
+
81
+ // Register agent with MAP hub (use map/agents/register to preserve
82
+ // per-agent capabilities; map/agents/spawn drops them).
83
+ // Include the local MAP server's ID in metadata so clients can route
84
+ // ACP messages to the correct agent on the macro-agent's own MAP server.
85
+ // Also include provider_session_id so OpenHive can find the underlying
86
+ // Claude Code JSONL transcript on disk for history recovery.
87
+ const agentMetadata = (agent as any).metadata as Record<string, unknown> | undefined;
88
+ const providerSessionId =
89
+ typeof agentMetadata?.provider_session_id === "string"
90
+ ? agentMetadata.provider_session_id
91
+ : undefined;
92
+ (async () => {
93
+ const peerMapId = await waitForLocalMapId(agent.id);
94
+ try {
95
+ const result: any = await connection.callExtension("map/agents/register", {
96
+ name,
97
+ role,
98
+ capabilities,
99
+ metadata: {
100
+ // From the hub's perspective these IDs identify this agent on
101
+ // the macro-agent (peer) side. `peerAgentId` is macro-agent's
102
+ // internal store id; `peerMapId` is its local MAP server ULID.
103
+ // Hub callers use these to address the agent in routing
104
+ // (ACP streams target peerMapId, lifecycle ops use peerAgentId).
105
+ peerAgentId: agent.id,
106
+ peerMapId,
107
+ provider_session_id: providerSessionId,
108
+ parent: (agent as any).parent_id ?? undefined,
109
+ team: (agent as any).team ?? undefined,
110
+ cwd: (agent as any).cwd ?? undefined,
111
+ },
112
+ });
113
+ // Track the MAP-assigned agent ID for unregistration
114
+ const mapId = result?.agent?.id ?? result?.id;
115
+ if (mapId) {
116
+ entry.mapId = mapId;
117
+ }
118
+ } catch {
72
119
  // Silent — MAP hub may be temporarily unavailable
73
- });
120
+ }
121
+ })();
74
122
 
75
123
  // Bridge task creation if agent has a task
76
124
  if (taskBridge && (agent as any).task_id) {
@@ -88,12 +136,14 @@ export function createLifecycleBridge(
88
136
 
89
137
  case "stopped": {
90
138
  const agent = event.agent;
139
+ const entry = registered.get(agent.id);
91
140
  registered.delete(agent.id);
92
141
 
93
- // Unregister agent from MAP hub
142
+ // Unregister agent from MAP hub (use MAP-assigned ID if available)
143
+ const unregId = entry?.mapId ?? agent.id;
94
144
  connection
95
145
  .callExtension("map/agents/unregister", {
96
- agentId: agent.id,
146
+ agentId: unregId,
97
147
  reason: event.reason ?? "stopped",
98
148
  })
99
149
  .catch(() => {
@@ -123,11 +173,11 @@ export function createLifecycleBridge(
123
173
  registered.clear();
124
174
  return;
125
175
  }
126
- // Unregister all tracked agents
127
- const promises = Array.from(registered.keys()).map((agentId) =>
176
+ // Unregister all tracked agents (use MAP-assigned IDs)
177
+ const promises = Array.from(registered.values()).map((entry) =>
128
178
  connection
129
179
  .callExtension("map/agents/unregister", {
130
- agentId,
180
+ agentId: entry.mapId ?? entry.id,
131
181
  reason: "sidecar_shutdown",
132
182
  })
133
183
  .catch(() => {}),
package/src/map/server.ts CHANGED
@@ -107,7 +107,7 @@ export function createMAPServerInstance(
107
107
  role: params.role ?? "worker",
108
108
  state: "idle",
109
109
  sessionId: ctx?.session?.id,
110
- metadata: { localAgentId: spawned.id, task: params.task },
110
+ metadata: { peerAgentId: spawned.id, task: params.task },
111
111
  });
112
112
  if (registered?.id) {
113
113
  mapIdToLocalId.set(registered.id, spawned.id);
@@ -145,6 +145,38 @@ export function createMAPServerInstance(
145
145
  return { agent: { id: spawned.id } };
146
146
  };
147
147
 
148
+ /**
149
+ * Terminate a running agent. Accepts either the agent's local ID or the
150
+ * MAP-assigned ULID (we resolve back to local via mapIdToLocalId).
151
+ * Reason defaults to "stopped"; use "cancelled" for user-initiated stops.
152
+ */
153
+ handlers["_macro/terminateAgent"] = async (params) => {
154
+ const agentIdParam = params.agentId as string | undefined;
155
+ const reason = (params.reason as string) ?? "cancelled";
156
+ if (!agentIdParam) {
157
+ return { success: false, error: "agentId is required" };
158
+ }
159
+ // Resolve either a MAP ULID or a local agent ID to our internal ID.
160
+ const localId = mapIdToLocalId.get(agentIdParam) ?? agentIdParam;
161
+ try {
162
+ await agentManager.terminate(localId as any, reason as any);
163
+ return { success: true };
164
+ } catch (err) {
165
+ return { success: false, error: (err as Error).message };
166
+ }
167
+ };
168
+
169
+ /**
170
+ * Inspect ACP stream → peer agent bindings on this MAP server.
171
+ * Each stream carries the peer agent id (macro-agent's internal store id)
172
+ * it was opened against, set by the bridge from MAP routing. Useful for
173
+ * routing tests and debugging multi-coordinator scenarios.
174
+ */
175
+ handlers["_macro/getAcpStreamBindings"] = async () => {
176
+ if (!acpBridge) return { bindings: [] };
177
+ return { bindings: acpBridge.getStreamBindings() };
178
+ };
179
+
148
180
  // ── Task extensions ───────────────────────────────────────────
149
181
  handlers["_macro/task/list"] = async () => {
150
182
  if (!tasksAdapter.connected) return { tasks: [] };
@@ -404,16 +436,21 @@ export function createMAPServerInstance(
404
436
  try {
405
437
  if (event.type === "spawned" || event.type === "started") {
406
438
  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.
439
+ // Register agent ONCE. spawn() fires "spawned" immediately followed
440
+ // by "started", so without this guard the listener re-registers
441
+ // on the second event generating a fresh MAP ULID and overwriting
442
+ // localIdToMapId. Consumers racing against that overwrite (like the
443
+ // sidecar's lifecycle bridge, which snapshots peerMapId into hub
444
+ // metadata) end up disagreeing with _macro/spawnAgent's return
445
+ // value on which ULID refers to this agent.
446
+ if (localIdToMapId.has(agent.id)) return;
410
447
  try {
411
448
  const registered = mapServer.agents.register({
412
449
  name: agent.name ?? agent.id,
413
450
  role: agent.role ?? "worker",
414
451
  state: "idle",
415
452
  metadata: {
416
- localAgentId: agent.id, // Store local ID in metadata
453
+ peerAgentId: agent.id, // macro-agent's internal store id
417
454
  parent: (agent as any).parent ?? null,
418
455
  task: (agent as any).task ?? null,
419
456
  cwd: (agent as any).cwd ?? null,
@@ -454,7 +491,7 @@ export function createMAPServerInstance(
454
491
  role: agent.role ?? "worker",
455
492
  state: agent.state === "running" ? "busy" : "idle",
456
493
  metadata: {
457
- localAgentId: agent.id,
494
+ peerAgentId: agent.id,
458
495
  parent: agent.parent ?? null,
459
496
  task: agent.task ?? null,
460
497
  },
@@ -535,5 +572,9 @@ export function createMAPServerInstance(
535
572
  getConnectionCount(): number {
536
573
  return connectionCount;
537
574
  },
575
+
576
+ getLocalMapId(localAgentId: string): string | undefined {
577
+ return localIdToMapId.get(localAgentId);
578
+ },
538
579
  };
539
580
  }
@@ -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;
@@ -115,8 +120,6 @@ export function createMAPSidecar(
115
120
  capabilities: {
116
121
  messaging: { canSend: true, canReceive: true },
117
122
  mail: { canCreate: true, canJoin: true, canViewHistory: true },
118
- protocols: ['acp'],
119
- acp: { version: '2024-10-07' },
120
123
  trajectory: { canReport: true, canServeContent: false },
121
124
  tasks: {
122
125
  canCreate: true,
@@ -128,6 +131,10 @@ export function createMAPSidecar(
128
131
  metadata: {
129
132
  systemId: config.systemId ?? "macro-agent",
130
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,
131
138
  },
132
139
  reconnection: {
133
140
  enabled: config.reconnection?.enabled ?? true,
@@ -177,6 +184,19 @@ export function createMAPSidecar(
177
184
  isConnected = true;
178
185
  } // end if (!isConnected)
179
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
+
180
200
  // Monitor connection state
181
201
  connection.onStateChange(
182
202
  (newState: string, _oldState: string) => {
@@ -254,6 +274,7 @@ export function createMAPSidecar(
254
274
  agentStore,
255
275
  scope,
256
276
  taskBridge,
277
+ getLocalMapId,
257
278
  );
258
279
  lifecycleCallback = bridge.callback;
259
280
  lifecycleCleanup = bridge.cleanup;
@@ -272,6 +293,13 @@ export function createMAPSidecar(
272
293
  tasksAdapter,
273
294
  trajectoryReporter,
274
295
  });
296
+
297
+ // 5. Cascade Bridge (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
+ cascadeBridgeCleanup = cascadeBridge.dispose;
302
+ }
275
303
  }
276
304
 
277
305
  return {
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
  // =============================================================================
@@ -244,4 +259,9 @@ export interface MAPServerInstance {
244
259
  getUrl(): string;
245
260
  /** Get number of active connections */
246
261
  getConnectionCount(): number;
262
+ /**
263
+ * Resolve a local agent ID (macro-agent internal) to its MAP server-assigned ULID.
264
+ * Returns undefined if the agent is not registered on the MAP server yet.
265
+ */
266
+ getLocalMapId(localAgentId: string): string | undefined;
247
267
  }
@@ -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 {