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
12 changes: 6 additions & 6 deletions apps/mcp-server/src/agent/agent-stack.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('AgentStackService', () => {
description: 'API development stack',
category: 'development',
primary_agent: 'backend-developer',
specialists: ['security-specialist', 'test-engineer'],
specialist_agents: ['security-specialist', 'test-engineer'],
tags: ['api', 'backend'],
};

Expand All @@ -32,7 +32,7 @@ describe('AgentStackService', () => {
description: 'Frontend review stack',
category: 'review',
primary_agent: 'frontend-developer',
specialists: ['accessibility-specialist', 'performance-specialist'],
specialist_agents: ['accessibility-specialist', 'performance-specialist'],
tags: ['frontend', 'ui'],
};

Expand Down Expand Up @@ -79,7 +79,7 @@ describe('AgentStackService', () => {
const customStack = {
...sampleStack,
description: 'Custom API stack',
specialists: ['security-specialist'],
specialist_agents: ['security-specialist'],
};

mockReaddir.mockImplementation(async (dirPath: string) => {
Expand Down Expand Up @@ -188,13 +188,13 @@ describe('AgentStackService', () => {

expect(stack.name).toBe('api-development');
expect(stack.primary_agent).toBe('backend-developer');
expect(stack.specialists).toEqual(['security-specialist', 'test-engineer']);
expect(stack.specialist_agents).toEqual(['security-specialist', 'test-engineer']);
});

it('should prefer custom stack over default', async () => {
const customStack = {
...sampleStack,
specialists: ['security-specialist', 'test-engineer', 'code-quality-specialist'],
specialist_agents: ['security-specialist', 'test-engineer', 'code-quality-specialist'],
};

mockReadFile.mockImplementation(async (filePath: string) => {
Expand All @@ -207,7 +207,7 @@ describe('AgentStackService', () => {

const stack = await service.resolveStack('api-development');

expect(stack.specialists).toHaveLength(3);
expect(stack.specialist_agents).toHaveLength(3);
});

it('should throw when stack not found', async () => {
Expand Down
6 changes: 3 additions & 3 deletions apps/mcp-server/src/agent/agent-stack.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class AgentStackService {
typeof obj.description === 'string' &&
typeof obj.category === 'string' &&
typeof obj.primary_agent === 'string' &&
Array.isArray(obj.specialists)
Array.isArray(obj.specialist_agents)
);
}

Expand All @@ -114,8 +114,8 @@ export class AgentStackService {
description: stack.description,
category: stack.category,
primary_agent: stack.primary_agent,
specialist_count: stack.specialists.length,
tags: stack.tags ?? [],
specialist_count: stack.specialist_agents.length,
tags: stack.tags,
};
}
}
12 changes: 0 additions & 12 deletions apps/mcp-server/src/agent/agent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,18 +184,6 @@ export interface InlineAgentDefinition {
[key: string]: unknown;
}

/**
* Agent stack definition — a preset combination of primary agent + specialists
*/
export interface AgentStack {
name: string;
description: string;
category: string;
primary_agent: string;
specialists: string[];
tags?: string[];
}

/**
* Summary of an agent stack for listing
*/
Expand Down
2 changes: 1 addition & 1 deletion apps/mcp-server/src/mcp/handlers/agent.handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -977,7 +977,7 @@ describe('AgentHandler', () => {
description: 'API development stack',
category: 'development',
primary_agent: 'backend-developer',
specialists: ['security-specialist', 'test-engineer'],
specialist_agents: ['security-specialist', 'test-engineer'],
tags: ['api', 'backend'],
};

Expand Down
4 changes: 2 additions & 2 deletions apps/mcp-server/src/mcp/handlers/agent.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,8 @@ export class AgentHandler extends AbstractHandler {
try {
const stack = await this.agentStackService.resolveStack(agentStack);
primaryAgent = primaryAgent ?? stack.primary_agent;
specialists = specialists?.length ? specialists : stack.specialists;
includeParallel = includeParallel || stack.specialists.length > 0;
specialists = specialists?.length ? specialists : stack.specialist_agents;
includeParallel = includeParallel || stack.specialist_agents.length > 0;
} catch (error) {
return createErrorResponse(
`Failed to resolve agent stack: ${error instanceof Error ? error.message : 'Unknown error'}`,
Expand Down
177 changes: 177 additions & 0 deletions packages/claude-code-plugin/hooks/lib/mode_engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""
Self-contained Mode Engine for CodingBuddy plugin.

Provides PLAN/ACT/EVAL/AUTO mode instructions without requiring MCP server.
Reads .ai-rules/ files directly and outputs complete mode instructions.
"""

import os
from typing import Optional


# Default agents per mode
DEFAULT_AGENTS = {
"PLAN": {"name": "technical-planner", "title": "Technical Planner"},
"ACT": {"name": "software-engineer", "title": "Software Engineer"},
"EVAL": {"name": "code-reviewer", "title": "Code Reviewer"},
"AUTO": {"name": "auto-mode-agent", "title": "Auto Mode Agent"},
}

# Mode instruction templates (compact, within ~2000 char hook limit)
MODE_TEMPLATES = {
"PLAN": """# Mode: PLAN
## Agent: {agent_name}

You are in PLAN mode. Design the implementation approach.

Rules:
- Define test cases first (TDD perspective)
- Review architecture before implementation
- Output full plan in every response
- Do NOT auto-proceed to ACT — wait for user
- Consider alternatives for non-trivial decisions

Checklist:
- [ ] Problem decomposed into sub-problems
- [ ] File paths identified
- [ ] TDD strategy defined
- [ ] Alternatives considered""",
"ACT": """# Mode: ACT
## Agent: {agent_name}

You are in ACT mode. Execute the plan.

Rules:
- Red -> Green -> Refactor cycle
- Implement minimally first
- Run tests after each change
- Proceed autonomously until blocked
- Only stop for errors or blockers""",
"EVAL": """# Mode: EVAL
## Agent: {agent_name}

You are in EVAL mode. Review and improve.

Rules:
- Check code quality (SOLID, DRY, complexity)
- Verify test coverage
- Security scan (OWASP top 10)
- Performance review
- Propose concrete improvements""",
"AUTO": """# Mode: AUTO
Autonomous PLAN -> ACT -> EVAL cycle.
Continue until: Critical=0, High=0.
Report progress at each cycle iteration.""",
}


def _resolve_rules_dir(cwd: Optional[str] = None) -> Optional[str]:
"""
Resolve path to .ai-rules/ directory.

Resolution order:
1. CODINGBUDDY_RULES_DIR env var
2. Project local: {cwd}/.ai-rules/
3. Plugin bundled: {plugin_root}/../rules/.ai-rules/ (dev mode)
4. Installed package: find via codingbuddy-rules npm

Returns:
Path to .ai-rules/ directory, or None if not found.
"""
# 1. Environment variable
env_dir = os.environ.get("CODINGBUDDY_RULES_DIR")
if env_dir and os.path.isdir(env_dir):
return env_dir

# 2. Project local
working_dir = cwd or os.getcwd()
local_dir = os.path.join(working_dir, ".ai-rules")
if os.path.isdir(local_dir):
return local_dir

# 3. Plugin bundled (dev mode)
plugin_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
bundled_dir = os.path.join(plugin_root, "..", "rules", ".ai-rules")
bundled_dir = os.path.normpath(bundled_dir)
if os.path.isdir(bundled_dir):
return bundled_dir

return None


class ModeEngine:
"""Self-contained mode engine that works without MCP server."""

def __init__(self, rules_dir: Optional[str] = None, cwd: Optional[str] = None):
"""
Initialize with path to .ai-rules/ directory.

Args:
rules_dir: Explicit path to .ai-rules/ directory.
If None, auto-resolved via _resolve_rules_dir.
cwd: Working directory for resolution. Defaults to os.getcwd().
"""
self.rules_dir = rules_dir or _resolve_rules_dir(cwd)

def load_mode_rules(self, mode: str) -> Optional[str]:
"""
Read core.md and extract mode-specific section.

Args:
mode: Mode name (PLAN, ACT, EVAL, AUTO)

Returns:
Extracted mode rules text, or None if not found.
"""
if not self.rules_dir:
return None

core_path = os.path.join(self.rules_dir, "rules", "core.md")
if not os.path.isfile(core_path):
return None

try:
with open(core_path, "r", encoding="utf-8") as f:
return f.read()
except OSError:
return None

def get_default_agent(self, mode: str) -> dict:
"""
Return default agent for the given mode.

Args:
mode: Mode name (PLAN, ACT, EVAL, AUTO)

Returns:
Dict with 'name' and 'title' keys.
"""
mode_upper = mode.upper()
return DEFAULT_AGENTS.get(mode_upper, DEFAULT_AGENTS["ACT"])

def build_instructions(self, mode: str) -> str:
"""
Build complete mode instructions for hook output.

Output is kept within ~2000 char limit for UserPromptSubmit hooks.
Falls back to minimal template if .ai-rules/ is not found.

Args:
mode: Mode name (PLAN, ACT, EVAL, AUTO)

Returns:
Complete mode instructions string.
"""
mode_upper = mode.upper()
agent = self.get_default_agent(mode_upper)
template = MODE_TEMPLATES.get(mode_upper, MODE_TEMPLATES["ACT"])

instructions = template.format(agent_name=agent["name"])

# Add MCP enhancement hint
mcp_hint = (
"\n\nIf mcp__codingbuddy__parse_mode is available, "
"call it for enhanced features (checklists, specialist agents, context tracking)."
)

return instructions + mcp_hint
Loading
Loading