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.
- package/.sudocode/issues.jsonl +28 -0
- package/.sudocode/specs.jsonl +4 -0
- package/CLAUDE.md +9 -3
- package/dist/agent/agent-manager.d.ts.map +1 -1
- package/dist/agent/agent-manager.js +111 -48
- package/dist/agent/agent-manager.js.map +1 -1
- package/dist/agent/types.d.ts +7 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/api/server.d.ts +5 -1
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +100 -3
- package/dist/api/server.js.map +1 -1
- package/dist/api/types.d.ts +1 -1
- package/dist/api/types.d.ts.map +1 -1
- package/dist/cli/acp.d.ts.map +1 -1
- package/dist/cli/acp.js +71 -1
- package/dist/cli/acp.js.map +1 -1
- package/dist/cli/index.js +5 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp.js +27 -8
- package/dist/cli/mcp.js.map +1 -1
- package/dist/config/project-config.d.ts +13 -2
- package/dist/config/project-config.d.ts.map +1 -1
- package/dist/config/project-config.js +12 -2
- package/dist/config/project-config.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lifecycle/handlers/index.d.ts +7 -3
- package/dist/lifecycle/handlers/index.d.ts.map +1 -1
- package/dist/lifecycle/handlers/index.js +25 -8
- package/dist/lifecycle/handlers/index.js.map +1 -1
- package/dist/lifecycle/types.d.ts +2 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/dist/map/adapter/extensions/index.d.ts +4 -1
- package/dist/map/adapter/extensions/index.d.ts.map +1 -1
- package/dist/map/adapter/extensions/index.js +27 -0
- package/dist/map/adapter/extensions/index.js.map +1 -1
- package/dist/map/adapter/extensions/streams.d.ts +95 -0
- package/dist/map/adapter/extensions/streams.d.ts.map +1 -0
- package/dist/map/adapter/extensions/streams.js +515 -0
- package/dist/map/adapter/extensions/streams.js.map +1 -0
- package/dist/map/adapter/index.d.ts +1 -1
- package/dist/map/adapter/index.d.ts.map +1 -1
- package/dist/map/adapter/index.js +3 -1
- package/dist/map/adapter/index.js.map +1 -1
- package/dist/map/adapter/types.d.ts +1 -1
- package/dist/map/adapter/types.d.ts.map +1 -1
- package/dist/mcp/mcp-server.d.ts +2 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -1
- package/dist/mcp/mcp-server.js +12 -3
- package/dist/mcp/mcp-server.js.map +1 -1
- package/dist/mcp/tools/done.d.ts.map +1 -1
- package/dist/mcp/tools/done.js +18 -0
- package/dist/mcp/tools/done.js.map +1 -1
- package/dist/roles/builtin/coordinator.d.ts.map +1 -1
- package/dist/roles/builtin/coordinator.js +2 -1
- package/dist/roles/builtin/coordinator.js.map +1 -1
- package/dist/roles/builtin/integrator.d.ts.map +1 -1
- package/dist/roles/builtin/integrator.js +2 -1
- package/dist/roles/builtin/integrator.js.map +1 -1
- package/dist/roles/builtin/worker.d.ts.map +1 -1
- package/dist/roles/builtin/worker.js +3 -1
- package/dist/roles/builtin/worker.js.map +1 -1
- package/dist/roles/capabilities.d.ts +6 -0
- package/dist/roles/capabilities.d.ts.map +1 -1
- package/dist/roles/capabilities.js +10 -0
- package/dist/roles/capabilities.js.map +1 -1
- package/dist/roles/config-loader.d.ts +1 -1
- package/dist/roles/config-loader.d.ts.map +1 -1
- package/dist/roles/config-loader.js +3 -2
- package/dist/roles/config-loader.js.map +1 -1
- package/dist/roles/types.d.ts +3 -1
- package/dist/roles/types.d.ts.map +1 -1
- package/dist/server/combined-server.d.ts +8 -1
- package/dist/server/combined-server.d.ts.map +1 -1
- package/dist/server/combined-server.js +6 -2
- package/dist/server/combined-server.js.map +1 -1
- package/dist/store/event-store.d.ts.map +1 -1
- package/dist/store/event-store.js +12 -5
- package/dist/store/event-store.js.map +1 -1
- package/dist/store/instance.d.ts +1 -1
- package/dist/store/instance.d.ts.map +1 -1
- package/dist/store/instance.js +2 -2
- package/dist/store/instance.js.map +1 -1
- package/dist/store/types/agents.d.ts +5 -0
- package/dist/store/types/agents.d.ts.map +1 -1
- package/dist/task/backend/opentasks/daemon-manager.d.ts.map +1 -1
- package/dist/task/backend/opentasks/daemon-manager.js +1 -1
- package/dist/task/backend/opentasks/daemon-manager.js.map +1 -1
- package/dist/teams/index.d.ts +3 -1
- package/dist/teams/index.d.ts.map +1 -1
- package/dist/teams/index.js +2 -0
- package/dist/teams/index.js.map +1 -1
- package/dist/teams/seed-defaults.d.ts +20 -0
- package/dist/teams/seed-defaults.d.ts.map +1 -0
- package/dist/teams/seed-defaults.js +71 -0
- package/dist/teams/seed-defaults.js.map +1 -0
- package/dist/teams/team-loader.d.ts +6 -2
- package/dist/teams/team-loader.d.ts.map +1 -1
- package/dist/teams/team-loader.js +154 -162
- package/dist/teams/team-loader.js.map +1 -1
- package/dist/teams/team-manager.d.ts +112 -0
- package/dist/teams/team-manager.d.ts.map +1 -0
- package/dist/teams/team-manager.js +305 -0
- package/dist/teams/team-manager.js.map +1 -0
- package/dist/teams/team-runtime.d.ts +125 -19
- package/dist/teams/team-runtime.d.ts.map +1 -1
- package/dist/teams/team-runtime.js +527 -119
- package/dist/teams/team-runtime.js.map +1 -1
- package/dist/teams/types.d.ts +41 -151
- package/dist/teams/types.d.ts.map +1 -1
- package/dist/teams/types.js +2 -3
- package/dist/teams/types.js.map +1 -1
- package/docs/teams.md +73 -0
- package/package.json +2 -1
- package/references/minimem/.claude/settings.json +7 -0
- package/references/minimem/.sudocode/issues.jsonl +18 -0
- package/references/minimem/.sudocode/specs.jsonl +1 -0
- package/references/minimem/CLAUDE.md +310 -0
- package/references/minimem/README.md +562 -0
- package/references/minimem/claude-plugin/.claude-plugin/plugin.json +10 -0
- package/references/minimem/claude-plugin/.mcp.json +7 -0
- package/references/minimem/claude-plugin/README.md +158 -0
- package/references/minimem/claude-plugin/commands/recall.md +47 -0
- package/references/minimem/claude-plugin/commands/remember.md +41 -0
- package/references/minimem/claude-plugin/hooks/__tests__/hooks.test.ts +272 -0
- package/references/minimem/claude-plugin/hooks/hooks.json +27 -0
- package/references/minimem/claude-plugin/hooks/session-end.sh +86 -0
- package/references/minimem/claude-plugin/hooks/session-start.sh +85 -0
- package/references/minimem/claude-plugin/skills/memory/SKILL.md +108 -0
- package/references/minimem/media/banner.png +0 -0
- package/references/minimem/package-lock.json +5373 -0
- package/references/minimem/package.json +72 -0
- package/references/minimem/scripts/postbuild.js +35 -0
- package/references/minimem/src/__tests__/edge-cases.test.ts +371 -0
- package/references/minimem/src/__tests__/errors.test.ts +265 -0
- package/references/minimem/src/__tests__/helpers.ts +199 -0
- package/references/minimem/src/__tests__/internal.test.ts +407 -0
- package/references/minimem/src/__tests__/knowledge.test.ts +287 -0
- package/references/minimem/src/__tests__/minimem.integration.test.ts +1127 -0
- package/references/minimem/src/__tests__/session.test.ts +190 -0
- package/references/minimem/src/cli/__tests__/commands.test.ts +759 -0
- package/references/minimem/src/cli/commands/__tests__/conflicts.test.ts +141 -0
- package/references/minimem/src/cli/commands/append.ts +76 -0
- package/references/minimem/src/cli/commands/config.ts +262 -0
- package/references/minimem/src/cli/commands/conflicts.ts +413 -0
- package/references/minimem/src/cli/commands/daemon.ts +169 -0
- package/references/minimem/src/cli/commands/index.ts +12 -0
- package/references/minimem/src/cli/commands/init.ts +88 -0
- package/references/minimem/src/cli/commands/mcp.ts +177 -0
- package/references/minimem/src/cli/commands/push-pull.ts +213 -0
- package/references/minimem/src/cli/commands/search.ts +158 -0
- package/references/minimem/src/cli/commands/status.ts +84 -0
- package/references/minimem/src/cli/commands/sync-init.ts +290 -0
- package/references/minimem/src/cli/commands/sync.ts +70 -0
- package/references/minimem/src/cli/commands/upsert.ts +197 -0
- package/references/minimem/src/cli/config.ts +584 -0
- package/references/minimem/src/cli/index.ts +264 -0
- package/references/minimem/src/cli/shared.ts +161 -0
- package/references/minimem/src/cli/sync/__tests__/central.test.ts +152 -0
- package/references/minimem/src/cli/sync/__tests__/conflicts.test.ts +209 -0
- package/references/minimem/src/cli/sync/__tests__/daemon.test.ts +118 -0
- package/references/minimem/src/cli/sync/__tests__/detection.test.ts +207 -0
- package/references/minimem/src/cli/sync/__tests__/integration.test.ts +476 -0
- package/references/minimem/src/cli/sync/__tests__/registry.test.ts +363 -0
- package/references/minimem/src/cli/sync/__tests__/state.test.ts +255 -0
- package/references/minimem/src/cli/sync/__tests__/validation.test.ts +193 -0
- package/references/minimem/src/cli/sync/__tests__/watcher.test.ts +178 -0
- package/references/minimem/src/cli/sync/central.ts +292 -0
- package/references/minimem/src/cli/sync/conflicts.ts +204 -0
- package/references/minimem/src/cli/sync/daemon.ts +407 -0
- package/references/minimem/src/cli/sync/detection.ts +138 -0
- package/references/minimem/src/cli/sync/index.ts +107 -0
- package/references/minimem/src/cli/sync/operations.ts +373 -0
- package/references/minimem/src/cli/sync/registry.ts +279 -0
- package/references/minimem/src/cli/sync/state.ts +355 -0
- package/references/minimem/src/cli/sync/validation.ts +206 -0
- package/references/minimem/src/cli/sync/watcher.ts +234 -0
- package/references/minimem/src/cli/version.ts +34 -0
- package/references/minimem/src/core/index.ts +9 -0
- package/references/minimem/src/core/indexer.ts +628 -0
- package/references/minimem/src/core/searcher.ts +221 -0
- package/references/minimem/src/db/schema.ts +183 -0
- package/references/minimem/src/db/sqlite-vec.ts +24 -0
- package/references/minimem/src/embeddings/__tests__/embeddings.test.ts +431 -0
- package/references/minimem/src/embeddings/batch-gemini.ts +392 -0
- package/references/minimem/src/embeddings/batch-openai.ts +409 -0
- package/references/minimem/src/embeddings/embeddings.ts +434 -0
- package/references/minimem/src/index.ts +109 -0
- package/references/minimem/src/internal.ts +299 -0
- package/references/minimem/src/minimem.ts +1276 -0
- package/references/minimem/src/search/__tests__/hybrid.test.ts +247 -0
- package/references/minimem/src/search/graph.ts +234 -0
- package/references/minimem/src/search/hybrid.ts +151 -0
- package/references/minimem/src/search/search.ts +256 -0
- package/references/minimem/src/server/__tests__/mcp.test.ts +341 -0
- package/references/minimem/src/server/__tests__/tools.test.ts +364 -0
- package/references/minimem/src/server/mcp.ts +326 -0
- package/references/minimem/src/server/tools.ts +720 -0
- package/references/minimem/src/session.ts +460 -0
- package/references/minimem/tsconfig.json +19 -0
- package/references/minimem/tsup.config.ts +26 -0
- package/references/minimem/vitest.config.ts +24 -0
- package/references/openteams/.claude/settings.json +6 -0
- package/references/openteams/README.md +1 -0
- package/references/openteams/SKILL.md +341 -0
- package/references/openteams/design.md +411 -0
- package/references/openteams/examples/bmad-method/prompts/analyst/ROLE.md +16 -0
- package/references/openteams/examples/bmad-method/prompts/analyst/SOUL.md +5 -0
- package/references/openteams/examples/bmad-method/prompts/architect/ROLE.md +24 -0
- package/references/openteams/examples/bmad-method/prompts/architect/SOUL.md +5 -0
- package/references/openteams/examples/bmad-method/prompts/developer/ROLE.md +25 -0
- package/references/openteams/examples/bmad-method/prompts/developer/SOUL.md +5 -0
- package/references/openteams/examples/bmad-method/prompts/master/ROLE.md +21 -0
- package/references/openteams/examples/bmad-method/prompts/master/SOUL.md +5 -0
- package/references/openteams/examples/bmad-method/prompts/pm/ROLE.md +20 -0
- package/references/openteams/examples/bmad-method/prompts/pm/SOUL.md +5 -0
- package/references/openteams/examples/bmad-method/prompts/qa/ROLE.md +17 -0
- package/references/openteams/examples/bmad-method/prompts/qa/SOUL.md +5 -0
- package/references/openteams/examples/bmad-method/prompts/quick-flow-dev/ROLE.md +23 -0
- package/references/openteams/examples/bmad-method/prompts/quick-flow-dev/SOUL.md +5 -0
- package/references/openteams/examples/bmad-method/prompts/scrum-master/ROLE.md +27 -0
- package/references/openteams/examples/bmad-method/prompts/scrum-master/SOUL.md +5 -0
- package/references/openteams/examples/bmad-method/prompts/tech-writer/ROLE.md +21 -0
- package/references/openteams/examples/bmad-method/prompts/tech-writer/SOUL.md +5 -0
- package/references/openteams/examples/bmad-method/prompts/ux-designer/ROLE.md +16 -0
- package/references/openteams/examples/bmad-method/prompts/ux-designer/SOUL.md +5 -0
- package/references/openteams/examples/bmad-method/roles/analyst.yaml +9 -0
- package/references/openteams/examples/bmad-method/roles/architect.yaml +9 -0
- package/references/openteams/examples/bmad-method/roles/developer.yaml +8 -0
- package/references/openteams/examples/bmad-method/roles/master.yaml +8 -0
- package/references/openteams/examples/bmad-method/roles/pm.yaml +9 -0
- package/references/openteams/examples/bmad-method/roles/qa.yaml +8 -0
- package/references/openteams/examples/bmad-method/roles/quick-flow-dev.yaml +8 -0
- package/references/openteams/examples/bmad-method/roles/scrum-master.yaml +9 -0
- package/references/openteams/examples/bmad-method/roles/tech-writer.yaml +8 -0
- package/references/openteams/examples/bmad-method/roles/ux-designer.yaml +8 -0
- package/references/openteams/examples/bmad-method/team.yaml +161 -0
- package/references/openteams/examples/get-shit-done/prompts/codebase-mapper/ROLE.md +17 -0
- package/references/openteams/examples/get-shit-done/prompts/codebase-mapper/SOUL.md +5 -0
- package/references/openteams/examples/get-shit-done/prompts/debugger/ROLE.md +25 -0
- package/references/openteams/examples/get-shit-done/prompts/debugger/SOUL.md +5 -0
- package/references/openteams/examples/get-shit-done/prompts/executor/ROLE.md +34 -0
- package/references/openteams/examples/get-shit-done/prompts/executor/SOUL.md +5 -0
- package/references/openteams/examples/get-shit-done/prompts/integration-checker/ROLE.md +18 -0
- package/references/openteams/examples/get-shit-done/prompts/integration-checker/SOUL.md +3 -0
- package/references/openteams/examples/get-shit-done/prompts/orchestrator/ROLE.md +42 -0
- package/references/openteams/examples/get-shit-done/prompts/orchestrator/SOUL.md +5 -0
- package/references/openteams/examples/get-shit-done/prompts/phase-researcher/ROLE.md +15 -0
- package/references/openteams/examples/get-shit-done/prompts/phase-researcher/SOUL.md +3 -0
- package/references/openteams/examples/get-shit-done/prompts/plan-checker/ROLE.md +17 -0
- package/references/openteams/examples/get-shit-done/prompts/plan-checker/SOUL.md +3 -0
- package/references/openteams/examples/get-shit-done/prompts/planner/ROLE.md +28 -0
- package/references/openteams/examples/get-shit-done/prompts/planner/SOUL.md +5 -0
- package/references/openteams/examples/get-shit-done/prompts/project-researcher/ROLE.md +16 -0
- package/references/openteams/examples/get-shit-done/prompts/project-researcher/SOUL.md +3 -0
- package/references/openteams/examples/get-shit-done/prompts/research-synthesizer/ROLE.md +13 -0
- package/references/openteams/examples/get-shit-done/prompts/research-synthesizer/SOUL.md +3 -0
- package/references/openteams/examples/get-shit-done/prompts/roadmapper/ROLE.md +14 -0
- package/references/openteams/examples/get-shit-done/prompts/roadmapper/SOUL.md +3 -0
- package/references/openteams/examples/get-shit-done/prompts/verifier/ROLE.md +19 -0
- package/references/openteams/examples/get-shit-done/prompts/verifier/SOUL.md +5 -0
- package/references/openteams/examples/get-shit-done/roles/codebase-mapper.yaml +8 -0
- package/references/openteams/examples/get-shit-done/roles/debugger.yaml +8 -0
- package/references/openteams/examples/get-shit-done/roles/executor.yaml +8 -0
- package/references/openteams/examples/get-shit-done/roles/integration-checker.yaml +8 -0
- package/references/openteams/examples/get-shit-done/roles/orchestrator.yaml +9 -0
- package/references/openteams/examples/get-shit-done/roles/phase-researcher.yaml +7 -0
- package/references/openteams/examples/get-shit-done/roles/plan-checker.yaml +8 -0
- package/references/openteams/examples/get-shit-done/roles/planner.yaml +8 -0
- package/references/openteams/examples/get-shit-done/roles/project-researcher.yaml +8 -0
- package/references/openteams/examples/get-shit-done/roles/research-synthesizer.yaml +7 -0
- package/references/openteams/examples/get-shit-done/roles/roadmapper.yaml +7 -0
- package/references/openteams/examples/get-shit-done/roles/verifier.yaml +8 -0
- package/references/openteams/examples/get-shit-done/team.yaml +154 -0
- package/references/openteams/package-lock.json +2181 -0
- package/references/openteams/package.json +48 -0
- package/references/openteams/schema/role.schema.json +125 -0
- package/references/openteams/schema/team.schema.json +284 -0
- package/references/openteams/src/cli/agent.ts +104 -0
- package/references/openteams/src/cli/cli.test.ts +381 -0
- package/references/openteams/src/cli/generate.ts +220 -0
- package/references/openteams/src/cli/message.ts +241 -0
- package/references/openteams/src/cli/task.ts +154 -0
- package/references/openteams/src/cli/team.ts +104 -0
- package/references/openteams/src/cli/template.ts +207 -0
- package/references/openteams/src/cli.ts +45 -0
- package/references/openteams/src/db/database.test.ts +185 -0
- package/references/openteams/src/db/database.ts +240 -0
- package/references/openteams/src/generators/agent-prompt-generator.test.ts +332 -0
- package/references/openteams/src/generators/agent-prompt-generator.ts +521 -0
- package/references/openteams/src/generators/package-generator.test.ts +129 -0
- package/references/openteams/src/generators/package-generator.ts +102 -0
- package/references/openteams/src/generators/skill-generator.test.ts +246 -0
- package/references/openteams/src/generators/skill-generator.ts +374 -0
- package/references/openteams/src/index.ts +104 -0
- package/references/openteams/src/services/agent-service.test.ts +158 -0
- package/references/openteams/src/services/agent-service.ts +84 -0
- package/references/openteams/src/services/communication-service.test.ts +455 -0
- package/references/openteams/src/services/communication-service.ts +371 -0
- package/references/openteams/src/services/message-service.test.ts +342 -0
- package/references/openteams/src/services/message-service.ts +203 -0
- package/references/openteams/src/services/task-service.test.ts +434 -0
- package/references/openteams/src/services/task-service.ts +239 -0
- package/references/openteams/src/services/team-service.test.ts +181 -0
- package/references/openteams/src/services/team-service.ts +139 -0
- package/references/openteams/src/services/template-service.test.ts +306 -0
- package/references/openteams/src/services/template-service.ts +182 -0
- package/references/openteams/src/spawner/acp-factory.ts +96 -0
- package/references/openteams/src/spawner/interface.ts +31 -0
- package/references/openteams/src/spawner/mock.test.ts +93 -0
- package/references/openteams/src/spawner/mock.ts +59 -0
- package/references/openteams/src/template/loader.test.ts +1319 -0
- package/references/openteams/src/template/loader.ts +698 -0
- package/references/openteams/src/template/types.ts +200 -0
- package/references/openteams/src/types.ts +205 -0
- package/references/openteams/tsconfig.json +18 -0
- package/references/openteams/vitest.config.ts +9 -0
- package/references/skill-tree/.claude/settings.json +6 -0
- package/references/skill-tree/.sudocode/issues.jsonl +11 -0
- package/references/skill-tree/.sudocode/specs.jsonl +1 -0
- package/references/skill-tree/CLAUDE.md +150 -0
- package/references/skill-tree/README.md +324 -0
- package/references/skill-tree/docs/GAPS_v1.md +221 -0
- package/references/skill-tree/docs/INTEGRATION_PLAN.md +467 -0
- package/references/skill-tree/docs/TODOS.md +91 -0
- package/references/skill-tree/docs/anthropic_skill_guide.md +1364 -0
- package/references/skill-tree/docs/design/federated-skill-trees.md +524 -0
- package/references/skill-tree/docs/design/multi-agent-sync.md +759 -0
- package/references/skill-tree/docs/scraper/BRAINSTORM.md +583 -0
- package/references/skill-tree/docs/scraper/POC_PLAN.md +420 -0
- package/references/skill-tree/docs/scraper/README.md +170 -0
- package/references/skill-tree/examples/basic-usage.ts +190 -0
- package/references/skill-tree/package-lock.json +1509 -0
- package/references/skill-tree/package.json +66 -0
- package/references/skill-tree/scraper/README.md +123 -0
- package/references/skill-tree/scraper/docs/DESIGN.md +683 -0
- package/references/skill-tree/scraper/docs/PLAN.md +336 -0
- package/references/skill-tree/scraper/drizzle.config.ts +10 -0
- package/references/skill-tree/scraper/package-lock.json +6329 -0
- package/references/skill-tree/scraper/package.json +68 -0
- package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-description.md +7 -0
- package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-name.md +7 -0
- package/references/skill-tree/scraper/test/fixtures/minimal-skill/SKILL.md +27 -0
- package/references/skill-tree/scraper/test/fixtures/skill-json/SKILL.json +21 -0
- package/references/skill-tree/scraper/test/fixtures/skill-with-meta/SKILL.md +54 -0
- package/references/skill-tree/scraper/test/fixtures/skill-with-meta/_meta.json +24 -0
- package/references/skill-tree/scraper/test/fixtures/valid-skill/SKILL.md +93 -0
- package/references/skill-tree/scraper/test/fixtures/valid-skill/_meta.json +22 -0
- package/references/skill-tree/scraper/tsup.config.ts +14 -0
- package/references/skill-tree/scraper/vitest.config.ts +17 -0
- package/references/skill-tree/scripts/convert-to-vitest.ts +166 -0
- package/references/skill-tree/skills/skill-writer/SKILL.md +339 -0
- package/references/skill-tree/skills/skill-writer/references/examples.md +326 -0
- package/references/skill-tree/skills/skill-writer/references/patterns.md +210 -0
- package/references/skill-tree/skills/skill-writer/references/quality-checklist.md +123 -0
- package/references/skill-tree/test/run-all.ts +106 -0
- package/references/skill-tree/test/utils.ts +128 -0
- package/references/skill-tree/vitest.config.ts +16 -0
- package/src/agent/agent-manager.ts +143 -72
- package/src/agent/types.ts +9 -0
- package/src/api/__tests__/server.test.ts +203 -4
- package/src/api/server.ts +130 -5
- package/src/api/types.ts +3 -1
- package/src/cli/acp.ts +68 -1
- package/src/cli/index.ts +5 -1
- package/src/cli/mcp.ts +27 -13
- package/src/config/project-config.ts +27 -3
- package/src/index.ts +3 -0
- package/src/lifecycle/__tests__/handlers.test.ts +53 -0
- package/src/lifecycle/handlers/index.ts +25 -8
- package/src/lifecycle/types.ts +3 -0
- package/src/map/adapter/__tests__/stream-extensions.test.ts +494 -0
- package/src/map/adapter/extensions/index.ts +36 -0
- package/src/map/adapter/extensions/streams.ts +839 -0
- package/src/map/adapter/index.ts +5 -0
- package/src/map/adapter/types.ts +8 -1
- package/src/mcp/mcp-server.ts +14 -3
- package/src/mcp/tools/done.ts +19 -0
- package/src/roles/builtin/coordinator.ts +2 -0
- package/src/roles/builtin/integrator.ts +2 -0
- package/src/roles/builtin/worker.ts +3 -0
- package/src/roles/capabilities.ts +11 -0
- package/src/roles/config-loader.ts +3 -2
- package/src/roles/types.ts +7 -0
- package/src/server/combined-server.ts +15 -1
- package/src/store/__tests__/event-store-oob.test.ts +109 -0
- package/src/store/event-store.ts +13 -3
- package/src/store/instance.ts +2 -2
- package/src/store/types/agents.ts +5 -0
- package/src/task/backend/__tests__/memory-pull-mode.test.ts +153 -0
- package/src/task/backend/opentasks/daemon-manager.ts +4 -1
- package/src/teams/CLAUDE.md +180 -0
- package/src/teams/__tests__/e2e/workspace-isolation.e2e.test.ts +1263 -0
- package/src/teams/__tests__/team-manager.test.ts +814 -0
- package/src/teams/__tests__/team-system.test.ts +1291 -8
- package/src/teams/index.ts +21 -3
- package/src/teams/seed-defaults.ts +79 -0
- package/src/teams/team-loader.ts +200 -234
- package/src/teams/team-manager.ts +387 -0
- package/src/teams/team-runtime.ts +590 -121
- 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
|
+
}
|