diff --git a/apps/mcp-server/src/agent/agent.service.ts b/apps/mcp-server/src/agent/agent.service.ts index 0a3f7e33..08d1e901 100644 --- a/apps/mcp-server/src/agent/agent.service.ts +++ b/apps/mcp-server/src/agent/agent.service.ts @@ -11,6 +11,7 @@ import type { DispatchResult, DispatchedAgent, TaskmaestroAssignment, + TeamsTeammate, } from './agent.types'; import { FILE_PATTERN_SPECIALISTS } from './agent.types'; import { @@ -247,6 +248,32 @@ export class AgentService { }; } + // Dispatch teams strategy + if (input.executionStrategy === 'teams' && input.specialists?.length) { + const uniqueSpecialists = Array.from(new Set(input.specialists)); + const teamName = `${(input.mode ?? 'eval').toLowerCase()}-specialists`; + const { agents, failedAgents } = await this.loadAgents(uniqueSpecialists, context, true); + + const teammates: TeamsTeammate[] = agents.map(agent => ({ + name: agent.id, + subagent_type: 'general-purpose' as const, + team_name: teamName, + prompt: agent.taskPrompt || `Perform ${agent.displayName} analysis in ${input.mode} mode`, + })); + + return { + primaryAgent: result.primaryAgent, + teams: { + team_name: teamName, + description: `${input.mode} mode specialist team`, + teammates, + }, + executionStrategy: 'teams', + executionHint: this.buildTeamsHint(teamName, teammates.length), + failedAgents: failedAgents.length > 0 ? failedAgents : result.failedAgents, + }; + } + // Dispatch parallel agents (subagent strategy) if (input.includeParallel && input.specialists?.length) { const uniqueSpecialists = Array.from(new Set(input.specialists)); @@ -306,4 +333,14 @@ When done, provide a summary of all findings.`; 5. When all panes show idle: collect results 6. /taskmaestro stop all — cleanup`; } + + private buildTeamsHint(teamName: string, teammateCount: number): string { + return `Teams execution: +1. TeamCreate: { team_name: "${teamName}" } +2. Spawn ${teammateCount} teammate(s) via Agent tool with team_name: "${teamName}" +3. Create tasks via TaskCreate and assign with TaskUpdate +4. Monitor via TaskList — teammates coordinate via shared task list +5. Collect results when all teammates go idle +6. Shutdown teammates via SendMessage with shutdown_request`; + } } diff --git a/apps/mcp-server/src/agent/agent.types.ts b/apps/mcp-server/src/agent/agent.types.ts index c097bb1c..1ca0ecf2 100644 --- a/apps/mcp-server/src/agent/agent.types.ts +++ b/apps/mcp-server/src/agent/agent.types.ts @@ -91,6 +91,25 @@ export interface TaskmaestroDispatch { assignments: TaskmaestroAssignment[]; } +/** + * A single teammate in a Teams dispatch configuration + */ +export interface TeamsTeammate { + name: string; + subagent_type: 'general-purpose'; + team_name: string; + prompt: string; +} + +/** + * Teams dispatch configuration for Claude Code native team coordination + */ +export interface TeamsDispatch { + team_name: string; + description: string; + teammates: TeamsTeammate[]; +} + /** * Result of dispatching agents for execution */ @@ -102,6 +121,8 @@ export interface DispatchResult { failedAgents?: FailedAgent[]; /** TaskMaestro dispatch data when executionStrategy is 'taskmaestro' */ taskmaestro?: TaskmaestroDispatch; + /** Teams dispatch data when executionStrategy is 'teams' */ + teams?: TeamsDispatch; /** Execution strategy used for this dispatch */ executionStrategy?: string; } @@ -116,8 +137,8 @@ export interface DispatchAgentsInput { specialists?: string[]; includeParallel?: boolean; primaryAgent?: string; - /** Execution strategy: 'subagent' (default) or 'taskmaestro' */ - executionStrategy?: 'subagent' | 'taskmaestro'; + /** Execution strategy: 'subagent' (default), 'taskmaestro', or 'teams' */ + executionStrategy?: 'subagent' | 'taskmaestro' | 'teams'; } /** diff --git a/apps/mcp-server/src/mcp/handlers/agent.handler.spec.ts b/apps/mcp-server/src/mcp/handlers/agent.handler.spec.ts index c75acc77..6c006080 100644 --- a/apps/mcp-server/src/mcp/handlers/agent.handler.spec.ts +++ b/apps/mcp-server/src/mcp/handlers/agent.handler.spec.ts @@ -466,6 +466,76 @@ describe('AgentHandler', () => { expect.objectContaining({ executionStrategy: 'subagent' }), ); }); + + it('should pass executionStrategy "teams" to service', async () => { + await handler.handle('dispatch_agents', { + mode: 'EVAL', + specialists: ['security-specialist', 'accessibility-specialist'], + executionStrategy: 'teams', + }); + expect(mockAgentService.dispatchAgents).toHaveBeenCalledWith( + expect.objectContaining({ executionStrategy: 'teams' }), + ); + }); + + it('should return teams dispatch data when strategy is "teams"', async () => { + const teamsResult = { + primaryAgent: mockDispatchResult.primaryAgent, + teams: { + team_name: 'eval-specialists', + description: 'EVAL mode specialist team', + teammates: [ + { + name: 'security-specialist', + subagent_type: 'general-purpose', + team_name: 'eval-specialists', + prompt: 'You are a security specialist...', + }, + ], + }, + executionStrategy: 'teams', + executionHint: 'Teams execution hint', + }; + mockAgentService.dispatchAgents = vi.fn().mockResolvedValue(teamsResult); + + const result = await handler.handle('dispatch_agents', { + mode: 'EVAL', + primaryAgent: 'security-specialist', + specialists: ['security-specialist'], + executionStrategy: 'teams', + }); + + expect(result?.isError).toBeFalsy(); + const content = JSON.parse(result?.content[0]?.text as string); + expect(content.teams).toBeDefined(); + expect(content.teams.team_name).toBe('eval-specialists'); + expect(content.teams.teammates).toHaveLength(1); + expect(content.teams.teammates[0].name).toBe('security-specialist'); + expect(content.executionStrategy).toBe('teams'); + }); + + it('should pass all parameters with teams strategy', async () => { + await handler.handle('dispatch_agents', { + mode: 'PLAN', + primaryAgent: 'solution-architect', + specialists: ['security-specialist', 'performance-specialist'], + targetFiles: ['src/app.ts'], + taskDescription: 'Review architecture', + includeParallel: true, + executionStrategy: 'teams', + }); + expect(mockAgentService.dispatchAgents).toHaveBeenCalledWith( + expect.objectContaining({ + mode: 'PLAN', + primaryAgent: 'solution-architect', + specialists: ['security-specialist', 'performance-specialist'], + targetFiles: ['src/app.ts'], + taskDescription: 'Review architecture', + includeParallel: true, + executionStrategy: 'teams', + }), + ); + }); }); }); }); @@ -482,6 +552,17 @@ describe('AgentHandler', () => { ]); }); + it('should include teams in executionStrategy enum', () => { + const definitions = handler.getToolDefinitions(); + const dispatchAgents = definitions.find(d => d.name === 'dispatch_agents'); + const strategyEnum = ( + dispatchAgents?.inputSchema.properties as Record + )?.executionStrategy?.enum; + expect(strategyEnum).toContain('teams'); + expect(strategyEnum).toContain('subagent'); + expect(strategyEnum).toContain('taskmaestro'); + }); + it('should have correct required parameters', () => { const definitions = handler.getToolDefinitions(); diff --git a/apps/mcp-server/src/mcp/handlers/agent.handler.ts b/apps/mcp-server/src/mcp/handlers/agent.handler.ts index a65e71b8..da6a5a6a 100644 --- a/apps/mcp-server/src/mcp/handlers/agent.handler.ts +++ b/apps/mcp-server/src/mcp/handlers/agent.handler.ts @@ -160,9 +160,9 @@ export class AgentHandler extends AbstractHandler { }, executionStrategy: { type: 'string', - enum: ['subagent', 'taskmaestro'], + enum: ['subagent', 'taskmaestro', 'teams'], description: - 'Execution strategy for specialist agents. "subagent" (default) uses Claude Code Agent tool with run_in_background. "taskmaestro" returns tmux pane assignments for /taskmaestro skill.', + 'Execution strategy for specialist agents. "subagent" (default) uses Claude Code Agent tool with run_in_background. "taskmaestro" returns tmux pane assignments for /taskmaestro skill. "teams" uses Claude Code native teams with shared TaskList coordination.', }, }, required: ['mode'], @@ -189,7 +189,7 @@ export class AgentHandler extends AbstractHandler { const taskDescription = extractOptionalString(args, 'taskDescription'); const includeParallel = args?.includeParallel === true; const executionStrategy = - (args?.executionStrategy as 'subagent' | 'taskmaestro' | undefined) ?? 'subagent'; + (args?.executionStrategy as 'subagent' | 'taskmaestro' | 'teams' | undefined) ?? 'subagent'; try { const result = await this.agentService.dispatchAgents({