Skip to content
Closed
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
9 changes: 9 additions & 0 deletions .claude/rules/custom-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ Examples:

**When `parse_mode` returns `dispatchReady`, use it directly with the Task tool — no extra calls needed.**

**Strategy Selection (before dispatch):**
- [ ] Check `availableStrategies` in `parse_mode` response
- [ ] If `["subagent", "taskmaestro"]` → AskUserQuestion to choose
- [ ] If `taskmaestroInstallHint` present and user wants taskmaestro → guide installation
- [ ] Pass chosen strategy to `dispatch_agents(executionStrategy: ...)`

**Quick Checklist (Auto-Dispatch - Preferred):**
- [ ] Check `dispatchReady` in `parse_mode` response
- [ ] Use `dispatchReady.primaryAgent.dispatchParams` with Task tool
Expand All @@ -143,6 +149,9 @@ Examples:
| **EVAL** | 🔒 security, ♿ accessibility, ⚡ performance, 📏 code-quality, 📨 event-architecture, 🔗 integration, 📊 observability, 🔄 migration |
| **AUTO** | 🏛️ architecture, 🧪 test-strategy, 🔒 security, 📏 code-quality, 📨 event-architecture, 🔗 integration, 📊 observability, 🔄 migration |

> **Note:** All modes support both SubAgent and TaskMaestro execution strategies.
> The strategy is selected per-invocation via user choice.

**📖 Full Guide:** [Parallel Specialist Agents Execution](../../packages/rules/.ai-rules/adapters/claude-code.md#parallel-specialist-agents-execution)

</PARALLEL_EXECUTION_MANDATORY_RULE>
Expand Down
110 changes: 110 additions & 0 deletions apps/mcp-server/src/agent/agent.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,4 +578,114 @@ describe('AgentService', () => {
expect(result.parallelAgents).toHaveLength(1);
});
});

