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
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Claude Code JSONL → ACP SessionUpdate replay
3
+ *
4
+ * Works around a gap in claude-code-acp where `loadSession` does NOT emit
5
+ * `session/update` notifications for historical conversation content — it only
6
+ * tells the Claude Code subprocess to restore context internally. That means
7
+ * clients (like OpenHive) calling ACP's `session/load` get back an empty
8
+ * session.
9
+ *
10
+ * Per the ACP spec (https://agentclientprotocol.com/protocol/session-setup#loading-sessions),
11
+ * loadSession SHOULD stream the conversation history back via session/update
12
+ * notifications. This module reads Claude Code's native JSONL transcript (which
13
+ * Claude Code always persists, no hooks required) and converts each entry to
14
+ * ACP SessionUpdate events so the client can reconstruct the conversation.
15
+ *
16
+ * TODO(upstream): fix claude-code-acp so this workaround becomes unnecessary.
17
+ * See node_modules/@sudocode-ai/claude-code-acp/dist/acp-agent.js:344-353.
18
+ *
19
+ * @module acp/claude-code-replay
20
+ */
21
+ import * as fs from "node:fs/promises";
22
+ import * as path from "node:path";
23
+ import * as os from "node:os";
24
+ import type { SessionUpdate } from "@agentclientprotocol/sdk";
25
+
26
+ /**
27
+ * Locate Claude Code's JSONL transcript for the given session.
28
+ *
29
+ * Claude Code writes transcripts to `~/.claude/projects/{encoded-cwd}/{session-id}.jsonl`.
30
+ * The cwd encoding replaces `/` with `-` (e.g., `/tmp/x` → `-private-tmp-x` on macOS
31
+ * where /tmp is a symlink). Rather than replicating the encoding exactly, we scan
32
+ * project directories for a file matching the session ID — the ID is a UUID so
33
+ * collisions are not a concern.
34
+ */
35
+ async function locateTranscript(providerSessionId: string): Promise<string | null> {
36
+ const projectsRoot = path.join(os.homedir(), ".claude", "projects");
37
+ let dirents: Awaited<ReturnType<typeof fs.readdir>>;
38
+ try {
39
+ // @ts-expect-error — withFileTypes overload returns Dirent[]
40
+ dirents = await fs.readdir(projectsRoot, { withFileTypes: true });
41
+ } catch {
42
+ return null;
43
+ }
44
+ for (const d of dirents as unknown as Array<{ isDirectory(): boolean; name: string }>) {
45
+ if (!d.isDirectory()) continue;
46
+ const candidate = path.join(projectsRoot, d.name, `${providerSessionId}.jsonl`);
47
+ try {
48
+ await fs.access(candidate);
49
+ return candidate;
50
+ } catch {
51
+ // Not in this dir — keep scanning
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ type ContentBlock =
58
+ | { type: "text"; text: string }
59
+ | { type: "tool_use"; id: string; name: string; input: unknown }
60
+ | { type: "tool_result"; tool_use_id: string; content: unknown; is_error?: boolean }
61
+ | { type: "thinking"; thinking?: string; text?: string };
62
+
63
+ interface JsonlEntry {
64
+ type?: string;
65
+ isMeta?: boolean;
66
+ message?: {
67
+ role?: string;
68
+ content?: string | ContentBlock[];
69
+ };
70
+ uuid?: string;
71
+ timestamp?: string;
72
+ }
73
+
74
+ /**
75
+ * Convert a user message content block into an ACP SessionUpdate.
76
+ */
77
+ function userBlockToUpdate(block: ContentBlock): SessionUpdate | null {
78
+ if (block.type === "text" && block.text) {
79
+ return {
80
+ sessionUpdate: "user_message_chunk",
81
+ content: { type: "text", text: block.text },
82
+ } as unknown as SessionUpdate;
83
+ }
84
+ if (block.type === "tool_result") {
85
+ const output =
86
+ typeof block.content === "string"
87
+ ? block.content
88
+ : JSON.stringify(block.content);
89
+ return {
90
+ sessionUpdate: "tool_call_update",
91
+ toolCallId: block.tool_use_id,
92
+ output,
93
+ status: block.is_error ? "failed" : "completed",
94
+ } as unknown as SessionUpdate;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * Convert an assistant message content block into an ACP SessionUpdate.
101
+ */
102
+ function assistantBlockToUpdate(block: ContentBlock): SessionUpdate | null {
103
+ if (block.type === "text" && block.text) {
104
+ return {
105
+ sessionUpdate: "agent_message_chunk",
106
+ content: { type: "text", text: block.text },
107
+ } as unknown as SessionUpdate;
108
+ }
109
+ if (block.type === "tool_use") {
110
+ return {
111
+ sessionUpdate: "tool_call",
112
+ toolCallId: block.id,
113
+ title: block.name,
114
+ rawInput: block.input,
115
+ status: "pending",
116
+ } as unknown as SessionUpdate;
117
+ }
118
+ if (block.type === "thinking") {
119
+ const text = block.thinking ?? block.text ?? "";
120
+ if (!text) return null;
121
+ return {
122
+ sessionUpdate: "agent_thought_chunk",
123
+ content: { type: "text", text },
124
+ } as unknown as SessionUpdate;
125
+ }
126
+ return null;
127
+ }
128
+
129
+ /**
130
+ * Check if a string user-message looks like a Claude Code internal command
131
+ * (e.g., `<command-name>/model</command-name>`, `<local-command-stdout>...`).
132
+ * These get recorded in the JSONL but aren't part of the conversation UX.
133
+ */
134
+ function isInternalCommand(text: string): boolean {
135
+ const t = text.trimStart();
136
+ return (
137
+ t.startsWith("<command-") ||
138
+ t.startsWith("<local-command-") ||
139
+ t.startsWith("<system-reminder") ||
140
+ t.startsWith("Caveat:")
141
+ );
142
+ }
143
+
144
+ /**
145
+ * Read Claude Code's JSONL transcript for a session and yield ACP SessionUpdate
146
+ * events suitable for emitting via `connection.sessionUpdate({ sessionId, update })`.
147
+ *
148
+ * Yields events in chronological order. Returns early (yields nothing) if the
149
+ * transcript file doesn't exist — e.g., the agent is running on a different
150
+ * machine.
151
+ */
152
+ export async function* replayClaudeCodeTranscript(
153
+ providerSessionId: string,
154
+ ): AsyncGenerator<SessionUpdate> {
155
+ const jsonlPath = await locateTranscript(providerSessionId);
156
+ if (!jsonlPath) return;
157
+
158
+ let raw: string;
159
+ try {
160
+ raw = await fs.readFile(jsonlPath, "utf-8");
161
+ } catch {
162
+ return;
163
+ }
164
+
165
+ for (const line of raw.split("\n")) {
166
+ const trimmed = line.trim();
167
+ if (!trimmed) continue;
168
+
169
+ let entry: JsonlEntry;
170
+ try {
171
+ entry = JSON.parse(trimmed);
172
+ } catch {
173
+ continue;
174
+ }
175
+
176
+ // Skip non-conversation entries (queue-operations, summaries, etc.)
177
+ if (entry.type !== "user" && entry.type !== "assistant") continue;
178
+
179
+ // Skip meta messages (local command output, system injections)
180
+ if (entry.isMeta) continue;
181
+
182
+ const message = entry.message;
183
+ if (!message) continue;
184
+ const content = message.content;
185
+
186
+ if (entry.type === "user") {
187
+ if (typeof content === "string") {
188
+ if (isInternalCommand(content)) continue;
189
+ yield {
190
+ sessionUpdate: "user_message_chunk",
191
+ content: { type: "text", text: content },
192
+ } as unknown as SessionUpdate;
193
+ } else if (Array.isArray(content)) {
194
+ for (const block of content) {
195
+ const upd = userBlockToUpdate(block);
196
+ if (upd) yield upd;
197
+ }
198
+ }
199
+ } else if (entry.type === "assistant") {
200
+ if (Array.isArray(content)) {
201
+ for (const block of content) {
202
+ const upd = assistantBlockToUpdate(block);
203
+ if (upd) yield upd;
204
+ }
205
+ }
206
+ }
207
+ }
208
+ }
@@ -154,6 +154,20 @@ export function createMacroAgent(
154
154
  let firstPrompt: string | null = null;
155
155
  const sessionStartedAt = new Date().toISOString();
156
156
 
157
+ // Per-session update history for ACP session/load replay.
158
+ // Per the ACP spec, loadSession should re-emit all session/update notifications
159
+ // previously sent so the client can reconstruct state. We buffer them here
160
+ // keyed by ACP session ID.
161
+ const sessionUpdateHistory = new Map<string, SessionUpdate[]>();
162
+ function appendSessionUpdate(acpSessionId: string, update: SessionUpdate): void {
163
+ const arr = sessionUpdateHistory.get(acpSessionId);
164
+ if (arr) {
165
+ arr.push(update);
166
+ } else {
167
+ sessionUpdateHistory.set(acpSessionId, [update]);
168
+ }
169
+ }
170
+
157
171
  // Git info (cached — read once per session)
158
172
  let gitInfo: { branch: string | null; commitHash: string | null; remoteUrl: string | null } | null = null;
159
173
  function getGitInfo(cwd: string): { branch: string | null; commitHash: string | null; remoteUrl: string | null } {
@@ -414,20 +428,46 @@ export function createMacroAgent(
414
428
  params: NewSessionRequest,
415
429
  ): Promise<NewSessionResponse> {
416
430
  const cwd = params.cwd ?? defaultCwd;
417
- // Get or create a head manager for this workspace
418
- const headManager = await agentManager.getOrCreateHeadManager({ cwd });
431
+
432
+ // Two paths into newSession:
433
+ //
434
+ // 1. MAP-bound stream — initConfig.targetAgentId is set by the ACP
435
+ // bridge to the local agent ID this stream was opened against.
436
+ // Bind the session to that specific agent (any role), preserving
437
+ // the routing intent that brought the stream here. This matters
438
+ // when multiple coordinators share a cwd: cwd-based lookup would
439
+ // pick whichever the store returned first, which may not be the
440
+ // one the client actually wanted to talk to.
441
+ //
442
+ // 2. Fallback — pure ACP client with no agent context. Use
443
+ // cwd-based head-manager lookup (spawning one if needed). This
444
+ // keeps stock ACP clients working without protocol changes.
445
+ let target: { id: string; session_id: string };
446
+ if (initConfig?.targetAgentId) {
447
+ const bound = agentManager.getActiveAgentSession(
448
+ initConfig.targetAgentId as any,
449
+ );
450
+ if (!bound) {
451
+ throw new ACPError(
452
+ `Agent ${initConfig.targetAgentId} is not running or has no active session`,
453
+ "AGENT_NOT_FOUND",
454
+ { agentId: initConfig.targetAgentId },
455
+ );
456
+ }
457
+ target = { id: bound.id, session_id: bound.session_id };
458
+ } else {
459
+ const headManager = await agentManager.getOrCreateHeadManager({ cwd });
460
+ target = { id: headManager.id, session_id: headManager.session_id };
461
+ }
419
462
 
420
463
  // Create session mapping
421
- const mapping = sessionMapper.createMapping(
422
- headManager.session_id,
423
- headManager.id,
424
- );
464
+ const mapping = sessionMapper.createMapping(target.session_id, target.id);
425
465
 
426
466
  // Annotate sessionlog with swarm metadata (best effort)
427
467
  try {
428
468
  const { annotateSession } = await import("../integrations/sessionlog.js");
429
469
  annotateSession(cwd, {
430
- swarmId: headManager.id,
470
+ swarmId: target.id,
431
471
  scope: "macro-agent",
432
472
  });
433
473
  } catch {
@@ -444,9 +484,93 @@ export function createMacroAgent(
444
484
  ): Promise<LoadSessionResponse> {
445
485
  const sessionId = params.sessionId;
446
486
 
487
+ /**
488
+ * Resolve the provider_session_id (Claude Code's UUID for the session)
489
+ * for a given macro-agent acp session. Needed to locate the JSONL
490
+ * transcript on disk as a fallback when the in-memory buffer is empty.
491
+ */
492
+ const resolveProviderSessionId = (agentId: string): string | undefined => {
493
+ const store = (system as any).agentStore;
494
+ const rec = store?.getAgent?.(agentId);
495
+ const meta = rec?.metadata as Record<string, unknown> | undefined;
496
+ const psid = meta?.provider_session_id;
497
+ return typeof psid === "string" ? psid : undefined;
498
+ };
499
+
500
+ // Replay helper: re-emit session/update notifications so the client can
501
+ // rebuild its view of the conversation. Per the ACP spec, loadSession
502
+ // SHOULD emit all previously-sent session/update events.
503
+ //
504
+ // Two sources in priority order:
505
+ // 1. In-memory buffer (populated by prior prompts in this process).
506
+ // Fast and accurate — exactly what was streamed to the client.
507
+ // 2. Claude Code's JSONL transcript on disk (durable across restarts).
508
+ // Used when the buffer is empty — e.g., we got a loadSession for a
509
+ // session that was prompted but whose updates were never buffered,
510
+ // or after a process restart within the same session's lifetime.
511
+ //
512
+ // This is a workaround: the ACP spec expects the underlying agent
513
+ // (claude-code-acp) to emit session/update during loadSession, but its
514
+ // current implementation only passes `resume: sessionId` to Claude Code
515
+ // SDK for internal context restoration without surfacing history to the
516
+ // client. See claude-code-replay.ts for details.
517
+ const replayHistory = async (agentIdForLookup?: string, explicitProviderSessionId?: string): Promise<void> => {
518
+ const buffered = sessionUpdateHistory.get(sessionId);
519
+ if (buffered && buffered.length > 0) {
520
+ for (const update of buffered) {
521
+ try {
522
+ await connection.sessionUpdate({ sessionId, update });
523
+ } catch {
524
+ // Best effort — continue replaying remaining updates
525
+ }
526
+ }
527
+ return;
528
+ }
529
+
530
+ // Buffer empty — fall back to Claude Code JSONL if we can locate it.
531
+ // Provider session ID resolution priority:
532
+ // 1. Explicit — passed via ACP LoadSessionRequest._meta.provider_session_id
533
+ // (survives macro-agent restart since it comes from the client)
534
+ // 2. From agent metadata — when sessionMapper still has the mapping
535
+ const providerSessionId =
536
+ explicitProviderSessionId ??
537
+ (agentIdForLookup ? resolveProviderSessionId(agentIdForLookup) : undefined);
538
+ if (!providerSessionId) return;
539
+
540
+ try {
541
+ const { replayClaudeCodeTranscript } = await import("./claude-code-replay.js");
542
+ const bufferForFuture: SessionUpdate[] = [];
543
+ for await (const update of replayClaudeCodeTranscript(providerSessionId)) {
544
+ try {
545
+ await connection.sessionUpdate({ sessionId, update });
546
+ bufferForFuture.push(update);
547
+ } catch {
548
+ // Best effort — continue
549
+ }
550
+ }
551
+ // Populate the in-memory buffer so subsequent loadSession calls use
552
+ // the fast path. Only if we actually read something.
553
+ if (bufferForFuture.length > 0) {
554
+ sessionUpdateHistory.set(sessionId, bufferForFuture);
555
+ }
556
+ } catch {
557
+ // Transcript read failed — no history to emit
558
+ }
559
+ };
560
+
561
+ // Extract optional provider_session_id from ACP LoadSessionRequest._meta.
562
+ // Clients (like OpenHive) that know the underlying Claude Code session ID
563
+ // can pass it here to enable history recovery across macro-agent restarts
564
+ // (when sessionMapper is empty because it's in-memory only).
565
+ const metaProviderSessionId =
566
+ typeof (params as any)._meta?.provider_session_id === "string"
567
+ ? ((params as any)._meta.provider_session_id as string)
568
+ : undefined;
569
+
447
570
  // Check if we already have a mapping for this session
448
571
  let mapping = sessionMapper.getMapping(sessionId);
449
572
  if (mapping) {
573
+ await replayHistory(mapping.agentId, metaProviderSessionId);
450
574
  return {};
451
575
  }
452
576
 
@@ -459,6 +583,52 @@ export function createMacroAgent(
459
583
  resumed.session_id,
460
584
  resumed.id,
461
585
  );
586
+ await replayHistory(resumed.id, metaProviderSessionId);
587
+ return {};
588
+ }
589
+
590
+ // No in-memory mapping and the ACP sessionId isn't an agent ID. If the
591
+ // client supplied provider_session_id via _meta, reverse-lookup the
592
+ // owning agent in the agent-store and resume it. This is the durable
593
+ // cross-restart recovery path: macro-agent's sessionMapper is
594
+ // in-memory, so after a process restart it's empty, but the agent +
595
+ // session records survive on disk keyed by provider_session_id.
596
+ //
597
+ // The critical piece is creating the sessionMapper entry here — without
598
+ // it, subsequent `prompt` calls throw `session not found` and the ACP
599
+ // layer catches that and returns stopReason:"cancelled", making the
600
+ // session appear unresponsive.
601
+ if (metaProviderSessionId) {
602
+ const store = (system as any).agentStore;
603
+ const sessionRec = typeof store?.findSessionByProviderSessionId === "function"
604
+ ? store.findSessionByProviderSessionId(metaProviderSessionId)
605
+ : undefined;
606
+ if (sessionRec?.agent_id) {
607
+ const agentId = sessionRec.agent_id;
608
+ try {
609
+ // Idempotent: resume() throws ALREADY_RUNNING if the agent is
610
+ // already active (e.g. _macro/resumeAgent just brought it back).
611
+ // We still need to bind the ACP session to this agent.
612
+ if (!agentManager.hasActiveSession(agentId as any)) {
613
+ await agentManager.resume(agentId as any);
614
+ }
615
+ // Bind under BOTH the macro-agent ACP sessionId AND the provider
616
+ // session UUID. The MAP SDK's ACPStreamConnection often ends up
617
+ // storing _meta.provider_session_id as its stream.sessionId —
618
+ // which is what swarmcraft echoes back in session/prompt. Without
619
+ // the UUID mapping, prompt hits sessionMapper with the UUID key
620
+ // and fails (→ stopReason: cancelled).
621
+ sessionMapper.createMapping(sessionId, agentId);
622
+ if (metaProviderSessionId !== sessionId) {
623
+ sessionMapper.createMapping(metaProviderSessionId as any, agentId);
624
+ }
625
+ await replayHistory(agentId, metaProviderSessionId);
626
+ return {};
627
+ } catch {
628
+ // Fall through to history-only replay below
629
+ }
630
+ }
631
+ await replayHistory(undefined, metaProviderSessionId);
462
632
  return {};
463
633
  }
464
634
 
@@ -488,6 +658,20 @@ export function createMacroAgent(
488
658
  }
489
659
  const message = textParts.join("\n") || "";
490
660
 
661
+ // Record the user prompt as a user_message_chunk update in the replay
662
+ // buffer so session/load can reconstruct the full conversation. The
663
+ // agent itself doesn't emit this — the client sent it — but it IS part
664
+ // of the session's logical state.
665
+ for (const block of params.prompt) {
666
+ if ("text" in block && typeof block.text === "string" && block.text) {
667
+ const userChunk = {
668
+ sessionUpdate: "user_message_chunk",
669
+ content: { type: "text", text: block.text },
670
+ } as unknown as SessionUpdate;
671
+ appendSessionUpdate(params.sessionId, userChunk);
672
+ }
673
+ }
674
+
491
675
  sessionMapper.setProcessing(params.sessionId, true);
492
676
 
493
677
  // Capture first prompt for trajectory metadata (used as session description in OpenHive UI)
@@ -507,7 +691,7 @@ export function createMacroAgent(
507
691
  if (mapServer) {
508
692
  const agents = mapServer.agents?.list?.() ?? [];
509
693
  const mapAgent = agents.find(
510
- (a: any) => a.metadata?.localAgentId === agentId,
694
+ (a: any) => a.metadata?.peerAgentId === agentId,
511
695
  );
512
696
  if (mapAgent) {
513
697
  mapServer.agents.updateState(mapAgent.id, "busy");
@@ -703,6 +887,10 @@ export function createMacroAgent(
703
887
  sessionId: params.sessionId,
704
888
  update,
705
889
  };
890
+ // Buffer for replay on session/load. Per the ACP spec, loadSession
891
+ // should re-emit all previously sent session/update notifications
892
+ // so the client can reconstruct state after reconnect.
893
+ appendSessionUpdate(params.sessionId, update);
706
894
  await connection.sessionUpdate(notification);
707
895
  }
708
896
  }
@@ -742,6 +930,11 @@ export function createMacroAgent(
742
930
  phase: "active",
743
931
  startedAt: sessionStartedAt,
744
932
  label: `Step ${checkpointCounter} (${toolCallCount} tool calls)`,
933
+ // Underlying Claude Code session ID — lets OpenHive find the
934
+ // JSONL transcript on disk for history recovery even if the
935
+ // macro-agent process dies.
936
+ provider_session_id:
937
+ (agentRecord as any)?.metadata?.provider_session_id ?? undefined,
745
938
  // metrics fields
746
939
  ...(isMetrics ? {
747
940
  duration_ms: Date.now() - promptStartTime,
@@ -773,7 +966,7 @@ export function createMacroAgent(
773
966
  }
774
967
 
775
968
  return { stopReason: "end_turn" };
776
- } catch (err) {
969
+ } catch {
777
970
  // If prompt fails, still return a valid response
778
971
  return { stopReason: "cancelled" };
779
972
  } finally {
@@ -785,7 +978,7 @@ export function createMacroAgent(
785
978
  if (mapServer) {
786
979
  const agents = mapServer.agents?.list?.() ?? [];
787
980
  const mapAgent = agents.find(
788
- (a: any) => a.metadata?.localAgentId === agentId,
981
+ (a: any) => a.metadata?.peerAgentId === agentId,
789
982
  );
790
983
  if (mapAgent) {
791
984
  mapServer.agents.updateState(mapAgent.id, "idle");
package/src/acp/types.ts CHANGED
@@ -54,6 +54,16 @@ export interface MacroAgentInitConfig {
54
54
 
55
55
  /** Suffix appended to system prompts */
56
56
  systemPromptSuffix?: string;
57
+
58
+ /**
59
+ * Local agent ID this ACP stream is bound to. When set, `session/new` binds
60
+ * the new session to this specific agent (any role) instead of falling back
61
+ * to cwd-based head-manager lookup. Set by the ACP-over-MAP bridge so that
62
+ * MAP-level routing (which already targets a specific agent) is preserved
63
+ * end-to-end through the ACP layer — important when multiple coordinators
64
+ * share the same cwd.
65
+ */
66
+ targetAgentId?: string;
57
67
  }
58
68
 
59
69
  // ─────────────────────────────────────────────────────────────────
@@ -185,6 +185,7 @@ describe("TasksAdapter", () => {
185
185
 
186
186
  expect(mockClient.query).toHaveBeenCalledWith({
187
187
  ready: { tags: ["backend"], limit: 5 },
188
+ verbose: true,
188
189
  });
189
190
  });
190
191
  });
@@ -88,6 +88,7 @@ export class DefaultTasksAdapter implements ITasksAdapter {
88
88
  parent_id: opts.parent,
89
89
  tags: opts.tags,
90
90
  priority: opts.priority,
91
+ metadata: opts.metadata,
91
92
  });
92
93
 
93
94
  return node?.id ?? "";
@@ -129,6 +130,7 @@ export class DefaultTasksAdapter implements ITasksAdapter {
129
130
  limit: opts?.limit,
130
131
  tags: opts?.tags,
131
132
  },
133
+ verbose: true,
132
134
  });
133
135
 
134
136
  return (result.items ?? []).map((n: NodeSummaryLike) =>
@@ -146,6 +148,7 @@ export class DefaultTasksAdapter implements ITasksAdapter {
146
148
  tags: filter?.tags,
147
149
  limit: filter?.limit,
148
150
  },
151
+ verbose: true,
149
152
  });
150
153
 
151
154
  return (result.items ?? []).map((n: NodeSummaryLike) =>
@@ -188,6 +188,7 @@ export interface CreateTaskOptions {
188
188
  parent?: string;
189
189
  tags?: string[];
190
190
  priority?: number;
191
+ metadata?: Record<string, unknown>;
191
192
  }
192
193
 
193
194
  /**
@@ -0,0 +1,73 @@
1
+ /**
2
+ * AgentManagerV2 + TopologyPolicy integration (Phase 4).
3
+ *
4
+ * Verifies that when a TopologyPolicy is set, workspace allocation goes
5
+ * through the V3 path (YamlDrivenTopology → WorkspaceDecision → V3 methods).
6
+ * When unset, legacy role-name dispatch is preserved (regression guard).
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
10
+ import { YamlDrivenTopology } from '../../workspace/topology/yaml-driven.js';
11
+ import { parseTeamWorkspaceConfig } from '../../workspace/yaml-schema.js';
12
+
13
+ // Isolated test: verify the TopologyPolicy → WorkspaceDecision compilation
14
+ // happens when policy is set. Full AgentManagerV2 spawn integration is
15
+ // covered by E2E tests; here we validate the delegation logic in isolation.
16
+
17
+ describe('AgentManagerV2 + TopologyPolicy (Phase 4)', () => {
18
+ describe('policy presence', () => {
19
+ it('YamlDrivenTopology is the primary policy for declarative teams', async () => {
20
+ const config = parseTeamWorkspaceConfig({
21
+ roles: {
22
+ peer: {
23
+ workspace: 'new_stream',
24
+ stream_lineage: 'fork_from_team_root',
25
+ },
26
+ },
27
+ });
28
+ const topology = new YamlDrivenTopology(config!);
29
+
30
+ // Policy name is stable (used for introspection)
31
+ expect(topology.name).toBe('yaml-driven');
32
+ });
33
+
34
+ it('onAgentSpawn is invocable via policy reference', async () => {
35
+ const config = parseTeamWorkspaceConfig({
36
+ roles: {
37
+ peer: { workspace: 'none' },
38
+ },
39
+ });
40
+ const topology = new YamlDrivenTopology(config!);
41
+
42
+ const mockWs = {
43
+ createStreamV3: vi.fn(() => 'stream-1'),
44
+ allocateWorktree: vi.fn(),
45
+ } as unknown as import('../../workspace/types.js').WorkspaceManager;
46
+
47
+ await topology.onTeamStart({
48
+ teamName: 't',
49
+ teamInstanceId: 't-1',
50
+ workspaceConfig: config,
51
+ workspaceManager: mockWs,
52
+ });
53
+
54
+ const decision = await topology.onAgentSpawn({
55
+ agentId: 'a1',
56
+ role: 'peer',
57
+ workspaceManager: mockWs,
58
+ });
59
+
60
+ expect(decision.kind).toBe('none');
61
+ });
62
+ });
63
+
64
+ describe('policy absence (legacy path)', () => {
65
+ it('policy=null means AgentManagerV2 uses role-name dispatch (regression guard)', () => {
66
+ // This test documents the behavior contract: when
67
+ // setTopologyPolicy(null) or never called, the legacy switch(role)
68
+ // path in agent-manager-v2.ts:createWorkspaceForRole is the active
69
+ // code path. Validated by existing E2E tests for self-driving.
70
+ expect(true).toBe(true);
71
+ });
72
+ });
73
+ });