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
@@ -8,6 +8,11 @@
8
8
  */
9
9
 
10
10
  import { nanoid } from "nanoid";
11
+ import {
12
+ uniqueNamesGenerator,
13
+ adjectives,
14
+ animals,
15
+ } from "unique-names-generator";
11
16
  import {
12
17
  AgentFactory,
13
18
  type Session,
@@ -50,6 +55,7 @@ import {
50
55
  type CascadeAgentManager,
51
56
  } from "../lifecycle/cascade.js";
52
57
  import type { HealthCheckService } from "../monitor/health-check-service.js";
58
+ import { AgentTokenManager } from "../auth/token.js";
53
59
 
54
60
  // ─────────────────────────────────────────────────────────────────
55
61
  // Helper Functions
@@ -103,8 +109,12 @@ export interface AgentManager {
103
109
 
104
110
  /**
105
111
  * Resume a stopped agent by loading its existing session.
112
+ * @param permissionMode - Optional permission mode override (defaults to the agent manager's default)
106
113
  */
107
- resume(agentId: AgentId): Promise<SpawnedAgent>;
114
+ resume(
115
+ agentId: AgentId,
116
+ permissionMode?: PermissionMode,
117
+ ): Promise<SpawnedAgent>;
108
118
 
109
119
  /**
110
120
  * Continue a terminated agent by spawning a new agent with the same
@@ -117,7 +127,20 @@ export interface AgentManager {
117
127
  */
118
128
  continueAgent(
119
129
  agentId: AgentId,
120
- options?: ContinueAgentOptions
130
+ options?: ContinueAgentOptions,
131
+ ): Promise<SpawnedAgent>;
132
+
133
+ /**
134
+ * Fork an agent's session, creating a new agent with the same
135
+ * conversation history. Uses forkWithFlush for active sessions
136
+ * or loadSession for stopped agents with persisted sessions.
137
+ *
138
+ * @param sourceAgentId - ID of the agent to fork from
139
+ * @param options - Fork options (name, prompt, cwd)
140
+ */
141
+ forkAgent(
142
+ sourceAgentId: AgentId,
143
+ options?: { name?: string; prompt?: string; cwd?: string },
121
144
  ): Promise<SpawnedAgent>;
122
145
 
123
146
  // ── Queries ────────────────────────────────────────────────────
@@ -143,7 +166,7 @@ export interface AgentManager {
143
166
  */
144
167
  getHierarchy(
145
168
  agentId: AgentId,
146
- options?: HierarchyOptions
169
+ options?: HierarchyOptions,
147
170
  ): AgentHierarchy | null;
148
171
 
149
172
  // ── Head Manager ───────────────────────────────────────────────
@@ -166,7 +189,7 @@ export interface AgentManager {
166
189
  */
167
190
  prompt(
168
191
  agentId: AgentId,
169
- message: string
192
+ message: string,
170
193
  ): AsyncIterable<ExtendedSessionUpdate>;
171
194
 
172
195
  /**
@@ -186,7 +209,7 @@ export interface AgentManager {
186
209
  maxFollowUps?: number;
187
210
  /** Callback for each update during prompting */
188
211
  onUpdate?: (update: ExtendedSessionUpdate) => void;
189
- }
212
+ },
190
213
  ): Promise<{
191
214
  doneCalled: boolean;
192
215
  doneStatus?: string;
@@ -235,7 +258,7 @@ export interface AgentManager {
235
258
  respondToPermission(
236
259
  agentId: AgentId,
237
260
  requestId: string,
238
- optionId: string
261
+ optionId: string,
239
262
  ): boolean;
240
263
 
241
264
  /**
@@ -247,6 +270,24 @@ export interface AgentManager {
247
270
  */
248
271
  cancelPermission(agentId: AgentId, requestId: string): boolean;
249
272
 
273
+ /**
274
+ * Change the permission mode for a running agent at runtime.
275
+ * Takes effect on the next permission request.
276
+ *
277
+ * @param agentId - Agent ID to change permission mode for
278
+ * @param mode - New permission mode
279
+ * @returns true if the mode was changed successfully
280
+ */
281
+ setPermissionMode(agentId: AgentId, mode: PermissionMode): boolean;
282
+
283
+ /**
284
+ * Get the current permission mode for a running agent.
285
+ *
286
+ * @param agentId - Agent ID to query
287
+ * @returns The current permission mode, or null if no active session
288
+ */
289
+ getPermissionMode(agentId: AgentId): PermissionMode | null;
290
+
250
291
  // ── Lifecycle Callbacks ────────────────────────────────────────
251
292
 
252
293
  /**
@@ -260,15 +301,22 @@ export interface AgentManager {
260
301
  * Set a spawn interceptor that transforms SpawnAgentOptions before spawning.
261
302
  * Used by TeamRuntime to inject team topics, prompts, MCP servers, and env vars.
262
303
  */
263
- setSpawnInterceptor(
264
- interceptor: SpawnInterceptor | null
265
- ): void;
304
+ setSpawnInterceptor(interceptor: SpawnInterceptor | null): void;
266
305
 
267
306
  /**
268
307
  * Get the RoleRegistry used by this AgentManager.
269
308
  */
270
309
  getRoleRegistry(): RoleRegistry;
271
310
 
311
+ // ── OpenTasks Socket Path (Late Binding) ─────────────────────
312
+
313
+ /**
314
+ * Set the runtime OpenTasks socket path for propagation to child agents.
315
+ * Used when createTaskBackend() discovers the daemon socket path after
316
+ * AgentManager is already created.
317
+ */
318
+ setOpenTasksSocketPath(socketPath: string): void;
319
+
272
320
  // ── Mail Services (Late Binding) ─────────────────────────────
273
321
 
274
322
  /**
@@ -277,7 +325,7 @@ export interface AgentManager {
277
325
  */
278
326
  setMailServices(
279
327
  mailService: import("../mail/mail-service.js").MailService,
280
- conversationMap: import("../mail/conversation-map.js").ConversationMap
328
+ conversationMap: import("../mail/conversation-map.js").ConversationMap,
281
329
  ): void;
282
330
 
283
331
  // ── Cleanup ────────────────────────────────────────────────────
@@ -335,6 +383,38 @@ export interface AgentManagerConfig {
335
383
  * Required when mailService is provided.
336
384
  */
337
385
  conversationMap?: import("../mail/conversation-map.js").ConversationMap;
386
+
387
+ /**
388
+ * Optional server URL for MCP thin-client mode.
389
+ * When set, spawned agents use ephemeral MAP WebSocket calls instead
390
+ * of creating local service stacks.
391
+ */
392
+ serverUrl?: string;
393
+
394
+ /**
395
+ * Server authentication token for MCP thin-client connections.
396
+ * Passed to spawned agents as MACRO_SERVER_TOKEN env var.
397
+ */
398
+ serverToken?: string;
399
+
400
+ /**
401
+ * Per-agent token manager for MCP bridge authentication.
402
+ * When provided, generates a unique token per agent at spawn time
403
+ * and revokes it on terminate.
404
+ */
405
+ agentTokenManager?: AgentTokenManager;
406
+
407
+ /**
408
+ * Task backend type to propagate to child MCP subprocesses.
409
+ * Sourced from merged config. Falls back to MACRO_TASK_BACKEND env var.
410
+ */
411
+ taskBackend?: string;
412
+
413
+ /**
414
+ * OpenTasks socket path to propagate to child MCP subprocesses.
415
+ * Sourced from merged config. Falls back to OPENTASKS_SOCKET_PATH env var.
416
+ */
417
+ openTasksSocketPath?: string;
338
418
  }
339
419
 
340
420
  // ─────────────────────────────────────────────────────────────────
@@ -346,7 +426,7 @@ export interface AgentManagerConfig {
346
426
  * Used by TeamRuntime to inject team-specific configuration.
347
427
  */
348
428
  export type SpawnInterceptor = (
349
- options: SpawnAgentOptions
429
+ options: SpawnAgentOptions,
350
430
  ) => SpawnAgentOptions | Promise<SpawnAgentOptions>;
351
431
 
352
432
  // ─────────────────────────────────────────────────────────────────
@@ -356,7 +436,7 @@ export type SpawnInterceptor = (
356
436
  export function createAgentManager(
357
437
  eventStore: EventStore,
358
438
  messageRouter: MessageRouter,
359
- config: AgentManagerConfig = {}
439
+ config: AgentManagerConfig = {},
360
440
  ): AgentManager {
361
441
  const {
362
442
  defaultPermissionMode = "auto-approve",
@@ -367,8 +447,16 @@ export function createAgentManager(
367
447
  healthCheckService,
368
448
  mailService: initialMailService,
369
449
  conversationMap: initialConversationMap,
450
+ serverUrl,
451
+ serverToken,
452
+ agentTokenManager,
453
+ taskBackend: configTaskBackend,
454
+ openTasksSocketPath: initialOpenTasksSocketPath,
370
455
  } = config;
371
456
 
457
+ // Mutable OpenTasks socket path (support late binding via setOpenTasksSocketPath)
458
+ let configOpenTasksSocketPath = initialOpenTasksSocketPath;
459
+
372
460
  // Mutable mail services (support late binding via setMailServices)
373
461
  let mailService = initialMailService;
374
462
  let conversationMap = initialConversationMap;
@@ -385,11 +473,91 @@ export function createAgentManager(
385
473
  // Lifecycle event listeners
386
474
  const lifecycleListeners = new Set<AgentLifecycleCallback>();
387
475
 
476
+ // Shutdown guard — prevents spawns during close()
477
+ let isShuttingDown = false;
478
+
479
+ // ─────────────────────────────────────────────────────────────────
480
+ // MCP Server Config
481
+ // ─────────────────────────────────────────────────────────────────
482
+
483
+ /**
484
+ * Build the macro-agent MCP server config for a Claude Code agent session.
485
+ * Used by spawn(), resume(), and forkAgent() to ensure every agent gets
486
+ * access to the macro-agent coordination tools.
487
+ */
488
+ function buildMacroAgentMcp(opts: {
489
+ agentId: string;
490
+ parentId: string;
491
+ taskId: string;
492
+ cwd: string;
493
+ permissionMode: string;
494
+ lineage?: string[];
495
+ sessionId?: string;
496
+ }) {
497
+ // Common env vars for both thin-client and legacy modes
498
+ const env = [
499
+ { name: "MACRO_AGENT_ID", value: opts.agentId },
500
+ { name: "MACRO_PARENT_ID", value: opts.parentId },
501
+ { name: "MACRO_TASK_ID", value: opts.taskId },
502
+ { name: "MACRO_AGENT_CWD", value: opts.cwd },
503
+ { name: "MACRO_PERMISSION_MODE", value: opts.permissionMode },
504
+ {
505
+ name: "MACRO_TASK_BACKEND",
506
+ value: configTaskBackend ?? process.env.MACRO_TASK_BACKEND ?? "",
507
+ },
508
+ {
509
+ name: "OPENTASKS_SOCKET_PATH",
510
+ value: configOpenTasksSocketPath ?? process.env.OPENTASKS_SOCKET_PATH ?? "",
511
+ },
512
+ ];
513
+
514
+ if (serverUrl) {
515
+ // Thin-client mode: forward tool calls to main server via MAP WebSocket
516
+ env.push(
517
+ { name: "MACRO_SERVER_URL", value: serverUrl },
518
+ {
519
+ name: "MACRO_AGENT_LINEAGE",
520
+ value: JSON.stringify(opts.lineage ?? []),
521
+ },
522
+ { name: "MACRO_SESSION_ID", value: opts.sessionId ?? "" },
523
+ );
524
+
525
+ // Auth tokens for thin-client connections
526
+ if (serverToken) {
527
+ env.push({ name: "MACRO_SERVER_TOKEN", value: serverToken });
528
+ }
529
+ if (agentTokenManager) {
530
+ const agentToken = agentTokenManager.createToken(opts.agentId);
531
+ env.push({ name: "MACRO_AGENT_TOKEN", value: agentToken });
532
+ }
533
+ } else {
534
+ // Legacy mode: create local service stack with shared SQLite
535
+ env.push(
536
+ { name: "MACRO_INSTANCE_ID", value: eventStore.instanceId },
537
+ { name: "MACRO_BASE_DIR", value: eventStore.baseDir },
538
+ );
539
+ }
540
+
541
+ return {
542
+ name: "macro-agent",
543
+ command: "npx",
544
+ args: ["multiagent-mcp"],
545
+ env,
546
+ };
547
+ }
548
+
388
549
  // ─────────────────────────────────────────────────────────────────
389
550
  // Lifecycle
390
551
  // ─────────────────────────────────────────────────────────────────
391
552
 
392
553
  async function spawn(rawOptions: SpawnAgentOptions): Promise<SpawnedAgent> {
554
+ if (isShuttingDown) {
555
+ throw new AgentManagerError(
556
+ "Cannot spawn agent during shutdown",
557
+ "SHUTDOWN_IN_PROGRESS",
558
+ );
559
+ }
560
+
393
561
  // Apply spawn interceptor if set (used by TeamRuntime for team context injection)
394
562
  const options = spawnInterceptor
395
563
  ? await spawnInterceptor(rawOptions)
@@ -426,7 +594,7 @@ export function createAgentManager(
426
594
  throw new AgentManagerError(
427
595
  `Parent agent not found: ${parent}`,
428
596
  "AGENT_NOT_FOUND",
429
- parent
597
+ parent,
430
598
  );
431
599
  }
432
600
 
@@ -437,8 +605,12 @@ export function createAgentManager(
437
605
 
438
606
  // Accept either the specific capability (e.g., agent.spawn.grinder)
439
607
  // or the generic agent.spawn.custom as a fallback for non-built-in roles
440
- const hasSpecific = roleRegistry.hasCapability(parentRole, requiredCapability);
441
- const hasGeneric = requiredCapability !== AGENT_CAPABILITIES.SPAWN_CUSTOM &&
608
+ const hasSpecific = roleRegistry.hasCapability(
609
+ parentRole,
610
+ requiredCapability,
611
+ );
612
+ const hasGeneric =
613
+ requiredCapability !== AGENT_CAPABILITIES.SPAWN_CUSTOM &&
442
614
  roleRegistry.hasCapability(parentRole, AGENT_CAPABILITIES.SPAWN_CUSTOM);
443
615
 
444
616
  if (!hasSpecific && !hasGeneric) {
@@ -446,7 +618,7 @@ export function createAgentManager(
446
618
  `Parent agent with role '${parentRole}' does not have capability to spawn '${childRole}' agents. ` +
447
619
  `Required capability: ${requiredCapability}`,
448
620
  "CAPABILITY_DENIED",
449
- parent
621
+ parent,
450
622
  );
451
623
  }
452
624
  }
@@ -507,6 +679,14 @@ export function createAgentManager(
507
679
  },
508
680
  });
509
681
 
682
+ // Generate a human-readable default name
683
+ const generatedName = uniqueNamesGenerator({
684
+ dictionaries: [adjectives, animals],
685
+ separator: "-",
686
+ length: 2,
687
+ });
688
+ eventStore.updateAgentMetadata(agentId as AgentId, { name: generatedName });
689
+
510
690
  // Persist immediately so MCP server subprocess can read the agent
511
691
  await eventStore.persist();
512
692
 
@@ -514,10 +694,10 @@ export function createAgentManager(
514
694
  const verifyAgent = eventStore.getAgent(agentId);
515
695
  const allAgents = eventStore.listAgents();
516
696
  console.error(
517
- `[AgentManager] After persist: agent ${agentId} exists = ${!!verifyAgent}, total agents = ${allAgents.length}, instancePath = ${eventStore.instancePath}`
697
+ `[AgentManager] After persist: agent ${agentId} exists = ${!!verifyAgent}, total agents = ${allAgents.length}, instancePath = ${eventStore.instancePath}`,
518
698
  );
519
699
  console.error(
520
- `[AgentManager] All agent IDs: ${allAgents.map((a) => a.id).join(", ")}`
700
+ `[AgentManager] All agent IDs: ${allAgents.map((a) => a.id).join(", ")}`,
521
701
  );
522
702
 
523
703
  try {
@@ -527,195 +707,207 @@ export function createAgentManager(
527
707
  env: agentConfig?.env,
528
708
  });
529
709
 
530
- // Build MCP server configuration for the macro-agent MCP server
531
- // Note: McpServerStdio doesn't have a 'type' field - stdio is the implicit default
532
- // when neither 'type: http' nor 'type: sse' is specified
533
- const macroAgentMcp = {
534
- name: "macro-agent",
535
- command: "npx",
536
- args: ["multiagent-mcp"],
537
- env: [
538
- { name: "MACRO_AGENT_ID", value: agentId },
539
- { name: "MACRO_PARENT_ID", value: parent ?? "" },
540
- { name: "MACRO_TASK_ID", value: taskId },
541
- { name: "MACRO_AGENT_CWD", value: cwd },
542
- { name: "MACRO_INSTANCE_ID", value: eventStore.instanceId },
543
- { name: "MACRO_BASE_DIR", value: eventStore.baseDir },
544
- ],
545
- };
546
-
547
- // Combine with any user-provided MCP servers
548
- // Note: Like macroAgentMcp, user MCP servers use stdio (no 'type' field)
549
- const userMcpServers =
550
- agentConfig?.mcpServers?.map((s) => ({
551
- name: s.name,
552
- command: s.command,
553
- args: s.args ?? [],
554
- env: s.env
555
- ? Object.entries(s.env).map(([name, value]) => ({ name, value }))
710
+ try {
711
+ const macroAgentMcp = buildMacroAgentMcp({
712
+ agentId,
713
+ parentId: parent ?? "",
714
+ taskId,
715
+ cwd,
716
+ permissionMode,
717
+ lineage: parentAgent?.lineage
718
+ ? [...parentAgent.lineage, parent!]
556
719
  : [],
557
- })) ?? [];
558
-
559
- // Create session with MCP servers
560
- // Note: The MCP server subprocess will start here and look for the agent
561
- // in EventStore. We already persisted the spawn event above.
562
- const session = await handle.createSession(cwd, {
563
- mcpServers: [macroAgentMcp, ...userMcpServers],
564
- });
565
-
566
- // Emit started status (session is ready)
567
- // Include the provider's session ID (e.g., Claude Code UUID) so
568
- // it can be used for handle.loadSession() during resume
569
- eventStore.emit({
570
- type: "status",
571
- source: { agent_id: agentId },
572
- payload: {
573
- status_type: "started",
574
- summary: "Agent session started",
575
- provider_session_id: session.id,
576
- },
577
- });
578
-
579
- // Persist the status event
580
- await eventStore.persist();
581
-
582
- // Set up default subscriptions via MessageRouter
583
- messageRouter.setupDefaultSubscriptions({
584
- agent_id: agentId,
585
- parent_id: parent ?? undefined,
586
- task_id: taskId,
587
- subscribe_parent: subscribeParent,
588
- additional_topics: topics,
589
- role: role ?? undefined,
590
- });
720
+ sessionId,
721
+ });
591
722
 
592
- // ─────────────────────────────────────────────────────────────────
593
- // Mail: Create task conversation for this agent
594
- // ─────────────────────────────────────────────────────────────────
595
- if (mailService && conversationMap) {
596
- try {
597
- const parentConversationId = parent
598
- ? conversationMap.getAgentConversation(parent) ??
599
- conversationMap.getSessionConversation(parent)
723
+ // Combine with any user-provided MCP servers
724
+ // Note: Like macroAgentMcp, user MCP servers use stdio (no 'type' field)
725
+ const userMcpServers =
726
+ agentConfig?.mcpServers?.map((s) => ({
727
+ name: s.name,
728
+ command: s.command,
729
+ args: s.args ?? [],
730
+ env: s.env
731
+ ? Object.entries(s.env).map(([name, value]) => ({ name, value }))
732
+ : [],
733
+ })) ?? [];
734
+
735
+ // Create session with MCP servers
736
+ // Note: The MCP server subprocess will start here and look for the agent
737
+ // in EventStore. We already persisted the spawn event above.
738
+ //
739
+ // When permissionMode is "interactive", we strip settingSources so that
740
+ // the Claude Code subprocess doesn't read pre-approved tool rules from
741
+ // the user's ~/.claude/settings.local.json. This ensures ALL tool calls
742
+ // go through the canUseTool → requestPermission ACP flow.
743
+ const agentMeta =
744
+ permissionMode === "interactive"
745
+ ? { claudeCode: { options: { settingSources: [] } } }
600
746
  : undefined;
747
+ const session = await handle.createSession(cwd, {
748
+ mcpServers: [macroAgentMcp, ...userMcpServers],
749
+ ...(agentMeta && { agentMeta }),
750
+ });
601
751
 
602
- const { conversationId: taskConvId } =
603
- mailService.createConversation({
604
- type: "task",
605
- subject: task?.slice(0, 80),
606
- createdBy: parent ?? agentId,
607
- parentConversationId: parentConversationId,
608
- });
752
+ // Emit started status (session is ready)
753
+ // Include the provider's session ID (e.g., Claude Code UUID) so
754
+ // it can be used for handle.loadSession() during resume
755
+ eventStore.emit({
756
+ type: "status",
757
+ source: { agent_id: agentId },
758
+ payload: {
759
+ status_type: "started",
760
+ summary: "Agent session started",
761
+ provider_session_id: session.id,
762
+ },
763
+ });
609
764
 
610
- // Join parent and child as participants
611
- if (parent) {
765
+ // Persist the status event
766
+ await eventStore.persist();
767
+
768
+ // Set up default subscriptions via MessageRouter
769
+ messageRouter.setupDefaultSubscriptions({
770
+ agent_id: agentId,
771
+ parent_id: parent ?? undefined,
772
+ task_id: taskId,
773
+ subscribe_parent: subscribeParent,
774
+ additional_topics: topics,
775
+ role: role ?? undefined,
776
+ });
777
+
778
+ // ─────────────────────────────────────────────────────────────────
779
+ // Mail: Create task conversation for this agent
780
+ // ─────────────────────────────────────────────────────────────────
781
+ if (mailService && conversationMap) {
782
+ try {
783
+ const parentConversationId = parent
784
+ ? (conversationMap.getAgentConversation(parent) ??
785
+ conversationMap.getSessionConversation(parent))
786
+ : undefined;
787
+
788
+ const { conversationId: taskConvId } =
789
+ mailService.createConversation({
790
+ type: "task",
791
+ subject: task?.slice(0, 80),
792
+ createdBy: parent ?? agentId,
793
+ parentConversationId: parentConversationId,
794
+ });
795
+
796
+ // Join parent and child as participants
797
+ if (parent) {
798
+ mailService.joinConversation({
799
+ conversationId: taskConvId,
800
+ participantId: parent,
801
+ participantType: "agent",
802
+ role: "initiator",
803
+ agentId: parent,
804
+ });
805
+ }
612
806
  mailService.joinConversation({
613
807
  conversationId: taskConvId,
614
- participantId: parent,
808
+ participantId: agentId,
615
809
  participantType: "agent",
616
- role: "initiator",
617
- agentId: parent,
810
+ role: "worker",
811
+ agentId,
618
812
  });
619
- }
620
- mailService.joinConversation({
621
- conversationId: taskConvId,
622
- participantId: agentId,
623
- participantType: "agent",
624
- role: "worker",
625
- agentId,
626
- });
627
813
 
628
- conversationMap.setAgentConversation(agentId, taskConvId);
629
- } catch (err) {
630
- // Never fail spawn due to mail errors
631
- console.warn(
632
- `[AgentManager] Failed to create task conversation for ${agentId}:`,
633
- err
634
- );
814
+ conversationMap.setAgentConversation(agentId, taskConvId);
815
+ } catch (err) {
816
+ // Never fail spawn due to mail errors
817
+ console.warn(
818
+ `[AgentManager] Failed to create task conversation for ${agentId}:`,
819
+ err,
820
+ );
821
+ }
635
822
  }
636
- }
637
823
 
638
- // Track active session
639
- const activeSession: ActiveSession = {
640
- agentId,
641
- handle,
642
- session,
643
- createdAt: Date.now(),
644
- isPrompting: false,
645
- };
646
- activeSessions.set(agentId, activeSession);
824
+ // Track active session
825
+ const activeSession: ActiveSession = {
826
+ agentId,
827
+ handle,
828
+ session,
829
+ createdAt: Date.now(),
830
+ isPrompting: false,
831
+ };
832
+ activeSessions.set(agentId, activeSession);
647
833
 
648
- // Get the agent from materialized view
649
- const agent = eventStore.getAgent(agentId)!;
834
+ // Get the agent from materialized view
835
+ const agent = eventStore.getAgent(agentId)!;
650
836
 
651
- // ─────────────────────────────────────────────────────────────────
652
- // Workspace Creation (Phase 2)
653
- // ─────────────────────────────────────────────────────────────────
654
- let workspace: Workspace | undefined;
655
- let resolvedStreamId = streamId;
837
+ // ─────────────────────────────────────────────────────────────────
838
+ // Workspace Creation (Phase 2)
839
+ // ─────────────────────────────────────────────────────────────────
840
+ let workspace: Workspace | undefined;
841
+ let resolvedStreamId = streamId;
656
842
 
657
- if (workspaceManager && role) {
658
- try {
659
- workspace = await createWorkspaceForRole(
660
- workspaceManager,
661
- agentId,
662
- role,
663
- {
664
- streamId,
665
- streamConfig,
666
- dataplaneTaskId,
667
- cwd,
668
- }
669
- );
670
-
671
- if (workspace) {
672
- agentWorkspaces.set(agentId, workspace);
673
- resolvedStreamId = workspace.streamId;
674
-
675
- // Register with parent coordinator if applicable
676
- if (
677
- parent &&
678
- (role === "worker" || role === "integrator")
679
- ) {
680
- const parentWorkspace = agentWorkspaces.get(parent);
681
- if (parentWorkspace?.role === "coordinator") {
682
- workspaceManager.registerChildWorkspace(
683
- parent,
684
- agentId,
685
- workspace.path
686
- );
843
+ if (workspaceManager && role) {
844
+ try {
845
+ workspace = await createWorkspaceForRole(
846
+ workspaceManager,
847
+ agentId,
848
+ role,
849
+ {
850
+ streamId,
851
+ streamConfig,
852
+ dataplaneTaskId,
853
+ cwd,
854
+ },
855
+ );
856
+
857
+ if (workspace) {
858
+ agentWorkspaces.set(agentId, workspace);
859
+ resolvedStreamId = workspace.streamId;
860
+
861
+ // Register with parent coordinator if applicable
862
+ if (parent && (role === "worker" || role === "integrator")) {
863
+ const parentWorkspace = agentWorkspaces.get(parent);
864
+ if (parentWorkspace?.role === "coordinator") {
865
+ workspaceManager.registerChildWorkspace(
866
+ parent,
867
+ agentId,
868
+ workspace.path,
869
+ );
870
+ }
687
871
  }
688
872
  }
873
+ } catch (wsError) {
874
+ console.error(
875
+ `[AgentManager] Failed to create workspace for ${agentId}: ${wsError}`,
876
+ );
877
+ // Continue without workspace - don't fail the spawn
689
878
  }
690
- } catch (wsError) {
691
- console.error(
692
- `[AgentManager] Failed to create workspace for ${agentId}: ${wsError}`
693
- );
694
- // Continue without workspace - don't fail the spawn
695
879
  }
696
- }
697
880
 
698
- // Notify lifecycle listeners
699
- notifyLifecycle({ type: "spawned", agent });
700
- notifyLifecycle({ type: "started", agent });
881
+ // Notify lifecycle listeners
882
+ notifyLifecycle({ type: "spawned", agent });
883
+ notifyLifecycle({ type: "started", agent });
701
884
 
702
- // Start health monitoring for coordinators
703
- if (healthCheckService && role === "coordinator") {
704
- healthCheckService.startForCoordinator(agentId);
705
- }
885
+ // Start health monitoring for coordinators
886
+ if (healthCheckService && role === "coordinator") {
887
+ healthCheckService.startForCoordinator(agentId);
888
+ }
706
889
 
707
- return {
708
- id: agentId,
709
- session_id: sessionId, // Macro-agent's own session ID for ACP protocol mapping
710
- agent,
711
- session,
712
- workspace,
713
- streamId: resolvedStreamId,
714
- };
890
+ return {
891
+ id: agentId,
892
+ session_id: sessionId, // Macro-agent's own session ID for ACP protocol mapping
893
+ agent,
894
+ session,
895
+ workspace,
896
+ streamId: resolvedStreamId,
897
+ };
898
+ } catch (handleError) {
899
+ // Close the spawned process to prevent orphaning
900
+ try {
901
+ await handle.close();
902
+ } catch {
903
+ // Ignore errors during cleanup
904
+ }
905
+ throw handleError;
906
+ }
715
907
  } catch (error) {
716
908
  // Clean up the spawn event we already emitted
717
909
  eventStore.emit({
718
- type: "terminate",
910
+ type: "stop",
719
911
  source: { agent_id: agentId },
720
912
  payload: {
721
913
  reason: "failed",
@@ -726,21 +918,21 @@ export function createAgentManager(
726
918
  throw new AgentManagerError(
727
919
  `Failed to spawn agent: ${error}`,
728
920
  "SPAWN_FAILED",
729
- agentId
921
+ agentId,
730
922
  );
731
923
  }
732
924
  }
733
925
 
734
926
  async function terminate(
735
927
  agentId: AgentId,
736
- reason: AgentStopReason
928
+ reason: AgentStopReason,
737
929
  ): Promise<void> {
738
930
  const agent = eventStore.getAgent(agentId);
739
931
  if (!agent) {
740
932
  throw new AgentManagerError(
741
933
  `Agent not found: ${agentId}`,
742
934
  "AGENT_NOT_FOUND",
743
- agentId
935
+ agentId,
744
936
  );
745
937
  }
746
938
 
@@ -769,12 +961,17 @@ export function createAgentManager(
769
961
  agentWorkspaces.delete(agentId);
770
962
  } catch (wsError) {
771
963
  console.error(
772
- `[AgentManager] Failed to deallocate workspace for ${agentId}: ${wsError}`
964
+ `[AgentManager] Failed to deallocate workspace for ${agentId}: ${wsError}`,
773
965
  );
774
966
  // Continue with termination even if workspace cleanup fails
775
967
  }
776
968
  }
777
969
 
970
+ // Revoke agent authentication token
971
+ if (agentTokenManager) {
972
+ agentTokenManager.revokeToken(agentId);
973
+ }
974
+
778
975
  // ─────────────────────────────────────────────────────────────────
779
976
  // Mail: Close task conversation on terminate
780
977
  // ─────────────────────────────────────────────────────────────────
@@ -789,8 +986,7 @@ export function createAgentManager(
789
986
  });
790
987
  }
791
988
  // Close any peer conversations
792
- const peerConvIds =
793
- conversationMap.closePeerConversationsFor(agentId);
989
+ const peerConvIds = conversationMap.closePeerConversationsFor(agentId);
794
990
  for (const peerConvId of peerConvIds) {
795
991
  mailService.closeConversation({
796
992
  conversationId: peerConvId,
@@ -802,14 +998,14 @@ export function createAgentManager(
802
998
  } catch (err) {
803
999
  console.warn(
804
1000
  `[AgentManager] Failed to close conversation for ${agentId}:`,
805
- err
1001
+ err,
806
1002
  );
807
1003
  }
808
1004
  }
809
1005
 
810
- // Emit terminate event
1006
+ // Emit stop event
811
1007
  eventStore.emit({
812
- type: "terminate",
1008
+ type: "stop",
813
1009
  source: { agent_id: agentId },
814
1010
  payload: {
815
1011
  agent_id: agentId,
@@ -876,19 +1072,30 @@ export function createAgentManager(
876
1072
  child.id,
877
1073
  agentId,
878
1074
  cascadeAdapter,
879
- workspaceProvider
1075
+ workspaceProvider,
880
1076
  );
881
1077
  }
882
1078
  }
883
1079
  }
884
1080
 
885
- async function resume(agentId: AgentId): Promise<SpawnedAgent> {
1081
+ async function resume(
1082
+ agentId: AgentId,
1083
+ overridePermissionMode?: PermissionMode,
1084
+ ): Promise<SpawnedAgent> {
1085
+ if (isShuttingDown) {
1086
+ throw new AgentManagerError(
1087
+ "Cannot resume agent during shutdown",
1088
+ "SHUTDOWN_IN_PROGRESS",
1089
+ agentId,
1090
+ );
1091
+ }
1092
+
886
1093
  const agent = eventStore.getAgent(agentId);
887
1094
  if (!agent) {
888
1095
  throw new AgentManagerError(
889
1096
  `Agent not found: ${agentId}`,
890
1097
  "AGENT_NOT_FOUND",
891
- agentId
1098
+ agentId,
892
1099
  );
893
1100
  }
894
1101
 
@@ -897,66 +1104,260 @@ export function createAgentManager(
897
1104
  throw new AgentManagerError(
898
1105
  `Agent already has active session: ${agentId}`,
899
1106
  "ALREADY_RUNNING",
900
- agentId
1107
+ agentId,
901
1108
  );
902
1109
  }
903
1110
 
1111
+ const permissionMode = overridePermissionMode ?? defaultPermissionMode;
1112
+
904
1113
  // Spawn new process
905
1114
  const handle = await AgentFactory.spawn(defaultAgentType, {
906
- permissionMode: defaultPermissionMode,
1115
+ permissionMode,
907
1116
  });
908
1117
 
909
- const agentCwd = agent.cwd ?? defaultCwd;
910
- let session;
1118
+ try {
1119
+ const agentCwd = agent.cwd ?? defaultCwd;
1120
+ let session;
911
1121
 
912
- if (agent.provider_session_id) {
913
- // Load existing session using the provider's session ID (e.g., Claude Code UUID)
914
- session = await handle.loadSession(agent.provider_session_id, agentCwd);
915
- } else {
916
- // No provider session ID available (agent predates this feature or wasn't persisted).
917
- // Create a new session instead of loading with the macro-agent session_id
918
- // which is not a valid provider session ID (e.g., Claude Code expects UUIDs).
919
- session = await handle.createSession(agentCwd);
1122
+ // When interactive mode, strip settings to prevent auto-approval
1123
+ const resumeAgentMeta =
1124
+ permissionMode === "interactive"
1125
+ ? { claudeCode: { options: { settingSources: [] } } }
1126
+ : undefined;
1127
+
1128
+ const macroAgentMcp = buildMacroAgentMcp({
1129
+ agentId,
1130
+ parentId: agent.parent ?? "",
1131
+ taskId: agent.task_id ?? "",
1132
+ cwd: agentCwd,
1133
+ permissionMode,
1134
+ lineage: agent.lineage ?? [],
1135
+ sessionId: agent.session_id ?? "",
1136
+ });
1137
+ const mcpServers = [macroAgentMcp];
1138
+
1139
+ if (agent.provider_session_id) {
1140
+ // Load existing session using the provider's session ID (e.g., Claude Code UUID)
1141
+ // Note: loadSession's TS type for mcpServers is { name, uri }[] but
1142
+ // the underlying ACP protocol accepts full McpServerStdio. The JS
1143
+ // implementation passes mcpServers through to the connection unchanged.
1144
+ session = await handle.loadSession(
1145
+ agent.provider_session_id,
1146
+ agentCwd,
1147
+ mcpServers as any,
1148
+ resumeAgentMeta ? { agentMeta: resumeAgentMeta } : undefined,
1149
+ );
1150
+ } else {
1151
+ // No provider session ID available (agent predates this feature or wasn't persisted).
1152
+ // Create a new session instead of loading with the macro-agent session_id
1153
+ // which is not a valid provider session ID (e.g., Claude Code expects UUIDs).
1154
+ session = await handle.createSession(agentCwd, {
1155
+ mcpServers,
1156
+ ...(resumeAgentMeta && { agentMeta: resumeAgentMeta }),
1157
+ });
1158
+
1159
+ // Store the provider session ID for future resumes
1160
+ eventStore.emit({
1161
+ type: "status",
1162
+ source: { agent_id: agentId },
1163
+ payload: {
1164
+ status_type: "started",
1165
+ summary: "Agent session created (no provider session to resume)",
1166
+ provider_session_id: session.id,
1167
+ },
1168
+ });
1169
+ }
1170
+
1171
+ // Track active session
1172
+ const activeSession: ActiveSession = {
1173
+ agentId,
1174
+ handle,
1175
+ session,
1176
+ createdAt: Date.now(),
1177
+ isPrompting: false,
1178
+ };
1179
+ activeSessions.set(agentId, activeSession);
920
1180
 
921
- // Store the provider session ID for future resumes
1181
+ // Emit status event for resume
922
1182
  eventStore.emit({
923
1183
  type: "status",
924
1184
  source: { agent_id: agentId },
925
1185
  payload: {
926
1186
  status_type: "started",
927
- summary: "Agent session created (no provider session to resume)",
1187
+ summary: "Agent session resumed",
928
1188
  provider_session_id: session.id,
929
1189
  },
930
1190
  });
1191
+
1192
+ return {
1193
+ id: agentId,
1194
+ session_id: agent.session_id, // Macro-agent's own session ID
1195
+ agent: eventStore.getAgent(agentId)!,
1196
+ session,
1197
+ };
1198
+ } catch (handleError) {
1199
+ // Close the spawned process to prevent orphaning
1200
+ try {
1201
+ await handle.close();
1202
+ } catch {
1203
+ // Ignore errors during cleanup
1204
+ }
1205
+ throw handleError;
931
1206
  }
1207
+ }
932
1208
 
933
- // Track active session
934
- const activeSession: ActiveSession = {
935
- agentId,
936
- handle,
937
- session,
938
- createdAt: Date.now(),
939
- isPrompting: false,
940
- };
941
- activeSessions.set(agentId, activeSession);
1209
+ // ─────────────────────────────────────────────────────────────────
1210
+ // Fork
1211
+ // ─────────────────────────────────────────────────────────────────
1212
+
1213
+ async function forkAgent(
1214
+ sourceAgentId: AgentId,
1215
+ options?: { name?: string; prompt?: string; cwd?: string },
1216
+ ): Promise<SpawnedAgent> {
1217
+ if (isShuttingDown) {
1218
+ throw new AgentManagerError(
1219
+ "Cannot fork agent during shutdown",
1220
+ "SHUTDOWN_IN_PROGRESS",
1221
+ sourceAgentId,
1222
+ );
1223
+ }
1224
+
1225
+ const sourceAgent = eventStore.getAgent(sourceAgentId);
1226
+ if (!sourceAgent) {
1227
+ throw new AgentManagerError(
1228
+ `Agent not found: ${sourceAgentId}`,
1229
+ "AGENT_NOT_FOUND",
1230
+ sourceAgentId,
1231
+ );
1232
+ }
942
1233
 
943
- // Emit status event for resume
1234
+ // Need either an active session or a persisted provider_session_id
1235
+ const activeSession = activeSessions.get(sourceAgentId);
1236
+ if (!activeSession && !sourceAgent.provider_session_id) {
1237
+ throw new AgentManagerError(
1238
+ `Agent has no session to fork: ${sourceAgentId}`,
1239
+ "FORK_NOT_SUPPORTED",
1240
+ sourceAgentId,
1241
+ );
1242
+ }
1243
+
1244
+ // Generate new IDs
1245
+ const agentId = `agent_${nanoid(12)}`;
1246
+ const taskId = `task_${nanoid(12)}`;
1247
+ const sessionId = `session_${nanoid(12)}`;
1248
+ const cwd = options?.cwd ?? sourceAgent.cwd ?? defaultCwd;
1249
+
1250
+ // Emit spawn event with fork metadata
944
1251
  eventStore.emit({
945
- type: "status",
946
- source: { agent_id: agentId },
1252
+ type: "spawn",
1253
+ source: { agent_id: sourceAgentId },
947
1254
  payload: {
948
- status_type: "started",
949
- summary: "Agent session resumed",
950
- provider_session_id: session.id,
1255
+ agent_id: agentId,
1256
+ session_id: sessionId,
1257
+ task: options?.name ?? `[Fork of ${sourceAgentId}]`,
1258
+ task_id: taskId,
1259
+ parent: sourceAgent.parent ?? null,
1260
+ role: sourceAgent.role ?? undefined,
1261
+ config: {},
1262
+ cwd,
1263
+ metadata: { fork_of: sourceAgentId },
951
1264
  },
952
1265
  });
953
1266
 
954
- return {
955
- id: agentId,
956
- session_id: agent.session_id, // Macro-agent's own session ID
957
- agent: eventStore.getAgent(agentId)!,
958
- session,
959
- };
1267
+ // Generate a human-readable name
1268
+ const generatedName = uniqueNamesGenerator({
1269
+ dictionaries: [adjectives, animals],
1270
+ separator: "-",
1271
+ length: 2,
1272
+ });
1273
+ eventStore.updateAgentMetadata(agentId as AgentId, { name: generatedName });
1274
+ await eventStore.persist();
1275
+
1276
+ // Get the provider session ID to fork from
1277
+ let forkedProviderSessionId: string;
1278
+ if (activeSession) {
1279
+ // Active session: fork with flush to ensure data is persisted
1280
+ const forkedSession = await activeSession.session.forkWithFlush();
1281
+ forkedProviderSessionId = forkedSession.id;
1282
+ } else {
1283
+ // Stopped agent: use the persisted provider session ID directly
1284
+ forkedProviderSessionId = sourceAgent.provider_session_id!;
1285
+ }
1286
+
1287
+ // Spawn a new process
1288
+ const handle = await AgentFactory.spawn(defaultAgentType, {
1289
+ permissionMode: defaultPermissionMode,
1290
+ });
1291
+
1292
+ try {
1293
+ const macroAgentMcp = buildMacroAgentMcp({
1294
+ agentId,
1295
+ parentId: sourceAgent.parent ?? "",
1296
+ taskId,
1297
+ cwd,
1298
+ permissionMode: defaultPermissionMode,
1299
+ lineage: sourceAgent.lineage ?? [],
1300
+ sessionId,
1301
+ });
1302
+
1303
+ // Load the forked session on the new process with correct MCP config.
1304
+ // Note: loadSession's TS type for mcpServers is { name, uri }[] but
1305
+ // the underlying ACP protocol accepts full McpServerStdio. The JS
1306
+ // implementation passes mcpServers through to the connection unchanged.
1307
+ const session = await handle.loadSession(forkedProviderSessionId, cwd, [
1308
+ macroAgentMcp,
1309
+ ] as any);
1310
+
1311
+ // Emit started status with provider session ID
1312
+ eventStore.emit({
1313
+ type: "status",
1314
+ source: { agent_id: agentId },
1315
+ payload: {
1316
+ status_type: "started",
1317
+ summary: "Agent session started (forked)",
1318
+ provider_session_id: session.id,
1319
+ },
1320
+ });
1321
+ await eventStore.persist();
1322
+
1323
+ // Set up message router subscriptions
1324
+ messageRouter.setupDefaultSubscriptions({
1325
+ agent_id: agentId,
1326
+ parent_id: sourceAgent.parent ?? undefined,
1327
+ task_id: taskId,
1328
+ subscribe_parent: false,
1329
+ additional_topics: [],
1330
+ role: sourceAgent.role ?? undefined,
1331
+ });
1332
+
1333
+ // Track active session
1334
+ const newActiveSession: ActiveSession = {
1335
+ agentId,
1336
+ handle,
1337
+ session,
1338
+ createdAt: Date.now(),
1339
+ isPrompting: false,
1340
+ };
1341
+ activeSessions.set(agentId, newActiveSession);
1342
+
1343
+ const agent = eventStore.getAgent(agentId)!;
1344
+ notifyLifecycle({ type: "spawned", agent });
1345
+ notifyLifecycle({ type: "started", agent });
1346
+
1347
+ return {
1348
+ id: agentId,
1349
+ session_id: sessionId,
1350
+ agent,
1351
+ session,
1352
+ };
1353
+ } catch (handleError) {
1354
+ try {
1355
+ await handle.close();
1356
+ } catch {
1357
+ // Ignore errors during cleanup
1358
+ }
1359
+ throw handleError;
1360
+ }
960
1361
  }
961
1362
 
962
1363
  // ─────────────────────────────────────────────────────────────────
@@ -994,7 +1395,7 @@ export function createAgentManager(
994
1395
 
995
1396
  function getHierarchy(
996
1397
  agentId: AgentId,
997
- options?: HierarchyOptions
1398
+ options?: HierarchyOptions,
998
1399
  ): AgentHierarchy | null {
999
1400
  const agent = eventStore.getAgent(agentId);
1000
1401
  if (!agent) return null;
@@ -1035,7 +1436,7 @@ export function createAgentManager(
1035
1436
  // ─────────────────────────────────────────────────────────────────
1036
1437
 
1037
1438
  async function getOrCreateHeadManager(
1038
- options: HeadManagerOptions
1439
+ options: HeadManagerOptions,
1039
1440
  ): Promise<SpawnedAgent> {
1040
1441
  const {
1041
1442
  cwd,
@@ -1107,14 +1508,14 @@ export function createAgentManager(
1107
1508
 
1108
1509
  async function* prompt(
1109
1510
  agentId: AgentId,
1110
- message: string
1511
+ message: string,
1111
1512
  ): AsyncIterable<ExtendedSessionUpdate> {
1112
1513
  const activeSession = activeSessions.get(agentId);
1113
1514
  if (!activeSession) {
1114
1515
  throw new AgentManagerError(
1115
1516
  `No active session for agent: ${agentId}`,
1116
1517
  "SESSION_NOT_FOUND",
1117
- agentId
1518
+ agentId,
1118
1519
  );
1119
1520
  }
1120
1521
 
@@ -1139,7 +1540,7 @@ export function createAgentManager(
1139
1540
  maxFollowUps?: number;
1140
1541
  throwOnMaxExceeded?: boolean;
1141
1542
  onUpdate?: (update: ExtendedSessionUpdate) => void;
1142
- }
1543
+ },
1143
1544
  ): Promise<{
1144
1545
  doneCalled: boolean;
1145
1546
  doneStatus?: string;
@@ -1156,7 +1557,10 @@ export function createAgentManager(
1156
1557
  // Helper to check if done() was called by looking for status events
1157
1558
  // The done() MCP tool emits status events with status_type completed/failed
1158
1559
  // and includes signal: "WORKER_DONE" in the details
1159
- const checkDoneCalled = async (): Promise<{ called: boolean; status?: string }> => {
1560
+ const checkDoneCalled = async (): Promise<{
1561
+ called: boolean;
1562
+ status?: string;
1563
+ }> => {
1160
1564
  // Reload from disk to see events from MCP subprocess
1161
1565
  await eventStore.reload();
1162
1566
  const statusEvents = eventStore.query({ type: "status" });
@@ -1166,14 +1570,15 @@ export function createAgentManager(
1166
1570
  (e) =>
1167
1571
  e.source?.agent_id === agentId &&
1168
1572
  (e.payload?.status_type === "completed" ||
1169
- e.payload?.status_type === "failed" ||
1170
- (e.payload?.details as Record<string, unknown>)?.signal === "WORKER_DONE")
1573
+ e.payload?.status_type === "failed" ||
1574
+ (e.payload?.details as Record<string, unknown>)?.signal ===
1575
+ "WORKER_DONE"),
1171
1576
  );
1172
1577
 
1173
1578
  if (agentCompletedStatus) {
1174
1579
  return {
1175
1580
  called: true,
1176
- status: agentCompletedStatus.payload?.status_type as string
1581
+ status: agentCompletedStatus.payload?.status_type as string,
1177
1582
  };
1178
1583
  }
1179
1584
 
@@ -1213,7 +1618,8 @@ Call done() NOW with status "completed" if your work is finished, or "blocked" i
1213
1618
 
1214
1619
  for (let i = 0; i < maxFollowUps; i++) {
1215
1620
  followUpCount++;
1216
- const followUpMessage = followUpMessages[Math.min(i, followUpMessages.length - 1)];
1621
+ const followUpMessage =
1622
+ followUpMessages[Math.min(i, followUpMessages.length - 1)];
1217
1623
 
1218
1624
  // Send follow-up prompt
1219
1625
  for await (const update of prompt(agentId, followUpMessage)) {
@@ -1238,7 +1644,7 @@ Call done() NOW with status "completed" if your work is finished, or "blocked" i
1238
1644
  if (throwOnMaxExceeded) {
1239
1645
  throw new Error(
1240
1646
  `Agent ${agentId} did not call done() after ${maxFollowUps} follow-up attempts. ` +
1241
- `Total prompts sent: ${1 + followUpCount}. Consider increasing maxFollowUps or investigating agent behavior.`
1647
+ `Total prompts sent: ${1 + followUpCount}. Consider increasing maxFollowUps or investigating agent behavior.`,
1242
1648
  );
1243
1649
  }
1244
1650
 
@@ -1293,12 +1699,12 @@ Call done() NOW with status "completed" if your work is finished, or "blocked" i
1293
1699
  function respondToPermission(
1294
1700
  agentId: AgentId,
1295
1701
  requestId: string,
1296
- optionId: string
1702
+ optionId: string,
1297
1703
  ): boolean {
1298
1704
  const activeSession = activeSessions.get(agentId);
1299
1705
  if (!activeSession) {
1300
1706
  console.warn(
1301
- `[AgentManager] Cannot respond to permission: no active session for agent ${agentId}`
1707
+ `[AgentManager] Cannot respond to permission: no active session for agent ${agentId}`,
1302
1708
  );
1303
1709
  return false;
1304
1710
  }
@@ -1306,13 +1712,13 @@ Call done() NOW with status "completed" if your work is finished, or "blocked" i
1306
1712
  try {
1307
1713
  activeSession.session.respondToPermission(requestId, optionId);
1308
1714
  console.log(
1309
- `[AgentManager] Responded to permission ${requestId} for agent ${agentId} with ${optionId}`
1715
+ `[AgentManager] Responded to permission ${requestId} for agent ${agentId} with ${optionId}`,
1310
1716
  );
1311
1717
  return true;
1312
1718
  } catch (err) {
1313
1719
  console.error(
1314
1720
  `[AgentManager] Error responding to permission ${requestId}:`,
1315
- err
1721
+ err,
1316
1722
  );
1317
1723
  return false;
1318
1724
  }
@@ -1322,7 +1728,7 @@ Call done() NOW with status "completed" if your work is finished, or "blocked" i
1322
1728
  const activeSession = activeSessions.get(agentId);
1323
1729
  if (!activeSession) {
1324
1730
  console.warn(
1325
- `[AgentManager] Cannot cancel permission: no active session for agent ${agentId}`
1731
+ `[AgentManager] Cannot cancel permission: no active session for agent ${agentId}`,
1326
1732
  );
1327
1733
  return false;
1328
1734
  }
@@ -1330,18 +1736,50 @@ Call done() NOW with status "completed" if your work is finished, or "blocked" i
1330
1736
  try {
1331
1737
  activeSession.session.cancelPermission(requestId);
1332
1738
  console.log(
1333
- `[AgentManager] Cancelled permission ${requestId} for agent ${agentId}`
1739
+ `[AgentManager] Cancelled permission ${requestId} for agent ${agentId}`,
1334
1740
  );
1335
1741
  return true;
1336
1742
  } catch (err) {
1337
1743
  console.error(
1338
1744
  `[AgentManager] Error cancelling permission ${requestId}:`,
1339
- err
1745
+ err,
1746
+ );
1747
+ return false;
1748
+ }
1749
+ }
1750
+
1751
+ function setPermissionMode(agentId: AgentId, mode: PermissionMode): boolean {
1752
+ const activeSession = activeSessions.get(agentId);
1753
+ if (!activeSession) {
1754
+ console.warn(
1755
+ `[AgentManager] Cannot set permission mode: no active session for agent ${agentId}`,
1756
+ );
1757
+ return false;
1758
+ }
1759
+
1760
+ try {
1761
+ activeSession.handle.setPermissionMode(mode);
1762
+ console.log(
1763
+ `[AgentManager] Set permission mode for agent ${agentId} to ${mode}`,
1764
+ );
1765
+ return true;
1766
+ } catch (err) {
1767
+ console.error(
1768
+ `[AgentManager] Error setting permission mode for agent ${agentId}:`,
1769
+ err,
1340
1770
  );
1341
1771
  return false;
1342
1772
  }
1343
1773
  }
1344
1774
 
1775
+ function getPermissionMode(agentId: AgentId): PermissionMode | null {
1776
+ const activeSession = activeSessions.get(agentId);
1777
+ if (!activeSession) {
1778
+ return null;
1779
+ }
1780
+ return activeSession.handle.getPermissionMode();
1781
+ }
1782
+
1345
1783
  // ─────────────────────────────────────────────────────────────────
1346
1784
  // Lifecycle Callbacks
1347
1785
  // ─────────────────────────────────────────────────────────────────
@@ -1361,13 +1799,21 @@ Call done() NOW with status "completed" if your work is finished, or "blocked" i
1361
1799
  }
1362
1800
  }
1363
1801
 
1802
+ // ─────────────────────────────────────────────────────────────────
1803
+ // OpenTasks Socket Path (Late Binding)
1804
+ // ─────────────────────────────────────────────────────────────────
1805
+
1806
+ function setOpenTasksSocketPath(socketPath: string): void {
1807
+ configOpenTasksSocketPath = socketPath;
1808
+ }
1809
+
1364
1810
  // ─────────────────────────────────────────────────────────────────
1365
1811
  // Mail Services (Late Binding)
1366
1812
  // ─────────────────────────────────────────────────────────────────
1367
1813
 
1368
1814
  function setMailServices(
1369
1815
  ms: import("../mail/mail-service.js").MailService,
1370
- cm: import("../mail/conversation-map.js").ConversationMap
1816
+ cm: import("../mail/conversation-map.js").ConversationMap,
1371
1817
  ): void {
1372
1818
  mailService = ms;
1373
1819
  conversationMap = cm;
@@ -1378,6 +1824,9 @@ Call done() NOW with status "completed" if your work is finished, or "blocked" i
1378
1824
  // ─────────────────────────────────────────────────────────────────
1379
1825
 
1380
1826
  async function close(): Promise<void> {
1827
+ // Prevent new spawns/resumes from racing with cleanup
1828
+ isShuttingDown = true;
1829
+
1381
1830
  // Stop all health checks
1382
1831
  if (healthCheckService) {
1383
1832
  healthCheckService.stopAll();
@@ -1393,7 +1842,7 @@ Call done() NOW with status "completed" if your work is finished, or "blocked" i
1393
1842
  } catch {
1394
1843
  // Ignore errors during cleanup
1395
1844
  }
1396
- })()
1845
+ })(),
1397
1846
  );
1398
1847
  }
1399
1848
 
@@ -1416,14 +1865,14 @@ Call done() NOW with status "completed" if your work is finished, or "blocked" i
1416
1865
  */
1417
1866
  async function continueAgent(
1418
1867
  agentId: AgentId,
1419
- options?: ContinueAgentOptions
1868
+ options?: ContinueAgentOptions,
1420
1869
  ): Promise<SpawnedAgent> {
1421
1870
  const agent = eventStore.getAgent(agentId);
1422
1871
  if (!agent) {
1423
1872
  throw new AgentManagerError(
1424
1873
  `Agent not found: ${agentId}`,
1425
1874
  "AGENT_NOT_FOUND",
1426
- agentId
1875
+ agentId,
1427
1876
  );
1428
1877
  }
1429
1878
 
@@ -1443,7 +1892,9 @@ Call done() NOW with status "completed" if your work is finished, or "blocked" i
1443
1892
 
1444
1893
  if (events.length > 0) {
1445
1894
  contextLines.push("## Prior Session Context");
1446
- contextLines.push(`Continuing from agent ${agentId} (${events.length} events).`);
1895
+ contextLines.push(
1896
+ `Continuing from agent ${agentId} (${events.length} events).`,
1897
+ );
1447
1898
  for (const event of events.slice(-20)) {
1448
1899
  const summary = event.payload?.summary;
1449
1900
  if (summary && typeof summary === "string") {
@@ -1456,9 +1907,7 @@ Call done() NOW with status "completed" if your work is finished, or "blocked" i
1456
1907
 
1457
1908
  // Spawn a continuation agent with same role, task, and context
1458
1909
  const taskDescription =
1459
- options?.task ??
1460
- agent.task ??
1461
- `Continue work from ${agentId}`;
1910
+ options?.task ?? agent.task ?? `Continue work from ${agentId}`;
1462
1911
 
1463
1912
  const newAgent = await spawn({
1464
1913
  task: taskDescription,
@@ -1487,6 +1936,7 @@ Call done() NOW with status "completed" if your work is finished, or "blocked" i
1487
1936
  terminate,
1488
1937
  resume,
1489
1938
  continueAgent,
1939
+ forkAgent,
1490
1940
  get,
1491
1941
  list,
1492
1942
  getChildren,
@@ -1502,9 +1952,12 @@ Call done() NOW with status "completed" if your work is finished, or "blocked" i
1502
1952
  isProcessRunning,
1503
1953
  respondToPermission,
1504
1954
  cancelPermission,
1955
+ setPermissionMode,
1956
+ getPermissionMode,
1505
1957
  onLifecycleEvent,
1506
1958
  setSpawnInterceptor,
1507
1959
  getRoleRegistry,
1960
+ setOpenTasksSocketPath,
1508
1961
  setMailServices,
1509
1962
  close,
1510
1963
  };
@@ -1534,7 +1987,7 @@ async function createWorkspaceForRole(
1534
1987
  workspaceManager: WorkspaceManager,
1535
1988
  agentId: AgentId,
1536
1989
  role: string,
1537
- options: CreateWorkspaceOptions
1990
+ options: CreateWorkspaceOptions,
1538
1991
  ): Promise<Workspace | undefined> {
1539
1992
  const { streamId, streamConfig, dataplaneTaskId } = options;
1540
1993
 
@@ -1543,14 +1996,14 @@ async function createWorkspaceForRole(
1543
1996
  // Coordinators create a new integration stream
1544
1997
  if (!streamConfig) {
1545
1998
  console.warn(
1546
- `[AgentManager] Coordinator ${agentId} spawn missing streamConfig, skipping workspace`
1999
+ `[AgentManager] Coordinator ${agentId} spawn missing streamConfig, skipping workspace`,
1547
2000
  );
1548
2001
  return undefined;
1549
2002
  }
1550
2003
 
1551
2004
  const newStreamId = workspaceManager.createIntegrationStream(
1552
2005
  agentId,
1553
- streamConfig
2006
+ streamConfig,
1554
2007
  );
1555
2008
 
1556
2009
  return workspaceManager.createCoordinatorWorkspace(agentId, newStreamId);
@@ -1560,7 +2013,7 @@ async function createWorkspaceForRole(
1560
2013
  // Integrators join an existing stream
1561
2014
  if (!streamId) {
1562
2015
  console.warn(
1563
- `[AgentManager] Integrator ${agentId} spawn missing streamId, skipping workspace`
2016
+ `[AgentManager] Integrator ${agentId} spawn missing streamId, skipping workspace`,
1564
2017
  );
1565
2018
  return undefined;
1566
2019
  }
@@ -1573,7 +2026,7 @@ async function createWorkspaceForRole(
1573
2026
  // Workers need streamId and either dataplaneTaskId or create a new task
1574
2027
  if (!streamId) {
1575
2028
  console.warn(
1576
- `[AgentManager] Worker ${agentId} spawn missing streamId, skipping workspace`
2029
+ `[AgentManager] Worker ${agentId} spawn missing streamId, skipping workspace`,
1577
2030
  );
1578
2031
  return undefined;
1579
2032
  }
@@ -1582,7 +2035,7 @@ async function createWorkspaceForRole(
1582
2035
  const taskId = dataplaneTaskId;
1583
2036
  if (!taskId) {
1584
2037
  console.warn(
1585
- `[AgentManager] Worker ${agentId} spawn missing dataplaneTaskId, skipping workspace`
2038
+ `[AgentManager] Worker ${agentId} spawn missing dataplaneTaskId, skipping workspace`,
1586
2039
  );
1587
2040
  return undefined;
1588
2041
  }