macro-agent 0.1.1 → 0.1.2

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 (406) hide show
  1. package/.sudocode/issues.jsonl +28 -0
  2. package/.sudocode/specs.jsonl +4 -0
  3. package/CLAUDE.md +9 -3
  4. package/dist/agent/agent-manager.d.ts.map +1 -1
  5. package/dist/agent/agent-manager.js +111 -48
  6. package/dist/agent/agent-manager.js.map +1 -1
  7. package/dist/agent/types.d.ts +7 -0
  8. package/dist/agent/types.d.ts.map +1 -1
  9. package/dist/agent/types.js.map +1 -1
  10. package/dist/api/server.d.ts +5 -1
  11. package/dist/api/server.d.ts.map +1 -1
  12. package/dist/api/server.js +100 -3
  13. package/dist/api/server.js.map +1 -1
  14. package/dist/api/types.d.ts +1 -1
  15. package/dist/api/types.d.ts.map +1 -1
  16. package/dist/cli/acp.d.ts.map +1 -1
  17. package/dist/cli/acp.js +71 -1
  18. package/dist/cli/acp.js.map +1 -1
  19. package/dist/cli/index.js +5 -1
  20. package/dist/cli/index.js.map +1 -1
  21. package/dist/cli/mcp.js +27 -8
  22. package/dist/cli/mcp.js.map +1 -1
  23. package/dist/config/project-config.d.ts +13 -2
  24. package/dist/config/project-config.d.ts.map +1 -1
  25. package/dist/config/project-config.js +12 -2
  26. package/dist/config/project-config.js.map +1 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/lifecycle/handlers/index.d.ts +7 -3
  32. package/dist/lifecycle/handlers/index.d.ts.map +1 -1
  33. package/dist/lifecycle/handlers/index.js +25 -8
  34. package/dist/lifecycle/handlers/index.js.map +1 -1
  35. package/dist/lifecycle/types.d.ts +2 -0
  36. package/dist/lifecycle/types.d.ts.map +1 -1
  37. package/dist/lifecycle/types.js.map +1 -1
  38. package/dist/map/adapter/extensions/index.d.ts +4 -1
  39. package/dist/map/adapter/extensions/index.d.ts.map +1 -1
  40. package/dist/map/adapter/extensions/index.js +27 -0
  41. package/dist/map/adapter/extensions/index.js.map +1 -1
  42. package/dist/map/adapter/extensions/streams.d.ts +95 -0
  43. package/dist/map/adapter/extensions/streams.d.ts.map +1 -0
  44. package/dist/map/adapter/extensions/streams.js +515 -0
  45. package/dist/map/adapter/extensions/streams.js.map +1 -0
  46. package/dist/map/adapter/index.d.ts +1 -1
  47. package/dist/map/adapter/index.d.ts.map +1 -1
  48. package/dist/map/adapter/index.js +3 -1
  49. package/dist/map/adapter/index.js.map +1 -1
  50. package/dist/map/adapter/types.d.ts +1 -1
  51. package/dist/map/adapter/types.d.ts.map +1 -1
  52. package/dist/mcp/mcp-server.d.ts +2 -0
  53. package/dist/mcp/mcp-server.d.ts.map +1 -1
  54. package/dist/mcp/mcp-server.js +12 -3
  55. package/dist/mcp/mcp-server.js.map +1 -1
  56. package/dist/mcp/tools/done.d.ts.map +1 -1
  57. package/dist/mcp/tools/done.js +18 -0
  58. package/dist/mcp/tools/done.js.map +1 -1
  59. package/dist/roles/builtin/coordinator.d.ts.map +1 -1
  60. package/dist/roles/builtin/coordinator.js +2 -1
  61. package/dist/roles/builtin/coordinator.js.map +1 -1
  62. package/dist/roles/builtin/integrator.d.ts.map +1 -1
  63. package/dist/roles/builtin/integrator.js +2 -1
  64. package/dist/roles/builtin/integrator.js.map +1 -1
  65. package/dist/roles/builtin/worker.d.ts.map +1 -1
  66. package/dist/roles/builtin/worker.js +3 -1
  67. package/dist/roles/builtin/worker.js.map +1 -1
  68. package/dist/roles/capabilities.d.ts +6 -0
  69. package/dist/roles/capabilities.d.ts.map +1 -1
  70. package/dist/roles/capabilities.js +10 -0
  71. package/dist/roles/capabilities.js.map +1 -1
  72. package/dist/roles/config-loader.d.ts +1 -1
  73. package/dist/roles/config-loader.d.ts.map +1 -1
  74. package/dist/roles/config-loader.js +3 -2
  75. package/dist/roles/config-loader.js.map +1 -1
  76. package/dist/roles/types.d.ts +3 -1
  77. package/dist/roles/types.d.ts.map +1 -1
  78. package/dist/server/combined-server.d.ts +8 -1
  79. package/dist/server/combined-server.d.ts.map +1 -1
  80. package/dist/server/combined-server.js +6 -2
  81. package/dist/server/combined-server.js.map +1 -1
  82. package/dist/store/event-store.d.ts.map +1 -1
  83. package/dist/store/event-store.js +12 -5
  84. package/dist/store/event-store.js.map +1 -1
  85. package/dist/store/instance.d.ts +1 -1
  86. package/dist/store/instance.d.ts.map +1 -1
  87. package/dist/store/instance.js +2 -2
  88. package/dist/store/instance.js.map +1 -1
  89. package/dist/store/types/agents.d.ts +5 -0
  90. package/dist/store/types/agents.d.ts.map +1 -1
  91. package/dist/task/backend/opentasks/daemon-manager.d.ts.map +1 -1
  92. package/dist/task/backend/opentasks/daemon-manager.js +1 -1
  93. package/dist/task/backend/opentasks/daemon-manager.js.map +1 -1
  94. package/dist/teams/index.d.ts +3 -1
  95. package/dist/teams/index.d.ts.map +1 -1
  96. package/dist/teams/index.js +2 -0
  97. package/dist/teams/index.js.map +1 -1
  98. package/dist/teams/seed-defaults.d.ts +20 -0
  99. package/dist/teams/seed-defaults.d.ts.map +1 -0
  100. package/dist/teams/seed-defaults.js +71 -0
  101. package/dist/teams/seed-defaults.js.map +1 -0
  102. package/dist/teams/team-loader.d.ts +6 -2
  103. package/dist/teams/team-loader.d.ts.map +1 -1
  104. package/dist/teams/team-loader.js +154 -162
  105. package/dist/teams/team-loader.js.map +1 -1
  106. package/dist/teams/team-manager.d.ts +112 -0
  107. package/dist/teams/team-manager.d.ts.map +1 -0
  108. package/dist/teams/team-manager.js +305 -0
  109. package/dist/teams/team-manager.js.map +1 -0
  110. package/dist/teams/team-runtime.d.ts +125 -19
  111. package/dist/teams/team-runtime.d.ts.map +1 -1
  112. package/dist/teams/team-runtime.js +527 -119
  113. package/dist/teams/team-runtime.js.map +1 -1
  114. package/dist/teams/types.d.ts +41 -151
  115. package/dist/teams/types.d.ts.map +1 -1
  116. package/dist/teams/types.js +2 -3
  117. package/dist/teams/types.js.map +1 -1
  118. package/docs/teams.md +73 -0
  119. package/package.json +2 -1
  120. package/references/minimem/.claude/settings.json +7 -0
  121. package/references/minimem/.sudocode/issues.jsonl +18 -0
  122. package/references/minimem/.sudocode/specs.jsonl +1 -0
  123. package/references/minimem/CLAUDE.md +310 -0
  124. package/references/minimem/README.md +562 -0
  125. package/references/minimem/claude-plugin/.claude-plugin/plugin.json +10 -0
  126. package/references/minimem/claude-plugin/.mcp.json +7 -0
  127. package/references/minimem/claude-plugin/README.md +158 -0
  128. package/references/minimem/claude-plugin/commands/recall.md +47 -0
  129. package/references/minimem/claude-plugin/commands/remember.md +41 -0
  130. package/references/minimem/claude-plugin/hooks/__tests__/hooks.test.ts +272 -0
  131. package/references/minimem/claude-plugin/hooks/hooks.json +27 -0
  132. package/references/minimem/claude-plugin/hooks/session-end.sh +86 -0
  133. package/references/minimem/claude-plugin/hooks/session-start.sh +85 -0
  134. package/references/minimem/claude-plugin/skills/memory/SKILL.md +108 -0
  135. package/references/minimem/media/banner.png +0 -0
  136. package/references/minimem/package-lock.json +5373 -0
  137. package/references/minimem/package.json +72 -0
  138. package/references/minimem/scripts/postbuild.js +35 -0
  139. package/references/minimem/src/__tests__/edge-cases.test.ts +371 -0
  140. package/references/minimem/src/__tests__/errors.test.ts +265 -0
  141. package/references/minimem/src/__tests__/helpers.ts +199 -0
  142. package/references/minimem/src/__tests__/internal.test.ts +407 -0
  143. package/references/minimem/src/__tests__/knowledge.test.ts +287 -0
  144. package/references/minimem/src/__tests__/minimem.integration.test.ts +1127 -0
  145. package/references/minimem/src/__tests__/session.test.ts +190 -0
  146. package/references/minimem/src/cli/__tests__/commands.test.ts +759 -0
  147. package/references/minimem/src/cli/commands/__tests__/conflicts.test.ts +141 -0
  148. package/references/minimem/src/cli/commands/append.ts +76 -0
  149. package/references/minimem/src/cli/commands/config.ts +262 -0
  150. package/references/minimem/src/cli/commands/conflicts.ts +413 -0
  151. package/references/minimem/src/cli/commands/daemon.ts +169 -0
  152. package/references/minimem/src/cli/commands/index.ts +12 -0
  153. package/references/minimem/src/cli/commands/init.ts +88 -0
  154. package/references/minimem/src/cli/commands/mcp.ts +177 -0
  155. package/references/minimem/src/cli/commands/push-pull.ts +213 -0
  156. package/references/minimem/src/cli/commands/search.ts +158 -0
  157. package/references/minimem/src/cli/commands/status.ts +84 -0
  158. package/references/minimem/src/cli/commands/sync-init.ts +290 -0
  159. package/references/minimem/src/cli/commands/sync.ts +70 -0
  160. package/references/minimem/src/cli/commands/upsert.ts +197 -0
  161. package/references/minimem/src/cli/config.ts +584 -0
  162. package/references/minimem/src/cli/index.ts +264 -0
  163. package/references/minimem/src/cli/shared.ts +161 -0
  164. package/references/minimem/src/cli/sync/__tests__/central.test.ts +152 -0
  165. package/references/minimem/src/cli/sync/__tests__/conflicts.test.ts +209 -0
  166. package/references/minimem/src/cli/sync/__tests__/daemon.test.ts +118 -0
  167. package/references/minimem/src/cli/sync/__tests__/detection.test.ts +207 -0
  168. package/references/minimem/src/cli/sync/__tests__/integration.test.ts +476 -0
  169. package/references/minimem/src/cli/sync/__tests__/registry.test.ts +363 -0
  170. package/references/minimem/src/cli/sync/__tests__/state.test.ts +255 -0
  171. package/references/minimem/src/cli/sync/__tests__/validation.test.ts +193 -0
  172. package/references/minimem/src/cli/sync/__tests__/watcher.test.ts +178 -0
  173. package/references/minimem/src/cli/sync/central.ts +292 -0
  174. package/references/minimem/src/cli/sync/conflicts.ts +204 -0
  175. package/references/minimem/src/cli/sync/daemon.ts +407 -0
  176. package/references/minimem/src/cli/sync/detection.ts +138 -0
  177. package/references/minimem/src/cli/sync/index.ts +107 -0
  178. package/references/minimem/src/cli/sync/operations.ts +373 -0
  179. package/references/minimem/src/cli/sync/registry.ts +279 -0
  180. package/references/minimem/src/cli/sync/state.ts +355 -0
  181. package/references/minimem/src/cli/sync/validation.ts +206 -0
  182. package/references/minimem/src/cli/sync/watcher.ts +234 -0
  183. package/references/minimem/src/cli/version.ts +34 -0
  184. package/references/minimem/src/core/index.ts +9 -0
  185. package/references/minimem/src/core/indexer.ts +628 -0
  186. package/references/minimem/src/core/searcher.ts +221 -0
  187. package/references/minimem/src/db/schema.ts +183 -0
  188. package/references/minimem/src/db/sqlite-vec.ts +24 -0
  189. package/references/minimem/src/embeddings/__tests__/embeddings.test.ts +431 -0
  190. package/references/minimem/src/embeddings/batch-gemini.ts +392 -0
  191. package/references/minimem/src/embeddings/batch-openai.ts +409 -0
  192. package/references/minimem/src/embeddings/embeddings.ts +434 -0
  193. package/references/minimem/src/index.ts +109 -0
  194. package/references/minimem/src/internal.ts +299 -0
  195. package/references/minimem/src/minimem.ts +1276 -0
  196. package/references/minimem/src/search/__tests__/hybrid.test.ts +247 -0
  197. package/references/minimem/src/search/graph.ts +234 -0
  198. package/references/minimem/src/search/hybrid.ts +151 -0
  199. package/references/minimem/src/search/search.ts +256 -0
  200. package/references/minimem/src/server/__tests__/mcp.test.ts +341 -0
  201. package/references/minimem/src/server/__tests__/tools.test.ts +364 -0
  202. package/references/minimem/src/server/mcp.ts +326 -0
  203. package/references/minimem/src/server/tools.ts +720 -0
  204. package/references/minimem/src/session.ts +460 -0
  205. package/references/minimem/tsconfig.json +19 -0
  206. package/references/minimem/tsup.config.ts +26 -0
  207. package/references/minimem/vitest.config.ts +24 -0
  208. package/references/openteams/.claude/settings.json +6 -0
  209. package/references/openteams/README.md +1 -0
  210. package/references/openteams/SKILL.md +341 -0
  211. package/references/openteams/design.md +411 -0
  212. package/references/openteams/examples/bmad-method/prompts/analyst/ROLE.md +16 -0
  213. package/references/openteams/examples/bmad-method/prompts/analyst/SOUL.md +5 -0
  214. package/references/openteams/examples/bmad-method/prompts/architect/ROLE.md +24 -0
  215. package/references/openteams/examples/bmad-method/prompts/architect/SOUL.md +5 -0
  216. package/references/openteams/examples/bmad-method/prompts/developer/ROLE.md +25 -0
  217. package/references/openteams/examples/bmad-method/prompts/developer/SOUL.md +5 -0
  218. package/references/openteams/examples/bmad-method/prompts/master/ROLE.md +21 -0
  219. package/references/openteams/examples/bmad-method/prompts/master/SOUL.md +5 -0
  220. package/references/openteams/examples/bmad-method/prompts/pm/ROLE.md +20 -0
  221. package/references/openteams/examples/bmad-method/prompts/pm/SOUL.md +5 -0
  222. package/references/openteams/examples/bmad-method/prompts/qa/ROLE.md +17 -0
  223. package/references/openteams/examples/bmad-method/prompts/qa/SOUL.md +5 -0
  224. package/references/openteams/examples/bmad-method/prompts/quick-flow-dev/ROLE.md +23 -0
  225. package/references/openteams/examples/bmad-method/prompts/quick-flow-dev/SOUL.md +5 -0
  226. package/references/openteams/examples/bmad-method/prompts/scrum-master/ROLE.md +27 -0
  227. package/references/openteams/examples/bmad-method/prompts/scrum-master/SOUL.md +5 -0
  228. package/references/openteams/examples/bmad-method/prompts/tech-writer/ROLE.md +21 -0
  229. package/references/openteams/examples/bmad-method/prompts/tech-writer/SOUL.md +5 -0
  230. package/references/openteams/examples/bmad-method/prompts/ux-designer/ROLE.md +16 -0
  231. package/references/openteams/examples/bmad-method/prompts/ux-designer/SOUL.md +5 -0
  232. package/references/openteams/examples/bmad-method/roles/analyst.yaml +9 -0
  233. package/references/openteams/examples/bmad-method/roles/architect.yaml +9 -0
  234. package/references/openteams/examples/bmad-method/roles/developer.yaml +8 -0
  235. package/references/openteams/examples/bmad-method/roles/master.yaml +8 -0
  236. package/references/openteams/examples/bmad-method/roles/pm.yaml +9 -0
  237. package/references/openteams/examples/bmad-method/roles/qa.yaml +8 -0
  238. package/references/openteams/examples/bmad-method/roles/quick-flow-dev.yaml +8 -0
  239. package/references/openteams/examples/bmad-method/roles/scrum-master.yaml +9 -0
  240. package/references/openteams/examples/bmad-method/roles/tech-writer.yaml +8 -0
  241. package/references/openteams/examples/bmad-method/roles/ux-designer.yaml +8 -0
  242. package/references/openteams/examples/bmad-method/team.yaml +161 -0
  243. package/references/openteams/examples/get-shit-done/prompts/codebase-mapper/ROLE.md +17 -0
  244. package/references/openteams/examples/get-shit-done/prompts/codebase-mapper/SOUL.md +5 -0
  245. package/references/openteams/examples/get-shit-done/prompts/debugger/ROLE.md +25 -0
  246. package/references/openteams/examples/get-shit-done/prompts/debugger/SOUL.md +5 -0
  247. package/references/openteams/examples/get-shit-done/prompts/executor/ROLE.md +34 -0
  248. package/references/openteams/examples/get-shit-done/prompts/executor/SOUL.md +5 -0
  249. package/references/openteams/examples/get-shit-done/prompts/integration-checker/ROLE.md +18 -0
  250. package/references/openteams/examples/get-shit-done/prompts/integration-checker/SOUL.md +3 -0
  251. package/references/openteams/examples/get-shit-done/prompts/orchestrator/ROLE.md +42 -0
  252. package/references/openteams/examples/get-shit-done/prompts/orchestrator/SOUL.md +5 -0
  253. package/references/openteams/examples/get-shit-done/prompts/phase-researcher/ROLE.md +15 -0
  254. package/references/openteams/examples/get-shit-done/prompts/phase-researcher/SOUL.md +3 -0
  255. package/references/openteams/examples/get-shit-done/prompts/plan-checker/ROLE.md +17 -0
  256. package/references/openteams/examples/get-shit-done/prompts/plan-checker/SOUL.md +3 -0
  257. package/references/openteams/examples/get-shit-done/prompts/planner/ROLE.md +28 -0
  258. package/references/openteams/examples/get-shit-done/prompts/planner/SOUL.md +5 -0
  259. package/references/openteams/examples/get-shit-done/prompts/project-researcher/ROLE.md +16 -0
  260. package/references/openteams/examples/get-shit-done/prompts/project-researcher/SOUL.md +3 -0
  261. package/references/openteams/examples/get-shit-done/prompts/research-synthesizer/ROLE.md +13 -0
  262. package/references/openteams/examples/get-shit-done/prompts/research-synthesizer/SOUL.md +3 -0
  263. package/references/openteams/examples/get-shit-done/prompts/roadmapper/ROLE.md +14 -0
  264. package/references/openteams/examples/get-shit-done/prompts/roadmapper/SOUL.md +3 -0
  265. package/references/openteams/examples/get-shit-done/prompts/verifier/ROLE.md +19 -0
  266. package/references/openteams/examples/get-shit-done/prompts/verifier/SOUL.md +5 -0
  267. package/references/openteams/examples/get-shit-done/roles/codebase-mapper.yaml +8 -0
  268. package/references/openteams/examples/get-shit-done/roles/debugger.yaml +8 -0
  269. package/references/openteams/examples/get-shit-done/roles/executor.yaml +8 -0
  270. package/references/openteams/examples/get-shit-done/roles/integration-checker.yaml +8 -0
  271. package/references/openteams/examples/get-shit-done/roles/orchestrator.yaml +9 -0
  272. package/references/openteams/examples/get-shit-done/roles/phase-researcher.yaml +7 -0
  273. package/references/openteams/examples/get-shit-done/roles/plan-checker.yaml +8 -0
  274. package/references/openteams/examples/get-shit-done/roles/planner.yaml +8 -0
  275. package/references/openteams/examples/get-shit-done/roles/project-researcher.yaml +8 -0
  276. package/references/openteams/examples/get-shit-done/roles/research-synthesizer.yaml +7 -0
  277. package/references/openteams/examples/get-shit-done/roles/roadmapper.yaml +7 -0
  278. package/references/openteams/examples/get-shit-done/roles/verifier.yaml +8 -0
  279. package/references/openteams/examples/get-shit-done/team.yaml +154 -0
  280. package/references/openteams/package-lock.json +2181 -0
  281. package/references/openteams/package.json +48 -0
  282. package/references/openteams/schema/role.schema.json +125 -0
  283. package/references/openteams/schema/team.schema.json +284 -0
  284. package/references/openteams/src/cli/agent.ts +104 -0
  285. package/references/openteams/src/cli/cli.test.ts +381 -0
  286. package/references/openteams/src/cli/generate.ts +220 -0
  287. package/references/openteams/src/cli/message.ts +241 -0
  288. package/references/openteams/src/cli/task.ts +154 -0
  289. package/references/openteams/src/cli/team.ts +104 -0
  290. package/references/openteams/src/cli/template.ts +207 -0
  291. package/references/openteams/src/cli.ts +45 -0
  292. package/references/openteams/src/db/database.test.ts +185 -0
  293. package/references/openteams/src/db/database.ts +240 -0
  294. package/references/openteams/src/generators/agent-prompt-generator.test.ts +332 -0
  295. package/references/openteams/src/generators/agent-prompt-generator.ts +521 -0
  296. package/references/openteams/src/generators/package-generator.test.ts +129 -0
  297. package/references/openteams/src/generators/package-generator.ts +102 -0
  298. package/references/openteams/src/generators/skill-generator.test.ts +246 -0
  299. package/references/openteams/src/generators/skill-generator.ts +374 -0
  300. package/references/openteams/src/index.ts +104 -0
  301. package/references/openteams/src/services/agent-service.test.ts +158 -0
  302. package/references/openteams/src/services/agent-service.ts +84 -0
  303. package/references/openteams/src/services/communication-service.test.ts +455 -0
  304. package/references/openteams/src/services/communication-service.ts +371 -0
  305. package/references/openteams/src/services/message-service.test.ts +342 -0
  306. package/references/openteams/src/services/message-service.ts +203 -0
  307. package/references/openteams/src/services/task-service.test.ts +434 -0
  308. package/references/openteams/src/services/task-service.ts +239 -0
  309. package/references/openteams/src/services/team-service.test.ts +181 -0
  310. package/references/openteams/src/services/team-service.ts +139 -0
  311. package/references/openteams/src/services/template-service.test.ts +306 -0
  312. package/references/openteams/src/services/template-service.ts +182 -0
  313. package/references/openteams/src/spawner/acp-factory.ts +96 -0
  314. package/references/openteams/src/spawner/interface.ts +31 -0
  315. package/references/openteams/src/spawner/mock.test.ts +93 -0
  316. package/references/openteams/src/spawner/mock.ts +59 -0
  317. package/references/openteams/src/template/loader.test.ts +1319 -0
  318. package/references/openteams/src/template/loader.ts +698 -0
  319. package/references/openteams/src/template/types.ts +200 -0
  320. package/references/openteams/src/types.ts +205 -0
  321. package/references/openteams/tsconfig.json +18 -0
  322. package/references/openteams/vitest.config.ts +9 -0
  323. package/references/skill-tree/.claude/settings.json +6 -0
  324. package/references/skill-tree/.sudocode/issues.jsonl +11 -0
  325. package/references/skill-tree/.sudocode/specs.jsonl +1 -0
  326. package/references/skill-tree/CLAUDE.md +150 -0
  327. package/references/skill-tree/README.md +324 -0
  328. package/references/skill-tree/docs/GAPS_v1.md +221 -0
  329. package/references/skill-tree/docs/INTEGRATION_PLAN.md +467 -0
  330. package/references/skill-tree/docs/TODOS.md +91 -0
  331. package/references/skill-tree/docs/anthropic_skill_guide.md +1364 -0
  332. package/references/skill-tree/docs/design/federated-skill-trees.md +524 -0
  333. package/references/skill-tree/docs/design/multi-agent-sync.md +759 -0
  334. package/references/skill-tree/docs/scraper/BRAINSTORM.md +583 -0
  335. package/references/skill-tree/docs/scraper/POC_PLAN.md +420 -0
  336. package/references/skill-tree/docs/scraper/README.md +170 -0
  337. package/references/skill-tree/examples/basic-usage.ts +190 -0
  338. package/references/skill-tree/package-lock.json +1509 -0
  339. package/references/skill-tree/package.json +66 -0
  340. package/references/skill-tree/scraper/README.md +123 -0
  341. package/references/skill-tree/scraper/docs/DESIGN.md +683 -0
  342. package/references/skill-tree/scraper/docs/PLAN.md +336 -0
  343. package/references/skill-tree/scraper/drizzle.config.ts +10 -0
  344. package/references/skill-tree/scraper/package-lock.json +6329 -0
  345. package/references/skill-tree/scraper/package.json +68 -0
  346. package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-description.md +7 -0
  347. package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-name.md +7 -0
  348. package/references/skill-tree/scraper/test/fixtures/minimal-skill/SKILL.md +27 -0
  349. package/references/skill-tree/scraper/test/fixtures/skill-json/SKILL.json +21 -0
  350. package/references/skill-tree/scraper/test/fixtures/skill-with-meta/SKILL.md +54 -0
  351. package/references/skill-tree/scraper/test/fixtures/skill-with-meta/_meta.json +24 -0
  352. package/references/skill-tree/scraper/test/fixtures/valid-skill/SKILL.md +93 -0
  353. package/references/skill-tree/scraper/test/fixtures/valid-skill/_meta.json +22 -0
  354. package/references/skill-tree/scraper/tsup.config.ts +14 -0
  355. package/references/skill-tree/scraper/vitest.config.ts +17 -0
  356. package/references/skill-tree/scripts/convert-to-vitest.ts +166 -0
  357. package/references/skill-tree/skills/skill-writer/SKILL.md +339 -0
  358. package/references/skill-tree/skills/skill-writer/references/examples.md +326 -0
  359. package/references/skill-tree/skills/skill-writer/references/patterns.md +210 -0
  360. package/references/skill-tree/skills/skill-writer/references/quality-checklist.md +123 -0
  361. package/references/skill-tree/test/run-all.ts +106 -0
  362. package/references/skill-tree/test/utils.ts +128 -0
  363. package/references/skill-tree/vitest.config.ts +16 -0
  364. package/src/agent/agent-manager.ts +143 -72
  365. package/src/agent/types.ts +9 -0
  366. package/src/api/__tests__/server.test.ts +203 -4
  367. package/src/api/server.ts +130 -5
  368. package/src/api/types.ts +3 -1
  369. package/src/cli/acp.ts +68 -1
  370. package/src/cli/index.ts +5 -1
  371. package/src/cli/mcp.ts +27 -13
  372. package/src/config/project-config.ts +27 -3
  373. package/src/index.ts +3 -0
  374. package/src/lifecycle/__tests__/handlers.test.ts +53 -0
  375. package/src/lifecycle/handlers/index.ts +25 -8
  376. package/src/lifecycle/types.ts +3 -0
  377. package/src/map/adapter/__tests__/stream-extensions.test.ts +494 -0
  378. package/src/map/adapter/extensions/index.ts +36 -0
  379. package/src/map/adapter/extensions/streams.ts +839 -0
  380. package/src/map/adapter/index.ts +5 -0
  381. package/src/map/adapter/types.ts +8 -1
  382. package/src/mcp/mcp-server.ts +14 -3
  383. package/src/mcp/tools/done.ts +19 -0
  384. package/src/roles/builtin/coordinator.ts +2 -0
  385. package/src/roles/builtin/integrator.ts +2 -0
  386. package/src/roles/builtin/worker.ts +3 -0
  387. package/src/roles/capabilities.ts +11 -0
  388. package/src/roles/config-loader.ts +3 -2
  389. package/src/roles/types.ts +7 -0
  390. package/src/server/combined-server.ts +15 -1
  391. package/src/store/__tests__/event-store-oob.test.ts +109 -0
  392. package/src/store/event-store.ts +13 -3
  393. package/src/store/instance.ts +2 -2
  394. package/src/store/types/agents.ts +5 -0
  395. package/src/task/backend/__tests__/memory-pull-mode.test.ts +153 -0
  396. package/src/task/backend/opentasks/daemon-manager.ts +4 -1
  397. package/src/teams/CLAUDE.md +180 -0
  398. package/src/teams/__tests__/e2e/workspace-isolation.e2e.test.ts +1263 -0
  399. package/src/teams/__tests__/team-manager.test.ts +814 -0
  400. package/src/teams/__tests__/team-system.test.ts +1291 -8
  401. package/src/teams/index.ts +21 -3
  402. package/src/teams/seed-defaults.ts +79 -0
  403. package/src/teams/team-loader.ts +200 -234
  404. package/src/teams/team-manager.ts +387 -0
  405. package/src/teams/team-runtime.ts +590 -121
  406. package/src/teams/types.ts +99 -200
