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
@@ -452,6 +452,72 @@ describe("AgentManagerV2", () => {
452
452
  });
453
453
  });
454
454
 
455
+ // ── getOrCreateHeadManager() ───────────────────────────────
456
+
457
+ describe("getOrCreateHeadManager()", () => {
458
+ it("reuses an existing head manager matching the requested cwd", async () => {
459
+ const first = await manager.getOrCreateHeadManager({ cwd: "/tmp/proj-a" });
460
+ const second = await manager.getOrCreateHeadManager({ cwd: "/tmp/proj-a" });
461
+ expect(second.id).toBe(first.id);
462
+ });
463
+
464
+ it("spawns a distinct head manager for a different cwd", async () => {
465
+ const a = await manager.getOrCreateHeadManager({ cwd: "/tmp/proj-a" });
466
+ const b = await manager.getOrCreateHeadManager({ cwd: "/tmp/proj-b" });
467
+ expect(b.id).not.toBe(a.id);
468
+ });
469
+
470
+ it("getActiveAgentSession returns null for unknown agents", () => {
471
+ expect(manager.getActiveAgentSession("never-spawned" as AgentId)).toBeNull();
472
+ });
473
+
474
+ it("getActiveAgentSession returns the spawned shape for any role with a live session", async () => {
475
+ // Spawn a non-coordinator (worker) so we exercise the role-agnostic path
476
+ const worker = await manager.spawn({ task: "Test work", role: "worker" });
477
+
478
+ const session = manager.getActiveAgentSession(worker.id as AgentId);
479
+ expect(session).not.toBeNull();
480
+ expect(session!.id).toBe(worker.id);
481
+ expect(session!.agent.role).toBe("worker");
482
+ });
483
+
484
+ it("getActiveAgentSession returns null after the agent is terminated", async () => {
485
+ const worker = await manager.spawn({ task: "Test work", role: "worker" });
486
+ await manager.terminate(worker.id as AgentId, "completed");
487
+ expect(manager.getActiveAgentSession(worker.id as AgentId)).toBeNull();
488
+ });
489
+
490
+ it("ignores stale 'running' store records whose session isn't live in this process", async () => {
491
+ // Simulate a crashed prior process: a coordinator record exists in the
492
+ // store with state='running' and parent_id=null, but has no matching
493
+ // activeSessions entry (the session was lost when the process died).
494
+ // Without the activeSessions filter in the predicate, getOrCreate would
495
+ // erroneously reuse this stale record and skip spawning.
496
+ agentStore.putAgent({
497
+ id: "stale-coord" as AgentId,
498
+ name: "stale-coord",
499
+ role: "coordinator",
500
+ state: "running",
501
+ parent_id: null,
502
+ lineage: [],
503
+ team: null,
504
+ scope: "default",
505
+ task: "",
506
+ task_id: "",
507
+ cwd: "/tmp/stale-proj",
508
+ capabilities: [],
509
+ created_at: Date.now() as any,
510
+ started_at: Date.now() as any,
511
+ config: {},
512
+ metadata: {},
513
+ });
514
+
515
+ const result = await manager.getOrCreateHeadManager({ cwd: "/tmp/stale-proj" });
516
+ expect(result.id).not.toBe("stale-coord");
517
+ expect(manager.hasActiveSession(result.id as AgentId)).toBe(true);
518
+ });
519
+ });
520
+
455
521
  // ── Lifecycle Callbacks ────────────────────────────────────
456
522
 
