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/boot-v2.ts CHANGED
@@ -22,6 +22,7 @@
22
22
  import * as path from "path";
23
23
  import * as os from "os";
24
24
  import * as fs from "fs";
25
+ import * as crypto from "crypto";
25
26
  import { AgentStore } from "./agent/agent-store.js";
26
27
  import {
27
28
  DefaultInboxAdapter,
@@ -52,7 +53,22 @@ export interface BootV2Config {
52
53
  /** Working directory (default: process.cwd()) */
53
54
  cwd?: string;
54
55
 
55
- /** Base directory for data storage (default: ~/.macro-agent) */
56
+ /**
57
+ * Stable identifier for this macro-agent run. Controls the default on-disk
58
+ * layout at `~/.macro-agent/<instanceId>/` (agents.db, inbox.db, sockets).
59
+ *
60
+ * Precedence when choosing an id:
61
+ * 1. explicit `instanceId` (this field)
62
+ * 2. `map.swarmId` (the MAP identity, when provided)
63
+ * 3. `inst_<sha256(cwd)[:12]>` (stable per-project fallback)
64
+ *
65
+ * Explicit `baseDir` overrides all of the above. Hosts that manage their
66
+ * own storage layout (openswarm spawns hosted swarms with a unique
67
+ * per-spawn data dir) still win by setting `baseDir` directly.
68
+ */
69
+ instanceId?: string;
70
+
71
+ /** Base directory for data storage. Default: `~/.macro-agent/<instanceId>/` */
56
72
  baseDir?: string;
57
73
 
58
74
  /** Default permission mode for spawned agents */
@@ -99,7 +115,13 @@ export interface BootV2Config {
99
115
  };
100
116
 
101
117
  /** MAP server config (accept inbound connections from TUI/clients) */
102
- mapServer?: { enabled?: boolean; port?: number; host?: string; path?: string; name?: string };
118
+ mapServer?: {
119
+ enabled?: boolean;
120
+ port?: number;
121
+ host?: string;
122
+ path?: string;
123
+ name?: string;
124
+ };
103
125
 
104
126
  /** MAP sidecar config (connect to OpenHive hub) */
105
127
  map?: {
@@ -121,18 +143,64 @@ export interface BootV2Config {
121
143
  };
122
144
  };
123
145
 
146
+ /**
147
+ * Cascade event binding config. Controls how cascade events emitted by
148
+ * git-cascade-backed agents get tagged with external task references for
149
+ * hub projection (changelog, task↔stream binding). Independent of MAP
150
+ * transport: cascade events are data/identity, not transport.
151
+ */
152
+ cascade?: {
153
+ /**
154
+ * Default OpenTasks resource ID for this swarm. When set, the agent
155
+ * manager auto-builds `taskRef = { resource_id, node_id: task_id }`
156
+ * for spawned agents so cascade events carry the binding without
157
+ * callers constructing refs by hand.
158
+ *
159
+ * Leave undefined if:
160
+ * - The swarm touches multiple opentasks graphs (use `resolveTaskRef`).
161
+ * - Every caller sets `SpawnAgentOptions.taskRef` explicitly.
162
+ * - You don't care about hub task↔stream binding.
163
+ */
164
+ taskResourceId?: string;
165
+
166
+ /**
167
+ * Custom resolver for multi-graph deployments. Called at every spawn;
168
+ * return a `TaskRef` to set the binding or `undefined` to skip.
169
+ * Precedence: explicit `SpawnAgentOptions.taskRef` > `resolveTaskRef` >
170
+ * `taskResourceId` fallback (combined with `spawnOptions.task_id`).
171
+ *
172
+ * Keep implementations cheap — this runs on every spawn.
173
+ *
174
+ * @example
175
+ * resolveTaskRef: (opts) => {
176
+ * const graph = graphForCwd(opts.cwd ?? process.cwd());
177
+ * return graph ? { resource_id: graph.resourceId, node_id: String(opts.task_id) } : undefined;
178
+ * }
179
+ */
180
+ resolveTaskRef?: (
181
+ spawnOptions: import("./agent/types.js").SpawnAgentOptions,
182
+ ) => import("git-cascade/events").TaskRef | undefined;
183
+
184
+ /**
185
+ * Override the default `x-cascade` event prefix. Useful for branded
186
+ * deployments or isolating cascade namespaces in testing. Affects all
187
+ * events emitted by the tracker embedded in this swarm.
188
+ */
189
+ eventPrefix?: string;
190
+ };
191
+
124
192
  /** minimem (agent memory) — registers as MCP server for all agents */
125
193
  minimem?: {
126
194
  enabled?: boolean;
127
- dir?: string; // default: ".swarm/minimem/"
128
- provider?: string; // "auto" | "openai" | "gemini" | "local"
129
- global?: boolean; // also search ~/.minimem
195
+ dir?: string; // default: ".swarm/minimem/"
196
+ provider?: string; // "auto" | "openai" | "gemini" | "local"
197
+ global?: boolean; // also search ~/.minimem
130
198
  };
131
199
 
132
200
  /** skill-tree (per-role skills) — compiles loadouts at team start, injects into prompts */
133
201
  skilltree?: {
134
202
  enabled?: boolean;
135
- basePath?: string; // default: ".swarm/skill-tree/"
203
+ basePath?: string; // default: ".swarm/skill-tree/"
136
204
  defaultProfile?: string;
137
205
  };
138
206
 
@@ -147,6 +215,76 @@ export interface BootV2Config {
147
215
  enabled?: boolean;
148
216
  peerId?: string;
149
217
  };
218
+
219
+ /** Task dispatch config — opt-in autonomous task dispatch mode */
220
+ dispatch?: {
221
+ enabled?: boolean;
222
+ pollIntervalMs?: number;
223
+ maxConcurrent?: number;
224
+ defaultRole?: string;
225
+ tags?: string[];
226
+ maxRetries?: number;
227
+ retryBaseDelayMs?: number;
228
+ retryMaxDelayMs?: number;
229
+ reconcile?: {
230
+ enabled?: boolean;
231
+ intervalMs?: number;
232
+ stallTimeoutMs?: number;
233
+ };
234
+ eligibility?: import("swarm-dispatch").EligibilityConfig;
235
+ /** Dispatch mode: route-only, spawn-only, prefer-route, prefer-spawn. Default: prefer-route when inbox available, spawn-only otherwise. */
236
+ dispatchMode?: import("swarm-dispatch").DispatchMode;
237
+ /** Enable mail-based work routing via agent-inbox (default: true when dispatch enabled). */
238
+ enableMailRouting?: boolean;
239
+ /** Enable roster-based agent discovery for route-first dispatch (default: true when dispatch enabled). */
240
+ enableRoster?: boolean;
241
+ /** Continuation config. */
242
+ continuation?: { delayMs?: number; maxTurns?: number };
243
+ };
244
+
245
+ /**
246
+ * Boot-time agents to spawn after AgentManager is ready.
247
+ *
248
+ * Currently supports `coordinator` — when set, fires a non-blocking
249
+ * `agentManager.spawn({ role: 'coordinator', parent: null, cwd, ... })`
250
+ * during boot so the swarm has a default head manager ready for chat
251
+ * without an explicit spawn call. Pass `true` for defaults (uses the
252
+ * boot config's cwd) or an object for fine control.
253
+ *
254
+ * Also driven by env var `MACRO_BOOTSTRAP_COORDINATOR=true` (with
255
+ * optional `MACRO_BOOTSTRAP_CWD=<path>`) when this field is unset —
256
+ * lets indirect callers (e.g. openswarm host) opt in without modifying
257
+ * the bootConfig pass-through whitelist.
258
+ */
259
+ bootstrap?: {
260
+ coordinator?: boolean | {
261
+ cwd?: string;
262
+ permissionMode?: PermissionMode;
263
+ agentType?: string;
264
+ customPrompt?: string;
265
+ task?: string;
266
+ };
267
+ /**
268
+ * Rehydration policy for agents that existed before this boot. Controls
269
+ * what the boot script does with agents that outlived their previous
270
+ * host process (agent-store is durable; a restart finds agents still
271
+ * marked `state='running'` but without any live ACP session).
272
+ *
273
+ * - `'none'` — skip rehydration entirely. Always fall through to fresh
274
+ * bootstrap spawn (or no spawn if `bootstrap.coordinator` is unset).
275
+ * - `'coordinators'` (default) — revive only root coordinators for
276
+ * this cwd. Matches the common openhive case where the workspace
277
+ * intent is "I want a coordinator here" and workers are ephemeral.
278
+ * - `'all'` — revive every `state='running'` agent at this cwd
279
+ * (coordinators plus workers/integrators/monitors). Parent-first
280
+ * ordering; children are skipped if their parent failed to revive
281
+ * or is `state='stopped'` (deliberately down).
282
+ *
283
+ * Hosted swarms pass `'all'` via `MACRO_BOOTSTRAP_REHYDRATE=all` so a
284
+ * restart restores the full macro-agent team, not just head managers.
285
+ */
286
+ rehydrate?: "none" | "coordinators" | "all";
287
+ };
150
288
  }
151
289
 
152
290
  // =============================================================================
@@ -178,6 +316,9 @@ export interface MacroAgentSystemV2 {
178
316
  /** Control socket path (for MCP subprocess connection) */
179
317
  controlSocketPath: string;
180
318
 
319
+ /** Task dispatcher (if dispatch mode enabled) */
320
+ taskDispatcher?: import("swarm-dispatch").TaskDispatcher;
321
+
181
322
  /** REST API server (if enabled) */
182
323
  apiServer?: ApiServer;
183
324
 
@@ -202,11 +343,63 @@ export interface MacroAgentSystemV2 {
202
343
  // =============================================================================
203
344
 
204
345
  export async function bootV2(
205
- config: BootV2Config = {}
346
+ config: BootV2Config = {},
206
347
  ): Promise<MacroAgentSystemV2> {
207
348
  const cwd = config.cwd ?? process.cwd();
208
- const baseDir =
209
- config.baseDir ?? path.join(os.homedir(), ".macro-agent");
349
+ // Resolve the instance id with three-tier precedence so the on-disk layout
350
+ // stays meaningful across standalone, MAP-connected, and hosted runs:
351
+ //
352
+ // 1. Explicit `instanceId` — caller-chosen, human-readable.
353
+ // 2. `map.swarmId` — the MAP identity when the caller has pre-registered
354
+ // one. This ties macro-agent's local store to its hub identity, so a
355
+ // swarm with swarm_id=X always resumes its own state.
356
+ // 3. A stable hash of the resolved cwd — the last-resort fallback so two
357
+ // processes in different projects never collide on agents.db, inbox.db,
358
+ // or the control socket. Reruns in the same project reuse their store.
359
+ //
360
+ // Hosts that manage their own storage layout (e.g. openswarm spawning
361
+ // per-swarm instances under a unique data dir) still win by passing
362
+ // `baseDir` directly. Legacy `~/.macro-agent/*.db` from pre-instancing
363
+ // versions is left alone — new boots start fresh under their own subdir.
364
+ const instanceId =
365
+ config.instanceId
366
+ ?? config.map?.swarmId
367
+ ?? ("inst_" + crypto.createHash("sha256").update(path.resolve(cwd)).digest("hex").slice(0, 12));
368
+ const baseDir = config.baseDir ?? path.join(os.homedir(), ".macro-agent", instanceId);
369
+
370
+ // Env-var bridge for hosts that pass through bootConfig with a fixed
371
+ // whitelist (e.g. openswarm). Translates MACRO_BOOTSTRAP_COORDINATOR /
372
+ // MACRO_BOOTSTRAP_CWD / MACRO_BOOTSTRAP_REHYDRATE into the structured
373
+ // bootstrap field if not already set programmatically. Programmatic
374
+ // config wins per field.
375
+ if (
376
+ process.env.MACRO_BOOTSTRAP_COORDINATOR === "true" &&
377
+ !config.bootstrap?.coordinator
378
+ ) {
379
+ const envCwd = process.env.MACRO_BOOTSTRAP_CWD;
380
+ config = {
381
+ ...config,
382
+ bootstrap: {
383
+ ...(config.bootstrap ?? {}),
384
+ coordinator: envCwd ? { cwd: envCwd } : true,
385
+ },
386
+ };
387
+ }
388
+ const envRehydrate = process.env.MACRO_BOOTSTRAP_REHYDRATE;
389
+ if (
390
+ (envRehydrate === "none" ||
391
+ envRehydrate === "coordinators" ||
392
+ envRehydrate === "all") &&
393
+ config.bootstrap?.rehydrate === undefined
394
+ ) {
395
+ config = {
396
+ ...config,
397
+ bootstrap: {
398
+ ...(config.bootstrap ?? {}),
399
+ rehydrate: envRehydrate,
400
+ },
401
+ };
402
+ }
210
403
 
211
404
  // Ensure base directory exists
212
405
  fs.mkdirSync(baseDir, { recursive: true });
@@ -217,8 +410,7 @@ export async function bootV2(
217
410
 
218
411
  // 2. Inbox Adapter (embedded agent-inbox, hybrid mode)
219
412
  const inboxSocketPath =
220
- config.inbox?.socketPath ??
221
- path.join(baseDir, "inbox.sock");
413
+ config.inbox?.socketPath ?? path.join(baseDir, "inbox.sock");
222
414
  const inboxSqlitePath = path.join(baseDir, "inbox.db");
223
415
 
224
416
  const inboxAdapter = new DefaultInboxAdapter({
@@ -248,7 +440,7 @@ export async function bootV2(
248
440
  } catch {
249
441
  // opentasks daemon may not be available — non-fatal
250
442
  console.warn(
251
- "[boot-v2] opentasks daemon not available. Task operations will fail until connected."
443
+ "[boot-v2] opentasks daemon not available. Task operations will fail until connected.",
252
444
  );
253
445
  }
254
446
 
@@ -270,14 +462,20 @@ export async function bootV2(
270
462
  serverUrl: config.serverUrl,
271
463
  serverToken: config.serverToken,
272
464
  controlSocketPath,
273
- }
465
+ taskResourceId: config.cascade?.taskResourceId,
466
+ resolveTaskRef: config.cascade?.resolveTaskRef,
467
+ },
274
468
  );
275
469
 
276
470
  // 6. Federation (cross-instance communication)
277
471
  let federationCleanup: (() => void) | null = null;
278
472
  if (config.federation) {
279
473
  const { setupFederation } = await import("./adapters/federation.js");
280
- federationCleanup = setupFederation(agentManager, inboxAdapter, config.federation);
474
+ federationCleanup = setupFederation(
475
+ agentManager,
476
+ inboxAdapter,
477
+ config.federation,
478
+ );
281
479
  }
282
480
 
283
481
  // 7. Trigger System V2
@@ -292,11 +490,178 @@ export async function bootV2(
292
490
  enableHeartbeat: config.trigger?.enableHeartbeat ?? false,
293
491
  heartbeatIntervalMs: config.trigger?.heartbeatIntervalMs,
294
492
  },
295
- }
493
+ },
296
494
  );
297
495
  await triggerSystem.start();
298
496
 
299
- // 7. Control Server (lifecycle RPC for MCP subprocesses)
497
+ // 7a. Task Dispatch (opt-in autonomous task dispatch mode)
498
+ let taskDispatcher: import("swarm-dispatch").TaskDispatcher | null = null;
499
+
500
+ if (config.dispatch?.enabled && tasksAdapter) {
501
+ const { createOrchestrator, createOpenTasksSource, createAgentInboxPort } =
502
+ await import("swarm-dispatch");
503
+ const { getStableInstanceId } = await import("./cli/stable-instance-id.js");
504
+
505
+ const claimantId = `${os.hostname()}:${process.pid}:${getStableInstanceId(cwd)}`;
506
+ const dispatchAgentId = `dispatcher:${claimantId}`;
507
+
508
+ // Adapt opentasks client → DispatchTaskSource
509
+ const opentasksClient = (tasksAdapter as any).client;
510
+ const source = opentasksClient
511
+ ? createOpenTasksSource(opentasksClient)
512
+ : {
513
+ // Fallback adapter when opentasks client is available via TasksAdapter methods
514
+ queryReady: async (opts?: { tags?: string[]; limit?: number }) =>
515
+ tasksAdapter.queryReady(opts),
516
+ claim: async (taskId: string, claimantIdArg: string) => {
517
+ try {
518
+ await tasksAdapter.assignTask(taskId, claimantIdArg);
519
+ return { success: true as const };
520
+ } catch {
521
+ return { success: false as const };
522
+ }
523
+ },
524
+ release: async (taskId: string) => tasksAdapter.unclaimTask(taskId),
525
+ transition: async (
526
+ taskId: string,
527
+ action: "start" | "complete" | "fail",
528
+ ) => tasksAdapter.transitionTask(taskId, action),
529
+ getTask: async (taskId: string) => tasksAdapter.getTask(taskId),
530
+ listInProgress: async () =>
531
+ tasksAdapter.listTasks({ status: "in_progress" }),
532
+ };
533
+
534
+ // Adapt AgentManagerV2 → DispatchAgentRuntime
535
+ const runtime: import("swarm-dispatch").DispatchAgentRuntime = {
536
+ spawn: async (opts: { prompt: string; taskId: string; role: string }) => {
537
+ const spawned = await agentManager.spawn({
538
+ task: opts.prompt,
539
+ task_id: opts.taskId,
540
+ role: opts.role,
541
+ parent: null,
542
+ });
543
+ return { id: spawned.id };
544
+ },
545
+ terminate: async (agentId: string, reason?: string) => {
546
+ await agentManager.terminate(agentId, (reason ?? "cancelled") as any);
547
+ },
548
+ onStopped: (callback: (agentId: string, reason: string) => void) =>
549
+ agentManager.onLifecycleEvent((event) => {
550
+ if (event.type === "stopped") {
551
+ callback(event.agent.id, event.reason);
552
+ }
553
+ }),
554
+ };
555
+
556
+ // Phase 2: Wire MessagePort via agent-inbox for mail-based work routing
557
+ let messagePort: import("swarm-dispatch").MessagePort | undefined;
558
+ if (config.dispatch.enableMailRouting !== false) {
559
+ const inbox = inboxAdapter.getInbox();
560
+ messagePort = createAgentInboxPort(
561
+ inbox.router as any,
562
+ inbox.events as any,
563
+ {
564
+ dispatcherAgentId: dispatchAgentId,
565
+ classifyMessage: (msg: any) => {
566
+ // Classify inbox messages as dispatchable work when they carry
567
+ // the x-dispatch/work schema. Other messages are ignored.
568
+ const content = msg.content as {
569
+ type?: string;
570
+ schema?: string;
571
+ data?: any;
572
+ };
573
+ if (content?.schema !== "x-dispatch/work") return null;
574
+ const data = content.data;
575
+ if (!data?.taskId) return null;
576
+ return {
577
+ messageId: msg.id,
578
+ correlationId: msg.thread_tag ?? msg.id,
579
+ replyTo: msg.sender_id ? { agentId: msg.sender_id } : undefined,
580
+ task: {
581
+ id: data.taskId,
582
+ title: data.title ?? `Delegated: ${data.taskId}`,
583
+ status: "open",
584
+ content: data.prompt ?? data.content,
585
+ tags: data.tags,
586
+ metadata: {
587
+ ...data.metadata,
588
+ role: data.role,
589
+ },
590
+ },
591
+ };
592
+ },
593
+ },
594
+ );
595
+
596
+ // Register the dispatcher as an agent in the inbox so it can receive messages
597
+ await inboxAdapter.registerAgent(dispatchAgentId, {
598
+ role: "dispatcher",
599
+ scope: "default",
600
+ });
601
+ }
602
+
603
+ // Phase 2: Wire AgentRoster via inbox agent listing for route-first dispatch
604
+ let roster: import("swarm-dispatch").AgentRoster | undefined;
605
+ if (config.dispatch.enableRoster !== false) {
606
+ const inbox = inboxAdapter.getInbox();
607
+ roster = {
608
+ async findAvailable(criteria) {
609
+ // List agents from inbox storage, filter by role and idle state
610
+ const agents = inbox.storage.listAgents();
611
+ return agents
612
+ .filter((a: any) => {
613
+ if (a.agentId === dispatchAgentId) return false;
614
+ if (criteria.role && a.role && a.role !== criteria.role)
615
+ return false;
616
+ if (criteria.notBusy && a.status === "busy") return false;
617
+ return true;
618
+ })
619
+ .map((a: any) => ({
620
+ agentId: a.agentId ?? a.agent_id ?? a.id,
621
+ host: a.host,
622
+ }));
623
+ },
624
+ };
625
+ }
626
+
627
+ // Determine dispatch mode
628
+ const hasRouting = !!messagePort && !!roster;
629
+ const dispatchMode =
630
+ config.dispatch.dispatchMode ??
631
+ (hasRouting ? ("prefer-route" as const) : ("spawn-only" as const));
632
+
633
+ taskDispatcher = createOrchestrator(source, runtime, {
634
+ claimantId,
635
+ pollIntervalMs: config.dispatch.pollIntervalMs ?? 15_000,
636
+ defaultRole: config.dispatch.defaultRole ?? "worker",
637
+ concurrency: { global: config.dispatch.maxConcurrent ?? 3 },
638
+ retry: {
639
+ maxRetries: config.dispatch.maxRetries ?? 3,
640
+ baseDelayMs: config.dispatch.retryBaseDelayMs ?? 10_000,
641
+ maxDelayMs: config.dispatch.retryMaxDelayMs ?? 300_000,
642
+ },
643
+ eligibility: config.dispatch.eligibility,
644
+ tags: config.dispatch.tags,
645
+ reconcile: {
646
+ enabled: config.dispatch.reconcile?.enabled ?? true,
647
+ intervalMs: config.dispatch.reconcile?.intervalMs ?? 60_000,
648
+ stallTimeoutMs: config.dispatch.reconcile?.stallTimeoutMs,
649
+ },
650
+ ...(config.dispatch.continuation && {
651
+ continuation: {
652
+ delayMs: config.dispatch.continuation.delayMs ?? 1_000,
653
+ maxTurns: config.dispatch.continuation.maxTurns ?? 20,
654
+ },
655
+ }),
656
+ messagePort,
657
+ roster,
658
+ dispatchMode,
659
+ });
660
+
661
+ await taskDispatcher.start();
662
+ }
663
+
664
+ // 7b. Control Server (lifecycle RPC for MCP subprocesses)
300
665
  const controlServer = new ControlServer(agentManager, {
301
666
  socketPath: controlSocketPath,
302
667
  });
@@ -310,7 +675,9 @@ export async function bootV2(
310
675
 
311
676
  const healthCheckTimer = setInterval(async () => {
312
677
  try {
313
- const unhealthy = controlServer.getUnhealthyAgents(UNHEALTHY_THRESHOLD_MS);
678
+ const unhealthy = controlServer.getUnhealthyAgents(
679
+ UNHEALTHY_THRESHOLD_MS,
680
+ );
314
681
  for (const { agentId, lastSeen } of unhealthy) {
315
682
  const agent = agentStore.getAgent(agentId);
316
683
  if (!agent || agent.state !== "running") continue;
@@ -331,7 +698,7 @@ export async function bootV2(
331
698
  staleSinceMs: Date.now() - lastSeen,
332
699
  },
333
700
  },
334
- { importance: "high", threadTag: `health:${agentId}` }
701
+ { importance: "high", threadTag: `health:${agentId}` },
335
702
  );
336
703
  } catch {
337
704
  // Best effort notification
@@ -373,20 +740,19 @@ export async function bootV2(
373
740
  // 10. ACP WebSocket server (optional)
374
741
  let acpServer: WebSocketACPServer | null = null;
375
742
  if (config.acp?.enabled) {
376
- const { createWebSocketACPServer } = await import("./acp/websocket-server.js");
377
- acpServer = createWebSocketACPServer(
378
- systemRef,
379
- {
380
- port: config.acp.port,
381
- host: config.acp.host,
382
- path: config.acp.path,
383
- },
384
- );
743
+ const { createWebSocketACPServer } =
744
+ await import("./acp/websocket-server.js");
745
+ acpServer = createWebSocketACPServer(systemRef, {
746
+ port: config.acp.port,
747
+ host: config.acp.host,
748
+ path: config.acp.path,
749
+ });
385
750
  await acpServer.start();
386
751
  }
387
752
 
388
753
  // 11. MAP Server (optional — accept inbound connections from TUI/clients)
389
- let mapServerInstance: import("./map/types.js").MAPServerInstance | null = null;
754
+ let mapServerInstance: import("./map/types.js").MAPServerInstance | null =
755
+ null;
390
756
  if (config.mapServer?.enabled) {
391
757
  try {
392
758
  const { createMAPServerInstance } = await import("./map/server.js");
@@ -419,15 +785,20 @@ export async function bootV2(
419
785
 
420
786
  // 12. Swarmkit integrations (minimem, skill-tree, sessionlog)
421
787
  agentManager.setIntegrationConfigs({
422
- minimem: config.minimem?.enabled ? config.minimem as any : undefined,
423
- skilltree: config.skilltree?.enabled ? config.skilltree as any : undefined,
424
- sessionlog: config.sessionlog?.enabled ? config.sessionlog as any : undefined,
788
+ minimem: config.minimem?.enabled ? (config.minimem as any) : undefined,
789
+ skilltree: config.skilltree?.enabled
790
+ ? (config.skilltree as any)
791
+ : undefined,
792
+ sessionlog: config.sessionlog?.enabled
793
+ ? (config.sessionlog as any)
794
+ : undefined,
425
795
  });
426
796
 
427
797
  // 12b. Skill-tree loadout compilation (if enabled)
428
798
  if (config.skilltree?.enabled) {
429
799
  try {
430
- const { compileAllRoleLoadouts } = await import("./integrations/skilltree.js");
800
+ const { compileAllRoleLoadouts } =
801
+ await import("./integrations/skilltree.js");
431
802
  // Gather roles from the role registry
432
803
  const registeredRoles = roleRegistry.listRoles();
433
804
  const roleNames = registeredRoles.map((r) => r.name);
@@ -450,8 +821,27 @@ export async function bootV2(
450
821
  if (config.map?.enabled && config.map.server) {
451
822
  try {
452
823
  const { createMAPSidecar } = await import("./map/sidecar.js");
824
+ // If a workspace manager is present, pull out its GitCascadeAdapter
825
+ // so the sidecar can forward cascade events to the hub.
826
+ const wsMgr = config.workspaceManager as
827
+ | {
828
+ getGitCascadeAdapter?: () =>
829
+ | import("./workspace/git-cascade-adapter.js").GitCascadeAdapter
830
+ | undefined;
831
+ }
832
+ | undefined;
833
+ const gitCascadeAdapter = wsMgr?.getGitCascadeAdapter?.();
453
834
  mapSidecar = createMAPSidecar(
454
- { agentManager, agentStore, inboxAdapter, tasksAdapter },
835
+ {
836
+ agentManager,
837
+ agentStore,
838
+ inboxAdapter,
839
+ tasksAdapter,
840
+ getLocalMapId: mapServerInstance
841
+ ? (id: string) => mapServerInstance!.getLocalMapId(id)
842
+ : undefined,
843
+ gitCascadeAdapter,
844
+ },
455
845
  {
456
846
  server: config.map.server,
457
847
  token: config.map.token,
@@ -469,6 +859,18 @@ export async function bootV2(
469
859
  await mapSidecar.start();
470
860
  // Wire sidecar into agent manager for session-end checkpoints
471
861
  agentManager.setSidecar(mapSidecar);
862
+
863
+ // Bridge dispatch events to MAP for observability. Spread the source
864
+ // event first, then namespace the `type` field — otherwise tsc warns
865
+ // about the literal key being overwritten by the spread.
866
+ if (taskDispatcher && mapSidecar.emitEvent) {
867
+ taskDispatcher.onEvent((event) => {
868
+ mapSidecar!.emitEvent!({
869
+ ...event,
870
+ type: `dispatch.${event.type}`,
871
+ });
872
+ });
873
+ }
472
874
  // Attach to shared system ref so ACP/MAP handlers can access it
473
875
  systemRef.mapSidecar = mapSidecar;
474
876
  } catch (err) {
@@ -479,7 +881,156 @@ export async function bootV2(
479
881
  }
480
882
  }
481
883
 
482
- // 13. Return system handle
884
+ // 13. Boot-time agents (opt-in)
885
+ // Fire after all subsystems are wired so the agent's lifecycle events
886
+ // (spawned/started) flow through the lifecycle bridge → MAP hub. Non-
887
+ // blocking: don't gate boot completion on agent process startup, which
888
+ // takes seconds. Failures are logged but do not abort boot.
889
+ //
890
+ // Rehydration on restart: the agent-store persists across process
891
+ // restarts. When we boot into a workspace that already has one or more
892
+ // coordinators (e.g. openhive spawned this swarm previously + auto-
893
+ // revived it), resume THOSE agents instead of spawning a brand-new one.
894
+ // Otherwise the UI shows a different coordinator name after every
895
+ // server restart — the prior conversations still exist on disk but get
896
+ // buried under stale, state='stopped' records that the UI treats as
897
+ // dead.
898
+ if (config.bootstrap?.coordinator) {
899
+ const opts = config.bootstrap.coordinator === true
900
+ ? {}
901
+ : config.bootstrap.coordinator;
902
+ const bootstrapCwd = opts.cwd ?? cwd;
903
+ const policy = config.bootstrap.rehydrate ?? "coordinators";
904
+
905
+ // Build the revival set based on policy:
906
+ // - 'none' → empty set (always fall through to fresh spawn)
907
+ // - 'coordinators' → root coordinators at this cwd, running or stopped
908
+ // - 'all' → every agent at this cwd, running or stopped
909
+ //
910
+ // Both 'running' and 'stopped' count as revival candidates. The
911
+ // hosted-swarm graceful-restart path transitions agents to 'stopped'
912
+ // on shutdown; an abrupt parent crash leaves them as 'running'.
913
+ // Either way, the workspace intent survives the restart and we want
914
+ // the same coordinators + their children back. Agents that a user
915
+ // explicitly terminated are tracked with a distinct `stop_reason`
916
+ // and — since explicit termination clears them from the cascade — do
917
+ // not appear in the listAgents result at a cwd they no longer
918
+ // inhabit. The `state='failed'` set is also excluded.
919
+ let priors: import("./agent/agent-store.js").AgentRecord[] = [];
920
+ if (policy === "coordinators") {
921
+ priors = agentStore
922
+ .listAgents({ parent_id: null, role: "coordinator" })
923
+ .filter(
924
+ (a) =>
925
+ a.cwd === bootstrapCwd &&
926
+ (a.state === "running" || a.state === "stopped"),
927
+ );
928
+ } else if (policy === "all") {
929
+ priors = agentStore
930
+ .listAgents()
931
+ .filter(
932
+ (a) =>
933
+ a.cwd === bootstrapCwd &&
934
+ (a.state === "running" || a.state === "stopped"),
935
+ );
936
+ }
937
+
938
+ const rehydrateOrSpawn = async () => {
939
+ if (priors.length > 0) {
940
+ // Parent-first ordering so a child's `resume()` sees its parent
941
+ // already back (lineage bookkeeping, inbox subscriptions). Depth
942
+ // = lineage.length: roots are 0, direct children of roots are 1.
943
+ const byDepth = new Map<number, typeof priors>();
944
+ for (const p of priors) {
945
+ const d = p.lineage.length;
946
+ if (!byDepth.has(d)) byDepth.set(d, []);
947
+ byDepth.get(d)!.push(p);
948
+ }
949
+ const depths = Array.from(byDepth.keys()).sort((a, b) => a - b);
950
+
951
+ const priorIds = new Set(priors.map((p) => p.id));
952
+ const resumed = new Set<string>();
953
+ const failed = new Set<string>();
954
+
955
+ // Stagger spawns — each resume fires a Claude Code subprocess and
956
+ // we don't want a coordinator + five workers all booting at once.
957
+ const CONCURRENCY = 2;
958
+
959
+ for (const depth of depths) {
960
+ const atDepth = byDepth.get(depth)!;
961
+ const eligible = atDepth.filter((a) => {
962
+ if (!a.parent_id) return true; // roots are always eligible
963
+ // Skip children whose parent isn't being revived at all
964
+ // (deliberately stopped, or out of scope for this policy).
965
+ if (!priorIds.has(a.parent_id)) {
966
+ console.warn(
967
+ `[boot-v2] Skipping ${a.role} ${a.id}: parent ${a.parent_id} not in revival set`,
968
+ );
969
+ return false;
970
+ }
971
+ // Skip children whose parent resume failed.
972
+ if (failed.has(a.parent_id)) {
973
+ console.warn(
974
+ `[boot-v2] Skipping ${a.role} ${a.id}: parent ${a.parent_id} failed to resume`,
975
+ );
976
+ return false;
977
+ }
978
+ return resumed.has(a.parent_id);
979
+ });
980
+
981
+ for (let i = 0; i < eligible.length; i += CONCURRENCY) {
982
+ const batch = eligible.slice(i, i + CONCURRENCY);
983
+ await Promise.all(
984
+ batch.map(async (prior) => {
985
+ try {
986
+ const r = await agentManager.resume(prior.id);
987
+ resumed.add(prior.id);
988
+ console.log(
989
+ `[boot-v2] Rehydrated ${prior.role}: ${(r as any).name ?? r.id} at ${prior.cwd}`,
990
+ );
991
+ } catch (err) {
992
+ const msg = (err as Error).message;
993
+ if (/ALREADY_RUNNING/i.test(msg)) {
994
+ // Rare lifecycle race — treat as success so children
995
+ // aren't held back waiting on a parent that's actually
996
+ // already alive.
997
+ resumed.add(prior.id);
998
+ } else {
999
+ failed.add(prior.id);
1000
+ console.warn(
1001
+ `[boot-v2] Failed to rehydrate ${prior.role} ${prior.id}: ${msg}`,
1002
+ );
1003
+ }
1004
+ }
1005
+ }),
1006
+ );
1007
+ }
1008
+ }
1009
+ return;
1010
+ }
1011
+ // No priors matched the policy → fresh spawn (first boot, or 'none').
1012
+ const spawned = await agentManager.spawn({
1013
+ role: "coordinator",
1014
+ parent: null,
1015
+ cwd: bootstrapCwd,
1016
+ task: opts.task ?? "Default coordinator (auto-spawn on boot)",
1017
+ permissionMode: opts.permissionMode,
1018
+ agentType: opts.agentType,
1019
+ customPrompt: opts.customPrompt,
1020
+ });
1021
+ console.log(
1022
+ `[boot-v2] Bootstrap coordinator spawned: ${(spawned as any).name ?? spawned.id} at ${bootstrapCwd}`,
1023
+ );
1024
+ };
1025
+
1026
+ rehydrateOrSpawn().catch((err: Error) => {
1027
+ console.warn(
1028
+ `[boot-v2] Bootstrap coordinator init failed: ${err.message}`,
1029
+ );
1030
+ });
1031
+ }
1032
+
1033
+ // 14. Return system handle
483
1034
  return {
484
1035
  agentManager,
485
1036
  agentStore,
@@ -489,14 +1040,17 @@ export async function bootV2(
489
1040
  controlServer,
490
1041
  roleRegistry,
491
1042
  controlSocketPath,
1043
+ ...(taskDispatcher ? { taskDispatcher } : {}),
492
1044
  ...(apiServer ? { apiServer } : {}),
493
1045
  ...(acpServer ? { acpServer } : {}),
494
1046
  ...(mapServerInstance ? { mapServerInstance } : {}),
495
1047
  ...(mapSidecar ? { mapSidecar } : {}),
496
- _sessionlogSyncLevel: config.sessionlog?.sync ?? config.map?.trajectorySyncLevel ?? "full",
1048
+ _sessionlogSyncLevel:
1049
+ config.sessionlog?.sync ?? config.map?.trajectorySyncLevel ?? "full",
497
1050
 
498
1051
  async shutdown(): Promise<void> {
499
1052
  clearInterval(healthCheckTimer);
1053
+ if (taskDispatcher) await taskDispatcher.stop();
500
1054
  if (mapSidecar) await mapSidecar.stop();
501
1055
  if (mapServerInstance) await mapServerInstance.stop();
502
1056
  if (federationCleanup) federationCleanup();