macro-agent 0.1.0 → 0.1.1

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 (337) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/.sudocode/specs.jsonl +4 -0
  3. package/CLAUDE.md +16 -14
  4. package/README.md +11 -29
  5. package/dist/acp/macro-agent.d.ts +15 -0
  6. package/dist/acp/macro-agent.d.ts.map +1 -1
  7. package/dist/acp/macro-agent.js +131 -35
  8. package/dist/acp/macro-agent.js.map +1 -1
  9. package/dist/acp/types.d.ts +32 -1
  10. package/dist/acp/types.d.ts.map +1 -1
  11. package/dist/acp/types.js.map +1 -1
  12. package/dist/agent/agent-manager.d.ts +65 -1
  13. package/dist/agent/agent-manager.d.ts.map +1 -1
  14. package/dist/agent/agent-manager.js +464 -183
  15. package/dist/agent/agent-manager.js.map +1 -1
  16. package/dist/agent/types.d.ts +1 -1
  17. package/dist/agent/types.d.ts.map +1 -1
  18. package/dist/api/server.d.ts +3 -0
  19. package/dist/api/server.d.ts.map +1 -1
  20. package/dist/api/server.js +37 -6
  21. package/dist/api/server.js.map +1 -1
  22. package/dist/auth/index.d.ts +2 -0
  23. package/dist/auth/index.d.ts.map +1 -0
  24. package/dist/auth/index.js +2 -0
  25. package/dist/auth/index.js.map +1 -0
  26. package/dist/auth/token.d.ts +41 -0
  27. package/dist/auth/token.d.ts.map +1 -0
  28. package/dist/auth/token.js +73 -0
  29. package/dist/auth/token.js.map +1 -0
  30. package/dist/cli/acp.d.ts +2 -23
  31. package/dist/cli/acp.d.ts.map +1 -1
  32. package/dist/cli/acp.js +127 -61
  33. package/dist/cli/acp.js.map +1 -1
  34. package/dist/cli/index.js +147 -15
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/cli/mcp.d.ts +6 -0
  37. package/dist/cli/mcp.d.ts.map +1 -1
  38. package/dist/cli/mcp.js +268 -181
  39. package/dist/cli/mcp.js.map +1 -1
  40. package/dist/cli/parse-args.d.ts +20 -0
  41. package/dist/cli/parse-args.d.ts.map +1 -0
  42. package/dist/cli/parse-args.js +43 -0
  43. package/dist/cli/parse-args.js.map +1 -0
  44. package/dist/cli/stable-instance-id.d.ts +8 -0
  45. package/dist/cli/stable-instance-id.d.ts.map +1 -0
  46. package/dist/cli/stable-instance-id.js +14 -0
  47. package/dist/cli/stable-instance-id.js.map +1 -0
  48. package/dist/config/project-config.d.ts +74 -7
  49. package/dist/config/project-config.d.ts.map +1 -1
  50. package/dist/config/project-config.js +123 -20
  51. package/dist/config/project-config.js.map +1 -1
  52. package/dist/map/adapter/acp-over-map.d.ts +17 -0
  53. package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
  54. package/dist/map/adapter/acp-over-map.js +384 -23
  55. package/dist/map/adapter/acp-over-map.js.map +1 -1
  56. package/dist/map/adapter/connection-manager.d.ts.map +1 -1
  57. package/dist/map/adapter/connection-manager.js +3 -0
  58. package/dist/map/adapter/connection-manager.js.map +1 -1
  59. package/dist/map/adapter/event-log.d.ts +87 -0
  60. package/dist/map/adapter/event-log.d.ts.map +1 -0
  61. package/dist/map/adapter/event-log.js +122 -0
  62. package/dist/map/adapter/event-log.js.map +1 -0
  63. package/dist/map/adapter/event-translator.js +6 -6
  64. package/dist/map/adapter/event-translator.js.map +1 -1
  65. package/dist/map/adapter/extensions/agent-lifecycle.d.ts +82 -0
  66. package/dist/map/adapter/extensions/agent-lifecycle.d.ts.map +1 -0
  67. package/dist/map/adapter/extensions/agent-lifecycle.js +164 -0
  68. package/dist/map/adapter/extensions/agent-lifecycle.js.map +1 -0
  69. package/dist/map/adapter/extensions/index.d.ts +10 -1
  70. package/dist/map/adapter/extensions/index.d.ts.map +1 -1
  71. package/dist/map/adapter/extensions/index.js +34 -0
  72. package/dist/map/adapter/extensions/index.js.map +1 -1
  73. package/dist/map/adapter/extensions/mcp-bridge.d.ts +57 -0
  74. package/dist/map/adapter/extensions/mcp-bridge.d.ts.map +1 -0
  75. package/dist/map/adapter/extensions/mcp-bridge.js +745 -0
  76. package/dist/map/adapter/extensions/mcp-bridge.js.map +1 -0
  77. package/dist/map/adapter/extensions/rename.d.ts +29 -0
  78. package/dist/map/adapter/extensions/rename.d.ts.map +1 -0
  79. package/dist/map/adapter/extensions/rename.js +49 -0
  80. package/dist/map/adapter/extensions/rename.js.map +1 -0
  81. package/dist/map/adapter/extensions/task.d.ts.map +1 -1
  82. package/dist/map/adapter/extensions/task.js +10 -0
  83. package/dist/map/adapter/extensions/task.js.map +1 -1
  84. package/dist/map/adapter/extensions/update-metadata.d.ts +29 -0
  85. package/dist/map/adapter/extensions/update-metadata.d.ts.map +1 -0
  86. package/dist/map/adapter/extensions/update-metadata.js +67 -0
  87. package/dist/map/adapter/extensions/update-metadata.js.map +1 -0
  88. package/dist/map/adapter/index.d.ts +2 -1
  89. package/dist/map/adapter/index.d.ts.map +1 -1
  90. package/dist/map/adapter/index.js +8 -2
  91. package/dist/map/adapter/index.js.map +1 -1
  92. package/dist/map/adapter/interface.d.ts +2 -0
  93. package/dist/map/adapter/interface.d.ts.map +1 -1
  94. package/dist/map/adapter/map-adapter.d.ts +3 -0
  95. package/dist/map/adapter/map-adapter.d.ts.map +1 -1
  96. package/dist/map/adapter/map-adapter.js +258 -35
  97. package/dist/map/adapter/map-adapter.js.map +1 -1
  98. package/dist/map/adapter/subscription-manager.d.ts.map +1 -1
  99. package/dist/map/adapter/subscription-manager.js +5 -1
  100. package/dist/map/adapter/subscription-manager.js.map +1 -1
  101. package/dist/map/adapter/types.d.ts +2 -0
  102. package/dist/map/adapter/types.d.ts.map +1 -1
  103. package/dist/mcp/map-client.d.ts +39 -0
  104. package/dist/mcp/map-client.d.ts.map +1 -0
  105. package/dist/mcp/map-client.js +129 -0
  106. package/dist/mcp/map-client.js.map +1 -0
  107. package/dist/mcp/mcp-server.d.ts +14 -0
  108. package/dist/mcp/mcp-server.d.ts.map +1 -1
  109. package/dist/mcp/mcp-server.js +113 -85
  110. package/dist/mcp/mcp-server.js.map +1 -1
  111. package/dist/mcp/types.d.ts +9 -1
  112. package/dist/mcp/types.d.ts.map +1 -1
  113. package/dist/mcp/types.js.map +1 -1
  114. package/dist/metrics/metrics.js +1 -1
  115. package/dist/metrics/metrics.js.map +1 -1
  116. package/dist/roles/capabilities.d.ts +3 -1
  117. package/dist/roles/capabilities.d.ts.map +1 -1
  118. package/dist/roles/capabilities.js +17 -7
  119. package/dist/roles/capabilities.js.map +1 -1
  120. package/dist/roles/config-loader.d.ts +6 -6
  121. package/dist/roles/config-loader.d.ts.map +1 -1
  122. package/dist/roles/config-loader.js +6 -6
  123. package/dist/roles/config-loader.js.map +1 -1
  124. package/dist/roles/registry.d.ts +2 -2
  125. package/dist/roles/registry.js +2 -2
  126. package/dist/server/combined-server.d.ts +20 -0
  127. package/dist/server/combined-server.d.ts.map +1 -1
  128. package/dist/server/combined-server.js +107 -8
  129. package/dist/server/combined-server.js.map +1 -1
  130. package/dist/store/event-store.d.ts +2 -1
  131. package/dist/store/event-store.d.ts.map +1 -1
  132. package/dist/store/event-store.js +69 -20
  133. package/dist/store/event-store.js.map +1 -1
  134. package/dist/store/types/agents.d.ts +18 -0
  135. package/dist/store/types/agents.d.ts.map +1 -1
  136. package/dist/store/types/events.d.ts +1 -1
  137. package/dist/store/types/events.d.ts.map +1 -1
  138. package/dist/task/backend/index.d.ts +47 -29
  139. package/dist/task/backend/index.d.ts.map +1 -1
  140. package/dist/task/backend/index.js +109 -71
  141. package/dist/task/backend/index.js.map +1 -1
  142. package/dist/task/backend/memory.d.ts +1 -0
  143. package/dist/task/backend/memory.d.ts.map +1 -1
  144. package/dist/task/backend/memory.js +3 -0
  145. package/dist/task/backend/memory.js.map +1 -1
  146. package/dist/task/backend/opentasks/backend.d.ts +140 -0
  147. package/dist/task/backend/opentasks/backend.d.ts.map +1 -0
  148. package/dist/task/backend/opentasks/backend.js +1023 -0
  149. package/dist/task/backend/opentasks/backend.js.map +1 -0
  150. package/dist/task/backend/opentasks/client.d.ts +337 -0
  151. package/dist/task/backend/opentasks/client.d.ts.map +1 -0
  152. package/dist/task/backend/opentasks/client.js +225 -0
  153. package/dist/task/backend/opentasks/client.js.map +1 -0
  154. package/dist/task/backend/opentasks/daemon-manager.d.ts +89 -0
  155. package/dist/task/backend/opentasks/daemon-manager.d.ts.map +1 -0
  156. package/dist/task/backend/opentasks/daemon-manager.js +195 -0
  157. package/dist/task/backend/opentasks/daemon-manager.js.map +1 -0
  158. package/dist/task/backend/opentasks/index.d.ts +21 -0
  159. package/dist/task/backend/opentasks/index.d.ts.map +1 -0
  160. package/dist/task/backend/opentasks/index.js +21 -0
  161. package/dist/task/backend/opentasks/index.js.map +1 -0
  162. package/dist/task/backend/opentasks/mapping.d.ts +48 -0
  163. package/dist/task/backend/opentasks/mapping.d.ts.map +1 -0
  164. package/dist/task/backend/opentasks/mapping.js +77 -0
  165. package/dist/task/backend/opentasks/mapping.js.map +1 -0
  166. package/dist/task/backend/types.d.ts +33 -53
  167. package/dist/task/backend/types.d.ts.map +1 -1
  168. package/dist/task/backend/types.js +7 -11
  169. package/dist/task/backend/types.js.map +1 -1
  170. package/dist/task/backend/unified-tool-provider.d.ts +57 -0
  171. package/dist/task/backend/unified-tool-provider.d.ts.map +1 -0
  172. package/dist/task/backend/unified-tool-provider.js +623 -0
  173. package/dist/task/backend/unified-tool-provider.js.map +1 -0
  174. package/dist/teams/team-loader.d.ts +2 -2
  175. package/dist/teams/team-loader.js +3 -3
  176. package/dist/teams/team-loader.js.map +1 -1
  177. package/dist/teams/team-runtime.d.ts.map +1 -1
  178. package/dist/teams/team-runtime.js +2 -0
  179. package/dist/teams/team-runtime.js.map +1 -1
  180. package/docs/architecture.md +7 -6
  181. package/docs/configuration.md +26 -62
  182. package/docs/implementation-details.md +5 -5
  183. package/docs/implementation-summary.md +17 -17
  184. package/docs/plan-self-driving-support.md +4 -4
  185. package/docs/spec-self-driving-support.md +10 -10
  186. package/docs/team-templates.md +2 -2
  187. package/docs/teams.md +3 -3
  188. package/docs/troubleshooting.md +10 -11
  189. package/package.json +6 -4
  190. package/src/__tests__/e2e/agent-spawn-visibility.e2e.test.ts +761 -0
  191. package/src/__tests__/e2e/full-agent-conflict-resolution.e2e.test.ts +2 -2
  192. package/src/__tests__/e2e/mcp-thin-client-bridge.e2e.test.ts +304 -0
  193. package/src/__tests__/e2e/mcp-tools-available.e2e.test.ts +324 -0
  194. package/src/__tests__/e2e/multi-agent.e2e.test.ts +5 -5
  195. package/src/__tests__/e2e/spawn-session-streaming.e2e.test.ts +563 -0
  196. package/src/acp/__tests__/integration.test.ts +56 -31
  197. package/src/acp/__tests__/macro-agent.test.ts +16 -7
  198. package/src/acp/macro-agent.ts +170 -36
  199. package/src/acp/types.ts +46 -1
  200. package/src/agent/__tests__/agent-manager.test.ts +228 -2
  201. package/src/agent/agent-manager.ts +714 -261
  202. package/src/agent/types.ts +3 -1
  203. package/src/api/server.ts +41 -7
  204. package/src/auth/__tests__/token.test.ts +100 -0
  205. package/src/auth/index.ts +1 -0
  206. package/src/auth/token.ts +82 -0
  207. package/src/cli/__tests__/acp.test.ts +1 -1
  208. package/src/cli/__tests__/stable-instance-id.test.ts +1 -1
  209. package/src/cli/acp.ts +130 -72
  210. package/src/cli/index.ts +120 -14
  211. package/src/cli/mcp.ts +311 -207
  212. package/src/cli/parse-args.ts +54 -0
  213. package/src/cli/stable-instance-id.ts +14 -0
  214. package/src/config/project-config.ts +190 -27
  215. package/src/lifecycle/__tests__/cascade-termination.test.ts +1 -1
  216. package/src/map/adapter/__tests__/acp-over-map-cancel.test.ts +22 -4
  217. package/src/map/adapter/__tests__/acp-over-map-getmodels.test.ts +355 -0
  218. package/src/map/adapter/__tests__/acp-over-map-history.test.ts +263 -0
  219. package/src/map/adapter/__tests__/acp-over-map-persistence.e2e.test.ts +1 -1
  220. package/src/map/adapter/__tests__/event-broadcast.test.ts +420 -0
  221. package/src/map/adapter/__tests__/event-log.test.ts +527 -0
  222. package/src/map/adapter/__tests__/event-translator.test.ts +3 -3
  223. package/src/map/adapter/__tests__/extensions.test.ts +408 -0
  224. package/src/map/adapter/__tests__/map-adapter.test.ts +99 -0
  225. package/src/map/adapter/__tests__/mcp-bridge.test.ts +1187 -0
  226. package/src/map/adapter/__tests__/multi-client-broadcast.test.ts +711 -0
  227. package/src/map/adapter/__tests__/websocket-integration.test.ts +218 -0
  228. package/src/map/adapter/acp-over-map.ts +678 -66
  229. package/src/map/adapter/connection-manager.ts +3 -0
  230. package/src/map/adapter/event-log.ts +208 -0
  231. package/src/map/adapter/event-translator.ts +6 -6
  232. package/src/map/adapter/extensions/agent-lifecycle.ts +267 -0
  233. package/src/map/adapter/extensions/index.ts +60 -0
  234. package/src/map/adapter/extensions/mcp-bridge.ts +995 -0
  235. package/src/map/adapter/extensions/task.ts +11 -0
  236. package/src/map/adapter/extensions/update-metadata.ts +126 -0
  237. package/src/map/adapter/index.ts +28 -0
  238. package/src/map/adapter/interface.ts +2 -0
  239. package/src/map/adapter/map-adapter.ts +312 -47
  240. package/src/map/adapter/subscription-manager.ts +5 -1
  241. package/src/map/adapter/types.ts +2 -0
  242. package/src/mcp/__tests__/map-client.test.ts +386 -0
  243. package/src/mcp/__tests__/mcp-server-thin-client.test.ts +368 -0
  244. package/src/mcp/__tests__/mcp-server.test.ts +100 -1
  245. package/src/mcp/map-client.ts +177 -0
  246. package/src/mcp/mcp-server.ts +191 -100
  247. package/src/mcp/types.ts +6 -1
  248. package/src/metrics/metrics.ts +1 -1
  249. package/src/monitor/__tests__/stale-agent-flow.integration.test.ts +1 -1
  250. package/src/roles/__tests__/config-loader.test.ts +7 -7
  251. package/src/roles/capabilities.ts +17 -7
  252. package/src/roles/config-loader.ts +6 -6
  253. package/src/roles/registry.ts +2 -2
  254. package/src/server/__tests__/combined-server.test.ts +94 -21
  255. package/src/server/combined-server.ts +189 -33
  256. package/src/steering/__tests__/steering-integration.test.ts +1 -1
  257. package/src/store/__tests__/event-store.test.ts +196 -1
  258. package/src/store/__tests__/instance.test.ts +3 -3
  259. package/src/store/event-store.ts +80 -21
  260. package/src/store/types/agents.ts +15 -0
  261. package/src/store/types/events.ts +1 -1
  262. package/src/task/backend/__tests__/create-task-backend.test.ts +225 -0
  263. package/src/task/backend/__tests__/e2e/unified-tool-provider-opentasks.e2e.test.ts +524 -0
  264. package/src/task/backend/__tests__/unified-tool-provider.test.ts +579 -0
  265. package/src/task/backend/index.ts +156 -106
  266. package/src/task/backend/memory.ts +4 -0
  267. package/src/task/backend/opentasks/__tests__/backend.test.ts +968 -0
  268. package/src/task/backend/opentasks/__tests__/daemon-manager.test.ts +406 -0
  269. package/src/task/backend/opentasks/__tests__/mapping.test.ts +84 -0
  270. package/src/task/backend/opentasks/__tests__/opentasks-backend.e2e.test.ts +1338 -0
  271. package/src/task/backend/opentasks/backend.ts +1323 -0
  272. package/src/task/backend/opentasks/client.ts +652 -0
  273. package/src/task/backend/opentasks/daemon-manager.ts +253 -0
  274. package/src/task/backend/opentasks/index.ts +69 -0
  275. package/src/task/backend/opentasks/mapping.ts +94 -0
  276. package/src/task/backend/types.ts +42 -66
  277. package/src/task/backend/unified-tool-provider.ts +779 -0
  278. package/src/teams/__tests__/cross-subsystem.integration.test.ts +1 -1
  279. package/src/teams/team-loader.ts +3 -3
  280. package/src/teams/team-runtime.ts +2 -0
  281. package/test_fixtures/README.md +2 -3
  282. package/test_fixtures/fixtures/index.ts +0 -3
  283. package/test_fixtures/fixtures/projects/project-with-specs.ts +7 -149
  284. package/test_fixtures/fixtures/repos/index.ts +1 -3
  285. package/test_fixtures/fixtures/repos/temp-repo-factory.ts +0 -116
  286. package/test_fixtures/fixtures/repos/types.ts +0 -11
  287. package/test_fixtures/harness/__tests__/fixtures.test.ts +10 -102
  288. package/test_fixtures/harness/__tests__/temp-repo-and-simulator.test.ts +0 -33
  289. package/test_fixtures/harness/simulator/agent-simulator.ts +4 -4
  290. package/vitest.config.ts +1 -1
  291. package/vitest.e2e.config.ts +1 -1
  292. package/vitest.setup.ts +1 -30
  293. package/.macro-agent/teams/self-driving/prompts/grinder.md +0 -27
  294. package/.macro-agent/teams/self-driving/prompts/judge.md +0 -27
  295. package/.macro-agent/teams/self-driving/prompts/planner.md +0 -33
  296. package/.macro-agent/teams/self-driving/roles/grinder.yaml +0 -17
  297. package/.macro-agent/teams/self-driving/roles/judge.yaml +0 -24
  298. package/.macro-agent/teams/self-driving/roles/planner.yaml +0 -18
  299. package/.macro-agent/teams/self-driving/team.yaml +0 -103
  300. package/.macro-agent/teams/structured/prompts/developer.md +0 -26
  301. package/.macro-agent/teams/structured/prompts/lead.md +0 -25
  302. package/.macro-agent/teams/structured/prompts/reviewer.md +0 -24
  303. package/.macro-agent/teams/structured/roles/developer.yaml +0 -12
  304. package/.macro-agent/teams/structured/roles/lead.yaml +0 -11
  305. package/.macro-agent/teams/structured/roles/reviewer.yaml +0 -19
  306. package/.macro-agent/teams/structured/team.yaml +0 -89
  307. package/docs/sudocode-integration.md +0 -383
  308. package/src/task/backend/__tests__/backend-parity.test.ts +0 -451
  309. package/src/task/backend/__tests__/tool-provider-edge-cases.test.ts +0 -430
  310. package/src/task/backend/__tests__/tool-provider.test.ts +0 -983
  311. package/src/task/backend/sudocode/__tests__/backend-edge-cases.test.ts +0 -575
  312. package/src/task/backend/sudocode/__tests__/backend.test.ts +0 -1194
  313. package/src/task/backend/sudocode/__tests__/client-integration.test.ts +0 -418
  314. package/src/task/backend/sudocode/__tests__/client.test.ts +0 -345
  315. package/src/task/backend/sudocode/__tests__/e2e/backend.e2e.test.ts +0 -753
  316. package/src/task/backend/sudocode/__tests__/e2e/server-client.e2e.test.ts +0 -680
  317. package/src/task/backend/sudocode/__tests__/e2e-workflow.test.ts +0 -666
  318. package/src/task/backend/sudocode/__tests__/integration/standalone-client.integration.test.ts +0 -396
  319. package/src/task/backend/sudocode/__tests__/integration/sudocode-cli.integration.test.ts +0 -328
  320. package/src/task/backend/sudocode/__tests__/integration/test-utils.ts +0 -175
  321. package/src/task/backend/sudocode/__tests__/mapping-edge-cases.test.ts +0 -265
  322. package/src/task/backend/sudocode/__tests__/server-client.test.ts +0 -675
  323. package/src/task/backend/sudocode/__tests__/sync-policy-edge-cases.test.ts +0 -521
  324. package/src/task/backend/sudocode/__tests__/sync-policy.test.ts +0 -519
  325. package/src/task/backend/sudocode/__tests__/tools.test.ts +0 -471
  326. package/src/task/backend/sudocode/backend.ts +0 -1237
  327. package/src/task/backend/sudocode/client.ts +0 -515
  328. package/src/task/backend/sudocode/index.ts +0 -120
  329. package/src/task/backend/sudocode/mapping.ts +0 -93
  330. package/src/task/backend/sudocode/server-client.ts +0 -522
  331. package/src/task/backend/sudocode/standalone-client.ts +0 -623
  332. package/src/task/backend/sudocode/sync-policy.ts +0 -387
  333. package/src/task/backend/sudocode/tools.ts +0 -896
  334. package/src/task/backend/tool-provider.ts +0 -506
  335. package/test_fixtures/fixtures/sudocode/index.ts +0 -29
  336. package/test_fixtures/fixtures/sudocode/issues.ts +0 -185
  337. package/test_fixtures/fixtures/sudocode/specs.ts +0 -159