457
523
  describe("lifecycle callbacks", () => {
@@ -409,5 +409,57 @@ describe("AgentStore", () => {
409
409
  const session = store.getSession("agent-1")!;
410
410
  expect(session.provider_session_id).toBeUndefined();
411
411
  });
412
+
413
+ it("findSessionByProviderSessionId returns the matching session", () => {
414
+ store.putAgent(makeAgent({ id: "agent-2" }));
415
+ store.putSession({
416
+ agent_id: "agent-1",
417
+ session_id: "session-abc",
418
+ provider_session_id: "psid-xyz",
419
+ created_at: Date.now(),
420
+ });
421
+ store.putSession({
422
+ agent_id: "agent-2",
423
+ session_id: "session-def",
424
+ provider_session_id: "psid-other",
425
+ created_at: Date.now(),
426
+ });
427
+
428
+ const found = store.findSessionByProviderSessionId("psid-xyz");
429
+ expect(found).not.toBeNull();
430
+ expect(found!.agent_id).toBe("agent-1");
431
+ expect(found!.session_id).toBe("session-abc");
432
+ });
433
+
434
+ it("findSessionByProviderSessionId returns null when no match", () => {
435
+ store.putSession({
436
+ agent_id: "agent-1",
437
+ session_id: "session-1",
438
+ provider_session_id: "psid-1",
439
+ created_at: Date.now(),
440
+ });
441
+ expect(store.findSessionByProviderSessionId("psid-missing")).toBeNull();
442
+ });
443
+
444
+ it("findSessionByProviderSessionId returns most recent on duplicate psid", () => {
445
+ // Shouldn't normally happen but verify the ORDER BY created_at DESC path.
446
+ store.putAgent(makeAgent({ id: "agent-old" }));
447
+ store.putAgent(makeAgent({ id: "agent-new" }));
448
+ store.putSession({
449
+ agent_id: "agent-old",
450
+ session_id: "session-old",
451
+ provider_session_id: "psid-dup",
452
+ created_at: 1000,
453
+ });
454
+ store.putSession({
455
+ agent_id: "agent-new",
456
+ session_id: "session-new",
457
+ provider_session_id: "psid-dup",
458
+ created_at: 2000,
459
+ });
460
+
461
+ const found = store.findSessionByProviderSessionId("psid-dup");
462
+ expect(found!.agent_id).toBe("agent-new");
463
+ });
412
464
  });