@@ -0,0 +1,1263 @@
1
+ /**
2
+ * Workspace Isolation E2E Tests
3
+ *
4
+ * Tests the full capability-based workspace isolation flow for teams:
5
+ * - TeamRuntime creates integration stream during bootstrap
6
+ * - Spawn interceptor injects workspace fields based on role capabilities
7
+ * - createWorkspaceForRole dispatches on capabilities (not role names)
8
+ * - Done handler resolves team roles to built-in handlers via capabilities
9
+ * - Merge queue mr:submitted events wake the integrator agent
10
+ *
11
+ * Layer 1: Infrastructure (real git + services, mocked agent spawns)
12
+ * Layer 2: Service-level (full workspace lifecycle simulation)
13
+ * Layer 3: Full agent (real Claude Code, gated behind RUN_FULL_AGENT_TESTS)
14
+ *
15
+ * Run:
16
+ * npm run test:e2e -- src/teams/__tests__/e2e/workspace-isolation.e2e.test.ts
17
+ * RUN_FULL_AGENT_TESTS=true npm run test:e2e -- src/teams/__tests__/e2e/workspace-isolation.e2e.test.ts
18
+ */
19
+
20
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
21
+ import * as fs from "fs";
22
+ import * as path from "path";
23
+ import * as os from "os";
24
+ import { execSync } from "child_process";
25
+ import Database from "better-sqlite3";
26
+
27
+ import { createEventStore, type EventStore } from "../../../store/event-store.js";
28
+ import { createAgentManager, type AgentManager } from "../../../agent/agent-manager.js";
29
+ import { createMessageRouter, type MessageRouter } from "../../../router/message-router.js";
30
+ import { DefaultRoleRegistry } from "../../../roles/registry.js";
31
+ import { loadTeam } from "../../team-loader.js";
32
+ import { TeamRuntime, type TeamServices } from "../../team-runtime.js";
33
+ import { createDataplaneAdapter, type DataplaneAdapter } from "../../../workspace/dataplane-adapter.js";
34
+ import { DefaultWorkspaceManager } from "../../../workspace/workspace-manager.js";
35
+ import { createMergeQueue, type MergeQueue } from "../../../workspace/merge-queue/index.js";
36
+ import type { WorkerWorkspace, IntegratorWorkspace } from "../../../workspace/types.js";
37
+ import { createHandlerRegistry, getHandler, type AllHandlerDeps } from "../../../lifecycle/handlers/index.js";
38
+ import { WORKSPACE_CAPABILITIES } from "../../../roles/capabilities.js";
39
+ import { handleWorkerDone, type WorkerHandlerDeps } from "../../../lifecycle/handlers/worker.js";
40
+ import type { LifecycleContext, CleanupStatus } from "../../../lifecycle/types.js";
41
+ import { QueueIntegrationStrategy } from "../../../workspace/strategies/queue.js";
42
+ import { OptimisticIntegrationStrategy } from "../../../workspace/strategies/optimistic.js";
43
+
44
+ // ─────────────────────────────────────────────────────────────────
45
+ // Configuration
46
+ // ─────────────────────────────────────────────────────────────────
47
+
48
+ const RUN_FULL_AGENT = !!process.env.RUN_FULL_AGENT_TESTS;
49
+ const fullAgentFn = RUN_FULL_AGENT ? it : it.skip;
50
+ const PROJECT_ROOT = path.resolve(import.meta.dirname, "../../../..");
51
+
52
+ const log = (msg: string) => console.log(`[WorkspaceIso-E2E] ${msg}`);
53
+
54
+ // ─────────────────────────────────────────────────────────────────
55
+ // Helpers
56
+ // ─────────────────────────────────────────────────────────────────
57
+
58
+ function git(args: string, cwd: string): string {
59
+ return execSync(`git ${args}`, { cwd, stdio: "pipe", encoding: "utf8" }).trim();
60
+ }
61
+
62
+ function writeAndCommit(
63
+ filePath: string,
64
+ content: string,
65
+ message: string,
66
+ cwd: string,
67
+ ): string {
68
+ const fullPath = path.join(cwd, filePath);
69
+ const dir = path.dirname(fullPath);
70
+ if (!fs.existsSync(dir)) {
71
+ fs.mkdirSync(dir, { recursive: true });
72
+ }
73
+ fs.writeFileSync(fullPath, content);
74
+ git("add .", cwd);
75
+ git(`commit -m "${message}"`, cwd);
76
+ return git("rev-parse HEAD", cwd);
77
+ }
78
+
79
+ function listWorktrees(cwd: string): string[] {
80
+ return git("worktree list --porcelain", cwd)
81
+ .split("\n")
82
+ .filter((line) => line.startsWith("worktree "))
83
+ .map((line) => line.replace("worktree ", ""));
84
+ }
85
+
86
+ function cleanupWorktrees(repoPath: string): void {
87
+ if (!repoPath || !fs.existsSync(repoPath)) return;
88
+ try {
89
+ const worktrees = listWorktrees(repoPath).filter((wt) => wt !== repoPath);
90
+ for (const wt of worktrees) {
91
+ try {
92
+ execSync(`git worktree remove --force "${wt}"`, {
93
+ cwd: repoPath,
94
+ stdio: "pipe",
95
+ });
96
+ } catch { /* ignore */ }
97
+ }
98
+ } catch { /* ignore */ }
99
+ }
100
+
101
+ async function waitForAgentState(
102
+ agentManager: AgentManager,
103
+ agentId: string,
104
+ state: "running" | "stopped",
105
+ timeoutMs = 60000,
106
+ ): Promise<void> {
107
+ const start = Date.now();
108
+ while (Date.now() - start < timeoutMs) {
109
+ const agent = agentManager.get(agentId);
110
+ if (agent?.state === state) return;
111
+ await new Promise((r) => setTimeout(r, 500));
112
+ }
113
+ throw new Error(`Timeout: agent ${agentId} did not reach state '${state}' in ${timeoutMs}ms`);
114
+ }
115
+
116
+ // ─────────────────────────────────────────────────────────────────
117
+ // Layer 1: Infrastructure E2E (real git + services, no real agents)
118
+ // ─────────────────────────────────────────────────────────────────
119
+
120
+ describe("Workspace Isolation E2E — Infrastructure", () => {
121
+ let tempDir: string;
122
+ let repoPath: string;
123
+ let dbPath: string;
124
+ let db: Database.Database;
125
+ let adapter: DataplaneAdapter;
126
+ let manager: DefaultWorkspaceManager;
127
+ let mergeQueue: MergeQueue;
128
+ let eventStore: EventStore;
129
+ let agentManager: AgentManager;
130
+ let messageRouter: MessageRouter;
131
+ let roleRegistry: DefaultRoleRegistry;
132
+ let runtime: TeamRuntime;
133
+
134
+ // Mock spawn counter for generating unique IDs
135
+ let spawnCounter: number;
136
+
137
+ beforeEach(async () => {
138
+ spawnCounter = 0;
139
+
140
+ // 1. Create temp dir + git repo
141
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ws-iso-e2e-"));
142
+ repoPath = path.join(tempDir, "repo");
143
+ dbPath = path.join(tempDir, "test.db");
144
+ fs.mkdirSync(repoPath);
145
+
146
+ git("init", repoPath);
147
+ git('config user.email "test@test.com"', repoPath);
148
+ git('config user.name "Test User"', repoPath);
149
+ fs.writeFileSync(path.join(repoPath, "README.md"), "# Test Project\n");
150
+ fs.mkdirSync(path.join(repoPath, "src"), { recursive: true });
151
+ fs.writeFileSync(path.join(repoPath, "src/index.ts"), 'export const version = "1.0.0";\n');
152
+ git("add .", repoPath);
153
+ git('commit -m "Initial commit"', repoPath);
154
+
155
+ // 2. Create workspace infrastructure
156
+ db = new Database(dbPath);
157
+ adapter = createDataplaneAdapter({
158
+ enabled: true,
159
+ repoPath,
160
+ db,
161
+ skipRecovery: true,
162
+ });
163
+ manager = new DefaultWorkspaceManager(adapter, {
164
+ worktreeBaseDir: path.join(tempDir, ".worktrees"),
165
+ });
166
+ mergeQueue = createMergeQueue({ db });
167
+
168
+ // 3. Create services
169
+ const instanceId = `ws-iso-${Date.now()}`;
170
+ eventStore = await createEventStore({ instanceId, baseDir: tempDir });
171
+ messageRouter = createMessageRouter(eventStore);
172
+ roleRegistry = new DefaultRoleRegistry();
173
+ agentManager = createAgentManager(eventStore, messageRouter, {
174
+ defaultPermissionMode: "auto-approve",
175
+ defaultCwd: repoPath,
176
+ });
177
+
178
+ // 4. Load structured team and create runtime with workspace manager
179
+ const manifest = await loadTeam("structured", roleRegistry, PROJECT_ROOT);
180
+ const services: TeamServices = {
181
+ agentManager,
182
+ messageRouter,
183
+ eventStore,
184
+ workspaceManager: manager as any,
185
+ };
186
+ runtime = new TeamRuntime(manifest, services);
187
+
188
+ // 5. Mock agentManager.spawn to return fake agents without real processes
189
+ vi.spyOn(agentManager, "spawn").mockImplementation(async (options) => {
190
+ spawnCounter++;
191
+ const id = `mock-${options.role ?? "agent"}-${spawnCounter}`;
192
+
193
+ // Emit spawn event so the agent appears in materialized view
194
+ eventStore.emit({
195
+ type: "spawn",
196
+ source: { agent_id: id },
197
+ payload: {
198
+ agent_id: id,
199
+ role: options.role ?? "worker",
200
+ parent: options.parent ?? null,
201
+ task: options.task,
202
+ },
203
+ });
204
+ await eventStore.persist();
205
+
206
+ return {
207
+ id,
208
+ session_id: `session-${id}`,
209
+ agent: eventStore.getAgent(id)!,
210
+ session: {} as any,
211
+ };
212
+ });
213
+
214
+ // 6. Initialize and bootstrap
215
+ await runtime.initialize();
216
+ await runtime.bootstrap();
217
+
218
+ log("Setup complete");
219
+ });
220
+
221
+ afterEach(async () => {
222
+ // Restore mocks
223
+ vi.restoreAllMocks();
224
+
225
+ // Teardown runtime
226
+ try { await runtime?.teardown(); } catch { /* ignore */ }
227
+
228
+ // Close workspace infrastructure
229
+ try { mergeQueue?.close(); } catch { /* ignore */ }
230
+ try { manager?.close(); } catch { /* ignore */ }
231
+ try { adapter?.close(); } catch { /* ignore */ }
232
+ try { db?.close(); } catch { /* ignore */ }
233
+
234
+ // Close services
235
+ try { await agentManager?.close(); } catch { /* ignore */ }
236
+ try { await eventStore?.close(); } catch { /* ignore */ }
237
+
238
+ // Clean up worktrees and temp dir
239
+ cleanupWorktrees(repoPath);
240
+ if (tempDir && fs.existsSync(tempDir)) {
241
+ fs.rmSync(tempDir, { recursive: true, force: true });
242
+ }
243
+ });
244
+
245
+ it("bootstrap creates integration stream when workspaceManager provided", () => {
246
+ const teamStreamId = runtime.getTeamStreamId();
247
+ expect(teamStreamId).toBeDefined();
248
+
249
+ // Verify the stream exists in the workspace manager
250
+ const stream = manager.getStream(teamStreamId!);
251
+ expect(stream).not.toBeNull();
252
+ log(`Integration stream created: ${teamStreamId}`);
253
+ });
254
+
255
+ it("spawn interceptor injects streamId + dataplaneTaskId for developer role", () => {
256
+ const interceptor = runtime.createSpawnInterceptor();
257
+ const result = interceptor({
258
+ task: "test task",
259
+ role: "developer",
260
+ parent: runtime.getRootAgentId() ?? null,
261
+ });
262
+
263
+ expect(result.streamId).toBe(runtime.getTeamStreamId());
264
+ expect(result.dataplaneTaskId).toBeDefined();
265
+ expect(result.dataplaneTaskId).toMatch(/^worker-/);
266
+ expect(result.capabilities).toContain(WORKSPACE_CAPABILITIES.WORKTREE);
267
+ log("Developer intercepted: streamId + dataplaneTaskId + workspace.worktree");
268
+ });
269
+
270
+ it("spawn interceptor injects streamId for merger role (no dataplaneTaskId)", () => {
271
+ const interceptor = runtime.createSpawnInterceptor();
272
+ const result = interceptor({
273
+ task: "test merger",
274
+ role: "merger",
275
+ parent: null,
276
+ });
277
+
278
+ expect(result.streamId).toBe(runtime.getTeamStreamId());
279
+ expect(result.capabilities).toContain(WORKSPACE_CAPABILITIES.INTEGRATE);
280
+ // Integrators don't get dataplaneTaskId
281
+ expect(result.dataplaneTaskId).toBeUndefined();
282
+ log("Merger intercepted: streamId + workspace.integrate, no dataplaneTaskId");
283
+ });
284
+
285
+ it("spawn interceptor does NOT inject workspace fields for reviewer", () => {
286
+ const interceptor = runtime.createSpawnInterceptor();
287
+ const result = interceptor({
288
+ task: "test reviewer",
289
+ role: "reviewer",
290
+ parent: null,
291
+ });
292
+
293
+ // Reviewer extends monitor — no workspace capabilities
294
+ expect(result.streamId).toBeUndefined();
295
+ expect(result.dataplaneTaskId).toBeUndefined();
296
+ log("Reviewer intercepted: no workspace fields");
297
+ });
298
+
299
+ it("spawn interceptor does not overwrite explicit workspace values", () => {
300
+ const interceptor = runtime.createSpawnInterceptor();
301
+ const result = interceptor({
302
+ task: "test",
303
+ role: "developer",
304
+ parent: null,
305
+ streamId: "explicit-stream",
306
+ dataplaneTaskId: "explicit-task",
307
+ });
308
+
309
+ expect(result.streamId).toBe("explicit-stream");
310
+ expect(result.dataplaneTaskId).toBe("explicit-task");
311
+ log("Explicit workspace values preserved");
312
+ });
313
+
314
+ it("createWorkerWorkspace works for capability-based developer role", () => {
315
+ const teamStreamId = runtime.getTeamStreamId()!;
316
+
317
+ // Create worker workspace (what createWorkspaceForRole does internally for workspace.worktree)
318
+ const workspace = manager.createWorkerWorkspace(
319
+ "dev-001",
320
+ "task-001",
321
+ teamStreamId,
322
+ ) as WorkerWorkspace;
323
+
324
+ expect(workspace).toBeDefined();
325
+ expect(workspace.role).toBe("worker");
326
+ expect(workspace.taskId).toBe("task-001");
327
+ expect(fs.existsSync(workspace.path)).toBe(true);
328
+
329
+ // Verify worktree has repo files
330
+ expect(fs.existsSync(path.join(workspace.path, "README.md"))).toBe(true);
331
+
332
+ // Cleanup
333
+ manager.deallocateWorkspace("dev-001");
334
+ log("Worker workspace created via capability-based path");
335
+ });
336
+
337
+ it("createIntegratorWorkspace works for capability-based merger role", () => {
338
+ const teamStreamId = runtime.getTeamStreamId()!;
339
+
340
+ // Create integrator workspace (what createWorkspaceForRole does for workspace.integrate)
341
+ const workspace = manager.createIntegratorWorkspace(
342
+ "merger-001",
343
+ teamStreamId,
344
+ ) as IntegratorWorkspace;
345
+
346
+ expect(workspace).toBeDefined();
347
+ expect(workspace.role).toBe("integrator");
348
+ expect(fs.existsSync(workspace.path)).toBe(true);
349
+ expect(workspace.integrationBranch).toBeDefined();
350
+
351
+ // Cleanup
352
+ manager.deallocateWorkspace("merger-001");
353
+ log("Integrator workspace created via capability-based path");
354
+ });
355
+
356
+ it("merge queue mr:submitted event wakes integrator agent", async () => {
357
+ const teamStreamId = runtime.getTeamStreamId()!;
358
+
359
+ // The bootstrap registered the merger companion in agentRoleMap.
360
+ // Find the merger agent ID from the companion IDs.
361
+ const companionIds = runtime.getCompanionAgentIds();
362
+ const agentRoleMap = runtime.getAgentRoleMap();
363
+ const mergerAgentId = companionIds.find(
364
+ (id) => agentRoleMap.get(id as any) === "merger",
365
+ );
366
+ expect(mergerAgentId).toBeDefined();
367
+
368
+ // Spy on prompt (fire-and-forget async generator)
369
+ const promptSpy = vi.spyOn(agentManager, "prompt").mockReturnValue(
370
+ (async function* () { /* no-op generator */ })() as any,
371
+ );
372
+
373
+ // Submit via the workspace manager's internal merge queue (same instance
374
+ // that TeamRuntime subscribed to — the test's `mergeQueue` is a separate instance)
375
+ const wmMergeQueue = manager.getMergeQueue();
376
+ wmMergeQueue.submit({
377
+ streamId: teamStreamId,
378
+ taskId: "task-001",
379
+ workerBranch: "worker/dev-001/task-001",
380
+ workerAgentId: "dev-001",
381
+ });
382
+
383
+ // Give the event handler a tick to fire
384
+ await new Promise((r) => setTimeout(r, 50));
385
+
386
+ // Assert prompt was called with the merger agent ID
387
+ expect(promptSpy).toHaveBeenCalled();
388
+ const [calledAgentId, calledMessage] = promptSpy.mock.calls[0];
389
+ expect(calledAgentId).toBe(mergerAgentId);
390
+ expect(calledMessage).toContain("Merge request");
391
+ expect(calledMessage).toContain("dev-001");
392
+ log("Merge queue mr:submitted woke integrator agent");
393
+ });
394
+
395
+ it("MERGE_REQUEST signal polling picks up worker signals and submits to merge queue", async () => {
396
+ // This test verifies the signal-based merge queue submission flow:
397
+ // Worker subprocess emits MERGE_REQUEST to EventStore → TeamRuntime polls → submits to merge queue
398
+ // Note: beforeEach already called runtime.initialize() + runtime.bootstrap()
399
+
400
+ const rootId = runtime.getRootAgentId()!;
401
+ const teamStreamId = runtime.getTeamStreamId()!;
402
+ const wmMergeQueue = manager.getMergeQueue();
403
+
404
+ // Simulate a worker subprocess emitting a MERGE_REQUEST signal to EventStore
405
+ // (this is what the worker handler does when it has no integrationStrategy/mergeQueue)
406
+ const workerAgentId = "mock-worker-signal-001";
407
+
408
+ // Register the worker agent as a child of root so polling recognizes it
409
+ eventStore.emit({
410
+ type: "spawn",
411
+ source: { agent_id: workerAgentId },
412
+ payload: {
413
+ agent_id: workerAgentId,
414
+ task_id: "task-signal-001",
415
+ task: "Test signal-based merge request",
416
+ parent: rootId,
417
+ role: "developer",
418
+ },
419
+ });
420
+ await eventStore.persist();
421
+
422
+ // Emit the MERGE_REQUEST signal (simulating worker handler fallback path)
423
+ messageRouter.emitStatus({
424
+ from: { agent_id: workerAgentId },
425
+ status_type: "checkpoint",
426
+ summary: "Merge request for branch worker/dev-signal-001",
427
+ details: {
428
+ signal: "MERGE_REQUEST",
429
+ sourceBranch: "worker/dev-signal-001",
430
+ targetBranch: "integration",
431
+ taskId: "task-signal-001",
432
+ workerId: workerAgentId,
433
+ },
434
+ });
435
+ await eventStore.persist();
436
+
437
+ // Wait for the polling interval to pick up the signal (polls every 2s)
438
+ const deadline = Date.now() + 8_000;
439
+ let depth = 0;
440
+ while (Date.now() < deadline) {
441
+ depth = wmMergeQueue.getQueueDepth(teamStreamId);
442
+ if (depth > 0) break;
443
+ await new Promise((r) => setTimeout(r, 500));
444
+ }
445
+
446
+ expect(depth).toBeGreaterThan(0);
447
+ log("MERGE_REQUEST signal polling submitted to merge queue successfully");
448
+ });
449
+
450
+ it("getHandler resolves developer role to worker handler via capabilities", () => {
451
+ const deps: AllHandlerDeps = {
452
+ messageRouter,
453
+ agentManager,
454
+ };
455
+ const registry = createHandlerRegistry(deps);
456
+
457
+ // "developer" doesn't match any registry entry directly
458
+ // but workspace.worktree capability should resolve to worker handler
459
+ const handler = getHandler("developer", registry, deps, [
460
+ WORKSPACE_CAPABILITIES.WORKTREE,
461
+ "lifecycle.done",
462
+ ]);
463
+
464
+ expect(handler).toBeDefined();
465
+
466
+ // Verify it's the worker handler by checking it doesn't throw
467
+ // and produces a result (worker handler returns strategy-related result)
468
+ // Just verify it's a function — the unit tests already validate behavior
469
+ expect(typeof handler).toBe("function");
470
+ log("developer role resolved to worker handler via capabilities");
471
+ });
472
+
473
+ it("agent resume reads workspace cwd from EventStore (survives rebuildViews)", async () => {
474
+ const agentId = "resume-cwd-test-001";
475
+ const worktreePath = path.join(tempDir, "worktree-resume-test");
476
+
477
+ // Create agent via spawn event with original cwd
478
+ eventStore.emit({
479
+ type: "spawn",
480
+ source: { agent_id: agentId },
481
+ payload: {
482
+ agent_id: agentId,
483
+ task: "Test resume cwd",
484
+ role: "worker",
485
+ cwd: repoPath,
486
+ },
487
+ });
488
+ await eventStore.persist();
489
+ expect(eventStore.getAgent(agentId)?.cwd).toBe(repoPath);
490
+
491
+ // Update cwd out-of-band (simulating what spawn() does after workspace creation)
492
+ eventStore.updateAgentMetadata(agentId as any, { cwd: worktreePath });
493
+ await eventStore.persist();
494
+ expect(eventStore.getAgent(agentId)?.cwd).toBe(worktreePath);
495
+
496
+ // Simulate auto-load rebuild (triggers rebuildViews which replays events)
497
+ await eventStore.reload();
498
+
499
+ // cwd must survive the rebuild — this is what resume() would read
500
+ const agent = eventStore.getAgent(agentId);
501
+ expect(agent).not.toBeNull();
502
+ expect(agent!.cwd).toBe(worktreePath);
503
+ log("Agent cwd survives rebuildViews for resume");
504
+ });
505
+
506
+ it("workspace deallocated when agent terminates", () => {
507
+ const teamStreamId = runtime.getTeamStreamId()!;
508
+ const devId = "cleanup-test-001";
509
+ const taskId = manager.createTask(teamStreamId, { title: "cleanup test" });
510
+ const workspace = manager.createWorkerWorkspace(devId, taskId, teamStreamId) as WorkerWorkspace;
511
+
512
+ // Verify worktree exists
513
+ expect(fs.existsSync(workspace.path)).toBe(true);
514
+
515
+ // Track deallocated event via the custom event system
516
+ let deallocatedEvent: any = null;
517
+ manager.onEvent((e: any) => {
518
+ if (e.type === "workspace:deallocated") deallocatedEvent = e;
519
+ });
520
+
521
+ // Deallocate (this is what AgentManager.terminate() calls)
522
+ manager.deallocateWorkspace(devId);
523
+
524
+ // Workspace mapping should be cleared
525
+ expect(manager.getWorkspace(devId)).toBeNull();
526
+
527
+ // Event should have been emitted
528
+ expect(deallocatedEvent).not.toBeNull();
529
+ expect(deallocatedEvent.data.agentId).toBe(devId);
530
+ expect(deallocatedEvent.data.role).toBe("worker");
531
+ log("Workspace deallocated and worktree removed");
532
+ });
533
+
534
+ it("deallocateWorkspace is idempotent", () => {
535
+ const teamStreamId = runtime.getTeamStreamId()!;
536
+ const devId = "idempotent-test-001";
537
+ const taskId = manager.createTask(teamStreamId, { title: "idempotent test" });
538
+ manager.createWorkerWorkspace(devId, taskId, teamStreamId);
539
+
540
+ // First deallocation should work
541
+ manager.deallocateWorkspace(devId);
542
+
543
+ // Second deallocation should not throw
544
+ expect(() => manager.deallocateWorkspace(devId)).not.toThrow();
545
+ log("deallocateWorkspace is idempotent");
546
+ });
547
+ });
548
+
549
+ // ─────────────────────────────────────────────────────────────────
550
+ // Layer 2: Service-Level E2E (full workspace lifecycle simulation)
551
+ // ─────────────────────────────────────────────────────────────────
552
+
553
+ describe("Workspace Isolation E2E — Service Lifecycle", () => {
554
+ let tempDir: string;
555
+ let repoPath: string;
556
+ let dbPath: string;
557
+ let db: Database.Database;
558
+ let adapter: DataplaneAdapter;
559
+ let manager: DefaultWorkspaceManager;
560
+ let mergeQueue: MergeQueue;
561
+ let eventStore: EventStore;
562
+ let agentManager: AgentManager;
563
+ let messageRouter: MessageRouter;
564
+ let roleRegistry: DefaultRoleRegistry;
565
+ let runtime: TeamRuntime;
566
+
567
+ let spawnCounter: number;
568
+
569
+ beforeEach(async () => {
570
+ spawnCounter = 0;
571
+
572
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ws-iso-svc-"));
573
+ repoPath = path.join(tempDir, "repo");
574
+ dbPath = path.join(tempDir, "test.db");
575
+ fs.mkdirSync(repoPath);
576
+
577
+ git("init", repoPath);
578
+ git('config user.email "test@test.com"', repoPath);
579
+ git('config user.name "Test User"', repoPath);
580
+ fs.writeFileSync(path.join(repoPath, "README.md"), "# Test Project\n");
581
+ fs.mkdirSync(path.join(repoPath, "src"), { recursive: true });
582
+ fs.writeFileSync(path.join(repoPath, "src/index.ts"), 'export const version = "1.0.0";\n');
583
+ git("add .", repoPath);
584
+ git('commit -m "Initial commit"', repoPath);
585
+
586
+ db = new Database(dbPath);
587
+ adapter = createDataplaneAdapter({
588
+ enabled: true,
589
+ repoPath,
590
+ db,
591
+ skipRecovery: true,
592
+ });
593
+ manager = new DefaultWorkspaceManager(adapter, {
594
+ worktreeBaseDir: path.join(tempDir, ".worktrees"),
595
+ });
596
+ mergeQueue = createMergeQueue({ db });
597
+
598
+ const instanceId = `ws-svc-${Date.now()}`;
599
+ eventStore = await createEventStore({ instanceId, baseDir: tempDir });
600
+ messageRouter = createMessageRouter(eventStore);
601
+ roleRegistry = new DefaultRoleRegistry();
602
+ agentManager = createAgentManager(eventStore, messageRouter, {
603
+ defaultPermissionMode: "auto-approve",
604
+ defaultCwd: repoPath,
605
+ });
606
+
607
+ const manifest = await loadTeam("structured", roleRegistry, PROJECT_ROOT);
608
+ const services: TeamServices = {
609
+ agentManager,
610
+ messageRouter,
611
+ eventStore,
612
+ workspaceManager: manager as any,
613
+ };
614
+ runtime = new TeamRuntime(manifest, services);
615
+
616
+ vi.spyOn(agentManager, "spawn").mockImplementation(async (options) => {
617
+ spawnCounter++;
618
+ const id = `mock-${options.role ?? "agent"}-${spawnCounter}`;
619
+ eventStore.emit({
620
+ type: "spawn",
621
+ source: { agent_id: id },
622
+ payload: {
623
+ agent_id: id,
624
+ role: options.role ?? "worker",
625
+ parent: options.parent ?? null,
626
+ task: options.task,
627
+ },
628
+ });
629
+ await eventStore.persist();
630
+ return {
631
+ id,
632
+ session_id: `session-${id}`,
633
+ agent: eventStore.getAgent(id)!,
634
+ session: {} as any,
635
+ };
636
+ });
637
+
638
+ await runtime.initialize();
639
+ await runtime.bootstrap();
640
+ log("Service-level setup complete");
641
+ });
642
+
643
+ afterEach(async () => {
644
+ vi.restoreAllMocks();
645
+ try { await runtime?.teardown(); } catch { /* ignore */ }
646
+ try { mergeQueue?.close(); } catch { /* ignore */ }
647
+ try { manager?.close(); } catch { /* ignore */ }
648
+ try { adapter?.close(); } catch { /* ignore */ }
649
+ try { db?.close(); } catch { /* ignore */ }
650
+ try { await agentManager?.close(); } catch { /* ignore */ }
651
+ try { await eventStore?.close(); } catch { /* ignore */ }
652
+ cleanupWorktrees(repoPath);
653
+ if (tempDir && fs.existsSync(tempDir)) {
654
+ fs.rmSync(tempDir, { recursive: true, force: true });
655
+ }
656
+ });
657
+
658
+ it("full lifecycle: developer worktree → commit → merge queue → integrator merge", () => {
659
+ const teamStreamId = runtime.getTeamStreamId()!;
660
+ expect(teamStreamId).toBeDefined();
661
+
662
+ // ═══════════════════════════════════════════════════════════════
663
+ // Phase 1: Developer creates workspace via capabilities
664
+ // ═══════════════════════════════════════════════════════════════
665
+ const devId = "dev-alpha-001";
666
+ const taskId = manager.createTask(teamStreamId, {
667
+ title: "Implement feature",
668
+ priority: 10,
669
+ });
670
+
671
+ const devWorkspace = manager.createWorkerWorkspace(
672
+ devId,
673
+ taskId,
674
+ teamStreamId,
675
+ ) as WorkerWorkspace;
676
+
677
+ expect(devWorkspace.role).toBe("worker");
678
+ expect(fs.existsSync(devWorkspace.path)).toBe(true);
679
+
680
+ // ═══════════════════════════════════════════════════════════════
681
+ // Phase 2: Developer makes changes and commits
682
+ // ═══════════════════════════════════════════════════════════════
683
+ const startResult = manager.claimTask(taskId, devId, devWorkspace.path);
684
+ expect(startResult.branchName).toContain(devId);
685
+
686
+ writeAndCommit(
687
+ "src/feature.ts",
688
+ 'export function greet() { return "hello"; }',
689
+ "feat: add greeting function",
690
+ devWorkspace.path,
691
+ );
692
+
693
+ // ═══════════════════════════════════════════════════════════════
694
+ // Phase 3: Submit to merge queue
695
+ // ═══════════════════════════════════════════════════════════════
696
+ const task = adapter.getTask(taskId)!;
697
+ const mrId = mergeQueue.submit({
698
+ streamId: teamStreamId,
699
+ taskId,
700
+ workerBranch: task.branchName!,
701
+ workerAgentId: devId,
702
+ priority: 10,
703
+ });
704
+
705
+ expect(mrId).toBeDefined();
706
+ expect(mergeQueue.getQueueDepth(teamStreamId)).toBe(1);
707
+
708
+ // ═══════════════════════════════════════════════════════════════
709
+ // Phase 4: Integrator processes merge queue
710
+ // ═══════════════════════════════════════════════════════════════
711
+ // Deallocate worker first (integrator needs the stream branch)
712
+ manager.deallocateWorkspace(devId);
713
+
714
+ const mergerId = "merger-001";
715
+ const mergerWorkspace = manager.createIntegratorWorkspace(
716
+ mergerId,
717
+ teamStreamId,
718
+ ) as IntegratorWorkspace;
719
+
720
+ expect(mergerWorkspace.role).toBe("integrator");
721
+
722
+ const nextMr = mergeQueue.getNext(teamStreamId);
723
+ expect(nextMr).not.toBeNull();
724
+ expect(nextMr!.taskId).toBe(taskId);
725
+
726
+ mergeQueue.markProcessing(nextMr!.id);
727
+ const result = adapter.completeTask({
728
+ taskId,
729
+ worktree: mergerWorkspace.path,
730
+ });
731
+ expect(result.mergeCommit).toBeDefined();
732
+ mergeQueue.markMerged(nextMr!.id, result.mergeCommit);
733
+
734
+ // ═══════════════════════════════════════════════════════════════
735
+ // Phase 5: Verify integration
736
+ // ═══════════════════════════════════════════════════════════════
737
+ expect(mergeQueue.getQueueDepth(teamStreamId)).toBe(0);
738
+
739
+ const files = git("ls-tree -r HEAD --name-only", mergerWorkspace.path).split("\n");
740
+ expect(files).toContain("src/feature.ts");
741
+
742
+ // Cleanup
743
+ manager.deallocateWorkspace(mergerId);
744
+ log("Full lifecycle complete: developer → merge queue → integrator");
745
+ });
746
+
747
+ it("parallel developers with capability-based workspace isolation", () => {
748
+ const teamStreamId = runtime.getTeamStreamId()!;
749
+ const workerCount = 3;
750
+
751
+ // ═══════════════════════════════════════════════════════════════
752
+ // Phase 1: Create parallel developer workspaces
753
+ // ═══════════════════════════════════════════════════════════════
754
+ const workers: Array<{
755
+ id: string;
756
+ taskId: string;
757
+ workspace: WorkerWorkspace;
758
+ branchName: string;
759
+ }> = [];
760
+
761
+ for (let i = 0; i < workerCount; i++) {
762
+ const devId = `dev-${String(i).padStart(3, "0")}`;
763
+ const taskId = manager.createTask(teamStreamId, {
764
+ title: `Task ${i}`,
765
+ priority: (i + 1) * 10,
766
+ });
767
+
768
+ const workspace = manager.createWorkerWorkspace(
769
+ devId,
770
+ taskId,
771
+ teamStreamId,
772
+ ) as WorkerWorkspace;
773
+
774
+ const startResult = manager.claimTask(taskId, devId, workspace.path);
775
+
776
+ writeAndCommit(
777
+ `src/module-${i}.ts`,
778
+ `export const module${i} = true;`,
779
+ `feat: add module ${i}`,
780
+ workspace.path,
781
+ );
782
+
783
+ workers.push({ id: devId, taskId, workspace, branchName: startResult.branchName });
784
+ }
785
+
786
+ // Verify isolation: each worker has unique path
787
+ const paths = workers.map((w) => w.workspace.path);
788
+ expect(new Set(paths).size).toBe(workerCount);
789
+
790
+ // ═══════════════════════════════════════════════════════════════
791
+ // Phase 2: Submit all to merge queue
792
+ // ═══════════════════════════════════════════════════════════════
793
+ for (const worker of workers) {
794
+ const task = adapter.getTask(worker.taskId)!;
795
+ mergeQueue.submit({
796
+ streamId: teamStreamId,
797
+ taskId: worker.taskId,
798
+ workerBranch: task.branchName!,
799
+ workerAgentId: worker.id,
800
+ priority: parseInt(worker.taskId, 10) || 10,
801
+ });
802
+ }
803
+
804
+ expect(mergeQueue.getQueueDepth(teamStreamId)).toBe(workerCount);
805
+
806
+ // ═══════════════════════════════════════════════════════════════
807
+ // Phase 3: Deallocate workers and create integrator
808
+ // ═══════════════════════════════════════════════════════════════
809
+ for (const worker of workers) {
810
+ manager.deallocateWorkspace(worker.id);
811
+ }
812
+
813
+ const mergerWorkspace = manager.createIntegratorWorkspace(
814
+ "merger-001",
815
+ teamStreamId,
816
+ ) as IntegratorWorkspace;
817
+
818
+ // ═══════════════════════════════════════════════════════════════
819
+ // Phase 4: Process merge queue in order
820
+ // ═══════════════════════════════════════════════════════════════
821
+ let mr = mergeQueue.getNext(teamStreamId);
822
+ let mergedCount = 0;
823
+
824
+ while (mr) {
825
+ mergeQueue.markProcessing(mr.id);
826
+ const result = adapter.completeTask({
827
+ taskId: mr.taskId,
828
+ worktree: mergerWorkspace.path,
829
+ });
830
+ expect(result.mergeCommit).toBeDefined();
831
+ mergeQueue.markMerged(mr.id, result.mergeCommit);
832
+ mergedCount++;
833
+ mr = mergeQueue.getNext(teamStreamId);
834
+ }
835
+
836
+ expect(mergedCount).toBe(workerCount);
837
+ expect(mergeQueue.getQueueDepth(teamStreamId)).toBe(0);
838
+
839
+ // ═══════════════════════════════════════════════════════════════
840
+ // Phase 5: Verify all changes integrated
841
+ // ═══════════════════════════════════════════════════════════════
842
+ const files = git("ls-tree -r HEAD --name-only", mergerWorkspace.path).split("\n");
843
+ for (let i = 0; i < workerCount; i++) {
844
+ expect(files).toContain(`src/module-${i}.ts`);
845
+ }
846
+
847
+ // Cleanup
848
+ manager.deallocateWorkspace("merger-001");
849
+ log(`Parallel lifecycle complete: ${workerCount} developers → merge queue → integrator`);
850
+ });
851
+
852
+ it("worker done handler dispatches to queue strategy via land()", async () => {
853
+ const teamStreamId = runtime.getTeamStreamId()!;
854
+
855
+ // ═══════════════════════════════════════════════════════════════
856
+ // Phase 1: Create developer workspace and make changes
857
+ // ═══════════════════════════════════════════════════════════════
858
+ const devId = "dev-strategy-001";
859
+ const taskId = manager.createTask(teamStreamId, {
860
+ title: "Strategy test task",
861
+ priority: 10,
862
+ });
863
+
864
+ const devWorkspace = manager.createWorkerWorkspace(
865
+ devId,
866
+ taskId,
867
+ teamStreamId,
868
+ ) as WorkerWorkspace;
869
+
870
+ const startResult = manager.claimTask(taskId, devId, devWorkspace.path);
871
+ writeAndCommit(
872
+ "src/strategy-test.ts",
873
+ 'export const strategy = "queue";',
874
+ "feat: add strategy test file",
875
+ devWorkspace.path,
876
+ );
877
+
878
+ // ═══════════════════════════════════════════════════════════════
879
+ // Phase 2: Wire queue strategy with real merge queue
880
+ // ═══════════════════════════════════════════════════════════════
881
+ const queueStrategy = new QueueIntegrationStrategy();
882
+ queueStrategy.setMergeQueue(manager.getMergeQueue());
883
+
884
+ // ═══════════════════════════════════════════════════════════════
885
+ // Phase 3: Call handleWorkerDone with strategy
886
+ // ═══════════════════════════════════════════════════════════════
887
+ const context: LifecycleContext = {
888
+ agentId: devId,
889
+ role: "worker",
890
+ workspacePath: devWorkspace.path,
891
+ streamId: teamStreamId,
892
+ taskId: taskId,
893
+ branch: startResult.branchName,
894
+ integrationBranch: "integration",
895
+ };
896
+
897
+ const cleanupStatus: CleanupStatus = {
898
+ ready: true,
899
+ };
900
+
901
+ const deps: WorkerHandlerDeps = {
902
+ messageRouter,
903
+ agentManager,
904
+ integrationStrategy: queueStrategy,
905
+ };
906
+
907
+ const result = await handleWorkerDone(
908
+ context,
909
+ { status: "completed", summary: "Strategy test complete" },
910
+ cleanupStatus,
911
+ deps,
912
+ );
913
+
914
+ // ═══════════════════════════════════════════════════════════════
915
+ // Phase 4: Verify strategy was used (not fallback MERGE_REQUEST)
916
+ // ═══════════════════════════════════════════════════════════════
917
+ expect(result.shouldTerminate).toBe(true);
918
+ expect(result.signalsEmitted).toContain("WORKER_DONE");
919
+ expect(result.signalsEmitted).toContain("WORKER_INTEGRATED");
920
+ expect(result.signalsEmitted).not.toContain("MERGE_REQUEST");
921
+
922
+ // Strategy submitted to merge queue
923
+ const wmMergeQueue = manager.getMergeQueue();
924
+ expect(wmMergeQueue.getQueueDepth(teamStreamId)).toBe(1);
925
+
926
+ const pending = wmMergeQueue.getPending(teamStreamId);
927
+ expect(pending[0].workerBranch).toBe(startResult.branchName);
928
+ expect(pending[0].workerAgentId).toBe(devId);
929
+
930
+ // cleanupActions should mention the strategy name
931
+ expect(result.cleanupActions?.some((a) => a.includes("queue"))).toBe(true);
932
+
933
+ // Cleanup
934
+ manager.deallocateWorkspace(devId);
935
+ log("Worker done handler dispatched to queue strategy successfully");
936
+ });
937
+
938
+ it("optimistic strategy emits validation event on successful land", async () => {
939
+ // ═══════════════════════════════════════════════════════════════
940
+ // Phase 1: Create bare repo as "remote" for push operations
941
+ // ═══════════════════════════════════════════════════════════════
942
+ const bareDir = path.join(tempDir, "bare.git");
943
+ execSync(`git init --bare "${bareDir}"`, { stdio: "pipe" });
944
+
945
+ const cloneDir = path.join(tempDir, "optimistic-clone");
946
+ execSync(`git clone "${bareDir}" "${cloneDir}"`, { stdio: "pipe" });
947
+ git('config user.email "test@test.com"', cloneDir);
948
+ git('config user.name "Test User"', cloneDir);
949
+
950
+ // Create initial commit and push to origin/main
951
+ writeAndCommit("README.md", "# Test", "initial commit", cloneDir);
952
+ git("push origin HEAD:main", cloneDir);
953
+
954
+ // Create a worker branch and make changes
955
+ git("checkout -b worker/opt-test-001", cloneDir);
956
+ writeAndCommit(
957
+ "src/optimistic.ts",
958
+ 'export const mode = "optimistic";',
959
+ "feat: add optimistic file",
960
+ cloneDir,
961
+ );
962
+
963
+ // ═══════════════════════════════════════════════════════════════
964
+ // Phase 2: Create optimistic strategy with EventStore
965
+ // ═══════════════════════════════════════════════════════════════
966
+ const optimistic = new OptimisticIntegrationStrategy();
967
+ optimistic.setEventStore(eventStore);
968
+
969
+ // ═══════════════════════════════════════════════════════════════
970
+ // Phase 3: Land changes
971
+ // ═══════════════════════════════════════════════════════════════
972
+ const landResult = await optimistic.land({
973
+ sourceBranch: "worker/opt-test-001",
974
+ targetBranch: "main",
975
+ workspacePath: cloneDir,
976
+ agentId: "agent-opt-001",
977
+ taskId: "task-opt-001",
978
+ });
979
+
980
+ // ═══════════════════════════════════════════════════════════════
981
+ // Phase 4: Verify land succeeded and validation event emitted
982
+ // ═══════════════════════════════════════════════════════════════
983
+ expect(landResult.status).toBe("landed");
984
+ expect(landResult.commitHash).toBeDefined();
985
+ expect(landResult.commitHash!.length).toBeGreaterThan(0);
986
+
987
+ expect(landResult.retryCount).toBe(0);
988
+
989
+ // Verify validation event was emitted to EventStore
990
+ const statusEvents = eventStore.query({
991
+ type: "status",
992
+ source_agent_id: "agent-opt-001" as any,
993
+ });
994
+ const validationEvent = statusEvents.find(
995
+ (e) => e.payload?.validation_requested === true,
996
+ );
997
+ expect(validationEvent).toBeDefined();
998
+ expect(validationEvent!.payload.commitHash).toBe(landResult.commitHash);
999
+ expect(validationEvent!.payload.taskId).toBe("task-opt-001");
1000
+ expect(validationEvent!.payload.agentId).toBe("agent-opt-001");
1001
+
1002
+ // Verify the commit actually landed on main at the remote
1003
+ const remoteMain = execSync(`git -C "${bareDir}" log --oneline -1 main`, {
1004
+ encoding: "utf-8",
1005
+ }).trim();
1006
+ expect(remoteMain).toContain("optimistic");
1007
+
1008
+ log(`Optimistic strategy landed: ${landResult.commitHash!.slice(0, 8)}`);
1009
+ });
1010
+ });
1011
+
1012
+ // ─────────────────────────────────────────────────────────────────
1013
+ // Layer 3: Full Agent E2E (requires RUN_FULL_AGENT_TESTS)
1014
+ // ─────────────────────────────────────────────────────────────────
1015
+
1016
+ describe("Workspace Isolation E2E — Full Agent", () => {
1017
+ let tempDir: string;
1018
+ let repoPath: string;
1019
+ let dbPath: string;
1020
+ let db: Database.Database;
1021
+ let adapter: DataplaneAdapter;
1022
+ let wsManager: DefaultWorkspaceManager;
1023
+ let mergeQueue: MergeQueue;
1024
+ let eventStore: EventStore;
1025
+ let agentManager: AgentManager;
1026
+ let messageRouter: MessageRouter;
1027
+ let roleRegistry: DefaultRoleRegistry;
1028
+ let runtime: TeamRuntime | null = null;
1029
+
1030
+ beforeEach(async () => {
1031
+ if (!RUN_FULL_AGENT) return;
1032
+
1033
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ws-iso-agent-"));
1034
+ repoPath = path.join(tempDir, "repo");
1035
+ dbPath = path.join(tempDir, "test.db");
1036
+ fs.mkdirSync(repoPath);
1037
+
1038
+ git("init", repoPath);
1039
+ git('config user.email "test@test.com"', repoPath);
1040
+ git('config user.name "Test User"', repoPath);
1041
+ fs.writeFileSync(path.join(repoPath, "README.md"), "# Test Project\n");
1042
+ fs.mkdirSync(path.join(repoPath, "src"), { recursive: true });
1043
+ fs.writeFileSync(path.join(repoPath, "src/index.ts"), 'export const version = "1.0.0";\n');
1044
+ git("add .", repoPath);
1045
+ git('commit -m "Initial commit"', repoPath);
1046
+
1047
+ db = new Database(dbPath);
1048
+ adapter = createDataplaneAdapter({
1049
+ enabled: true,
1050
+ repoPath,
1051
+ db,
1052
+ skipRecovery: true,
1053
+ });
1054
+ wsManager = new DefaultWorkspaceManager(adapter, {
1055
+ worktreeBaseDir: path.join(tempDir, ".worktrees"),
1056
+ });
1057
+ mergeQueue = createMergeQueue({ db });
1058
+
1059
+ const instanceId = `ws-agent-${Date.now()}`;
1060
+ eventStore = await createEventStore({ instanceId, baseDir: tempDir });
1061
+ messageRouter = createMessageRouter(eventStore);
1062
+ roleRegistry = new DefaultRoleRegistry();
1063
+ agentManager = createAgentManager(eventStore, messageRouter, {
1064
+ defaultPermissionMode: "auto-approve",
1065
+ defaultCwd: repoPath,
1066
+ workspaceManager: wsManager as any,
1067
+ });
1068
+
1069
+ log("Full agent services initialized");
1070
+ });
1071
+
1072
+ afterEach(async () => {
1073
+ if (!RUN_FULL_AGENT) return;
1074
+
1075
+ if (runtime) {
1076
+ try { await runtime.teardown(); } catch { /* ignore */ }
1077
+ runtime = null;
1078
+ }
1079
+
1080
+ try {
1081
+ for (const agent of agentManager.list()) {
1082
+ if (agent.state === "running") {
1083
+ try { await agentManager.terminate(agent.id, "test_cleanup"); } catch { /* ignore */ }
1084
+ }
1085
+ }
1086
+ } catch { /* ignore */ }
1087
+
1088
+ try { mergeQueue?.close(); } catch { /* ignore */ }
1089
+ try { wsManager?.close(); } catch { /* ignore */ }
1090
+ try { adapter?.close(); } catch { /* ignore */ }
1091
+ try { db?.close(); } catch { /* ignore */ }
1092
+ try { await agentManager?.close(); } catch { /* ignore */ }
1093
+ try { await eventStore?.close(); } catch { /* ignore */ }
1094
+
1095
+ cleanupWorktrees(repoPath);
1096
+ if (tempDir && fs.existsSync(tempDir)) {
1097
+ fs.rmSync(tempDir, { recursive: true, force: true });
1098
+ }
1099
+ log("Full agent cleanup complete");
1100
+ });
1101
+
1102
+ fullAgentFn(
1103
+ "team bootstrap with workspace isolation creates stream and agents get workspaces",
1104
+ async () => {
1105
+ const manifest = await loadTeam("structured", roleRegistry, PROJECT_ROOT);
1106
+ const services: TeamServices = {
1107
+ agentManager,
1108
+ messageRouter,
1109
+ eventStore,
1110
+ workspaceManager: wsManager as any,
1111
+ };
1112
+ runtime = new TeamRuntime(manifest, services);
1113
+
1114
+ await runtime.initialize();
1115
+ runtime.installOnServices();
1116
+ log("Runtime initialized with workspace isolation");
1117
+
1118
+ const result = await runtime.bootstrap();
1119
+ log(`Bootstrap complete: root=${result.rootId}, companions=${result.companionIds.join(", ")}`);
1120
+
1121
+ // Verify integration stream was created
1122
+ const teamStreamId = runtime.getTeamStreamId();
1123
+ expect(teamStreamId).toBeDefined();
1124
+ expect(wsManager.getStream(teamStreamId!)).not.toBeNull();
1125
+ log(`Integration stream: ${teamStreamId}`);
1126
+
1127
+ // Spawn a developer child — interceptor should inject workspace fields
1128
+ const developer = await agentManager.spawn({
1129
+ task: "You are a developer. Wait for instructions.",
1130
+ role: "developer",
1131
+ parent: result.rootId,
1132
+ cwd: repoPath,
1133
+ });
1134
+
1135
+ await waitForAgentState(agentManager, developer.id, "running");
1136
+ log(`Developer spawned: ${developer.id}`);
1137
+
1138
+ // Verify developer has workspace with correct stream
1139
+ if (developer.workspace) {
1140
+ expect(developer.workspace.role).toBe("worker");
1141
+ expect(developer.streamId).toBe(teamStreamId);
1142
+
1143
+ // Verify agent cwd is the workspace path (not repo root)
1144
+ const agentRecord = eventStore.getAgent(developer.id);
1145
+ expect(agentRecord?.cwd).toBe(developer.workspace.path);
1146
+ expect(agentRecord?.cwd).not.toBe(repoPath);
1147
+ log(`Developer cwd correctly set to workspace: ${agentRecord?.cwd}`);
1148
+ }
1149
+
1150
+ // Terminate agents
1151
+ await agentManager.terminate(developer.id, "completed");
1152
+ for (const companionId of result.companionIds) {
1153
+ try { await agentManager.terminate(companionId, "completed"); } catch { /* ignore */ }
1154
+ }
1155
+ try { await agentManager.terminate(result.rootId, "completed"); } catch { /* ignore */ }
1156
+ },
1157
+ { timeout: 180_000 },
1158
+ );
1159
+
1160
+ fullAgentFn(
1161
+ "developer agent calls done() and work is submitted to merge queue",
1162
+ async () => {
1163
+ const manifest = await loadTeam("structured", roleRegistry, PROJECT_ROOT);
1164
+ const services: TeamServices = {
1165
+ agentManager,
1166
+ messageRouter,
1167
+ eventStore,
1168
+ workspaceManager: wsManager as any,
1169
+ };
1170
+ runtime = new TeamRuntime(manifest, services);
1171
+
1172
+ await runtime.initialize();
1173
+ runtime.installOnServices();
1174
+ const result = await runtime.bootstrap();
1175
+
1176
+ const teamStreamId = runtime.getTeamStreamId()!;
1177
+ expect(teamStreamId).toBeDefined();
1178
+
1179
+ // Spawn a developer with a task focused solely on calling done().
1180
+ // Keeping the task minimal avoids Claude completing file creation
1181
+ // and ending its turn before invoking the MCP done() tool.
1182
+ const developer = await agentManager.spawn({
1183
+ task: [
1184
+ "Your ONLY task is to call the done() tool immediately.",
1185
+ "Do NOT create files, run commands, or do any other work.",
1186
+ "Just call: done({ status: \"completed\", summary: \"Task complete\" })",
1187
+ ].join("\n"),
1188
+ role: "developer",
1189
+ parent: result.rootId,
1190
+ cwd: repoPath,
1191
+ });
1192
+
1193
+ await waitForAgentState(agentManager, developer.id, "running");
1194
+ log(`Developer spawned for done() test: ${developer.id}`);
1195
+
1196
+ // Prompt the developer to call done() — keep it direct and unambiguous
1197
+ for await (const _update of agentManager.prompt(
1198
+ developer.id,
1199
+ 'Call the done tool now with status "completed" and summary "Task complete". Do nothing else.',
1200
+ )) {
1201
+ // Consume the stream
1202
+ }
1203
+
1204
+ // Wait for agent to stop (done() schedules termination)
1205
+ const stopDeadline = Date.now() + 30_000;
1206
+ while (Date.now() < stopDeadline) {
1207
+ const a = agentManager.get(developer.id);
1208
+ if (a?.state === "stopped") break;
1209
+ await new Promise(r => setTimeout(r, 500));
1210
+ }
1211
+ const agent = agentManager.get(developer.id);
1212
+ log(`Developer state after waiting: ${agent?.state}`);
1213
+ expect(agent?.state).toBe("stopped");
1214
+
1215
+ // Agent called done() — verify merge queue got the MERGE_REQUEST.
1216
+ // TeamRuntime polls EventStore every 2s for MERGE_REQUEST signals from
1217
+ // worker subprocesses and submits to the real merge queue.
1218
+ // Use wsManager.getMergeQueue() because it uses the same table prefix (macro_)
1219
+ // as the polling code in TeamRuntime.
1220
+ const wmMergeQueue = wsManager.getMergeQueue();
1221
+ const mqDeadline = Date.now() + 10_000;
1222
+ let depth = 0;
1223
+ while (Date.now() < mqDeadline) {
1224
+ depth = wmMergeQueue.getQueueDepth(teamStreamId);
1225
+ if (depth > 0) break;
1226
+ await new Promise(r => setTimeout(r, 500));
1227
+ }
1228
+ log(`Merge queue depth after developer done(): ${depth}`);
1229
+ expect(depth).toBeGreaterThan(0);
1230
+
1231
+ // Verify branch correctness — sourceBranch should be a worker branch, NOT "main"
1232
+ const pending = wmMergeQueue.getPending(teamStreamId);
1233
+ expect(pending.length).toBeGreaterThan(0);
1234
+ const mr = pending[0];
1235
+ expect(mr.workerBranch).toMatch(/^worker\//);
1236
+ log(`Merge request workerBranch: ${mr.workerBranch}`);
1237
+
1238
+ // Cleanup
1239
+ for (const companionId of result.companionIds) {
1240
+ try { await agentManager.terminate(companionId, "completed"); } catch { /* ignore */ }
1241
+ }
1242
+ try { await agentManager.terminate(result.rootId, "completed"); } catch { /* ignore */ }
1243
+ },
1244
+ { timeout: 180_000 },
1245
+ );
1246
+ });
1247
+
1248
+ // ─────────────────────────────────────────────────────────────────
1249
+ // Info message for running tests
1250
+ // ─────────────────────────────────────────────────────────────────
1251
+
1252
+ if (!RUN_FULL_AGENT) {
1253
+ console.log("\n┌──────────────────────────────────────────────────────────┐");
1254
+ console.log("│ Workspace Isolation full-agent tests are skipped │");
1255
+ console.log("│ (RUN_FULL_AGENT_TESTS not set) │");
1256
+ console.log("│ │");
1257
+ console.log("│ Layers 1-2 (Infrastructure + Service) will still run. │");
1258
+ console.log("│ │");
1259
+ console.log("│ To run with real agents: │");
1260
+ console.log("│ RUN_FULL_AGENT_TESTS=true npm run test:e2e -- \\ │");
1261
+ console.log("│ src/teams/__tests__/e2e/workspace-isolation.e2e.test.ts│");
1262
+ console.log("└──────────────────────────────────────────────────────────┘\n");
1263
+ }