diff --git a/apps/mcp-server/src/agent/agent.types.spec.ts b/apps/mcp-server/src/agent/agent.types.spec.ts new file mode 100644 index 0000000..72a6bcd --- /dev/null +++ b/apps/mcp-server/src/agent/agent.types.spec.ts @@ -0,0 +1,82 @@ +import { describe, it, expect } from 'vitest'; +import type { + TaskmaestroAssignment, + TaskmaestroDispatch, + DispatchAgentsInput, + DispatchResult, +} from './agent.types'; + +describe('agent.types - TaskMaestro types', () => { + describe('TaskmaestroAssignment', () => { + it('accepts object with name, displayName, prompt', () => { + const assignment: TaskmaestroAssignment = { + name: 'frontend-dev', + displayName: 'Frontend Developer', + prompt: 'Implement the UI component', + }; + expect(assignment.name).toBe('frontend-dev'); + expect(assignment.displayName).toBe('Frontend Developer'); + expect(assignment.prompt).toBe('Implement the UI component'); + }); + }); + + describe('TaskmaestroDispatch', () => { + it('accepts object with sessionName, paneCount, assignments', () => { + const dispatch: TaskmaestroDispatch = { + sessionName: 'workspace-1', + paneCount: 3, + assignments: [{ name: 'dev-1', displayName: 'Dev 1', prompt: 'Task 1' }], + }; + expect(dispatch.sessionName).toBe('workspace-1'); + expect(dispatch.paneCount).toBe(3); + expect(dispatch.assignments).toHaveLength(1); + }); + }); + + describe('DispatchAgentsInput - executionStrategy', () => { + it('accepts executionStrategy: subagent', () => { + const input: DispatchAgentsInput = { + mode: 'PLAN', + executionStrategy: 'subagent', + }; + expect(input.executionStrategy).toBe('subagent'); + }); + + it('accepts executionStrategy: taskmaestro', () => { + const input: DispatchAgentsInput = { + mode: 'ACT', + executionStrategy: 'taskmaestro', + }; + expect(input.executionStrategy).toBe('taskmaestro'); + }); + + it('executionStrategy is optional', () => { + const input: DispatchAgentsInput = { mode: 'PLAN' }; + expect(input.executionStrategy).toBeUndefined(); + }); + }); + + describe('DispatchResult - taskmaestro fields', () => { + it('accepts optional taskmaestro dispatch data', () => { + const result: DispatchResult = { + executionHint: 'Use taskmaestro for parallel execution', + taskmaestro: { + sessionName: 'ws-1', + paneCount: 2, + assignments: [], + }, + executionStrategy: 'taskmaestro', + }; + expect(result.taskmaestro?.sessionName).toBe('ws-1'); + expect(result.executionStrategy).toBe('taskmaestro'); + }); + + it('taskmaestro and executionStrategy are optional', () => { + const result: DispatchResult = { + executionHint: 'Use subagent', + }; + expect(result.taskmaestro).toBeUndefined(); + expect(result.executionStrategy).toBeUndefined(); + }); + }); +}); diff --git a/apps/mcp-server/src/agent/agent.types.ts b/apps/mcp-server/src/agent/agent.types.ts index 58c943f..c097bb1 100644 --- a/apps/mcp-server/src/agent/agent.types.ts +++ b/apps/mcp-server/src/agent/agent.types.ts @@ -73,6 +73,24 @@ export interface DispatchedAgent { dispatchParams: DispatchParams; } +/** + * A single TaskMaestro pane assignment with agent name and prompt + */ +export interface TaskmaestroAssignment { + name: string; + displayName: string; + prompt: string; +} + +/** + * TaskMaestro dispatch configuration for parallel tmux pane execution + */ +export interface TaskmaestroDispatch { + sessionName: string; + paneCount: number; + assignments: TaskmaestroAssignment[]; +} + /** * Result of dispatching agents for execution */ @@ -82,6 +100,10 @@ export interface DispatchResult { executionHint: string; /** Agents that failed to load */ failedAgents?: FailedAgent[]; + /** TaskMaestro dispatch data when executionStrategy is 'taskmaestro' */ + taskmaestro?: TaskmaestroDispatch; + /** Execution strategy used for this dispatch */ + executionStrategy?: string; } /** @@ -94,6 +116,8 @@ export interface DispatchAgentsInput { specialists?: string[]; includeParallel?: boolean; primaryAgent?: string; + /** Execution strategy: 'subagent' (default) or 'taskmaestro' */ + executionStrategy?: 'subagent' | 'taskmaestro'; } /** diff --git a/apps/mcp-server/src/keyword/keyword.service.spec.ts b/apps/mcp-server/src/keyword/keyword.service.spec.ts index 76c9774..c05ccd9 100644 --- a/apps/mcp-server/src/keyword/keyword.service.spec.ts +++ b/apps/mcp-server/src/keyword/keyword.service.spec.ts @@ -2677,4 +2677,31 @@ ${'Even more content.\n'.repeat(150)}`; expect(result.rules[0].content).toContain('## Section'); }); }); + + describe('taskmaestro detection', () => { + it('should include taskmaestro in availableStrategies when skill is available', async () => { + const detector = await import('./taskmaestro-detector'); + const spy = vi.spyOn(detector, 'isTaskmaestroAvailable').mockReturnValue(true); + + const result = await service.parseMode('PLAN test task', {}); + + expect(result.availableStrategies).toContain('subagent'); + expect(result.availableStrategies).toContain('taskmaestro'); + expect(result.taskmaestroInstallHint).toBeUndefined(); + + spy.mockRestore(); + }); + + it('should return only subagent with installHint when taskmaestro not installed', async () => { + const detector = await import('./taskmaestro-detector'); + const spy = vi.spyOn(detector, 'isTaskmaestroAvailable').mockReturnValue(false); + + const result = await service.parseMode('PLAN test task', {}); + + expect(result.availableStrategies).toEqual(['subagent']); + expect(result.taskmaestroInstallHint).toContain('TaskMaestro skill not found'); + + spy.mockRestore(); + }); + }); }); diff --git a/apps/mcp-server/src/keyword/keyword.service.ts b/apps/mcp-server/src/keyword/keyword.service.ts index 04e9415..081ab6e 100644 --- a/apps/mcp-server/src/keyword/keyword.service.ts +++ b/apps/mcp-server/src/keyword/keyword.service.ts @@ -32,6 +32,7 @@ import { truncateSkillContent } from '../skill/skill-content.utils'; import { createAgentSummary } from '../agent/agent-summary.utils'; import { truncateRuleContent } from '../rules/rules-content.utils'; import { getDefaultModeConfig } from '../shared/keyword-core'; +import { isTaskmaestroAvailable } from './taskmaestro-detector'; import { type ClientType } from '../shared/client-type'; /** @@ -496,6 +497,14 @@ export class KeywordService { // 9. Auto-include primary agent system prompt (for MCP mode to force AI execution) await this.addIncludedAgentToResult(result, mode, options); + // 10. Add available execution strategies (subagent, taskmaestro) + const taskmaestroInstalled = isTaskmaestroAvailable(); + result.availableStrategies = taskmaestroInstalled ? ['subagent', 'taskmaestro'] : ['subagent']; + if (!taskmaestroInstalled) { + result.taskmaestroInstallHint = + 'TaskMaestro skill not found at ~/.claude/skills/taskmaestro/SKILL.md. To enable tmux-based parallel specialist execution, install the taskmaestro skill.'; + } + return result; } diff --git a/apps/mcp-server/src/keyword/keyword.types.ts b/apps/mcp-server/src/keyword/keyword.types.ts index c2e9eda..fd9d718 100644 --- a/apps/mcp-server/src/keyword/keyword.types.ts +++ b/apps/mcp-server/src/keyword/keyword.types.ts @@ -440,6 +440,10 @@ export interface ParseModeResult { * without needing to call dispatch_agents or prepare_parallel_agents. */ dispatchReady?: DispatchReady; + /** @apiProperty External API - do not rename. Available execution strategies (e.g. ['subagent', 'taskmaestro']) */ + availableStrategies?: string[]; + /** @apiProperty External API - do not rename. Hint for installing TaskMaestro when not available */ + taskmaestroInstallHint?: string; } /** diff --git a/apps/mcp-server/src/keyword/taskmaestro-detector.ts b/apps/mcp-server/src/keyword/taskmaestro-detector.ts new file mode 100644 index 0000000..4d345c4 --- /dev/null +++ b/apps/mcp-server/src/keyword/taskmaestro-detector.ts @@ -0,0 +1,16 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +/** + * Check if the TaskMaestro skill is installed at the expected path. + * @returns true if ~/.claude/skills/taskmaestro/SKILL.md exists + */ +export function isTaskmaestroAvailable(): boolean { + const skillPath = path.join(os.homedir(), '.claude', 'skills', 'taskmaestro', 'SKILL.md'); + try { + return fs.existsSync(skillPath); + } catch { + return false; + } +} 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 29159b6..c75acc7 100644 --- a/apps/mcp-server/src/mcp/handlers/agent.handler.spec.ts +++ b/apps/mcp-server/src/mcp/handlers/agent.handler.spec.ts @@ -433,6 +433,40 @@ describe('AgentHandler', () => { text: expect.stringContaining('Dispatch failed'), }); }); + + describe('executionStrategy parameter', () => { + it('should pass executionStrategy "subagent" to service', async () => { + await handler.handle('dispatch_agents', { + mode: 'EVAL', + specialists: ['security-specialist'], + executionStrategy: 'subagent', + }); + expect(mockAgentService.dispatchAgents).toHaveBeenCalledWith( + expect.objectContaining({ executionStrategy: 'subagent' }), + ); + }); + + it('should pass executionStrategy "taskmaestro" to service', async () => { + await handler.handle('dispatch_agents', { + mode: 'EVAL', + specialists: ['security-specialist'], + executionStrategy: 'taskmaestro', + }); + expect(mockAgentService.dispatchAgents).toHaveBeenCalledWith( + expect.objectContaining({ executionStrategy: 'taskmaestro' }), + ); + }); + + it('should default executionStrategy to "subagent" when not specified', async () => { + await handler.handle('dispatch_agents', { + mode: 'EVAL', + specialists: ['security-specialist'], + }); + expect(mockAgentService.dispatchAgents).toHaveBeenCalledWith( + expect.objectContaining({ executionStrategy: 'subagent' }), + ); + }); + }); }); }); diff --git a/apps/mcp-server/src/mcp/handlers/agent.handler.ts b/apps/mcp-server/src/mcp/handlers/agent.handler.ts index 6062419..a65e71b 100644 --- a/apps/mcp-server/src/mcp/handlers/agent.handler.ts +++ b/apps/mcp-server/src/mcp/handlers/agent.handler.ts @@ -158,6 +158,12 @@ export class AgentHandler extends AbstractHandler { type: 'boolean', description: 'Whether to include parallel specialist agents (default: false)', }, + executionStrategy: { + type: 'string', + enum: ['subagent', 'taskmaestro'], + 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.', + }, }, required: ['mode'], }, @@ -182,6 +188,8 @@ export class AgentHandler extends AbstractHandler { const targetFiles = extractStringArray(args, 'targetFiles'); const taskDescription = extractOptionalString(args, 'taskDescription'); const includeParallel = args?.includeParallel === true; + const executionStrategy = + (args?.executionStrategy as 'subagent' | 'taskmaestro' | undefined) ?? 'subagent'; try { const result = await this.agentService.dispatchAgents({ @@ -191,6 +199,7 @@ export class AgentHandler extends AbstractHandler { targetFiles, taskDescription, includeParallel, + executionStrategy, }); return createJsonResponse(result); } catch (error) {