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
|
@@ -17,6 +17,8 @@ import type { MessageRouter } from "../../router/message-router.js";
|
|
|
17
17
|
import type { EventStore } from "../../store/event-store.js";
|
|
18
18
|
import type { SpawnAgentOptions } from "../../agent/types.js";
|
|
19
19
|
import type { AgentId, Event } from "../../store/types/index.js";
|
|
20
|
+
import { TeamLoadError } from "../types.js";
|
|
21
|
+
import type { MacroResolvedTemplate, ResolvedTeamRole, McpServerEntry } from "../types.js";
|
|
20
22
|
|
|
21
23
|
// =============================================================================
|
|
22
24
|
// Helpers
|
|
@@ -209,7 +211,7 @@ describe("Team Template Loading", () => {
|
|
|
209
211
|
const manifest = await loadTeam("structured", roleRegistry, PROJECT_ROOT);
|
|
210
212
|
|
|
211
213
|
expect(manifest.name).toBe("structured");
|
|
212
|
-
expect(manifest.roles).toEqual(["lead", "developer", "reviewer"]);
|
|
214
|
+
expect(manifest.roles).toEqual(["lead", "developer", "reviewer", "merger"]);
|
|
213
215
|
expect(manifest.macro_agent.task_assignment?.mode).toBe("push");
|
|
214
216
|
expect(manifest.macro_agent.integration?.strategy).toBe("queue");
|
|
215
217
|
});
|
|
@@ -283,12 +285,23 @@ describe("TeamRuntime", () => {
|
|
|
283
285
|
);
|
|
284
286
|
});
|
|
285
287
|
|
|
286
|
-
it("
|
|
288
|
+
it("does not install spawn interceptor directly (TeamManager responsibility)", async () => {
|
|
287
289
|
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
288
290
|
const runtime = new TeamRuntime(manifest, services);
|
|
289
291
|
|
|
290
292
|
await runtime.initialize();
|
|
291
293
|
|
|
294
|
+
// initialize() no longer installs interceptor — that's TeamManager's job
|
|
295
|
+
expect(agentManager.setSpawnInterceptor).not.toHaveBeenCalled();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("installOnServices() sets spawn interceptor on agent manager", async () => {
|
|
299
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
300
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
301
|
+
|
|
302
|
+
await runtime.initialize();
|
|
303
|
+
runtime.installOnServices();
|
|
304
|
+
|
|
292
305
|
expect(agentManager.setSpawnInterceptor).toHaveBeenCalledWith(
|
|
293
306
|
expect.any(Function)
|
|
294
307
|
);
|
|
@@ -401,6 +414,7 @@ describe("TeamRuntime", () => {
|
|
|
401
414
|
const runtime = new TeamRuntime(manifest, services);
|
|
402
415
|
|
|
403
416
|
await runtime.initialize();
|
|
417
|
+
runtime.installOnServices();
|
|
404
418
|
await runtime.bootstrap();
|
|
405
419
|
|
|
406
420
|
// Now spawn a grinder through the interceptor
|
|
@@ -422,6 +436,7 @@ describe("TeamRuntime", () => {
|
|
|
422
436
|
const runtime = new TeamRuntime(manifest, services);
|
|
423
437
|
|
|
424
438
|
await runtime.initialize();
|
|
439
|
+
runtime.installOnServices();
|
|
425
440
|
await runtime.bootstrap();
|
|
426
441
|
|
|
427
442
|
// Spawn a grinder
|
|
@@ -442,6 +457,7 @@ describe("TeamRuntime", () => {
|
|
|
442
457
|
const runtime = new TeamRuntime(manifest, services);
|
|
443
458
|
|
|
444
459
|
await runtime.initialize();
|
|
460
|
+
runtime.installOnServices();
|
|
445
461
|
await runtime.bootstrap();
|
|
446
462
|
|
|
447
463
|
const customPrompt = "My custom prompt";
|
|
@@ -479,14 +495,33 @@ describe("TeamRuntime", () => {
|
|
|
479
495
|
});
|
|
480
496
|
|
|
481
497
|
describe("teardown()", () => {
|
|
482
|
-
it("
|
|
498
|
+
it("does not clear interceptor directly (caller responsibility)", async () => {
|
|
483
499
|
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
484
500
|
const runtime = new TeamRuntime(manifest, services);
|
|
485
501
|
|
|
486
502
|
await runtime.initialize();
|
|
503
|
+
runtime.installOnServices();
|
|
487
504
|
await runtime.bootstrap();
|
|
505
|
+
|
|
506
|
+
// Reset mock to track only teardown-related calls
|
|
507
|
+
vi.mocked(agentManager.setSpawnInterceptor).mockClear();
|
|
508
|
+
|
|
488
509
|
await runtime.teardown();
|
|
489
510
|
|
|
511
|
+
// teardown() no longer clears interceptor — that's the caller's responsibility
|
|
512
|
+
expect(agentManager.setSpawnInterceptor).not.toHaveBeenCalled();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("uninstallFromServices() clears spawn interceptor", async () => {
|
|
516
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
517
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
518
|
+
|
|
519
|
+
await runtime.initialize();
|
|
520
|
+
runtime.installOnServices();
|
|
521
|
+
await runtime.bootstrap();
|
|
522
|
+
await runtime.teardown();
|
|
523
|
+
runtime.uninstallFromServices();
|
|
524
|
+
|
|
490
525
|
expect(agentManager.setSpawnInterceptor).toHaveBeenLastCalledWith(null);
|
|
491
526
|
});
|
|
492
527
|
});
|
|
@@ -679,13 +714,17 @@ describe("TeamRuntime", () => {
|
|
|
679
714
|
});
|
|
680
715
|
|
|
681
716
|
describe("signal filtering", () => {
|
|
682
|
-
it("installs signal filter on message router
|
|
717
|
+
it("installOnServices() installs signal filter on message router", async () => {
|
|
683
718
|
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
684
719
|
const runtime = new TeamRuntime(manifest, services);
|
|
685
720
|
|
|
686
721
|
await runtime.initialize();
|
|
722
|
+
// bootstrap/initialize alone should NOT install filter
|
|
687
723
|
await runtime.bootstrap();
|
|
724
|
+
expect(messageRouter.setSignalFilter).not.toHaveBeenCalled();
|
|
688
725
|
|
|
726
|
+
// installOnServices() installs the filter
|
|
727
|
+
runtime.installOnServices();
|
|
689
728
|
expect(messageRouter.setSignalFilter).toHaveBeenCalledTimes(1);
|
|
690
729
|
expect(messageRouter.setSignalFilter).toHaveBeenCalledWith(expect.any(Function));
|
|
691
730
|
});
|
|
@@ -696,6 +735,7 @@ describe("TeamRuntime", () => {
|
|
|
696
735
|
|
|
697
736
|
await runtime.initialize();
|
|
698
737
|
const result = await runtime.bootstrap();
|
|
738
|
+
runtime.installOnServices();
|
|
699
739
|
|
|
700
740
|
// Extract the installed filter
|
|
701
741
|
const filterFn = vi.mocked(messageRouter.setSignalFilter).mock.calls[0][0] as (
|
|
@@ -716,6 +756,7 @@ describe("TeamRuntime", () => {
|
|
|
716
756
|
|
|
717
757
|
await runtime.initialize();
|
|
718
758
|
const result = await runtime.bootstrap();
|
|
759
|
+
runtime.installOnServices();
|
|
719
760
|
|
|
720
761
|
const filterFn = vi.mocked(messageRouter.setSignalFilter).mock.calls[0][0] as (
|
|
721
762
|
from: string, to: string, signal: string | undefined
|
|
@@ -734,6 +775,7 @@ describe("TeamRuntime", () => {
|
|
|
734
775
|
|
|
735
776
|
await runtime.initialize();
|
|
736
777
|
const result = await runtime.bootstrap();
|
|
778
|
+
runtime.installOnServices();
|
|
737
779
|
|
|
738
780
|
const filterFn = vi.mocked(messageRouter.setSignalFilter).mock.calls[0][0] as (
|
|
739
781
|
from: string, to: string, signal: string | undefined
|
|
@@ -752,6 +794,7 @@ describe("TeamRuntime", () => {
|
|
|
752
794
|
|
|
753
795
|
await runtime.initialize();
|
|
754
796
|
const result = await runtime.bootstrap();
|
|
797
|
+
runtime.installOnServices();
|
|
755
798
|
|
|
756
799
|
const filterFn = vi.mocked(messageRouter.setSignalFilter).mock.calls[0][0] as (
|
|
757
800
|
from: string, to: string, signal: string | undefined
|
|
@@ -773,6 +816,7 @@ describe("TeamRuntime", () => {
|
|
|
773
816
|
const runtime2 = new TeamRuntime(manifest, services);
|
|
774
817
|
await runtime2.initialize();
|
|
775
818
|
const result2 = await runtime2.bootstrap();
|
|
819
|
+
runtime2.installOnServices();
|
|
776
820
|
|
|
777
821
|
// Simulate grinder spawn via lifecycle event
|
|
778
822
|
const deferredCallback = vi.mocked(agentManager.onLifecycleEvent).mock.calls.at(-2)![0];
|
|
@@ -799,6 +843,7 @@ describe("TeamRuntime", () => {
|
|
|
799
843
|
|
|
800
844
|
await runtime.initialize();
|
|
801
845
|
const result = await runtime.bootstrap();
|
|
846
|
+
runtime.installOnServices();
|
|
802
847
|
|
|
803
848
|
const filterFn = vi.mocked(messageRouter.setSignalFilter).mock.calls[0][0] as (
|
|
804
849
|
from: string, to: string, signal: string | undefined
|
|
@@ -817,6 +862,7 @@ describe("TeamRuntime", () => {
|
|
|
817
862
|
|
|
818
863
|
await runtime.initialize();
|
|
819
864
|
const result = await runtime.bootstrap();
|
|
865
|
+
runtime.installOnServices();
|
|
820
866
|
|
|
821
867
|
const filterFn = vi.mocked(messageRouter.setSignalFilter).mock.calls[0][0] as (
|
|
822
868
|
from: string, to: string, signal: string | undefined
|
|
@@ -833,13 +879,17 @@ describe("TeamRuntime", () => {
|
|
|
833
879
|
});
|
|
834
880
|
|
|
835
881
|
describe("emission validation", () => {
|
|
836
|
-
it("installs emission validator on message router
|
|
882
|
+
it("installOnServices() installs emission validator on message router", async () => {
|
|
837
883
|
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
838
884
|
const runtime = new TeamRuntime(manifest, services);
|
|
839
885
|
|
|
840
886
|
await runtime.initialize();
|
|
841
887
|
await runtime.bootstrap();
|
|
888
|
+
// bootstrap alone should NOT install validator
|
|
889
|
+
expect(messageRouter.setEmissionValidator).not.toHaveBeenCalled();
|
|
842
890
|
|
|
891
|
+
// installOnServices() installs the validator
|
|
892
|
+
runtime.installOnServices();
|
|
843
893
|
expect(messageRouter.setEmissionValidator).toHaveBeenCalledTimes(1);
|
|
844
894
|
expect(messageRouter.setEmissionValidator).toHaveBeenCalledWith(expect.any(Function));
|
|
845
895
|
});
|
|
@@ -850,6 +900,7 @@ describe("TeamRuntime", () => {
|
|
|
850
900
|
|
|
851
901
|
await runtime.initialize();
|
|
852
902
|
const result = await runtime.bootstrap();
|
|
903
|
+
runtime.installOnServices();
|
|
853
904
|
|
|
854
905
|
const validatorFn = vi.mocked(messageRouter.setEmissionValidator).mock.calls[0][0] as (
|
|
855
906
|
agentId: string, signal: string | undefined
|
|
@@ -869,6 +920,7 @@ describe("TeamRuntime", () => {
|
|
|
869
920
|
|
|
870
921
|
await runtime.initialize();
|
|
871
922
|
const result = await runtime.bootstrap();
|
|
923
|
+
runtime.installOnServices();
|
|
872
924
|
|
|
873
925
|
const validatorFn = vi.mocked(messageRouter.setEmissionValidator).mock.calls[0][0] as (
|
|
874
926
|
agentId: string, signal: string | undefined
|
|
@@ -890,6 +942,7 @@ describe("TeamRuntime", () => {
|
|
|
890
942
|
|
|
891
943
|
await runtime.initialize();
|
|
892
944
|
const result = await runtime.bootstrap();
|
|
945
|
+
runtime.installOnServices();
|
|
893
946
|
|
|
894
947
|
const validatorFn = vi.mocked(messageRouter.setEmissionValidator).mock.calls[0][0] as (
|
|
895
948
|
agentId: string, signal: string | undefined
|
|
@@ -909,6 +962,7 @@ describe("TeamRuntime", () => {
|
|
|
909
962
|
|
|
910
963
|
await runtime.initialize();
|
|
911
964
|
const result = await runtime.bootstrap();
|
|
965
|
+
runtime.installOnServices();
|
|
912
966
|
|
|
913
967
|
const validatorFn = vi.mocked(messageRouter.setEmissionValidator).mock.calls[0][0] as (
|
|
914
968
|
agentId: string, signal: string | undefined
|
|
@@ -928,6 +982,7 @@ describe("TeamRuntime", () => {
|
|
|
928
982
|
|
|
929
983
|
await runtime.initialize();
|
|
930
984
|
const result = await runtime.bootstrap();
|
|
985
|
+
runtime.installOnServices();
|
|
931
986
|
|
|
932
987
|
const validatorFn = vi.mocked(messageRouter.setEmissionValidator).mock.calls[0][0] as (
|
|
933
988
|
agentId: string, signal: string | undefined
|
|
@@ -945,6 +1000,7 @@ describe("TeamRuntime", () => {
|
|
|
945
1000
|
|
|
946
1001
|
await runtime.initialize();
|
|
947
1002
|
await runtime.bootstrap();
|
|
1003
|
+
runtime.installOnServices();
|
|
948
1004
|
|
|
949
1005
|
const validatorFn = vi.mocked(messageRouter.setEmissionValidator).mock.calls[0][0] as (
|
|
950
1006
|
agentId: string, signal: string | undefined
|
|
@@ -961,7 +1017,9 @@ describe("TeamRuntime", () => {
|
|
|
961
1017
|
|
|
962
1018
|
await runtime.initialize();
|
|
963
1019
|
await runtime.bootstrap();
|
|
1020
|
+
runtime.installOnServices();
|
|
964
1021
|
|
|
1022
|
+
// createEmissionValidator() returns null when no emissions → setEmissionValidator not called
|
|
965
1023
|
expect(messageRouter.setEmissionValidator).not.toHaveBeenCalled();
|
|
966
1024
|
});
|
|
967
1025
|
|
|
@@ -988,6 +1046,119 @@ describe("TeamRuntime", () => {
|
|
|
988
1046
|
});
|
|
989
1047
|
});
|
|
990
1048
|
|
|
1049
|
+
describe("exposed factory methods (for TeamManager)", () => {
|
|
1050
|
+
it("createSpawnInterceptor() returns a function", async () => {
|
|
1051
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1052
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
1053
|
+
|
|
1054
|
+
await runtime.initialize();
|
|
1055
|
+
|
|
1056
|
+
const interceptor = runtime.createSpawnInterceptor();
|
|
1057
|
+
expect(interceptor).toBeInstanceOf(Function);
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
it("createSpawnInterceptor() injects team context", async () => {
|
|
1061
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1062
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
1063
|
+
|
|
1064
|
+
await runtime.initialize();
|
|
1065
|
+
|
|
1066
|
+
const interceptor = runtime.createSpawnInterceptor();
|
|
1067
|
+
const result = interceptor({
|
|
1068
|
+
task: "test",
|
|
1069
|
+
role: "grinder",
|
|
1070
|
+
parent: "agent_0",
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
expect(result.config?.env?.MACRO_TEAM_NAME).toBe("self-driving");
|
|
1074
|
+
expect(result.config?.env?.MACRO_TASK_MODE).toBe("pull");
|
|
1075
|
+
expect(result.topics).toContain("work_coordination");
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it("createSignalFilter() returns a function", async () => {
|
|
1079
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1080
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
1081
|
+
|
|
1082
|
+
await runtime.initialize();
|
|
1083
|
+
await runtime.bootstrap();
|
|
1084
|
+
|
|
1085
|
+
const filter = runtime.createSignalFilter();
|
|
1086
|
+
expect(filter).toBeInstanceOf(Function);
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
it("createEmissionValidator() returns a function when emissions exist", async () => {
|
|
1090
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1091
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
1092
|
+
|
|
1093
|
+
await runtime.initialize();
|
|
1094
|
+
await runtime.bootstrap();
|
|
1095
|
+
|
|
1096
|
+
const validator = runtime.createEmissionValidator();
|
|
1097
|
+
expect(validator).toBeInstanceOf(Function);
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
it("createEmissionValidator() returns null when no emissions", async () => {
|
|
1101
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1102
|
+
manifest.communication.emissions = undefined;
|
|
1103
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
1104
|
+
|
|
1105
|
+
await runtime.initialize();
|
|
1106
|
+
await runtime.bootstrap();
|
|
1107
|
+
|
|
1108
|
+
const validator = runtime.createEmissionValidator();
|
|
1109
|
+
expect(validator).toBeNull();
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
it("getAgentRoleMap() returns role mappings after bootstrap", async () => {
|
|
1113
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1114
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
1115
|
+
|
|
1116
|
+
await runtime.initialize();
|
|
1117
|
+
const result = await runtime.bootstrap();
|
|
1118
|
+
|
|
1119
|
+
const roleMap = runtime.getAgentRoleMap();
|
|
1120
|
+
expect(roleMap.get(result.rootId as AgentId)).toBe("planner");
|
|
1121
|
+
expect(roleMap.get(result.companionIds[0] as AgentId)).toBe("judge");
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
it("registerAgent() adds agent to role map", async () => {
|
|
1125
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1126
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
1127
|
+
|
|
1128
|
+
await runtime.initialize();
|
|
1129
|
+
await runtime.bootstrap();
|
|
1130
|
+
|
|
1131
|
+
runtime.registerAgent("new_agent" as AgentId, "grinder");
|
|
1132
|
+
|
|
1133
|
+
const roleMap = runtime.getAgentRoleMap();
|
|
1134
|
+
expect(roleMap.get("new_agent" as AgentId)).toBe("grinder");
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
it("hasAgent() returns true for known agents", async () => {
|
|
1138
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1139
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
1140
|
+
|
|
1141
|
+
await runtime.initialize();
|
|
1142
|
+
const result = await runtime.bootstrap();
|
|
1143
|
+
|
|
1144
|
+
expect(runtime.hasAgent(result.rootId)).toBe(true);
|
|
1145
|
+
expect(runtime.hasAgent(result.companionIds[0])).toBe(true);
|
|
1146
|
+
expect(runtime.hasAgent("unknown_agent")).toBe(false);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
it("hasAgent() includes dynamically registered agents", async () => {
|
|
1150
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1151
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
1152
|
+
|
|
1153
|
+
await runtime.initialize();
|
|
1154
|
+
await runtime.bootstrap();
|
|
1155
|
+
|
|
1156
|
+
expect(runtime.hasAgent("dynamic_agent")).toBe(false);
|
|
1157
|
+
runtime.registerAgent("dynamic_agent" as AgentId, "grinder");
|
|
1158
|
+
expect(runtime.hasAgent("dynamic_agent")).toBe(true);
|
|
1159
|
+
});
|
|
1160
|
+
});
|
|
1161
|
+
|
|
991
1162
|
describe("monitorContinuations()", () => {
|
|
992
1163
|
it("auto-continues root agent on unexpected stop", async () => {
|
|
993
1164
|
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
@@ -1133,9 +1304,9 @@ describe("TeamRuntime", () => {
|
|
|
1133
1304
|
await runtime.initialize();
|
|
1134
1305
|
const result = await runtime.bootstrap();
|
|
1135
1306
|
|
|
1136
|
-
// Root (lead) +
|
|
1137
|
-
expect(agentManager.spawn).toHaveBeenCalledTimes(
|
|
1138
|
-
expect(result.companionIds).toHaveLength(
|
|
1307
|
+
// Root (lead) + 2 companions (reviewer, merger)
|
|
1308
|
+
expect(agentManager.spawn).toHaveBeenCalledTimes(3);
|
|
1309
|
+
expect(result.companionIds).toHaveLength(2);
|
|
1139
1310
|
|
|
1140
1311
|
// Verify no pull-mode interaction patterns injected
|
|
1141
1312
|
const rootCall = vi.mocked(agentManager.spawn).mock.calls[0][0];
|
|
@@ -1278,3 +1449,1115 @@ describe("Metrics Module", () => {
|
|
|
1278
1449
|
expect(metrics.recentErrors).toEqual([]);
|
|
1279
1450
|
});
|
|
1280
1451
|
});
|
|
1452
|
+
|
|
1453
|
+
// =============================================================================
|
|
1454
|
+
// Tests: openteams Migration — Loader Hooks & Error Mapping
|
|
1455
|
+
// =============================================================================
|
|
1456
|
+
|
|
1457
|
+
describe("openteams Migration: Team Loader", () => {
|
|
1458
|
+
let roleRegistry: DefaultRoleRegistry;
|
|
1459
|
+
|
|
1460
|
+
beforeEach(() => {
|
|
1461
|
+
roleRegistry = new DefaultRoleRegistry();
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
describe("buildResolvedTeamRole — enforcement fields", () => {
|
|
1465
|
+
it("maps macro_agent.workspace from role YAML to RoleDefinition", async () => {
|
|
1466
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1467
|
+
|
|
1468
|
+
// judge.yaml has workspace config
|
|
1469
|
+
const judge = manifest._resolvedRoles.get("judge")!;
|
|
1470
|
+
expect(judge.roleDefinition.workspace).toEqual({
|
|
1471
|
+
type: "own",
|
|
1472
|
+
branchPattern: "judge/{agent-id}",
|
|
1473
|
+
cleanupOnTerminate: true,
|
|
1474
|
+
});
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
it("maps macro_agent.lifecycle from role YAML to RoleDefinition", async () => {
|
|
1478
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1479
|
+
|
|
1480
|
+
// planner.yaml has lifecycle: type: daemon, cascade_terminate: true
|
|
1481
|
+
const planner = manifest._resolvedRoles.get("planner")!;
|
|
1482
|
+
expect(planner.roleDefinition.lifecycle).toEqual({
|
|
1483
|
+
type: "daemon",
|
|
1484
|
+
cascadeTerminate: true,
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
// grinder.yaml has lifecycle: type: ephemeral, task_bound: false, max_duration_ms, self_cleanup
|
|
1488
|
+
const grinder = manifest._resolvedRoles.get("grinder")!;
|
|
1489
|
+
expect(grinder.roleDefinition.lifecycle).toEqual({
|
|
1490
|
+
type: "ephemeral",
|
|
1491
|
+
taskBound: false,
|
|
1492
|
+
maxDurationMs: 3600000,
|
|
1493
|
+
selfCleanup: true,
|
|
1494
|
+
});
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
it("falls back to parent role workspace/lifecycle when no macro_agent override", async () => {
|
|
1498
|
+
const manifest = await loadTeam("structured", roleRegistry, PROJECT_ROOT);
|
|
1499
|
+
|
|
1500
|
+
// structured roles don't have macro_agent workspace/lifecycle in their YAML
|
|
1501
|
+
// so they inherit from parent role definitions
|
|
1502
|
+
const developer = manifest._resolvedRoles.get("developer")!;
|
|
1503
|
+
// Worker parent has workspace config
|
|
1504
|
+
const parentWorker = roleRegistry.resolveRole("worker");
|
|
1505
|
+
expect(developer.roleDefinition.workspace).toEqual(parentWorker.workspace);
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
it("preserves parent role tools and protocol", async () => {
|
|
1509
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1510
|
+
|
|
1511
|
+
const grinder = manifest._resolvedRoles.get("grinder")!;
|
|
1512
|
+
const parentWorker = roleRegistry.resolveRole("worker");
|
|
1513
|
+
expect(grinder.roleDefinition.tools).toEqual(parentWorker.tools);
|
|
1514
|
+
expect(grinder.roleDefinition.protocol).toEqual(parentWorker.protocol);
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
it("sets correct baseRole from extends chain", async () => {
|
|
1518
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1519
|
+
|
|
1520
|
+
// All roles should reference their base role
|
|
1521
|
+
expect(manifest._resolvedRoles.get("planner")!.baseRole).toBe("coordinator");
|
|
1522
|
+
expect(manifest._resolvedRoles.get("grinder")!.baseRole).toBe("worker");
|
|
1523
|
+
expect(manifest._resolvedRoles.get("judge")!.baseRole).toBe("monitor");
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
it("stores prompt file path in resolved role", async () => {
|
|
1527
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1528
|
+
|
|
1529
|
+
const planner = manifest._resolvedRoles.get("planner")!;
|
|
1530
|
+
expect(planner.prompt).toBe("prompts/planner.md");
|
|
1531
|
+
|
|
1532
|
+
const grinder = manifest._resolvedRoles.get("grinder")!;
|
|
1533
|
+
expect(grinder.prompt).toBe("prompts/grinder.md");
|
|
1534
|
+
});
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
describe("enrichRoleWithSpawnRules hook", () => {
|
|
1538
|
+
it("adds agent.spawn.* capabilities from spawn_rules", async () => {
|
|
1539
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1540
|
+
|
|
1541
|
+
// planner: [grinder, planner]
|
|
1542
|
+
const planner = manifest._resolvedRoles.get("planner")!;
|
|
1543
|
+
expect(planner.capabilities).toContain("agent.spawn.grinder");
|
|
1544
|
+
expect(planner.capabilities).toContain("agent.spawn.planner");
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
it("does not duplicate existing spawn capabilities", async () => {
|
|
1548
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1549
|
+
|
|
1550
|
+
const planner = manifest._resolvedRoles.get("planner")!;
|
|
1551
|
+
const spawnGrinderCount = planner.capabilities.filter(
|
|
1552
|
+
(c) => c === "agent.spawn.grinder"
|
|
1553
|
+
).length;
|
|
1554
|
+
expect(spawnGrinderCount).toBe(1);
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
it("does not add spawn capabilities for roles with empty spawn_rules", async () => {
|
|
1558
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1559
|
+
|
|
1560
|
+
// judge: [] in spawn_rules — should not have spawn caps added by enrichRoleWithSpawnRules
|
|
1561
|
+
// (judge extends monitor which has no spawn caps)
|
|
1562
|
+
const judge = manifest._resolvedRoles.get("judge")!;
|
|
1563
|
+
const spawnCaps = judge.capabilities.filter((c) => c.startsWith("agent.spawn."));
|
|
1564
|
+
expect(spawnCaps).toEqual([]);
|
|
1565
|
+
|
|
1566
|
+
// grinder: [] in spawn_rules — grinder extends worker which has agent.spawn.worker
|
|
1567
|
+
// from parent, but enrichRoleWithSpawnRules should NOT add any additional spawn caps
|
|
1568
|
+
const grinder = manifest._resolvedRoles.get("grinder")!;
|
|
1569
|
+
const grinderSpawnCaps = grinder.capabilities.filter((c) => c.startsWith("agent.spawn."));
|
|
1570
|
+
// Only has parent-inherited spawn caps, not new ones from spawn_rules
|
|
1571
|
+
expect(grinderSpawnCaps).not.toContain("agent.spawn.grinder");
|
|
1572
|
+
expect(grinderSpawnCaps).not.toContain("agent.spawn.planner");
|
|
1573
|
+
});
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
describe("mapRegistryRole hook", () => {
|
|
1577
|
+
it("resolves known registry roles for extends chains", async () => {
|
|
1578
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1579
|
+
|
|
1580
|
+
// planner extends coordinator — coordinator should be resolvable
|
|
1581
|
+
const planner = manifest._resolvedRoles.get("planner")!;
|
|
1582
|
+
expect(planner.baseRole).toBe("coordinator");
|
|
1583
|
+
|
|
1584
|
+
// Coordinator capabilities should be in the planner's set (minus removals, plus additions)
|
|
1585
|
+
const coordinatorRole = roleRegistry.resolveRole("coordinator");
|
|
1586
|
+
// planner should have coordinator capabilities minus removed ones
|
|
1587
|
+
for (const cap of coordinatorRole.capabilities) {
|
|
1588
|
+
if (cap !== "agent.spawn.integrator" && cap !== "agent.spawn.monitor") {
|
|
1589
|
+
expect(planner.capabilities).toContain(cap);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
describe("prompt loading and assembly", () => {
|
|
1596
|
+
it("loads prompts keyed by role promptFile path", async () => {
|
|
1597
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1598
|
+
|
|
1599
|
+
// Prompts should be stored under the role's promptFile key
|
|
1600
|
+
expect(manifest._loadedPrompts.has("prompts/planner.md")).toBe(true);
|
|
1601
|
+
expect(manifest._loadedPrompts.has("prompts/grinder.md")).toBe(true);
|
|
1602
|
+
expect(manifest._loadedPrompts.has("prompts/judge.md")).toBe(true);
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
it("loads prompts keyed by topology node prompt path", async () => {
|
|
1606
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1607
|
+
|
|
1608
|
+
// Root topology node has prompt: prompts/planner.md
|
|
1609
|
+
// It should also be stored under that topology key
|
|
1610
|
+
expect(manifest._loadedPrompts.get("prompts/planner.md")).toBeDefined();
|
|
1611
|
+
expect(manifest._loadedPrompts.get("prompts/planner.md")!.length).toBeGreaterThan(0);
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
it("prompt content matches actual file content", async () => {
|
|
1615
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1616
|
+
|
|
1617
|
+
const plannerPrompt = manifest._loadedPrompts.get("prompts/planner.md")!;
|
|
1618
|
+
// Should contain the role name from the actual prompt file
|
|
1619
|
+
expect(plannerPrompt).toContain("Planner");
|
|
1620
|
+
});
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
describe("MCP server loading", () => {
|
|
1624
|
+
it("provides _mcpServers map (even if empty)", async () => {
|
|
1625
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1626
|
+
|
|
1627
|
+
expect(manifest._mcpServers).toBeDefined();
|
|
1628
|
+
expect(manifest._mcpServers).toBeInstanceOf(Map);
|
|
1629
|
+
});
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
describe("error mapping", () => {
|
|
1633
|
+
it("throws MANIFEST_NOT_FOUND for non-existent team", async () => {
|
|
1634
|
+
await expect(
|
|
1635
|
+
loadTeam("nonexistent-team", roleRegistry, PROJECT_ROOT)
|
|
1636
|
+
).rejects.toThrow(TeamLoadError);
|
|
1637
|
+
|
|
1638
|
+
try {
|
|
1639
|
+
await loadTeam("nonexistent-team", roleRegistry, PROJECT_ROOT);
|
|
1640
|
+
} catch (e) {
|
|
1641
|
+
const err = e as TeamLoadError;
|
|
1642
|
+
expect(err.code).toBe("MANIFEST_NOT_FOUND");
|
|
1643
|
+
expect(err.teamName).toBe("nonexistent-team");
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
it("includes team name in error", async () => {
|
|
1648
|
+
try {
|
|
1649
|
+
await loadTeam("does-not-exist", roleRegistry, PROJECT_ROOT);
|
|
1650
|
+
} catch (e) {
|
|
1651
|
+
expect(e).toBeInstanceOf(TeamLoadError);
|
|
1652
|
+
expect((e as TeamLoadError).teamName).toBe("does-not-exist");
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
describe("communication validation", () => {
|
|
1658
|
+
it("validates self-driving team communication topology", async () => {
|
|
1659
|
+
// Should not throw — well-formed communication config
|
|
1660
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1661
|
+
expect(manifest.communication).toBeDefined();
|
|
1662
|
+
expect(manifest.communication.channels).toBeDefined();
|
|
1663
|
+
expect(manifest.communication.subscriptions).toBeDefined();
|
|
1664
|
+
expect(manifest.communication.emissions).toBeDefined();
|
|
1665
|
+
expect(manifest.communication.routing).toBeDefined();
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
it("validates structured team communication topology", async () => {
|
|
1669
|
+
const manifest = await loadTeam("structured", roleRegistry, PROJECT_ROOT);
|
|
1670
|
+
expect(manifest.communication.channels).toBeDefined();
|
|
1671
|
+
expect(Object.keys(manifest.communication.channels!)).toContain("task_updates");
|
|
1672
|
+
expect(Object.keys(manifest.communication.channels!)).toContain("merge_flow");
|
|
1673
|
+
expect(Object.keys(manifest.communication.channels!)).toContain("review_flow");
|
|
1674
|
+
});
|
|
1675
|
+
});
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
// =============================================================================
|
|
1679
|
+
// Tests: openteams Migration — TeamRuntime Type Detection
|
|
1680
|
+
// =============================================================================
|
|
1681
|
+
|
|
1682
|
+
describe("openteams Migration: TeamRuntime", () => {
|
|
1683
|
+
let roleRegistry: DefaultRoleRegistry;
|
|
1684
|
+
let agentManager: AgentManager;
|
|
1685
|
+
let messageRouter: MessageRouter;
|
|
1686
|
+
let eventStore: EventStore & { _events: Event[] };
|
|
1687
|
+
let services: TeamServices;
|
|
1688
|
+
|
|
1689
|
+
beforeEach(() => {
|
|
1690
|
+
roleRegistry = new DefaultRoleRegistry();
|
|
1691
|
+
eventStore = createMockEventStore() as EventStore & { _events: Event[] };
|
|
1692
|
+
messageRouter = createMockMessageRouter();
|
|
1693
|
+
agentManager = createMockAgentManager(roleRegistry);
|
|
1694
|
+
services = { agentManager, messageRouter, eventStore };
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
describe("MacroResolvedTemplate input", () => {
|
|
1698
|
+
it("accepts MacroResolvedTemplate directly", async () => {
|
|
1699
|
+
// Build a MacroResolvedTemplate from a loaded manifest
|
|
1700
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1701
|
+
|
|
1702
|
+
const resolved: MacroResolvedTemplate = {
|
|
1703
|
+
template: {
|
|
1704
|
+
manifest: {
|
|
1705
|
+
name: manifest.name,
|
|
1706
|
+
description: manifest.description,
|
|
1707
|
+
version: manifest.version,
|
|
1708
|
+
roles: manifest.roles,
|
|
1709
|
+
topology: manifest.topology,
|
|
1710
|
+
communication: manifest.communication,
|
|
1711
|
+
},
|
|
1712
|
+
roles: new Map(),
|
|
1713
|
+
prompts: new Map(),
|
|
1714
|
+
mcpServers: manifest._mcpServers,
|
|
1715
|
+
sourcePath: "",
|
|
1716
|
+
},
|
|
1717
|
+
resolvedRoles: manifest._resolvedRoles,
|
|
1718
|
+
macroAgent: manifest.macro_agent,
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
// Should not throw
|
|
1722
|
+
const runtime = new TeamRuntime(resolved, services);
|
|
1723
|
+
expect(runtime.getTaskMode()).toBe("pull");
|
|
1724
|
+
expect(runtime.getStrategyName()).toBe("trunk");
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
it("getResolvedTemplate() returns the resolved template", async () => {
|
|
1728
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1729
|
+
|
|
1730
|
+
const resolved: MacroResolvedTemplate = {
|
|
1731
|
+
template: {
|
|
1732
|
+
manifest: {
|
|
1733
|
+
name: manifest.name,
|
|
1734
|
+
description: manifest.description,
|
|
1735
|
+
version: manifest.version,
|
|
1736
|
+
roles: manifest.roles,
|
|
1737
|
+
topology: manifest.topology,
|
|
1738
|
+
communication: manifest.communication,
|
|
1739
|
+
},
|
|
1740
|
+
roles: new Map(),
|
|
1741
|
+
prompts: new Map(),
|
|
1742
|
+
mcpServers: manifest._mcpServers,
|
|
1743
|
+
sourcePath: "",
|
|
1744
|
+
},
|
|
1745
|
+
resolvedRoles: manifest._resolvedRoles,
|
|
1746
|
+
macroAgent: manifest.macro_agent,
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
const runtime = new TeamRuntime(resolved, services);
|
|
1750
|
+
const result = runtime.getResolvedTemplate();
|
|
1751
|
+
|
|
1752
|
+
expect(result).toBe(resolved);
|
|
1753
|
+
expect(result.resolvedRoles).toBe(manifest._resolvedRoles);
|
|
1754
|
+
expect(result.macroAgent).toBe(manifest.macro_agent);
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
it("initializes and bootstraps with MacroResolvedTemplate", async () => {
|
|
1758
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1759
|
+
|
|
1760
|
+
const resolved: MacroResolvedTemplate = {
|
|
1761
|
+
template: {
|
|
1762
|
+
manifest: {
|
|
1763
|
+
name: manifest.name,
|
|
1764
|
+
description: manifest.description,
|
|
1765
|
+
version: manifest.version,
|
|
1766
|
+
roles: manifest.roles,
|
|
1767
|
+
topology: manifest.topology,
|
|
1768
|
+
communication: manifest.communication,
|
|
1769
|
+
},
|
|
1770
|
+
roles: new Map(),
|
|
1771
|
+
prompts: new Map(),
|
|
1772
|
+
mcpServers: manifest._mcpServers,
|
|
1773
|
+
sourcePath: "",
|
|
1774
|
+
},
|
|
1775
|
+
resolvedRoles: manifest._resolvedRoles,
|
|
1776
|
+
macroAgent: manifest.macro_agent,
|
|
1777
|
+
};
|
|
1778
|
+
|
|
1779
|
+
const runtime = new TeamRuntime(resolved, services);
|
|
1780
|
+
await runtime.initialize();
|
|
1781
|
+
const result = await runtime.bootstrap();
|
|
1782
|
+
|
|
1783
|
+
expect(result.rootId).toBeDefined();
|
|
1784
|
+
expect(result.companionIds).toHaveLength(1);
|
|
1785
|
+
expect(agentManager.spawn).toHaveBeenCalledTimes(2);
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
it("emits team_config event with MacroResolvedTemplate", async () => {
|
|
1789
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1790
|
+
|
|
1791
|
+
const resolved: MacroResolvedTemplate = {
|
|
1792
|
+
template: {
|
|
1793
|
+
manifest: {
|
|
1794
|
+
name: manifest.name,
|
|
1795
|
+
description: manifest.description,
|
|
1796
|
+
version: manifest.version,
|
|
1797
|
+
roles: manifest.roles,
|
|
1798
|
+
topology: manifest.topology,
|
|
1799
|
+
communication: manifest.communication,
|
|
1800
|
+
},
|
|
1801
|
+
roles: new Map(),
|
|
1802
|
+
prompts: new Map(),
|
|
1803
|
+
mcpServers: manifest._mcpServers,
|
|
1804
|
+
sourcePath: "",
|
|
1805
|
+
},
|
|
1806
|
+
resolvedRoles: manifest._resolvedRoles,
|
|
1807
|
+
macroAgent: manifest.macro_agent,
|
|
1808
|
+
};
|
|
1809
|
+
|
|
1810
|
+
const runtime = new TeamRuntime(resolved, services);
|
|
1811
|
+
await runtime.initialize();
|
|
1812
|
+
|
|
1813
|
+
expect(eventStore.emit).toHaveBeenCalledWith(
|
|
1814
|
+
expect.objectContaining({
|
|
1815
|
+
type: "status",
|
|
1816
|
+
payload: expect.objectContaining({
|
|
1817
|
+
team_config: expect.objectContaining({
|
|
1818
|
+
teamName: "self-driving",
|
|
1819
|
+
strategy: "trunk",
|
|
1820
|
+
taskMode: "pull",
|
|
1821
|
+
}),
|
|
1822
|
+
}),
|
|
1823
|
+
})
|
|
1824
|
+
);
|
|
1825
|
+
});
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
describe("manifestToResolved conversion", () => {
|
|
1829
|
+
it("converts legacy TeamManifest to MacroResolvedTemplate internally", async () => {
|
|
1830
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1831
|
+
|
|
1832
|
+
// Pass TeamManifest (legacy path)
|
|
1833
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
1834
|
+
const resolved = runtime.getResolvedTemplate();
|
|
1835
|
+
|
|
1836
|
+
// Should have been converted internally
|
|
1837
|
+
expect(resolved.template.manifest.name).toBe("self-driving");
|
|
1838
|
+
expect(resolved.resolvedRoles).toBe(manifest._resolvedRoles);
|
|
1839
|
+
expect(resolved.macroAgent).toBe(manifest.macro_agent);
|
|
1840
|
+
expect(resolved.template.mcpServers).toBe(manifest._mcpServers);
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
it("preserves topology and communication in conversion", async () => {
|
|
1844
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1845
|
+
|
|
1846
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
1847
|
+
const resolved = runtime.getResolvedTemplate();
|
|
1848
|
+
|
|
1849
|
+
expect(resolved.template.manifest.topology).toBe(manifest.topology);
|
|
1850
|
+
expect(resolved.template.manifest.communication).toBe(manifest.communication);
|
|
1851
|
+
});
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
describe("getManifest() backward compatibility", () => {
|
|
1855
|
+
it("reconstructs TeamManifest from MacroResolvedTemplate", async () => {
|
|
1856
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1857
|
+
|
|
1858
|
+
const resolved: MacroResolvedTemplate = {
|
|
1859
|
+
template: {
|
|
1860
|
+
manifest: {
|
|
1861
|
+
name: manifest.name,
|
|
1862
|
+
description: manifest.description,
|
|
1863
|
+
version: manifest.version,
|
|
1864
|
+
roles: manifest.roles,
|
|
1865
|
+
topology: manifest.topology,
|
|
1866
|
+
communication: manifest.communication,
|
|
1867
|
+
},
|
|
1868
|
+
roles: new Map(),
|
|
1869
|
+
prompts: new Map(),
|
|
1870
|
+
mcpServers: manifest._mcpServers,
|
|
1871
|
+
sourcePath: "",
|
|
1872
|
+
},
|
|
1873
|
+
resolvedRoles: manifest._resolvedRoles,
|
|
1874
|
+
macroAgent: manifest.macro_agent,
|
|
1875
|
+
};
|
|
1876
|
+
|
|
1877
|
+
const runtime = new TeamRuntime(resolved, services);
|
|
1878
|
+
const backCompat = runtime.getManifest();
|
|
1879
|
+
|
|
1880
|
+
expect(backCompat.name).toBe("self-driving");
|
|
1881
|
+
expect(backCompat._resolvedRoles).toBe(manifest._resolvedRoles);
|
|
1882
|
+
expect(backCompat._mcpServers).toBe(manifest._mcpServers);
|
|
1883
|
+
expect(backCompat.macro_agent).toBe(manifest.macro_agent);
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
it("reconstructs TeamManifest from legacy TeamManifest input", async () => {
|
|
1887
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1888
|
+
|
|
1889
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
1890
|
+
const backCompat = runtime.getManifest();
|
|
1891
|
+
|
|
1892
|
+
expect(backCompat.name).toBe("self-driving");
|
|
1893
|
+
expect(backCompat._resolvedRoles).toBe(manifest._resolvedRoles);
|
|
1894
|
+
expect(backCompat._loadedPrompts).toBe(manifest._loadedPrompts);
|
|
1895
|
+
});
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
describe("spawn interceptor with MacroResolvedTemplate", () => {
|
|
1899
|
+
it("injects team context when using MacroResolvedTemplate", async () => {
|
|
1900
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1901
|
+
|
|
1902
|
+
const resolved: MacroResolvedTemplate = {
|
|
1903
|
+
template: {
|
|
1904
|
+
manifest: {
|
|
1905
|
+
name: manifest.name,
|
|
1906
|
+
description: manifest.description,
|
|
1907
|
+
version: manifest.version,
|
|
1908
|
+
roles: manifest.roles,
|
|
1909
|
+
topology: manifest.topology,
|
|
1910
|
+
communication: manifest.communication,
|
|
1911
|
+
},
|
|
1912
|
+
roles: new Map(),
|
|
1913
|
+
prompts: new Map(),
|
|
1914
|
+
mcpServers: manifest._mcpServers,
|
|
1915
|
+
sourcePath: "",
|
|
1916
|
+
},
|
|
1917
|
+
resolvedRoles: manifest._resolvedRoles,
|
|
1918
|
+
macroAgent: manifest.macro_agent,
|
|
1919
|
+
};
|
|
1920
|
+
|
|
1921
|
+
const runtime = new TeamRuntime(resolved, services);
|
|
1922
|
+
await runtime.initialize();
|
|
1923
|
+
runtime.installOnServices();
|
|
1924
|
+
await runtime.bootstrap();
|
|
1925
|
+
|
|
1926
|
+
// Spawn a grinder through the interceptor
|
|
1927
|
+
await agentManager.spawn({
|
|
1928
|
+
task: "test grinder task",
|
|
1929
|
+
role: "grinder",
|
|
1930
|
+
parent: "agent_0",
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
const lastOpts = interceptedSpawnOptions.at(-1)!;
|
|
1934
|
+
expect(lastOpts.config?.env?.MACRO_TEAM_NAME).toBe("self-driving");
|
|
1935
|
+
expect(lastOpts.config?.env?.MACRO_TASK_MODE).toBe("pull");
|
|
1936
|
+
expect(lastOpts.topics).toContain("work_coordination");
|
|
1937
|
+
});
|
|
1938
|
+
|
|
1939
|
+
it("resolves MCP servers from MacroResolvedTemplate", async () => {
|
|
1940
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1941
|
+
|
|
1942
|
+
// Add a mock MCP server for grinder
|
|
1943
|
+
const mcpServers = new Map<string, McpServerEntry[]>();
|
|
1944
|
+
mcpServers.set("grinder", [{
|
|
1945
|
+
name: "test-server",
|
|
1946
|
+
command: "node",
|
|
1947
|
+
args: ["test.js"],
|
|
1948
|
+
}]);
|
|
1949
|
+
|
|
1950
|
+
const resolved: MacroResolvedTemplate = {
|
|
1951
|
+
template: {
|
|
1952
|
+
manifest: {
|
|
1953
|
+
name: manifest.name,
|
|
1954
|
+
description: manifest.description,
|
|
1955
|
+
version: manifest.version,
|
|
1956
|
+
roles: manifest.roles,
|
|
1957
|
+
topology: manifest.topology,
|
|
1958
|
+
communication: manifest.communication,
|
|
1959
|
+
},
|
|
1960
|
+
roles: new Map(),
|
|
1961
|
+
prompts: new Map(),
|
|
1962
|
+
mcpServers,
|
|
1963
|
+
sourcePath: "",
|
|
1964
|
+
},
|
|
1965
|
+
resolvedRoles: manifest._resolvedRoles,
|
|
1966
|
+
macroAgent: manifest.macro_agent,
|
|
1967
|
+
};
|
|
1968
|
+
|
|
1969
|
+
const runtime = new TeamRuntime(resolved, services);
|
|
1970
|
+
await runtime.initialize();
|
|
1971
|
+
runtime.installOnServices();
|
|
1972
|
+
await runtime.bootstrap();
|
|
1973
|
+
|
|
1974
|
+
// Spawn a grinder
|
|
1975
|
+
await agentManager.spawn({
|
|
1976
|
+
task: "test task",
|
|
1977
|
+
role: "grinder",
|
|
1978
|
+
parent: "agent_0",
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
const lastOpts = interceptedSpawnOptions.at(-1)!;
|
|
1982
|
+
expect(lastOpts.config?.mcpServers).toBeDefined();
|
|
1983
|
+
expect(lastOpts.config?.mcpServers!.length).toBeGreaterThanOrEqual(1);
|
|
1984
|
+
expect(lastOpts.config?.mcpServers!.some((s: any) => s.name === "test-server")).toBe(true);
|
|
1985
|
+
});
|
|
1986
|
+
});
|
|
1987
|
+
|
|
1988
|
+
describe("serialized roles in team_config", () => {
|
|
1989
|
+
it("serializes resolved roles with capabilities", async () => {
|
|
1990
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
1991
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
1992
|
+
|
|
1993
|
+
await runtime.initialize();
|
|
1994
|
+
|
|
1995
|
+
const emitCall = vi.mocked(eventStore.emit).mock.calls[0][0] as any;
|
|
1996
|
+
const serializedRoles = emitCall.payload.team_config.roles;
|
|
1997
|
+
|
|
1998
|
+
expect(serializedRoles.planner).toBeDefined();
|
|
1999
|
+
expect(serializedRoles.planner.name).toBe("planner");
|
|
2000
|
+
expect(serializedRoles.planner.capabilities).toContain("task.claim");
|
|
2001
|
+
expect(serializedRoles.planner.capabilities).toContain("agent.spawn.grinder");
|
|
2002
|
+
|
|
2003
|
+
expect(serializedRoles.grinder).toBeDefined();
|
|
2004
|
+
expect(serializedRoles.grinder.name).toBe("grinder");
|
|
2005
|
+
expect(serializedRoles.grinder.capabilities).toContain("task.claim");
|
|
2006
|
+
expect(serializedRoles.grinder.capabilities).toContain("git.push");
|
|
2007
|
+
|
|
2008
|
+
expect(serializedRoles.judge).toBeDefined();
|
|
2009
|
+
expect(serializedRoles.judge.name).toBe("judge");
|
|
2010
|
+
});
|
|
2011
|
+
|
|
2012
|
+
it("includes lifecycle and description in serialized roles", async () => {
|
|
2013
|
+
const manifest = await loadTeam("self-driving", roleRegistry, PROJECT_ROOT);
|
|
2014
|
+
const runtime = new TeamRuntime(manifest, services);
|
|
2015
|
+
|
|
2016
|
+
await runtime.initialize();
|
|
2017
|
+
|
|
2018
|
+
const emitCall = vi.mocked(eventStore.emit).mock.calls[0][0] as any;
|
|
2019
|
+
const serializedRoles = emitCall.payload.team_config.roles;
|
|
2020
|
+
|
|
2021
|
+
// planner has lifecycle config
|
|
2022
|
+
expect(serializedRoles.planner.lifecycle).toBeDefined();
|
|
2023
|
+
|
|
2024
|
+
// All roles have descriptions
|
|
2025
|
+
expect(serializedRoles.planner.description).toBeDefined();
|
|
2026
|
+
expect(serializedRoles.grinder.description).toBeDefined();
|
|
2027
|
+
expect(serializedRoles.judge.description).toBeDefined();
|
|
2028
|
+
});
|
|
2029
|
+
});
|
|
2030
|
+
});
|
|
2031
|
+
|
|
2032
|
+
// =============================================================================
|
|
2033
|
+
// Tests: Auto-Scaling
|
|
2034
|
+
// =============================================================================
|
|
2035
|
+
|
|
2036
|
+
describe("TeamRuntime auto-scaling", () => {
|
|
2037
|
+
let roleRegistry: DefaultRoleRegistry;
|
|
2038
|
+
let agentManager: AgentManager;
|
|
2039
|
+
let messageRouter: MessageRouter;
|
|
2040
|
+
let eventStore: EventStore;
|
|
2041
|
+
|
|
2042
|
+
beforeEach(() => {
|
|
2043
|
+
roleRegistry = new DefaultRoleRegistry();
|
|
2044
|
+
eventStore = createMockEventStore();
|
|
2045
|
+
messageRouter = createMockMessageRouter();
|
|
2046
|
+
agentManager = createMockAgentManager(roleRegistry);
|
|
2047
|
+
vi.useFakeTimers();
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
afterEach(() => {
|
|
2051
|
+
vi.useRealTimers();
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
function createScalingTemplate(scalingConfig: {
|
|
2055
|
+
min_workers?: number;
|
|
2056
|
+
max_workers?: number;
|
|
2057
|
+
scale_on?: "task_queue_depth" | "manual";
|
|
2058
|
+
idle_drain?: boolean;
|
|
2059
|
+
}): MacroResolvedTemplate {
|
|
2060
|
+
const workerRole: ResolvedTeamRole = {
|
|
2061
|
+
name: "grinder",
|
|
2062
|
+
baseRole: "worker",
|
|
2063
|
+
capabilities: ["file.read", "file.write", "task.claim", "lifecycle.done"],
|
|
2064
|
+
roleDefinition: {
|
|
2065
|
+
name: "grinder",
|
|
2066
|
+
capabilities: ["file.read", "file.write", "task.claim", "lifecycle.done"],
|
|
2067
|
+
},
|
|
2068
|
+
};
|
|
2069
|
+
|
|
2070
|
+
const plannerRole: ResolvedTeamRole = {
|
|
2071
|
+
name: "planner",
|
|
2072
|
+
baseRole: "coordinator",
|
|
2073
|
+
capabilities: ["*"],
|
|
2074
|
+
roleDefinition: {
|
|
2075
|
+
name: "planner",
|
|
2076
|
+
capabilities: ["*"],
|
|
2077
|
+
},
|
|
2078
|
+
};
|
|
2079
|
+
|
|
2080
|
+
return {
|
|
2081
|
+
template: {
|
|
2082
|
+
manifest: {
|
|
2083
|
+
name: "test-scaling",
|
|
2084
|
+
version: 1,
|
|
2085
|
+
roles: ["planner", "grinder"],
|
|
2086
|
+
topology: {
|
|
2087
|
+
root: { role: "planner", prompt: "prompts/planner.md" },
|
|
2088
|
+
spawn_rules: { planner: ["grinder"], grinder: [] },
|
|
2089
|
+
},
|
|
2090
|
+
},
|
|
2091
|
+
roles: new Map(),
|
|
2092
|
+
prompts: new Map(),
|
|
2093
|
+
mcpServers: new Map<string, McpServerEntry[]>(),
|
|
2094
|
+
sourcePath: "",
|
|
2095
|
+
},
|
|
2096
|
+
resolvedRoles: new Map<string, ResolvedTeamRole>([
|
|
2097
|
+
["planner", plannerRole],
|
|
2098
|
+
["grinder", workerRole],
|
|
2099
|
+
]),
|
|
2100
|
+
macroAgent: {
|
|
2101
|
+
task_assignment: { mode: "pull" as const },
|
|
2102
|
+
lifecycle: { scaling: scalingConfig },
|
|
2103
|
+
},
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
it("spawns worker when task queue depth exceeds active workers", async () => {
|
|
2108
|
+
const mockTaskBackend = {
|
|
2109
|
+
listClaimable: vi.fn().mockResolvedValue([
|
|
2110
|
+
{ id: "t1", title: "Task 1" },
|
|
2111
|
+
{ id: "t2", title: "Task 2" },
|
|
2112
|
+
]),
|
|
2113
|
+
};
|
|
2114
|
+
|
|
2115
|
+
// No active workers initially
|
|
2116
|
+
vi.mocked(agentManager.list).mockReturnValue([]);
|
|
2117
|
+
|
|
2118
|
+
const services: TeamServices = {
|
|
2119
|
+
agentManager, messageRouter, eventStore,
|
|
2120
|
+
taskBackend: mockTaskBackend as any,
|
|
2121
|
+
};
|
|
2122
|
+
|
|
2123
|
+
const resolved = createScalingTemplate({
|
|
2124
|
+
max_workers: 5,
|
|
2125
|
+
scale_on: "task_queue_depth",
|
|
2126
|
+
});
|
|
2127
|
+
const runtime = new TeamRuntime(resolved, services);
|
|
2128
|
+
await runtime.initialize();
|
|
2129
|
+
await runtime.bootstrap();
|
|
2130
|
+
|
|
2131
|
+
// Advance timer to trigger scaling check (5s interval)
|
|
2132
|
+
await vi.advanceTimersByTimeAsync(5_100);
|
|
2133
|
+
|
|
2134
|
+
// Should have spawned a grinder as child of root
|
|
2135
|
+
const spawnCalls = vi.mocked(agentManager.spawn).mock.calls;
|
|
2136
|
+
const scalingSpawn = spawnCalls.find(
|
|
2137
|
+
call => call[0].role === "grinder" && call[0].parent !== null && call[0].task?.includes("auto-scaled")
|
|
2138
|
+
);
|
|
2139
|
+
expect(scalingSpawn).toBeDefined();
|
|
2140
|
+
|
|
2141
|
+
await runtime.teardown();
|
|
2142
|
+
});
|
|
2143
|
+
|
|
2144
|
+
it("respects max_workers cap", async () => {
|
|
2145
|
+
const mockTaskBackend = {
|
|
2146
|
+
listClaimable: vi.fn().mockResolvedValue([
|
|
2147
|
+
{ id: "t1", title: "Task 1" },
|
|
2148
|
+
{ id: "t2", title: "Task 2" },
|
|
2149
|
+
]),
|
|
2150
|
+
};
|
|
2151
|
+
|
|
2152
|
+
// Already at max workers
|
|
2153
|
+
vi.mocked(agentManager.list).mockReturnValue([
|
|
2154
|
+
{ id: "w1", role: "grinder", state: "running" } as any,
|
|
2155
|
+
{ id: "w2", role: "grinder", state: "running" } as any,
|
|
2156
|
+
]);
|
|
2157
|
+
|
|
2158
|
+
const services: TeamServices = {
|
|
2159
|
+
agentManager, messageRouter, eventStore,
|
|
2160
|
+
taskBackend: mockTaskBackend as any,
|
|
2161
|
+
};
|
|
2162
|
+
|
|
2163
|
+
const resolved = createScalingTemplate({
|
|
2164
|
+
max_workers: 2,
|
|
2165
|
+
scale_on: "task_queue_depth",
|
|
2166
|
+
});
|
|
2167
|
+
const runtime = new TeamRuntime(resolved, services);
|
|
2168
|
+
await runtime.initialize();
|
|
2169
|
+
await runtime.bootstrap();
|
|
2170
|
+
|
|
2171
|
+
// Register the workers in the runtime's agentRoleMap
|
|
2172
|
+
runtime.registerAgent("w1" as AgentId, "grinder");
|
|
2173
|
+
runtime.registerAgent("w2" as AgentId, "grinder");
|
|
2174
|
+
|
|
2175
|
+
const spawnCountBefore = vi.mocked(agentManager.spawn).mock.calls.length;
|
|
2176
|
+
|
|
2177
|
+
await vi.advanceTimersByTimeAsync(5_100);
|
|
2178
|
+
|
|
2179
|
+
// No additional spawns — already at max
|
|
2180
|
+
const scalingSpawns = vi.mocked(agentManager.spawn).mock.calls.slice(spawnCountBefore).filter(
|
|
2181
|
+
call => call[0].task?.includes("auto-scaled")
|
|
2182
|
+
);
|
|
2183
|
+
expect(scalingSpawns).toHaveLength(0);
|
|
2184
|
+
|
|
2185
|
+
await runtime.teardown();
|
|
2186
|
+
});
|
|
2187
|
+
|
|
2188
|
+
it("does not spawn when scale_on is not task_queue_depth", async () => {
|
|
2189
|
+
const mockTaskBackend = {
|
|
2190
|
+
listClaimable: vi.fn().mockResolvedValue([
|
|
2191
|
+
{ id: "t1", title: "Task 1" },
|
|
2192
|
+
]),
|
|
2193
|
+
};
|
|
2194
|
+
|
|
2195
|
+
const services: TeamServices = {
|
|
2196
|
+
agentManager, messageRouter, eventStore,
|
|
2197
|
+
taskBackend: mockTaskBackend as any,
|
|
2198
|
+
};
|
|
2199
|
+
|
|
2200
|
+
const resolved = createScalingTemplate({
|
|
2201
|
+
max_workers: 5,
|
|
2202
|
+
scale_on: "manual",
|
|
2203
|
+
});
|
|
2204
|
+
const runtime = new TeamRuntime(resolved, services);
|
|
2205
|
+
await runtime.initialize();
|
|
2206
|
+
await runtime.bootstrap();
|
|
2207
|
+
|
|
2208
|
+
const spawnCountBefore = vi.mocked(agentManager.spawn).mock.calls.length;
|
|
2209
|
+
|
|
2210
|
+
await vi.advanceTimersByTimeAsync(10_000);
|
|
2211
|
+
|
|
2212
|
+
// No scaling spawns
|
|
2213
|
+
const scalingSpawns = vi.mocked(agentManager.spawn).mock.calls.slice(spawnCountBefore).filter(
|
|
2214
|
+
call => call[0].task?.includes("auto-scaled")
|
|
2215
|
+
);
|
|
2216
|
+
expect(scalingSpawns).toHaveLength(0);
|
|
2217
|
+
|
|
2218
|
+
// listClaimable should not have been called
|
|
2219
|
+
expect(mockTaskBackend.listClaimable).not.toHaveBeenCalled();
|
|
2220
|
+
|
|
2221
|
+
await runtime.teardown();
|
|
2222
|
+
});
|
|
2223
|
+
|
|
2224
|
+
it("respects cooldown between scale-up actions", async () => {
|
|
2225
|
+
const mockTaskBackend = {
|
|
2226
|
+
listClaimable: vi.fn().mockResolvedValue([
|
|
2227
|
+
{ id: "t1" }, { id: "t2" }, { id: "t3" },
|
|
2228
|
+
]),
|
|
2229
|
+
};
|
|
2230
|
+
|
|
2231
|
+
vi.mocked(agentManager.list).mockReturnValue([]);
|
|
2232
|
+
|
|
2233
|
+
const services: TeamServices = {
|
|
2234
|
+
agentManager, messageRouter, eventStore,
|
|
2235
|
+
taskBackend: mockTaskBackend as any,
|
|
2236
|
+
};
|
|
2237
|
+
|
|
2238
|
+
const resolved = createScalingTemplate({
|
|
2239
|
+
max_workers: 10,
|
|
2240
|
+
scale_on: "task_queue_depth",
|
|
2241
|
+
});
|
|
2242
|
+
const runtime = new TeamRuntime(resolved, services);
|
|
2243
|
+
await runtime.initialize();
|
|
2244
|
+
await runtime.bootstrap();
|
|
2245
|
+
|
|
2246
|
+
const spawnCountBefore = vi.mocked(agentManager.spawn).mock.calls.length;
|
|
2247
|
+
|
|
2248
|
+
// First tick at 5s — should spawn
|
|
2249
|
+
await vi.advanceTimersByTimeAsync(5_100);
|
|
2250
|
+
const afterFirst = vi.mocked(agentManager.spawn).mock.calls.slice(spawnCountBefore).filter(
|
|
2251
|
+
call => call[0].task?.includes("auto-scaled")
|
|
2252
|
+
);
|
|
2253
|
+
expect(afterFirst).toHaveLength(1);
|
|
2254
|
+
|
|
2255
|
+
// Second tick at 10s — cooldown (10s from first spawn) not elapsed
|
|
2256
|
+
await vi.advanceTimersByTimeAsync(5_000);
|
|
2257
|
+
const afterSecond = vi.mocked(agentManager.spawn).mock.calls.slice(spawnCountBefore).filter(
|
|
2258
|
+
call => call[0].task?.includes("auto-scaled")
|
|
2259
|
+
);
|
|
2260
|
+
expect(afterSecond).toHaveLength(1); // Still just 1
|
|
2261
|
+
|
|
2262
|
+
// Third tick at 15s — cooldown elapsed, should spawn again
|
|
2263
|
+
await vi.advanceTimersByTimeAsync(5_100);
|
|
2264
|
+
const afterThird = vi.mocked(agentManager.spawn).mock.calls.slice(spawnCountBefore).filter(
|
|
2265
|
+
call => call[0].task?.includes("auto-scaled")
|
|
2266
|
+
);
|
|
2267
|
+
expect(afterThird).toHaveLength(2);
|
|
2268
|
+
|
|
2269
|
+
await runtime.teardown();
|
|
2270
|
+
});
|
|
2271
|
+
});
|
|
2272
|
+
|
|
2273
|
+
// =============================================================================
|
|
2274
|
+
// Tests: Workspace Isolation (Capability-Based)
|
|
2275
|
+
// =============================================================================
|
|
2276
|
+
|
|
2277
|
+
describe("Workspace Isolation", () => {
|
|
2278
|
+
let roleRegistry: DefaultRoleRegistry;
|
|
2279
|
+
let agentManager: AgentManager;
|
|
2280
|
+
let messageRouter: MessageRouter;
|
|
2281
|
+
let eventStore: EventStore;
|
|
2282
|
+
|
|
2283
|
+
beforeEach(() => {
|
|
2284
|
+
roleRegistry = new DefaultRoleRegistry();
|
|
2285
|
+
agentManager = createMockAgentManager(roleRegistry);
|
|
2286
|
+
messageRouter = createMockMessageRouter();
|
|
2287
|
+
eventStore = createMockEventStore();
|
|
2288
|
+
});
|
|
2289
|
+
|
|
2290
|
+
function createWorkspaceTemplate(): MacroResolvedTemplate {
|
|
2291
|
+
const resolvedRoles = new Map<string, ResolvedTeamRole>();
|
|
2292
|
+
resolvedRoles.set("lead", {
|
|
2293
|
+
name: "lead",
|
|
2294
|
+
baseRole: "coordinator",
|
|
2295
|
+
capabilities: [
|
|
2296
|
+
"file.read", "file.write", "task.create", "task.assign",
|
|
2297
|
+
"agent.spawn.worker", "agent.terminate", "workspace.stream",
|
|
2298
|
+
],
|
|
2299
|
+
roleDefinition: roleRegistry.resolveRole("coordinator"),
|
|
2300
|
+
});
|
|
2301
|
+
resolvedRoles.set("developer", {
|
|
2302
|
+
name: "developer",
|
|
2303
|
+
baseRole: "worker",
|
|
2304
|
+
capabilities: [
|
|
2305
|
+
"file.read", "file.write", "git.commit", "lifecycle.done",
|
|
2306
|
+
"workspace.worktree",
|
|
2307
|
+
],
|
|
2308
|
+
roleDefinition: roleRegistry.resolveRole("worker"),
|
|
2309
|
+
});
|
|
2310
|
+
resolvedRoles.set("merger", {
|
|
2311
|
+
name: "merger",
|
|
2312
|
+
baseRole: "integrator",
|
|
2313
|
+
capabilities: [
|
|
2314
|
+
"file.read", "file.write", "git.merge", "lifecycle.done",
|
|
2315
|
+
"workspace.integrate",
|
|
2316
|
+
],
|
|
2317
|
+
roleDefinition: roleRegistry.resolveRole("integrator"),
|
|
2318
|
+
});
|
|
2319
|
+
resolvedRoles.set("watcher", {
|
|
2320
|
+
name: "watcher",
|
|
2321
|
+
baseRole: "monitor",
|
|
2322
|
+
capabilities: ["file.read", "msg.send"],
|
|
2323
|
+
roleDefinition: roleRegistry.resolveRole("monitor"),
|
|
2324
|
+
});
|
|
2325
|
+
|
|
2326
|
+
return {
|
|
2327
|
+
template: {
|
|
2328
|
+
manifest: {
|
|
2329
|
+
name: "workspace-test",
|
|
2330
|
+
version: 1,
|
|
2331
|
+
roles: ["lead", "developer", "merger", "watcher"],
|
|
2332
|
+
topology: {
|
|
2333
|
+
root: { role: "lead" },
|
|
2334
|
+
companions: [{ role: "merger" }],
|
|
2335
|
+
},
|
|
2336
|
+
communication: {},
|
|
2337
|
+
},
|
|
2338
|
+
roles: new Map(),
|
|
2339
|
+
prompts: new Map(),
|
|
2340
|
+
mcpServers: new Map<string, McpServerEntry[]>(),
|
|
2341
|
+
sourcePath: "",
|
|
2342
|
+
},
|
|
2343
|
+
resolvedRoles,
|
|
2344
|
+
macroAgent: {
|
|
2345
|
+
integration: { strategy: "queue" },
|
|
2346
|
+
task_assignment: { mode: "push" as const },
|
|
2347
|
+
},
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
it("spawn interceptor injects streamId for role with workspace.worktree", async () => {
|
|
2352
|
+
const template = createWorkspaceTemplate();
|
|
2353
|
+
const mockWorkspaceManager = {
|
|
2354
|
+
createIntegrationStream: vi.fn().mockReturnValue("stream-1"),
|
|
2355
|
+
getMergeQueue: vi.fn().mockReturnValue(null),
|
|
2356
|
+
getWorkspace: vi.fn(),
|
|
2357
|
+
};
|
|
2358
|
+
|
|
2359
|
+
const services: TeamServices = {
|
|
2360
|
+
agentManager,
|
|
2361
|
+
messageRouter,
|
|
2362
|
+
eventStore,
|
|
2363
|
+
workspaceManager: mockWorkspaceManager as any,
|
|
2364
|
+
};
|
|
2365
|
+
|
|
2366
|
+
const runtime = new TeamRuntime(template, services);
|
|
2367
|
+
await runtime.initialize();
|
|
2368
|
+
await runtime.bootstrap();
|
|
2369
|
+
|
|
2370
|
+
// Now spawn a developer (extends worker) through the interceptor
|
|
2371
|
+
const interceptor = runtime.createSpawnInterceptor();
|
|
2372
|
+
const result = interceptor({
|
|
2373
|
+
task: "implement feature",
|
|
2374
|
+
role: "developer",
|
|
2375
|
+
parent: "agent_0",
|
|
2376
|
+
});
|
|
2377
|
+
|
|
2378
|
+
expect(result.streamId).toBe("stream-1");
|
|
2379
|
+
expect(result.dataplaneTaskId).toBeDefined();
|
|
2380
|
+
expect(result.capabilities).toContain("workspace.worktree");
|
|
2381
|
+
|
|
2382
|
+
await runtime.teardown();
|
|
2383
|
+
});
|
|
2384
|
+
|
|
2385
|
+
it("spawn interceptor injects streamId for role with workspace.integrate", async () => {
|
|
2386
|
+
const template = createWorkspaceTemplate();
|
|
2387
|
+
const mockWorkspaceManager = {
|
|
2388
|
+
createIntegrationStream: vi.fn().mockReturnValue("stream-1"),
|
|
2389
|
+
getMergeQueue: vi.fn().mockReturnValue(null),
|
|
2390
|
+
getWorkspace: vi.fn(),
|
|
2391
|
+
};
|
|
2392
|
+
|
|
2393
|
+
const services: TeamServices = {
|
|
2394
|
+
agentManager,
|
|
2395
|
+
messageRouter,
|
|
2396
|
+
eventStore,
|
|
2397
|
+
workspaceManager: mockWorkspaceManager as any,
|
|
2398
|
+
};
|
|
2399
|
+
|
|
2400
|
+
const runtime = new TeamRuntime(template, services);
|
|
2401
|
+
await runtime.initialize();
|
|
2402
|
+
await runtime.bootstrap();
|
|
2403
|
+
|
|
2404
|
+
const interceptor = runtime.createSpawnInterceptor();
|
|
2405
|
+
const result = interceptor({
|
|
2406
|
+
task: "merge changes",
|
|
2407
|
+
role: "merger",
|
|
2408
|
+
parent: "agent_0",
|
|
2409
|
+
});
|
|
2410
|
+
|
|
2411
|
+
expect(result.streamId).toBe("stream-1");
|
|
2412
|
+
expect(result.dataplaneTaskId).toBeUndefined(); // Integrators don't get dataplaneTaskId
|
|
2413
|
+
expect(result.capabilities).toContain("workspace.integrate");
|
|
2414
|
+
|
|
2415
|
+
await runtime.teardown();
|
|
2416
|
+
});
|
|
2417
|
+
|
|
2418
|
+
it("spawn interceptor does NOT inject workspace fields for role without workspace capability", async () => {
|
|
2419
|
+
const template = createWorkspaceTemplate();
|
|
2420
|
+
const mockWorkspaceManager = {
|
|
2421
|
+
createIntegrationStream: vi.fn().mockReturnValue("stream-1"),
|
|
2422
|
+
getMergeQueue: vi.fn().mockReturnValue(null),
|
|
2423
|
+
getWorkspace: vi.fn(),
|
|
2424
|
+
};
|
|
2425
|
+
|
|
2426
|
+
const services: TeamServices = {
|
|
2427
|
+
agentManager,
|
|
2428
|
+
messageRouter,
|
|
2429
|
+
eventStore,
|
|
2430
|
+
workspaceManager: mockWorkspaceManager as any,
|
|
2431
|
+
};
|
|
2432
|
+
|
|
2433
|
+
const runtime = new TeamRuntime(template, services);
|
|
2434
|
+
await runtime.initialize();
|
|
2435
|
+
await runtime.bootstrap();
|
|
2436
|
+
|
|
2437
|
+
const interceptor = runtime.createSpawnInterceptor();
|
|
2438
|
+
const result = interceptor({
|
|
2439
|
+
task: "monitor health",
|
|
2440
|
+
role: "watcher",
|
|
2441
|
+
parent: "agent_0",
|
|
2442
|
+
});
|
|
2443
|
+
|
|
2444
|
+
// Monitor role has no workspace capabilities
|
|
2445
|
+
expect(result.streamId).toBeUndefined();
|
|
2446
|
+
expect(result.streamConfig).toBeUndefined();
|
|
2447
|
+
expect(result.dataplaneTaskId).toBeUndefined();
|
|
2448
|
+
|
|
2449
|
+
await runtime.teardown();
|
|
2450
|
+
});
|
|
2451
|
+
|
|
2452
|
+
it("spawn interceptor does not overwrite explicit workspace values", async () => {
|
|
2453
|
+
const template = createWorkspaceTemplate();
|
|
2454
|
+
const mockWorkspaceManager = {
|
|
2455
|
+
createIntegrationStream: vi.fn().mockReturnValue("stream-1"),
|
|
2456
|
+
getMergeQueue: vi.fn().mockReturnValue(null),
|
|
2457
|
+
getWorkspace: vi.fn(),
|
|
2458
|
+
};
|
|
2459
|
+
|
|
2460
|
+
const services: TeamServices = {
|
|
2461
|
+
agentManager,
|
|
2462
|
+
messageRouter,
|
|
2463
|
+
eventStore,
|
|
2464
|
+
workspaceManager: mockWorkspaceManager as any,
|
|
2465
|
+
};
|
|
2466
|
+
|
|
2467
|
+
const runtime = new TeamRuntime(template, services);
|
|
2468
|
+
await runtime.initialize();
|
|
2469
|
+
await runtime.bootstrap();
|
|
2470
|
+
|
|
2471
|
+
const interceptor = runtime.createSpawnInterceptor();
|
|
2472
|
+
const result = interceptor({
|
|
2473
|
+
task: "implement feature",
|
|
2474
|
+
role: "developer",
|
|
2475
|
+
parent: "agent_0",
|
|
2476
|
+
streamId: "custom-stream",
|
|
2477
|
+
dataplaneTaskId: "custom-task",
|
|
2478
|
+
});
|
|
2479
|
+
|
|
2480
|
+
// Explicit values should NOT be overwritten
|
|
2481
|
+
expect(result.streamId).toBe("custom-stream");
|
|
2482
|
+
expect(result.dataplaneTaskId).toBe("custom-task");
|
|
2483
|
+
|
|
2484
|
+
await runtime.teardown();
|
|
2485
|
+
});
|
|
2486
|
+
|
|
2487
|
+
it("merge queue mr:submitted event wakes integrator agent", async () => {
|
|
2488
|
+
const template = createWorkspaceTemplate();
|
|
2489
|
+
let mergeQueueCallback: ((event: any) => void) | null = null;
|
|
2490
|
+
const mockMergeQueue = {
|
|
2491
|
+
onEvent: vi.fn((cb: (event: any) => void) => {
|
|
2492
|
+
mergeQueueCallback = cb;
|
|
2493
|
+
return () => { mergeQueueCallback = null; };
|
|
2494
|
+
}),
|
|
2495
|
+
};
|
|
2496
|
+
const mockWorkspaceManager = {
|
|
2497
|
+
createIntegrationStream: vi.fn().mockReturnValue("stream-1"),
|
|
2498
|
+
getMergeQueue: vi.fn().mockReturnValue(mockMergeQueue),
|
|
2499
|
+
getWorkspace: vi.fn(),
|
|
2500
|
+
};
|
|
2501
|
+
|
|
2502
|
+
const services: TeamServices = {
|
|
2503
|
+
agentManager,
|
|
2504
|
+
messageRouter,
|
|
2505
|
+
eventStore,
|
|
2506
|
+
workspaceManager: mockWorkspaceManager as any,
|
|
2507
|
+
};
|
|
2508
|
+
|
|
2509
|
+
const runtime = new TeamRuntime(template, services);
|
|
2510
|
+
await runtime.initialize();
|
|
2511
|
+
await runtime.bootstrap();
|
|
2512
|
+
|
|
2513
|
+
// Verify merge queue subscription was set up
|
|
2514
|
+
expect(mockMergeQueue.onEvent).toHaveBeenCalled();
|
|
2515
|
+
expect(mergeQueueCallback).not.toBeNull();
|
|
2516
|
+
|
|
2517
|
+
// Simulate a merge request submission
|
|
2518
|
+
mergeQueueCallback!({
|
|
2519
|
+
type: "mr:submitted",
|
|
2520
|
+
data: {
|
|
2521
|
+
mrId: "mr-123",
|
|
2522
|
+
workerAgentId: "worker-1",
|
|
2523
|
+
workerBranch: "worker/dev-1/task-1@123",
|
|
2524
|
+
},
|
|
2525
|
+
});
|
|
2526
|
+
|
|
2527
|
+
// Should have prompted the merger agent (companion, agent_1)
|
|
2528
|
+
expect(agentManager.prompt).toHaveBeenCalledWith(
|
|
2529
|
+
"agent_1", // merger is the companion agent
|
|
2530
|
+
expect.stringContaining("mr-123"),
|
|
2531
|
+
);
|
|
2532
|
+
|
|
2533
|
+
await runtime.teardown();
|
|
2534
|
+
});
|
|
2535
|
+
|
|
2536
|
+
it("creates integration stream during bootstrap when workspaceManager is available", async () => {
|
|
2537
|
+
const template = createWorkspaceTemplate();
|
|
2538
|
+
const mockWorkspaceManager = {
|
|
2539
|
+
createIntegrationStream: vi.fn().mockReturnValue("stream-42"),
|
|
2540
|
+
getMergeQueue: vi.fn().mockReturnValue(null),
|
|
2541
|
+
getWorkspace: vi.fn(),
|
|
2542
|
+
};
|
|
2543
|
+
|
|
2544
|
+
const services: TeamServices = {
|
|
2545
|
+
agentManager,
|
|
2546
|
+
messageRouter,
|
|
2547
|
+
eventStore,
|
|
2548
|
+
workspaceManager: mockWorkspaceManager as any,
|
|
2549
|
+
};
|
|
2550
|
+
|
|
2551
|
+
const runtime = new TeamRuntime(template, services);
|
|
2552
|
+
await runtime.initialize();
|
|
2553
|
+
await runtime.bootstrap();
|
|
2554
|
+
|
|
2555
|
+
expect(mockWorkspaceManager.createIntegrationStream).toHaveBeenCalledWith(
|
|
2556
|
+
"agent_0", // root agent ID
|
|
2557
|
+
{ name: "workspace-test", forkFrom: "main" },
|
|
2558
|
+
);
|
|
2559
|
+
expect(runtime.getTeamStreamId()).toBe("stream-42");
|
|
2560
|
+
|
|
2561
|
+
await runtime.teardown();
|
|
2562
|
+
});
|
|
2563
|
+
});
|