413
465
  });
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Tests for AgentManagerV2's three-level TaskRef resolution:
3
+ * 1. Explicit SpawnAgentOptions.taskRef wins.
4
+ * 2. resolveTaskRef(opts) runs when no explicit ref.
5
+ * 3. Fallback to taskResourceId + options.task_id when both above yield nothing.
6
+ *
7
+ * Verifies the G3/G14 closure: single-graph default via boot config + multi-graph
8
+ * resolver for deployments that touch more than one opentasks graph.
9
+ */
10
+
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
12
+ import { createAgentManagerV2 } from "../agent-manager-v2.js";
13
+ import { AgentStore } from "../agent-store.js";
14
+ import type { AgentManager } from "../agent-manager.js";
15
+ import type { InboxAdapter, TasksAdapter } from "../../adapters/types.js";
16
+ import type { SpawnAgentOptions } from "../types.js";
17
+ import type { TaskRef } from "git-cascade/events";
18
+
19
+ vi.mock("acp-factory", () => ({
20
+ AgentFactory: {
21
+ spawn: vi.fn().mockResolvedValue({
22
+ createSession: vi.fn().mockResolvedValue({
23
+ id: "provider-session-1",
24
+ prompt: vi.fn().mockReturnValue({
25
+ [Symbol.asyncIterator]: () => ({
26
+ next: () => Promise.resolve({ done: true, value: undefined }),
27
+ }),
28
+ }),
29
+ forkWithFlush: vi.fn().mockResolvedValue({ id: "forked-session-1" }),
30
+ }),
31
+ loadSession: vi.fn(),
32
+ close: vi.fn().mockResolvedValue(undefined),
33
+ isRunning: vi.fn().mockReturnValue(true),
34
+ }),
35
+ },
36
+ }));
37
+
38
+ function mockInbox(): InboxAdapter {
39
+ return {
40
+ registerAgent: vi.fn().mockResolvedValue(undefined),
41
+ deregisterAgent: vi.fn().mockResolvedValue(undefined),
42
+ send: vi.fn().mockResolvedValue("msg-1"),
43
+ onDelivery: vi.fn(),
44
+ offDelivery: vi.fn(),
45
+ checkInbox: vi.fn().mockResolvedValue([]),
46
+ readThread: vi.fn().mockResolvedValue([]),
47
+ setSignalFilter: vi.fn(),
48
+ setEmissionValidator: vi.fn(),
49
+ socketPath: "/tmp/test-inbox.sock",
50
+ stop: vi.fn().mockResolvedValue(undefined),
51
+ } as unknown as InboxAdapter;
52
+ }
53
+
54
+ function mockTasks(): TasksAdapter {
55
+ return {
56
+ createTask: vi.fn().mockResolvedValue("ot-task-1"),
57
+ assignTask: vi.fn().mockResolvedValue(undefined),
58
+ transitionTask: vi.fn().mockResolvedValue(undefined),
59
+ getTask: vi.fn().mockResolvedValue({ id: "t-1", title: "x", status: "open" }),
60
+ queryReady: vi.fn().mockResolvedValue([]),
61
+ listTasks: vi.fn().mockResolvedValue([]),
62
+ addBlocker: vi.fn().mockResolvedValue(undefined),
63
+ removeBlocker: vi.fn().mockResolvedValue(undefined),
64
+ claimTask: vi.fn().mockResolvedValue(null),
65
+ unclaimTask: vi.fn().mockResolvedValue(undefined),
66
+ listClaimable: vi.fn().mockResolvedValue([]),
67
+ connect: vi.fn().mockResolvedValue(undefined),
68
+ disconnect: vi.fn(),
69
+ connected: true,
70
+ } as unknown as TasksAdapter;
71
+ }
72
+
73
+ describe("AgentManagerV2 taskRef resolution", () => {
74
+ let agentStore: AgentStore;
75
+ let inbox: InboxAdapter;
76
+ let tasks: TasksAdapter;
77
+ let manager: AgentManager;
78
+
79
+ afterEach(async () => {
80
+ await manager?.close();
81
+ agentStore?.close();
82
+ });
83
+
84
+ beforeEach(() => {
85
+ agentStore = new AgentStore(":memory:");
86
+ inbox = mockInbox();
87
+ tasks = mockTasks();
88
+ });
89
+
90
+ function getStoredTaskRef(agentId: string): TaskRef | undefined {
91
+ const record = agentStore.getAgent(agentId);
92
+ const meta = record?.metadata as Record<string, unknown> | undefined;
93
+ return meta?.task_ref as TaskRef | undefined;
94
+ }
95
+
96
+ it("uses taskResourceId + task_id when no other source resolves a ref", async () => {
97
+ manager = createAgentManagerV2(agentStore, inbox, tasks, {
98
+ defaultCwd: "/tmp/t",
99
+ taskResourceId: "resource-default",
100
+ });
101
+
102
+ const result = await manager.spawn({
103
+ task: "work",
104
+ task_id: "node-42" as unknown as SpawnAgentOptions["task_id"],
105
+ role: "worker",
106
+ });
107
+
108
+ expect(getStoredTaskRef(result.id)).toEqual({
109
+ resource_id: "resource-default",
110
+ node_id: "node-42",
111
+ });
112
+ });
113
+
114
+ it("falls through to no taskRef when neither resolver nor taskResourceId apply", async () => {
115
+ manager = createAgentManagerV2(agentStore, inbox, tasks, {
116
+ defaultCwd: "/tmp/t",
117
+ // No taskResourceId, no resolveTaskRef.
118
+ });
119
+
120
+ const result = await manager.spawn({ task: "work", role: "worker" });
121
+
122
+ expect(getStoredTaskRef(result.id)).toBeUndefined();
123
+ });
124
+
125
+ it("resolveTaskRef wins over the taskResourceId fallback", async () => {
126
+ const resolveTaskRef = vi
127
+ .fn()
128
+ .mockReturnValue({ resource_id: "resource-from-resolver", node_id: "n1" });
129
+
130
+ manager = createAgentManagerV2(agentStore, inbox, tasks, {
131
+ defaultCwd: "/tmp/t",
132
+ taskResourceId: "resource-default",
133
+ resolveTaskRef,
134
+ });
135
+
136
+ const result = await manager.spawn({ task: "work", role: "worker" });
137
+
138
+ expect(resolveTaskRef).toHaveBeenCalledOnce();
139
+ expect(getStoredTaskRef(result.id)).toEqual({
140
+ resource_id: "resource-from-resolver",
141
+ node_id: "n1",
142
+ });
143
+ });
144
+
145
+ it("resolveTaskRef returning undefined falls through to taskResourceId", async () => {
146
+ const resolveTaskRef = vi.fn().mockReturnValue(undefined);
147
+
148
+ manager = createAgentManagerV2(agentStore, inbox, tasks, {
149
+ defaultCwd: "/tmp/t",
150
+ taskResourceId: "resource-default",
151
+ resolveTaskRef,
152
+ });
153
+
154
+ const result = await manager.spawn({
155
+ task: "work",
156
+ task_id: "node-9" as unknown as SpawnAgentOptions["task_id"],
157
+ role: "worker",
158
+ });
159
+
160
+ expect(resolveTaskRef).toHaveBeenCalledOnce();
161
+ expect(getStoredTaskRef(result.id)).toEqual({
162
+ resource_id: "resource-default",
163
+ node_id: "node-9",
164
+ });
165
+ });
166
+
167
+ it("explicit SpawnAgentOptions.taskRef wins over resolver AND taskResourceId", async () => {
168
+ const resolveTaskRef = vi
169
+ .fn()
170
+ .mockReturnValue({ resource_id: "resource-from-resolver", node_id: "r1" });
171
+
172
+ manager = createAgentManagerV2(agentStore, inbox, tasks, {
173
+ defaultCwd: "/tmp/t",
174
+ taskResourceId: "resource-default",
175
+ resolveTaskRef,
176
+ });
177
+
178
+ const explicit: TaskRef = { resource_id: "resource-explicit", node_id: "e1" };
179
+ const result = await manager.spawn({
180
+ task: "work",
181
+ role: "worker",
182
+ taskRef: explicit,
183
+ });
184
+
185
+ // Resolver should NOT be called when caller already supplied taskRef.
186
+ expect(resolveTaskRef).not.toHaveBeenCalled();
187
+ expect(getStoredTaskRef(result.id)).toEqual(explicit);
188
+ });
189
+
190
+ it("resolver receives the intercepted SpawnAgentOptions (post-interceptor)", async () => {
191
+ const resolveTaskRef = vi.fn().mockReturnValue(undefined);
192
+
193
+ manager = createAgentManagerV2(agentStore, inbox, tasks, {
194
+ defaultCwd: "/tmp/t",
195
+ resolveTaskRef,
196
+ });
197
+ manager.setSpawnInterceptor((opts) => ({ ...opts, cwd: "/overridden/cwd" }));
198
+
199
+ await manager.spawn({ task: "x", role: "worker" });
200
+
201
+ expect(resolveTaskRef).toHaveBeenCalledOnce();
202
+ const passed = resolveTaskRef.mock.calls[0][0];
203
+ expect(passed.cwd).toBe("/overridden/cwd");
204
+ });
205
+
206
+ it("resolver throwing does not block spawn; falls through to taskResourceId", async () => {
207
+ const resolveTaskRef = vi.fn().mockImplementation(() => {
208
+ throw new Error("resolver exploded");
209
+ });
210
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
211
+
212
+ manager = createAgentManagerV2(agentStore, inbox, tasks, {
213
+ defaultCwd: "/tmp/t",
214
+ taskResourceId: "resource-safe",
215
+ resolveTaskRef,
216
+ });
217
+
218
+ const result = await manager.spawn({
219
+ task: "x",
220
+ task_id: "safe-node" as unknown as SpawnAgentOptions["task_id"],
221
+ role: "worker",
222
+ });
223
+
224
+ expect(warnSpy).toHaveBeenCalled();
225
+ expect(getStoredTaskRef(result.id)).toEqual({
226
+ resource_id: "resource-safe",
227
+ node_id: "safe-node",
228
+ });
229
+ warnSpy.mockRestore();
230
+ });
231
+ });