Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions apps/mcp-server/src/agent/agent.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
DispatchResult,
DispatchedAgent,
TaskmaestroAssignment,
TeamsTeammate,
} from './agent.types';
import { FILE_PATTERN_SPECIALISTS } from './agent.types';
import {
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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`;
}
}
25 changes: 23 additions & 2 deletions apps/mcp-server/src/agent/agent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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;
}
Expand All @@ -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';
}

/**
Expand Down
81 changes: 81 additions & 0 deletions apps/mcp-server/src/mcp/handlers/agent.handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}),
);
});
});
});
});
Expand All @@ -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<string, { enum?: string[] }>
)?.executionStrategy?.enum;
expect(strategyEnum).toContain('teams');
expect(strategyEnum).toContain('subagent');
expect(strategyEnum).toContain('taskmaestro');
});

it('should have correct required parameters', () => {
const definitions = handler.getToolDefinitions();

Expand Down
6 changes: 3 additions & 3 deletions apps/mcp-server/src/mcp/handlers/agent.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand All @@ -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({
Expand Down
Loading