describe('execution strategy integration', () => {
it('subagent strategy returns parallelAgents with dispatchParams and run_in_background', async () => {
vi.mocked(mockRulesService.getAgent!)
.mockResolvedValueOnce(mockSecurityAgent)
.mockResolvedValueOnce(mockAccessibilityAgent);

const result = await service.dispatchAgents({
mode: 'EVAL',
specialists: ['security-specialist', 'accessibility-specialist'],
executionStrategy: 'subagent',
includeParallel: true,
});

expect(result.executionStrategy).toBe('subagent');
expect(result.parallelAgents).toBeDefined();
expect(result.parallelAgents!.length).toBe(2);
result.parallelAgents!.forEach(agent => {
expect(agent.dispatchParams).toBeDefined();
expect(agent.dispatchParams.subagent_type).toBe('general-purpose');
expect(agent.dispatchParams.run_in_background).toBe(true);
expect(agent.dispatchParams.prompt).toBeTruthy();
});
expect(result.taskmaestro).toBeUndefined();
});

it('taskmaestro strategy returns assignments without dispatchParams', async () => {
vi.mocked(mockRulesService.getAgent!)
.mockResolvedValueOnce(mockSecurityAgent)
.mockResolvedValueOnce(mockAccessibilityAgent);

const result = await service.dispatchAgents({
mode: 'EVAL',
specialists: ['security-specialist', 'accessibility-specialist'],
executionStrategy: 'taskmaestro',
});

expect(result.executionStrategy).toBe('taskmaestro');
expect(result.taskmaestro).toBeDefined();
expect(result.taskmaestro!.paneCount).toBe(2);
expect(result.taskmaestro!.assignments).toHaveLength(2);
result.taskmaestro!.assignments.forEach(assignment => {
expect(assignment.name).toBeTruthy();
expect(assignment.displayName).toBeTruthy();
expect(assignment.prompt).toBeTruthy();
});
expect(result.parallelAgents).toBeUndefined();
});

it('backward compatibility: no executionStrategy defaults to subagent behavior', async () => {
vi.mocked(mockRulesService.getAgent!).mockResolvedValue(mockSecurityAgent);

const result = await service.dispatchAgents({
mode: 'EVAL',
specialists: ['security-specialist'],
includeParallel: true,
// NO executionStrategy — must default to subagent
});

expect(result.executionStrategy).toBe('subagent');
expect(result.parallelAgents).toBeDefined();
expect(result.taskmaestro).toBeUndefined();
});

it('taskmaestro sessionName reflects the mode', async () => {
const modes = ['PLAN', 'ACT', 'EVAL', 'AUTO'] as const;
for (const mode of modes) {
vi.mocked(mockRulesService.getAgent!).mockResolvedValue(mockSecurityAgent);

const result = await service.dispatchAgents({
mode,
specialists: ['security-specialist'],
executionStrategy: 'taskmaestro',
});
expect(result.taskmaestro!.sessionName).toBe(`${mode.toLowerCase()}-specialists`);
}
});

it('taskmaestro prompt includes Output Format instructions', async () => {
vi.mocked(mockRulesService.getAgent!).mockResolvedValue(mockSecurityAgent);

const result = await service.dispatchAgents({
mode: 'EVAL',
specialists: ['security-specialist'],
executionStrategy: 'taskmaestro',
});

const prompt = result.taskmaestro!.assignments[0].prompt;
expect(prompt).toContain('Severity: CRITICAL / HIGH / MEDIUM / LOW / INFO');
expect(prompt).toContain('File reference');
expect(prompt).toContain('Recommendation');
});

it('taskmaestro executionHint includes all required commands', async () => {
vi.mocked(mockRulesService.getAgent!)
.mockResolvedValueOnce(mockSecurityAgent)
.mockResolvedValueOnce(mockAccessibilityAgent);

const result = await service.dispatchAgents({
mode: 'EVAL',
specialists: ['security-specialist', 'accessibility-specialist'],
executionStrategy: 'taskmaestro',
});

expect(result.executionHint).toContain('/taskmaestro start --panes 2');
expect(result.executionHint).toContain('/taskmaestro assign');
expect(result.executionHint).toContain('/taskmaestro status');
expect(result.executionHint).toContain('/taskmaestro stop all');
});
});
});
55 changes: 46 additions & 9 deletions apps/mcp-server/src/agent/agent.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import type {
DispatchAgentsInput,
DispatchResult,
DispatchedAgent,
ExecutionStrategy,
TaskMaestroAssignment,
} from './agent.types';
import { FILE_PATTERN_SPECIALISTS } from './agent.types';
import {
Expand Down Expand Up @@ -191,18 +193,20 @@ export class AgentService {
* Claude Code's Task tool, eliminating the need for manual prompt assembly.
*/
async dispatchAgents(input: DispatchAgentsInput): Promise<DispatchResult> {
const strategy: ExecutionStrategy = input.executionStrategy || 'subagent';
const context: AgentContext = {
mode: input.mode,
targetFiles: input.targetFiles,
taskDescription: input.taskDescription,
};

const result: DispatchResult = {
executionStrategy: strategy,
executionHint: buildParallelExecutionHint(),
};

// Dispatch primary agent
if (input.primaryAgent) {
// Dispatch primary agent (subagent strategy only)
if (strategy === 'subagent' && input.primaryAgent) {
try {
const agentPrompt = await this.getAgentSystemPrompt(input.primaryAgent, context);
result.primaryAgent = {
Expand All @@ -222,14 +226,34 @@ export class AgentService {
}
}

// Dispatch parallel agents
if (input.includeParallel && input.specialists?.length) {
if (strategy === 'taskmaestro' && input.specialists?.length) {
// TaskMaestro strategy: return tmux assignments
const uniqueSpecialists = Array.from(new Set(input.specialists));
const { agents, failedAgents } = await this.loadAgents(
uniqueSpecialists,
context,
true, // always include full prompt for dispatch
);
const { agents, failedAgents } = await this.loadAgents(uniqueSpecialists, context, true);

const assignments: TaskMaestroAssignment[] = agents.map(agent => ({
name: agent.id,
displayName: agent.displayName,
prompt: this.buildTaskMaestroPrompt(
agent.taskPrompt || `Perform ${agent.displayName} analysis in ${input.mode} mode`,
),
}));

const sessionName = `${input.mode.toLowerCase()}-specialists`;
result.taskmaestro = {
sessionName,
paneCount: assignments.length,
assignments,
};
result.executionHint = this.buildTaskMaestroHint(sessionName, assignments.length);

if (failedAgents.length > 0) {
result.failedAgents = failedAgents;
}
} else if (strategy === 'subagent' && input.includeParallel && input.specialists?.length) {
// SubAgent strategy: existing behavior
const uniqueSpecialists = Array.from(new Set(input.specialists));
const { agents, failedAgents } = await this.loadAgents(uniqueSpecialists, context, true);

result.parallelAgents = agents.map(
(agent): DispatchedAgent => ({
Expand All @@ -253,4 +277,17 @@ export class AgentService {

return result;
}

private buildTaskMaestroPrompt(basePrompt: string): string {
return `${basePrompt}\n\n## Output Format\n\nFor each finding, include:\n- Severity: CRITICAL / HIGH / MEDIUM / LOW / INFO\n- File reference\n- Recommendation`;
}

private buildTaskMaestroHint(sessionName: string, paneCount: number): string {
return [
`1. /taskmaestro start --panes ${paneCount}`,
`2. /taskmaestro assign --session ${sessionName}`,
`3. /taskmaestro status`,
`4. /taskmaestro stop all`,
].join('\n');
}
}
18 changes: 18 additions & 0 deletions apps/mcp-server/src/agent/agent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,30 @@ export interface DispatchedAgent {
dispatchParams: DispatchParams;
}

export type ExecutionStrategy = 'subagent' | 'taskmaestro';

export interface TaskMaestroAssignment {
name: string;
displayName: string;
prompt: string;
}

export interface TaskMaestroResult {
sessionName: string;
paneCount: number;
assignments: TaskMaestroAssignment[];
}

/**
* Result of dispatching agents for execution
*/
export interface DispatchResult {
primaryAgent?: DispatchedAgent;
parallelAgents?: DispatchedAgent[];
executionStrategy: ExecutionStrategy;
executionHint: string;
/** TaskMaestro-specific result (only when executionStrategy is 'taskmaestro') */
taskmaestro?: TaskMaestroResult;
/** Agents that failed to load */
failedAgents?: FailedAgent[];
}
Expand All @@ -94,6 +111,7 @@ export interface DispatchAgentsInput {
specialists?: string[];
includeParallel?: boolean;
primaryAgent?: string;
executionStrategy?: ExecutionStrategy;
}

/**
Expand Down
11 changes: 11 additions & 0 deletions apps/mcp-server/src/mcp/handlers/agent.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (default: subagent). Use "taskmaestro" for tmux-based parallel execution.',
},
},
required: ['mode'],
},
Expand All @@ -182,6 +188,10 @@ export class AgentHandler extends AbstractHandler {
const targetFiles = extractStringArray(args, 'targetFiles');
const taskDescription = extractOptionalString(args, 'taskDescription');
const includeParallel = args?.includeParallel === true;
const executionStrategy = extractOptionalString(args, 'executionStrategy') as
| 'subagent'
| 'taskmaestro'
| undefined;

try {
const result = await this.agentService.dispatchAgents({
Expand All @@ -191,6 +201,7 @@ export class AgentHandler extends AbstractHandler {
targetFiles,
taskDescription,
includeParallel,
executionStrategy,
});
return createJsonResponse(result);
} catch (error) {
Expand Down
39 changes: 39 additions & 0 deletions packages/rules/.ai-rules/adapters/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,45 @@ Each workflow mode activates different specialist agents:

**Important:** Specialists from one mode do NOT carry over to the next mode. Each mode has its own recommended specialist set.

### Execution Strategy Selection (MANDATORY)

When `parse_mode` returns `availableStrategies`:

1. **Check `availableStrategies`** in the response
2. **If both strategies available** (`["subagent", "taskmaestro"]`), ask user with AskUserQuestion:
- Option A: "SubAgent (background agents, fast)" (Recommended)
- Option B: "TaskMaestro (tmux parallel panes, visual monitoring)"
3. **If only `["subagent"]`** and `taskmaestroInstallHint` present:
- Ask: "TaskMaestro is not installed. Would you like to install it for tmux-based parallel execution?"
- Yes → invoke `/taskmaestro` skill to guide installation, then re-check
- No → proceed with subagent
4. **Call `dispatch_agents`** with chosen `executionStrategy` parameter:
- `dispatch_agents({ mode, specialists, executionStrategy: "subagent" })` — existing Agent tool flow
- `dispatch_agents({ mode, specialists, executionStrategy: "taskmaestro" })` — returns tmux assignments
5. **Execute** based on strategy:
- **subagent**: Use `dispatchParams` with Agent tool (`run_in_background: true`)
- **taskmaestro**: Follow `executionHint` — start panes, assign prompts, monitor, collect results

### TaskMaestro Execution Flow

When `executionStrategy: "taskmaestro"` is chosen, `dispatch_agents` returns:

```json
{
"taskmaestro": {
"sessionName": "eval-specialists",
"paneCount": 5,
"assignments": [
{ "name": "security-specialist", "displayName": "Security Specialist", "prompt": "..." },
{ "name": "performance-specialist", "displayName": "Performance Specialist", "prompt": "..." }
]
},
"executionHint": "1. /taskmaestro start --panes 5\n2. ..."
}
```

Execute by following the `executionHint` commands sequentially.

## PR All-in-One Skill

Unified commit and PR workflow that:
Expand Down
14 changes: 14 additions & 0 deletions scripts/bump-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,20 @@ node -e "
"
echo " ✅ packages/claude-code-plugin/.claude-plugin/plugin.json"

# 6. .mcp.json (if exists — gitignored, local only)
if [ -f ".mcp.json" ]; then
node -e "
const fs = require('fs');
const p = '.mcp.json';
const content = fs.readFileSync(p, 'utf-8');
const updated = content.replace(/codingbuddy@[0-9]+\.[0-9]+\.[0-9]+/, 'codingbuddy@$NEW_VERSION');
fs.writeFileSync(p, updated);
"
echo " ✅ .mcp.json"
else
echo " ⏭️ .mcp.json (not found, skipped)"
fi

echo ""
echo "✅ All files bumped to v$NEW_VERSION"
echo " Next: git commit -am \"chore(release): prepare v$NEW_VERSION\""
17 changes: 17 additions & 0 deletions scripts/verify-release-versions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,23 @@ for package_info in "${PACKAGES[@]}"; do
fi
done

# Check .mcp.json (gitignored, local only)
if [ -f ".mcp.json" ]; then
mcp_version=$(node -e "
const content = require('fs').readFileSync('.mcp.json', 'utf-8');
const match = content.match(/codingbuddy@([0-9]+\.[0-9]+\.[0-9]+)/);
console.log(match ? match[1] : '');
")
if [ "$mcp_version" = "$TAG_VERSION" ]; then
echo "✅ .mcp.json codingbuddy: @$mcp_version (matches tag)"
else
echo "❌ .mcp.json codingbuddy: @$mcp_version (tag is v$TAG_VERSION)"
ALL_MATCH=false
fi
else
echo "⏭️ .mcp.json (not found, skipped)"
fi

echo ""

if [ "$ALL_MATCH" = true ]; then
Expand Down
Loading