@@ -0,0 +1,1323 @@
1
+ /**
2
+ * OpenTasksTaskBackend Implementation
3
+ *
4
+ * Implements TaskBackend using OpenTasks as the source of truth for task storage,
5
+ * with EventStore for local event tracking and subscriptions.
6
+ *
7
+ * Tasks are stored as OpenTasks issues. Blocking dependencies use OpenTasks
8
+ * 'blocks' edges. The pull model uses OpenTasks' ready query and claimed_by field.
9
+ *
10
+ * @module task/backend/opentasks/backend
11
+ */
12
+
13
+ import { nanoid } from "nanoid";
14
+ import type { EventStore } from "../../../store/event-store.js";
15
+ import type {
16
+ Task,
17
+ TaskStatus,
18
+ AgentId,
19
+ TaskId,
20
+ AgentHistoryEntry,
21
+ } from "../../../store/types/index.js";
22
+ import type {
23
+ TaskBackend,
24
+ ExtendedTask,
25
+ CreateTaskOptions,
26
+ UpdateTaskOptions,
27
+ TaskFilter,
28
+ TaskOutputs,
29
+ TaskError,
30
+ SubtaskStatus,
31
+ AssignOptions,
32
+ ClaimFilter,
33
+ TaskChangeCallback,
34
+ TaskChangeEvent,
35
+ Unsubscribe,
36
+ } from "../types.js";
37
+ import type { OpenTasksClient, OpenTasksIssue } from "./client.js";
38
+ import { mapOpenTasksStatus, mapTaskStatus, isIssueComplete } from "./mapping.js";
39
+
40
+ // Valid status transitions
41
+ const VALID_STATUS_TRANSITIONS: Record<TaskStatus, TaskStatus[]> = {
42
+ pending: ["assigned", "in_progress", "failed"],
43
+ assigned: ["in_progress", "pending", "failed"],
44
+ in_progress: ["completed", "failed", "pending"],
45
+ completed: [],
46
+ failed: ["pending"],
47
+ };
48
+
49
+ /**
50
+ * Error thrown by OpenTasksTaskBackend operations
51
+ */
52
+ export class OpenTasksBackendError extends Error {
53
+ constructor(
54
+ message: string,
55
+ public readonly code: string,
56
+ public readonly taskId?: TaskId
57
+ ) {
58
+ super(message);
59
+ this.name = "OpenTasksBackendError";
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Configuration for OpenTasksTaskBackend
65
+ */
66
+ export interface OpenTasksBackendConfig {
67
+ /** Path to daemon socket (auto-discovered if not set) */
68
+ socketPath?: string;
69
+
70
+ /** Whether to sync status changes to OpenTasks (default: true) */
71
+ syncStatus?: boolean;
72
+
73
+ /** Source identifier for issues created by this backend (default: "macro-agent") */
74
+ sourceLabel?: string;
75
+ }
76
+
77
+ const DEFAULT_CONFIG: Required<OpenTasksBackendConfig> = {
78
+ socketPath: "",
79
+ syncStatus: true,
80
+ sourceLabel: "macro-agent",
81
+ };
82
+
83
+ /**
84
+ * OpenTasksTaskBackend implements TaskBackend using:
85
+ * - OpenTasks daemon for issue storage and graph relationships
86
+ * - EventStore for local event tracking and subscriptions
87
+ *
88
+ * Key features:
89
+ * - Tasks are stored as OpenTasks issues with macro-agent metadata
90
+ * - Blocking dependencies use OpenTasks 'blocks' edges
91
+ * - Pull model uses OpenTasks ready query and claimed_by
92
+ * - Bidirectional ID mapping (task_id <-> issue_id) via metadata
93
+ */
94
+ export class OpenTasksTaskBackend implements TaskBackend {
95
+ private readonly config: Required<OpenTasksBackendConfig>;
96
+ private closed = false;
97
+
98
+ /** Map from macro-agent task ID to OpenTasks issue ID */
99
+ private readonly taskToIssue = new Map<TaskId, string>();
100
+
101
+ /** Map from OpenTasks issue ID to macro-agent task ID */
102
+ private readonly issueToTask = new Map<string, TaskId>();
103
+
104
+ constructor(
105
+ private readonly eventStore: EventStore,
106
+ private readonly client: OpenTasksClient,
107
+ config?: Partial<OpenTasksBackendConfig>
108
+ ) {
109
+ this.config = { ...DEFAULT_CONFIG, ...config };
110
+ }
111
+
112
+ /**
113
+ * Throw if the backend has been closed.
114
+ */
115
+ private ensureOpen(): void {
116
+ if (this.closed) {
117
+ throw new OpenTasksBackendError("Backend is closed", "BACKEND_CLOSED");
118
+ }
119
+ }
120
+
121
+ // ─────────────────────────────────────────────────────────────────────────────
122
+ // Lifecycle
123
+ // ─────────────────────────────────────────────────────────────────────────────
124
+
125
+ async close(): Promise<void> {
126
+ this.closed = true;
127
+ }
128
+
129
+ async create(options: CreateTaskOptions): Promise<ExtendedTask> {
130
+ this.ensureOpen();
131
+ const taskId = `task_${nanoid(12)}`;
132
+
133
+ // Resolve parent issue ID if parent task specified
134
+ let parentIssueId: string | undefined;
135
+ if (options.parent_task) {
136
+ parentIssueId = this.taskToIssue.get(options.parent_task);
137
+ if (!parentIssueId) {
138
+ // Check EventStore as fallback
139
+ const parent = this.eventStore.getTask(options.parent_task);
140
+ if (!parent) {
141
+ throw new OpenTasksBackendError(
142
+ `Parent task not found: ${options.parent_task}`,
143
+ "PARENT_TASK_NOT_FOUND",
144
+ options.parent_task
145
+ );
146
+ }
147
+ }
148
+ }
149
+
150
+ // Create issue in OpenTasks
151
+ const issue = await this.client.createIssue({
152
+ title: options.description,
153
+ status: "open",
154
+ tags: options.tags,
155
+ parent_id: parentIssueId,
156
+ metadata: {
157
+ macro_agent_task_id: taskId,
158
+ created_by: options.created_by,
159
+ source: this.config.sourceLabel,
160
+ },
161
+ });
162
+
163
+ // Store bidirectional mapping
164
+ this.taskToIssue.set(taskId, issue.id);
165
+ this.issueToTask.set(issue.id, taskId);
166
+
167
+ // Emit to EventStore for local tracking
168
+ this.eventStore.emit({
169
+ type: "task",
170
+ source: { agent_id: options.created_by },
171
+ payload: {
172
+ task_id: taskId,
173
+ action: "created",
174
+ details: {
175
+ description: options.description,
176
+ parent_task: options.parent_task,
177
+ tags: options.tags,
178
+ external_id: issue.id,
179
+ },
180
+ },
181
+ });
182
+
183
+ // Update parent subtasks in EventStore
184
+ if (options.parent_task) {
185
+ this.eventStore.emit({
186
+ type: "task",
187
+ source: { agent_id: options.created_by },
188
+ payload: {
189
+ task_id: options.parent_task,
190
+ action: "status_change",
191
+ details: { subtask_added: taskId },
192
+ },
193
+ });
194
+ }
195
+
196
+ // Return from EventStore (which has the canonical local state)
197
+ const task = this.eventStore.getTask(taskId)!;
198
+ return this.toExtendedTask(task);
199
+ }
200
+
201
+ async get(id: TaskId): Promise<ExtendedTask | null> {
202
+ // EventStore is the local mirror with richer state
203
+ // (assigned status, outputs, agent_history, etc.)
204
+ const task = this.eventStore.getTask(id);
205
+ if (!task) return null;
206
+ return this.toExtendedTask(task);
207
+ }
208
+
209
+ async update(id: TaskId, updates: UpdateTaskOptions): Promise<ExtendedTask> {
210
+ this.ensureOpen();
211
+ const task = this.eventStore.getTask(id);
212
+ if (!task) {
213
+ throw new OpenTasksBackendError(
214
+ `Task not found: ${id}`,
215
+ "TASK_NOT_FOUND",
216
+ id
217
+ );
218
+ }
219
+
220
+ const source = task.assigned_agent ?? task.created_by;
221
+
222
+ // Handle status update with validation
223
+ if (updates.status !== undefined) {
224
+ const validTransitions = VALID_STATUS_TRANSITIONS[task.status];
225
+ if (!validTransitions.includes(updates.status)) {
226
+ throw new OpenTasksBackendError(
227
+ `Invalid status transition: ${task.status} -> ${updates.status}`,
228
+ "INVALID_STATUS_TRANSITION",
229
+ id
230
+ );
231
+ }
232
+
233
+ this.eventStore.emit({
234
+ type: "task",
235
+ source: { agent_id: source },
236
+ payload: {
237
+ task_id: id,
238
+ action: "status_change",
239
+ details: { status: updates.status },
240
+ },
241
+ });
242
+
243
+ // Sync to OpenTasks
244
+ await this.syncStatusToOpenTasks(id, updates.status);
245
+ }
246
+
247
+ // Handle other updates
248
+ if (updates.outputs !== undefined) {
249
+ this.eventStore.emit({
250
+ type: "task",
251
+ source: { agent_id: source },
252
+ payload: {
253
+ task_id: id,
254
+ action: "status_change",
255
+ details: { outputs: updates.outputs },
256
+ },
257
+ });
258
+ }
259
+
260
+ if (updates.artifacts !== undefined) {
261
+ this.eventStore.emit({
262
+ type: "task",
263
+ source: { agent_id: source },
264
+ payload: {
265
+ task_id: id,
266
+ action: "status_change",
267
+ details: { artifacts: updates.artifacts },
268
+ },
269
+ });
270
+ }
271
+
272
+ if (updates.description !== undefined) {
273
+ this.eventStore.emit({
274
+ type: "task",
275
+ source: { agent_id: source },
276
+ payload: {
277
+ task_id: id,
278
+ action: "status_change",
279
+ details: { description: updates.description },
280
+ },
281
+ });
282
+
283
+ // Sync description to OpenTasks
284
+ const issueId = this.taskToIssue.get(id);
285
+ if (issueId) {
286
+ await this.client.updateIssue(issueId, {
287
+ title: updates.description,
288
+ });
289
+ }
290
+ }
291
+
292
+ const updated = this.eventStore.getTask(id)!;
293
+ return this.toExtendedTask(updated);
294
+ }
295
+
296
+ async delete(id: TaskId): Promise<void> {
297
+ this.ensureOpen();
298
+ const issueId = this.taskToIssue.get(id);
299
+ if (issueId) {
300
+ await this.client.deleteIssue(issueId);
301
+ this.taskToIssue.delete(id);
302
+ this.issueToTask.delete(issueId);
303
+ }
304
+ }
305
+
306
+ // ─────────────────────────────────────────────────────────────────────────────
307
+ // Status Transitions
308
+ // ─────────────────────────────────────────────────────────────────────────────
309
+
310
+ async assign(
311
+ id: TaskId,
312
+ agentId: AgentId,
313
+ options?: AssignOptions
314
+ ): Promise<void> {
315
+ this.ensureOpen();
316
+ const task = this.eventStore.getTask(id);
317
+ if (!task) {
318
+ throw new OpenTasksBackendError(
319
+ `Task not found: ${id}`,
320
+ "TASK_NOT_FOUND",
321
+ id
322
+ );
323
+ }
324
+
325
+ this.eventStore.emit({
326
+ type: "task",
327
+ source: { agent_id: agentId },
328
+ payload: {
329
+ task_id: id,
330
+ action: "assigned",
331
+ details: {
332
+ agent_id: agentId,
333
+ role: options?.role,
334
+ },
335
+ },
336
+ });
337
+
338
+ // Sync assignee to OpenTasks
339
+ const issueId = this.taskToIssue.get(id);
340
+ if (issueId) {
341
+ await this.client.updateIssue(issueId, {
342
+ assignee: agentId,
343
+ metadata: { claimed_by: agentId },
344
+ });
345
+ }
346
+ }
347
+
348
+ async unassign(id: TaskId): Promise<void> {
349
+ this.ensureOpen();
350
+ const task = this.eventStore.getTask(id);
351
+ if (!task) {
352
+ throw new OpenTasksBackendError(
353
+ `Task not found: ${id}`,
354
+ "TASK_NOT_FOUND",
355
+ id
356
+ );
357
+ }
358
+
359
+ if (!task.assigned_agent) {
360
+ throw new OpenTasksBackendError(
361
+ `Task is not assigned: ${id}`,
362
+ "TASK_NOT_ASSIGNED",
363
+ id
364
+ );
365
+ }
366
+
367
+ this.eventStore.emit({
368
+ type: "task",
369
+ source: { agent_id: task.assigned_agent },
370
+ payload: {
371
+ task_id: id,
372
+ action: "unassigned",
373
+ details: { agent_id: task.assigned_agent },
374
+ },
375
+ });
376
+
377
+ // Clear assignee in OpenTasks
378
+ const issueId = this.taskToIssue.get(id);
379
+ if (issueId) {
380
+ await this.client.updateIssue(issueId, {
381
+ assignee: null,
382
+ metadata: { claimed_by: null },
383
+ });
384
+ }
385
+ }
386
+
387
+ async start(id: TaskId): Promise<void> {
388
+ this.ensureOpen();
389
+ const task = this.eventStore.getTask(id);
390
+ if (!task) {
391
+ throw new OpenTasksBackendError(
392
+ `Task not found: ${id}`,
393
+ "TASK_NOT_FOUND",
394
+ id
395
+ );
396
+ }
397
+
398
+ const validTransitions = VALID_STATUS_TRANSITIONS[task.status];
399
+ if (!validTransitions.includes("in_progress")) {
400
+ throw new OpenTasksBackendError(
401
+ `Invalid status transition: ${task.status} -> in_progress`,
402
+ "INVALID_STATUS_TRANSITION",
403
+ id
404
+ );
405
+ }
406
+
407
+ this.eventStore.emit({
408
+ type: "task",
409
+ source: { agent_id: task.assigned_agent ?? task.created_by },
410
+ payload: {
411
+ task_id: id,
412
+ action: "status_change",
413
+ details: { status: "in_progress" },
414
+ },
415
+ });
416
+
417
+ await this.syncStatusToOpenTasks(id, "in_progress");
418
+ }
419
+
420
+ async complete(id: TaskId, outputs?: TaskOutputs): Promise<void> {
421
+ this.ensureOpen();
422
+ const task = this.eventStore.getTask(id);
423
+ if (!task) {
424
+ throw new OpenTasksBackendError(
425
+ `Task not found: ${id}`,
426
+ "TASK_NOT_FOUND",
427
+ id
428
+ );
429
+ }
430
+
431
+ const validTransitions = VALID_STATUS_TRANSITIONS[task.status];
432
+ if (!validTransitions.includes("completed")) {
433
+ throw new OpenTasksBackendError(
434
+ `Invalid status transition: ${task.status} -> completed`,
435
+ "INVALID_STATUS_TRANSITION",
436
+ id
437
+ );
438
+ }
439
+
440
+ const agent = task.assigned_agent ?? task.created_by;
441
+
442
+ // Store outputs
443
+ if (outputs) {
444
+ const outputsToStore: Record<string, unknown> = {
445
+ ...(outputs.data ?? {}),
446
+ };
447
+ if (outputs.summary !== undefined) {
448
+ outputsToStore.summary = outputs.summary;
449
+ }
450
+ if (Object.keys(outputsToStore).length > 0) {
451
+ this.eventStore.emit({
452
+ type: "task",
453
+ source: { agent_id: agent },
454
+ payload: {
455
+ task_id: id,
456
+ action: "status_change",
457
+ details: { outputs: outputsToStore },
458
+ },
459
+ });
460
+ }
461
+ if (outputs.artifacts) {
462
+ this.eventStore.emit({
463
+ type: "task",
464
+ source: { agent_id: agent },
465
+ payload: {
466
+ task_id: id,
467
+ action: "status_change",
468
+ details: { artifacts: outputs.artifacts },
469
+ },
470
+ });
471
+ }
472
+ }
473
+
474
+ this.eventStore.emit({
475
+ type: "task",
476
+ source: { agent_id: agent },
477
+ payload: {
478
+ task_id: id,
479
+ action: "completed",
480
+ details: {},
481
+ },
482
+ });
483
+
484
+ // Close the issue in OpenTasks
485
+ await this.syncStatusToOpenTasks(id, "completed");
486
+ }
487
+
488
+ async fail(id: TaskId, error: TaskError): Promise<void> {
489
+ this.ensureOpen();
490
+ const task = this.eventStore.getTask(id);
491
+ if (!task) {
492
+ throw new OpenTasksBackendError(
493
+ `Task not found: ${id}`,
494
+ "TASK_NOT_FOUND",
495
+ id
496
+ );
497
+ }
498
+
499
+ const validTransitions = VALID_STATUS_TRANSITIONS[task.status];
500
+ if (!validTransitions.includes("failed")) {
501
+ throw new OpenTasksBackendError(
502
+ `Invalid status transition: ${task.status} -> failed`,
503
+ "INVALID_STATUS_TRANSITION",
504
+ id
505
+ );
506
+ }
507
+
508
+ const agent = task.assigned_agent ?? task.created_by;
509
+
510
+ // Store error in outputs
511
+ this.eventStore.emit({
512
+ type: "task",
513
+ source: { agent_id: agent },
514
+ payload: {
515
+ task_id: id,
516
+ action: "status_change",
517
+ details: {
518
+ outputs: {
519
+ error: {
520
+ message: error.message,
521
+ code: error.code,
522
+ details: error.details,
523
+ },
524
+ },
525
+ },
526
+ },
527
+ });
528
+
529
+ this.eventStore.emit({
530
+ type: "task",
531
+ source: { agent_id: agent },
532
+ payload: {
533
+ task_id: id,
534
+ action: "failed",
535
+ details: {},
536
+ },
537
+ });
538
+
539
+ // Close in OpenTasks with error metadata
540
+ const issueId = this.taskToIssue.get(id);
541
+ if (issueId) {
542
+ await this.client.updateIssue(issueId, {
543
+ status: "closed",
544
+ metadata: {
545
+ macro_agent_failed: true,
546
+ macro_agent_error: error.message,
547
+ },
548
+ });
549
+ }
550
+ }
551
+
552
+ // ─────────────────────────────────────────────────────────────────────────────
553
+ // Queries
554
+ // ─────────────────────────────────────────────────────────────────────────────
555
+
556
+ async list(filter?: TaskFilter): Promise<ExtendedTask[]> {
557
+ let tasks = this.eventStore.listTasks();
558
+
559
+ if (filter) {
560
+ if (filter.status) {
561
+ const statuses = Array.isArray(filter.status)
562
+ ? filter.status
563
+ : [filter.status];
564
+ tasks = tasks.filter((t) => statuses.includes(t.status));
565
+ }
566
+
567
+ if (filter.assigned_agent) {
568
+ tasks = tasks.filter((t) => t.assigned_agent === filter.assigned_agent);
569
+ }
570
+
571
+ if (filter.parent_task) {
572
+ tasks = tasks.filter((t) => t.parent_task === filter.parent_task);
573
+ }
574
+
575
+ if (filter.created_by) {
576
+ tasks = tasks.filter((t) => t.created_by === filter.created_by);
577
+ }
578
+
579
+ if (filter.rootTasksOnly) {
580
+ tasks = tasks.filter((t) => !t.parent_task);
581
+ }
582
+
583
+ if (filter.tags && filter.tags.length > 0) {
584
+ const filterTags = new Set(filter.tags);
585
+ tasks = tasks.filter(
586
+ (t) => t.tags?.some((tag) => filterTags.has(tag))
587
+ );
588
+ }
589
+ }
590
+
591
+ // Compute isBlocked using OpenTasks graph for mapped tasks,
592
+ // EventStore blockers for unmapped tasks
593
+ const extended = await Promise.all(
594
+ tasks.map((t) => this.toExtendedTaskAsync(t))
595
+ );
596
+
597
+ if (!filter?.includeBlocked) {
598
+ return extended.filter((t) => !t.isBlocked);
599
+ }
600
+
601
+ return extended;
602
+ }
603
+
604
+ async listReady(filter?: TaskFilter): Promise<ExtendedTask[]> {
605
+ return this.list({
606
+ ...filter,
607
+ status: filter?.status ?? ["pending", "assigned"],
608
+ includeBlocked: false,
609
+ });
610
+ }
611
+
612
+ async getChildren(parentId: TaskId): Promise<ExtendedTask[]> {
613
+ const tasks = this.eventStore.listTasks();
614
+ const children = tasks.filter((t) => t.parent_task === parentId);
615
+ return Promise.all(children.map((t) => this.toExtendedTaskAsync(t)));
616
+ }
617
+
618
+ async getSubtaskStatus(parentId: TaskId): Promise<SubtaskStatus> {
619
+ const children = await this.getChildren(parentId);
620
+
621
+ const status: SubtaskStatus = {
622
+ total: children.length,
623
+ pending: 0,
624
+ assigned: 0,
625
+ in_progress: 0,
626
+ completed: 0,
627
+ failed: 0,
628
+ allCompleted: false,
629
+ anyFailed: false,
630
+ };
631
+
632
+ for (const task of children) {
633
+ switch (task.status) {
634
+ case "pending":
635
+ status.pending++;
636
+ break;
637
+ case "assigned":
638
+ status.assigned++;
639
+ break;
640
+ case "in_progress":
641
+ status.in_progress++;
642
+ break;
643
+ case "completed":
644
+ status.completed++;
645
+ break;
646
+ case "failed":
647
+ status.failed++;
648
+ break;
649
+ }
650
+ }
651
+
652
+ status.allCompleted =
653
+ status.total > 0 && status.completed === status.total;
654
+ status.anyFailed = status.failed > 0;
655
+
656
+ return status;
657
+ }
658
+
659
+ // ─────────────────────────────────────────────────────────────────────────────
660
+ // Hierarchy
661
+ // ─────────────────────────────────────────────────────────────────────────────
662
+
663
+ async createSubtask(
664
+ parentId: TaskId,
665
+ options: CreateTaskOptions
666
+ ): Promise<ExtendedTask> {
667
+ return this.create({
668
+ ...options,
669
+ parent_task: parentId,
670
+ });
671
+ }
672
+
673
+ // ─────────────────────────────────────────────────────────────────────────────
674
+ // Dependencies (via OpenTasks edges)
675
+ // ─────────────────────────────────────────────────────────────────────────────
676
+
677
+ async addBlocker(taskId: TaskId, blockerId: TaskId): Promise<void> {
678
+ this.ensureOpen();
679
+ const task = this.eventStore.getTask(taskId);
680
+ if (!task) {
681
+ throw new OpenTasksBackendError(
682
+ `Task not found: ${taskId}`,
683
+ "TASK_NOT_FOUND",
684
+ taskId
685
+ );
686
+ }
687
+
688
+ const blocker = this.eventStore.getTask(blockerId);
689
+ if (!blocker) {
690
+ throw new OpenTasksBackendError(
691
+ `Blocker task not found: ${blockerId}`,
692
+ "TASK_NOT_FOUND",
693
+ blockerId
694
+ );
695
+ }
696
+
697
+ // Record in EventStore
698
+ this.eventStore.emit({
699
+ type: "task",
700
+ source: { agent_id: task.assigned_agent ?? task.created_by },
701
+ payload: {
702
+ task_id: taskId,
703
+ action: "blocker_added",
704
+ details: { blocker_id: blockerId },
705
+ },
706
+ });
707
+
708
+ // Create 'blocks' edge in OpenTasks if both tasks are mapped
709
+ const blockerIssueId = this.taskToIssue.get(blockerId);
710
+ const taskIssueId = this.taskToIssue.get(taskId);
711
+ if (blockerIssueId && taskIssueId) {
712
+ await this.client.createEdge(blockerIssueId, taskIssueId, "blocks");
713
+ }
714
+ }
715
+
716
+ async removeBlocker(taskId: TaskId, blockerId: TaskId): Promise<void> {
717
+ this.ensureOpen();
718
+ const task = this.eventStore.getTask(taskId);
719
+ if (!task) {
720
+ throw new OpenTasksBackendError(
721
+ `Task not found: ${taskId}`,
722
+ "TASK_NOT_FOUND",
723
+ taskId
724
+ );
725
+ }
726
+
727
+ // Record in EventStore
728
+ this.eventStore.emit({
729
+ type: "task",
730
+ source: { agent_id: task.assigned_agent ?? task.created_by },
731
+ payload: {
732
+ task_id: taskId,
733
+ action: "blocker_removed",
734
+ details: { blocker_id: blockerId },
735
+ },
736
+ });
737
+
738
+ // Remove 'blocks' edge in OpenTasks if both are mapped
739
+ const blockerIssueId = this.taskToIssue.get(blockerId);
740
+ const taskIssueId = this.taskToIssue.get(taskId);
741
+ if (blockerIssueId && taskIssueId) {
742
+ await this.client.removeEdge(blockerIssueId, taskIssueId, "blocks");
743
+ }
744
+ }
745
+
746
+ async getBlockers(taskId: TaskId): Promise<ExtendedTask[]> {
747
+ const task = this.eventStore.getTask(taskId);
748
+ if (!task) {
749
+ throw new OpenTasksBackendError(
750
+ `Task not found: ${taskId}`,
751
+ "TASK_NOT_FOUND",
752
+ taskId
753
+ );
754
+ }
755
+
756
+ // Try OpenTasks first for mapped tasks
757
+ const issueId = this.taskToIssue.get(taskId);
758
+ if (issueId) {
759
+ try {
760
+ const blockerSummaries = await this.client.getBlockers(issueId);
761
+ const blockers: ExtendedTask[] = [];
762
+ for (const summary of blockerSummaries) {
763
+ const blockerTaskId = this.issueToTask.get(summary.id);
764
+ if (blockerTaskId) {
765
+ const blockerTask = this.eventStore.getTask(blockerTaskId);
766
+ if (blockerTask) {
767
+ blockers.push(this.toExtendedTask(blockerTask));
768
+ }
769
+ }
770
+ }
771
+ return blockers;
772
+ } catch {
773
+ // Fall through to EventStore
774
+ }
775
+ }
776
+
777
+ // Fallback: use EventStore blockers
778
+ const blockerIds = task.blockers ?? [];
779
+ const blockers: ExtendedTask[] = [];
780
+ for (const blockerId of blockerIds) {
781
+ const blocker = this.eventStore.getTask(blockerId);
782
+ if (blocker) {
783
+ blockers.push(this.toExtendedTask(blocker));
784
+ }
785
+ }
786
+ return blockers;
787
+ }
788
+
789
+ async getBlocking(taskId: TaskId): Promise<ExtendedTask[]> {
790
+ const task = this.eventStore.getTask(taskId);
791
+ if (!task) {
792
+ throw new OpenTasksBackendError(
793
+ `Task not found: ${taskId}`,
794
+ "TASK_NOT_FOUND",
795
+ taskId
796
+ );
797
+ }
798
+
799
+ // Try OpenTasks first for mapped tasks
800
+ const issueId = this.taskToIssue.get(taskId);
801
+ if (issueId) {
802
+ try {
803
+ const blockingSummaries = await this.client.getBlocking(issueId);
804
+ const blocking: ExtendedTask[] = [];
805
+ for (const summary of blockingSummaries) {
806
+ const blockedTaskId = this.issueToTask.get(summary.id);
807
+ if (blockedTaskId) {
808
+ const blockedTask = this.eventStore.getTask(blockedTaskId);
809
+ if (blockedTask) {
810
+ blocking.push(this.toExtendedTask(blockedTask));
811
+ }
812
+ }
813
+ }
814
+ return blocking;
815
+ } catch {
816
+ // Fall through to EventStore
817
+ }
818
+ }
819
+
820
+ // Fallback: scan EventStore
821
+ const allTasks = this.eventStore.listTasks();
822
+ const blocking = allTasks.filter((t) => t.blockers?.includes(taskId));
823
+ return blocking.map((t) => this.toExtendedTask(t));
824
+ }
825
+
826
+ // ─────────────────────────────────────────────────────────────────────────────
827
+ // Pull Model (Claim/Unclaim)
828
+ // ─────────────────────────────────────────────────────────────────────────────
829
+
830
+ async claim(
831
+ agentId: AgentId,
832
+ filter?: ClaimFilter
833
+ ): Promise<ExtendedTask | null> {
834
+ this.ensureOpen();
835
+ const candidates = await this.listClaimable(filter);
836
+
837
+ if (candidates.length === 0) {
838
+ return null;
839
+ }
840
+
841
+ const task = candidates[0];
842
+
843
+ // Re-check for contention
844
+ const current = this.eventStore.getTask(task.id);
845
+ if (!current || current.status !== "pending" || current.assigned_agent) {
846
+ return null;
847
+ }
848
+
849
+ // Assign locally
850
+ this.eventStore.emit({
851
+ type: "task",
852
+ source: { agent_id: agentId },
853
+ payload: {
854
+ task_id: task.id,
855
+ action: "assigned",
856
+ details: { agent_id: agentId },
857
+ },
858
+ });
859
+
860
+ // Claim in OpenTasks
861
+ const issueId = this.taskToIssue.get(task.id);
862
+ if (issueId) {
863
+ await this.client.updateIssue(issueId, {
864
+ assignee: agentId,
865
+ metadata: { claimed_by: agentId },
866
+ });
867
+ }
868
+
869
+ const assigned = this.eventStore.getTask(task.id)!;
870
+ return this.toExtendedTask(assigned);
871
+ }
872
+
873
+ async unclaim(taskId: TaskId): Promise<void> {
874
+ this.ensureOpen();
875
+ const task = this.eventStore.getTask(taskId);
876
+ if (!task) {
877
+ throw new OpenTasksBackendError(
878
+ `Task not found: ${taskId}`,
879
+ "TASK_NOT_FOUND",
880
+ taskId
881
+ );
882
+ }
883
+
884
+ if (!task.assigned_agent) {
885
+ throw new OpenTasksBackendError(
886
+ `Task is not assigned: ${taskId}`,
887
+ "TASK_NOT_ASSIGNED",
888
+ taskId
889
+ );
890
+ }
891
+
892
+ this.eventStore.emit({
893
+ type: "task",
894
+ source: { agent_id: task.assigned_agent },
895
+ payload: {
896
+ task_id: taskId,
897
+ action: "unassigned",
898
+ details: { agent_id: task.assigned_agent },
899
+ },
900
+ });
901
+
902
+ // Unclaim in OpenTasks
903
+ const issueId = this.taskToIssue.get(taskId);
904
+ if (issueId) {
905
+ await this.client.updateIssue(issueId, {
906
+ assignee: null,
907
+ metadata: { claimed_by: null },
908
+ });
909
+ }
910
+ }
911
+
912
+ async listClaimable(filter?: ClaimFilter): Promise<ExtendedTask[]> {
913
+ let tasks = this.eventStore.listTasks();
914
+
915
+ // Only pending, unassigned tasks
916
+ tasks = tasks.filter(
917
+ (t) => t.status === "pending" && !t.assigned_agent
918
+ );
919
+
920
+ if (filter) {
921
+ if (filter.tags && filter.tags.length > 0) {
922
+ const filterTags = new Set(filter.tags);
923
+ tasks = tasks.filter(
924
+ (t) => t.tags?.some((tag) => filterTags.has(tag))
925
+ );
926
+ }
927
+
928
+ if (filter.rootTasksOnly) {
929
+ tasks = tasks.filter((t) => !t.parent_task);
930
+ }
931
+
932
+ if (filter.created_by) {
933
+ tasks = tasks.filter((t) => t.created_by === filter.created_by);
934
+ }
935
+ }
936
+
937
+ // Filter out blocked tasks
938
+ const extended = await Promise.all(
939
+ tasks.map((t) => this.toExtendedTaskAsync(t))
940
+ );
941
+ return extended.filter((t) => !t.isBlocked);
942
+ }
943
+
944
+ // ─────────────────────────────────────────────────────────────────────────────
945
+ // History
946
+ // ─────────────────────────────────────────────────────────────────────────────
947
+
948
+ async getAgentHistory(taskId: TaskId): Promise<AgentHistoryEntry[]> {
949
+ const task = this.eventStore.getTask(taskId);
950
+ if (!task) {
951
+ throw new OpenTasksBackendError(
952
+ `Task not found: ${taskId}`,
953
+ "TASK_NOT_FOUND",
954
+ taskId
955
+ );
956
+ }
957
+ return task.agent_history ?? [];
958
+ }
959
+
960
+ // ─────────────────────────────────────────────────────────────────────────────
961
+ // Event Subscriptions
962
+ // ─────────────────────────────────────────────────────────────────────────────
963
+
964
+ onTaskChange(callback: TaskChangeCallback): Unsubscribe;
965
+ onTaskChange(taskId: TaskId, callback: TaskChangeCallback): Unsubscribe;
966
+ onTaskChange(
967
+ callbackOrTaskId: TaskChangeCallback | TaskId,
968
+ maybeCallback?: TaskChangeCallback
969
+ ): Unsubscribe {
970
+ const filterTaskId =
971
+ typeof callbackOrTaskId === "string" ? callbackOrTaskId : undefined;
972
+ const callback =
973
+ typeof callbackOrTaskId === "function"
974
+ ? callbackOrTaskId
975
+ : maybeCallback!;
976
+
977
+ const seenTaskIds = new Set<TaskId>();
978
+
979
+ return this.eventStore.onTaskChange((taskId, task) => {
980
+ if (filterTaskId && taskId !== filterTaskId) return;
981
+
982
+ let eventType: TaskChangeEvent["type"];
983
+ if (!task) {
984
+ eventType = "deleted";
985
+ } else if (seenTaskIds.has(taskId)) {
986
+ eventType = "updated";
987
+ } else {
988
+ eventType = "created";
989
+ seenTaskIds.add(taskId);
990
+ }
991
+
992
+ const event: TaskChangeEvent = {
993
+ type: eventType,
994
+ taskId,
995
+ task: task ? this.toExtendedTask(task) : ({} as ExtendedTask),
996
+ };
997
+
998
+ callback(event);
999
+ });
1000
+ }
1001
+
1002
+ // ─────────────────────────────────────────────────────────────────────────────
1003
+ // Public Utility Methods
1004
+ // ─────────────────────────────────────────────────────────────────────────────
1005
+
1006
+ /**
1007
+ * Get the OpenTasks issue ID for a macro-agent task.
1008
+ * Returns undefined if the task is not mapped to an issue.
1009
+ */
1010
+ getIssueForTask(taskId: TaskId): string | undefined {
1011
+ return this.taskToIssue.get(taskId);
1012
+ }
1013
+
1014
+ /**
1015
+ * Get the macro-agent task ID for an OpenTasks issue.
1016
+ * Returns undefined if the issue is not mapped to a task.
1017
+ */
1018
+ getTaskForIssue(issueId: string): TaskId | undefined {
1019
+ return this.issueToTask.get(issueId);
1020
+ }
1021
+
1022
+ /**
1023
+ * Import an existing OpenTasks issue as a macro-agent task.
1024
+ * This is useful for pulling tasks from OpenTasks into macro-agent.
1025
+ */
1026
+ async importIssue(
1027
+ issueId: string,
1028
+ createdBy: AgentId
1029
+ ): Promise<ExtendedTask> {
1030
+ this.ensureOpen();
1031
+ // Check if already imported
1032
+ const existingTaskId = this.issueToTask.get(issueId);
1033
+ if (existingTaskId) {
1034
+ const existing = await this.get(existingTaskId);
1035
+ if (existing) return existing;
1036
+ }
1037
+
1038
+ const issue = await this.client.getIssue(issueId);
1039
+ if (!issue) {
1040
+ throw new OpenTasksBackendError(
1041
+ `OpenTasks issue not found: ${issueId}`,
1042
+ "NOT_FOUND"
1043
+ );
1044
+ }
1045
+
1046
+ const taskId = `task_${nanoid(12)}`;
1047
+
1048
+ // Store mapping
1049
+ this.taskToIssue.set(taskId, issueId);
1050
+ this.issueToTask.set(issueId, taskId);
1051
+
1052
+ // Determine task status from issue
1053
+ const taskStatus = mapOpenTasksStatus(issue.status);
1054
+
1055
+ // Create in EventStore
1056
+ this.eventStore.emit({
1057
+ type: "task",
1058
+ source: { agent_id: createdBy },
1059
+ payload: {
1060
+ task_id: taskId,
1061
+ action: "created",
1062
+ details: {
1063
+ description: issue.title,
1064
+ tags: issue.tags,
1065
+ external_id: issueId,
1066
+ },
1067
+ },
1068
+ });
1069
+
1070
+ // If issue is not open/pending, transition to the right status
1071
+ if (taskStatus === "in_progress") {
1072
+ this.eventStore.emit({
1073
+ type: "task",
1074
+ source: { agent_id: createdBy },
1075
+ payload: {
1076
+ task_id: taskId,
1077
+ action: "status_change",
1078
+ details: { status: "in_progress" },
1079
+ },
1080
+ });
1081
+ } else if (taskStatus === "completed") {
1082
+ this.eventStore.emit({
1083
+ type: "task",
1084
+ source: { agent_id: createdBy },
1085
+ payload: {
1086
+ task_id: taskId,
1087
+ action: "completed",
1088
+ details: {},
1089
+ },
1090
+ });
1091
+ }
1092
+
1093
+ // If issue has an assignee, assign
1094
+ if (issue.assignee) {
1095
+ this.eventStore.emit({
1096
+ type: "task",
1097
+ source: { agent_id: issue.assignee },
1098
+ payload: {
1099
+ task_id: taskId,
1100
+ action: "assigned",
1101
+ details: { agent_id: issue.assignee },
1102
+ },
1103
+ });
1104
+ }
1105
+
1106
+ return this.issueToExtendedTask(issue, taskId);
1107
+ }
1108
+
1109
+ /**
1110
+ * Bulk import all open issues from OpenTasks as tasks.
1111
+ */
1112
+ async importOpenIssues(createdBy: AgentId): Promise<ExtendedTask[]> {
1113
+ this.ensureOpen();
1114
+ const issues = await this.client.listIssues({
1115
+ status: ["open", "in_progress"],
1116
+ archived: false,
1117
+ });
1118
+
1119
+ const tasks: ExtendedTask[] = [];
1120
+ for (const issue of issues) {
1121
+ // Skip already-imported issues
1122
+ if (this.issueToTask.has(issue.id)) continue;
1123
+
1124
+ // Skip issues not created by macro-agent (unless they have no source)
1125
+ // This allows importing issues from other sources too
1126
+ const task = await this.importIssue(issue.id, createdBy);
1127
+ tasks.push(task);
1128
+ }
1129
+
1130
+ return tasks;
1131
+ }
1132
+
1133
+ // ─────────────────────────────────────────────────────────────────────────────
1134
+ // Private Helpers
1135
+ // ─────────────────────────────────────────────────────────────────────────────
1136
+
1137
+ /**
1138
+ * Sync a task status change to OpenTasks.
1139
+ */
1140
+ private async syncStatusToOpenTasks(
1141
+ taskId: TaskId,
1142
+ status: TaskStatus
1143
+ ): Promise<void> {
1144
+ if (!this.config.syncStatus) return;
1145
+
1146
+ const issueId = this.taskToIssue.get(taskId);
1147
+ if (!issueId) return;
1148
+
1149
+ const openTasksStatus = mapTaskStatus(status);
1150
+ try {
1151
+ await this.client.updateIssue(issueId, {
1152
+ status: openTasksStatus,
1153
+ });
1154
+ } catch (error) {
1155
+ // Log but don't fail - sync is best-effort
1156
+ console.warn(
1157
+ `Failed to sync status to OpenTasks for ${taskId} (${issueId}): ${error}`
1158
+ );
1159
+ }
1160
+ }
1161
+
1162
+ /**
1163
+ * Sync a transition that happened via the opentasks daemon's `task` tool.
1164
+ * Updates the EventStore without re-syncing back to opentasks (since the
1165
+ * daemon already processed the transition).
1166
+ *
1167
+ * Accepts either an opentasks issue ID (e.g., "i-xxxx") or an EventStore
1168
+ * task ID (e.g., "task-xxx") — resolves to the EventStore ID either way.
1169
+ *
1170
+ * @param externalId - The opentasks issue ID or EventStore task ID
1171
+ * @param action - The transition action ("complete", "start", "close", "block", "reopen", "assign")
1172
+ * @param agentId - The agent that performed the transition or the assignee for "assign"
1173
+ */
1174
+ async syncExternalTransition(externalId: string, action: string, agentId?: string): Promise<void> {
1175
+ // Resolve to EventStore task ID: try opentasks ID lookup first, then direct
1176
+ const taskId = this.issueToTask.get(externalId)
1177
+ ?? (this.eventStore.getTask(externalId as TaskId) ? externalId as TaskId : undefined);
1178
+ if (!taskId) return;
1179
+
1180
+ const task = this.eventStore.getTask(taskId);
1181
+ if (!task) return;
1182
+
1183
+ const agent = (agentId as AgentId | undefined) ?? task.assigned_agent ?? task.created_by;
1184
+
1185
+ // Handle assignment separately — it updates assignee, not status
1186
+ if (action === "assign") {
1187
+ if (task.assigned_agent !== agent) {
1188
+ this.eventStore.emit({
1189
+ type: "task",
1190
+ source: { agent_id: agent },
1191
+ payload: {
1192
+ task_id: taskId,
1193
+ action: "assigned",
1194
+ details: { agent_id: agent },
1195
+ },
1196
+ });
1197
+ }
1198
+ return;
1199
+ }
1200
+
1201
+ // Map action to target status
1202
+ const ACTION_TO_STATUS: Record<string, TaskStatus> = {
1203
+ complete: "completed",
1204
+ close: "completed",
1205
+ start: "in_progress",
1206
+ block: "pending",
1207
+ reopen: "pending",
1208
+ };
1209
+ const targetStatus = ACTION_TO_STATUS[action];
1210
+ if (!targetStatus || task.status === targetStatus) return;
1211
+
1212
+ // Emit the appropriate EventStore event
1213
+ if (targetStatus === "completed") {
1214
+ this.eventStore.emit({
1215
+ type: "task",
1216
+ source: { agent_id: agent },
1217
+ payload: { task_id: taskId, action: "completed", details: {} },
1218
+ });
1219
+ } else {
1220
+ this.eventStore.emit({
1221
+ type: "task",
1222
+ source: { agent_id: agent },
1223
+ payload: {
1224
+ task_id: taskId,
1225
+ action: "status_change",
1226
+ details: { status: targetStatus },
1227
+ },
1228
+ });
1229
+ }
1230
+ }
1231
+
1232
+ /**
1233
+ * Convert an EventStore Task to ExtendedTask with isBlocked computed
1234
+ * from local blockers.
1235
+ */
1236
+ private toExtendedTask(task: Task): ExtendedTask {
1237
+ const blockerIds = task.blockers ?? [];
1238
+ let isBlocked = false;
1239
+
1240
+ for (const blockerId of blockerIds) {
1241
+ const blocker = this.eventStore.getTask(blockerId);
1242
+ if (blocker && blocker.status !== "completed") {
1243
+ isBlocked = true;
1244
+ break;
1245
+ }
1246
+ }
1247
+
1248
+ return {
1249
+ ...task,
1250
+ isBlocked,
1251
+ external_id: this.taskToIssue.get(task.id),
1252
+ };
1253
+ }
1254
+
1255
+ /**
1256
+ * Convert an EventStore Task to ExtendedTask with isBlocked computed
1257
+ * from OpenTasks graph (async, checks remote blockers).
1258
+ */
1259
+ private async toExtendedTaskAsync(task: Task): Promise<ExtendedTask> {
1260
+ const issueId = this.taskToIssue.get(task.id);
1261
+
1262
+ // If mapped to OpenTasks, use graph-based blocking
1263
+ if (issueId) {
1264
+ try {
1265
+ const blockers = await this.client.getBlockers(issueId);
1266
+ const hasActiveBlockers = blockers.some(
1267
+ (b) => b.status && !isIssueComplete(b.status)
1268
+ );
1269
+ return {
1270
+ ...task,
1271
+ isBlocked: hasActiveBlockers,
1272
+ external_id: issueId,
1273
+ };
1274
+ } catch {
1275
+ // Fall through to local check
1276
+ }
1277
+ }
1278
+
1279
+ return this.toExtendedTask(task);
1280
+ }
1281
+
1282
+ /**
1283
+ * Convert an OpenTasks issue to an ExtendedTask.
1284
+ */
1285
+ private issueToExtendedTask(
1286
+ issue: OpenTasksIssue,
1287
+ taskId: TaskId
1288
+ ): ExtendedTask {
1289
+ const taskStatus = mapOpenTasksStatus(issue.status);
1290
+
1291
+ return {
1292
+ id: taskId,
1293
+ description: issue.title,
1294
+ status: taskStatus,
1295
+ assigned_agent: issue.assignee,
1296
+ parent_task: undefined, // Resolved separately if needed
1297
+ subtasks: undefined,
1298
+ blockers: undefined,
1299
+ created_at: new Date(issue.created_at).getTime(),
1300
+ started_at: taskStatus === "in_progress"
1301
+ ? new Date(issue.updated_at).getTime()
1302
+ : undefined,
1303
+ completed_at: issue.closed_at
1304
+ ? new Date(issue.closed_at).getTime()
1305
+ : undefined,
1306
+ created_by: (issue.metadata?.created_by as string) ?? "unknown",
1307
+ tags: issue.tags,
1308
+ isBlocked: issue.status === "blocked",
1309
+ external_id: issue.id,
1310
+ };
1311
+ }
1312
+ }
1313
+
1314
+ /**
1315
+ * Create an OpenTasksTaskBackend instance.
1316
+ */
1317
+ export function createOpenTasksTaskBackend(
1318
+ eventStore: EventStore,
1319
+ client: OpenTasksClient,
1320
+ config?: Partial<OpenTasksBackendConfig>
1321
+ ): OpenTasksTaskBackend {
1322
+ return new OpenTasksTaskBackend(eventStore, client, config);
1323
+ }