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
5 changes: 3 additions & 2 deletions apps/mcp-server/src/agent/agent.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Module } from '@nestjs/common';
import { AgentService } from './agent.service';
import { AgentStackService } from './agent-stack.service';
import { CouncilPresetService } from './council-preset.service';
import { RulesModule } from '../rules/rules.module';
import { CustomModule } from '../custom';
import { CodingBuddyConfigModule } from '../config/config.module';

@Module({
imports: [RulesModule, CustomModule, CodingBuddyConfigModule],
providers: [AgentService, AgentStackService],
exports: [AgentService, AgentStackService],
providers: [AgentService, AgentStackService, CouncilPresetService],
exports: [AgentService, AgentStackService, CouncilPresetService],
})
export class AgentModule {}
115 changes: 115 additions & 0 deletions apps/mcp-server/src/agent/council-preset.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { describe, it, expect } from 'vitest';
import { CouncilPresetService } from './council-preset.service';

describe('CouncilPresetService', () => {
let service: CouncilPresetService;

beforeEach(() => {
service = new CouncilPresetService();
});

describe('resolvePreset', () => {
describe('PLAN mode', () => {
it('should return technical-planner as primary agent', () => {
const preset = service.resolvePreset('PLAN');
expect(preset).not.toBeNull();
expect(preset!.primary).toBe('technical-planner');
});

it('should include architecture, test-strategy, code-quality, and security specialists', () => {
const preset = service.resolvePreset('PLAN');
expect(preset).not.toBeNull();
expect(preset!.specialists).toEqual([
'architecture-specialist',
'test-strategy-specialist',
'code-quality-specialist',
'security-specialist',
]);
});

it('should set mode to PLAN', () => {
const preset = service.resolvePreset('PLAN');
expect(preset).not.toBeNull();
expect(preset!.mode).toBe('PLAN');
});
});

describe('EVAL mode', () => {
it('should return code-reviewer as primary agent', () => {
const preset = service.resolvePreset('EVAL');
expect(preset).not.toBeNull();
expect(preset!.primary).toBe('code-reviewer');
});

it('should include security, performance, and accessibility specialists', () => {
const preset = service.resolvePreset('EVAL');
expect(preset).not.toBeNull();
expect(preset!.specialists).toEqual([
'security-specialist',
'performance-specialist',
'accessibility-specialist',
]);
});

it('should set mode to EVAL', () => {
const preset = service.resolvePreset('EVAL');
expect(preset).not.toBeNull();
expect(preset!.mode).toBe('EVAL');
});
});

describe('unsupported modes', () => {
it('should return null for ACT mode', () => {
const preset = service.resolvePreset('ACT');
expect(preset).toBeNull();
});

it('should return null for AUTO mode', () => {
const preset = service.resolvePreset('AUTO');
expect(preset).toBeNull();
});
});

describe('determinism', () => {
it('should return the same PLAN preset on every call', () => {
const first = service.resolvePreset('PLAN');
const second = service.resolvePreset('PLAN');
expect(first).not.toBeNull();
expect(first).toEqual(second);
});

it('should return the same EVAL preset on every call', () => {
const first = service.resolvePreset('EVAL');
const second = service.resolvePreset('EVAL');
expect(first).not.toBeNull();
expect(first).toEqual(second);
});
});

describe('catalog validation', () => {
it('should only reference valid agent catalog names in PLAN preset', () => {
const preset = service.resolvePreset('PLAN');
const allAgents = [preset!.primary, ...preset!.specialists];
for (const agent of allAgents) {
expect(agent).toMatch(/^[a-z][a-z0-9-]*$/);
}
});

it('should only reference valid agent catalog names in EVAL preset', () => {
const preset = service.resolvePreset('EVAL');
const allAgents = [preset!.primary, ...preset!.specialists];
for (const agent of allAgents) {
expect(agent).toMatch(/^[a-z][a-z0-9-]*$/);
}
});
});
});

describe('listPresets', () => {
it('should return both PLAN and EVAL presets', () => {
const presets = service.listPresets();
expect(presets).toHaveLength(2);
expect(presets.map(p => p.mode)).toEqual(['PLAN', 'EVAL']);
});
});
});
54 changes: 54 additions & 0 deletions apps/mcp-server/src/agent/council-preset.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Injectable } from '@nestjs/common';
import type { Mode } from '../keyword/keyword.types';

/**
* A deterministic council preset: a primary agent plus specialist reviewers
*/
export interface CouncilPreset {
mode: 'PLAN' | 'EVAL';
primary: string;
specialists: string[];
}

type CouncilMode = Extract<Mode, 'PLAN' | 'EVAL'>;

const COUNCIL_PRESETS: Record<CouncilMode, CouncilPreset> = {
PLAN: {
mode: 'PLAN',
primary: 'technical-planner',
specialists: [
'architecture-specialist',
'test-strategy-specialist',
'code-quality-specialist',
'security-specialist',
],
},
EVAL: {
mode: 'EVAL',
primary: 'code-reviewer',
specialists: ['security-specialist', 'performance-specialist', 'accessibility-specialist'],
},
};

@Injectable()
export class CouncilPresetService {
/**
* Resolve the council preset for a given mode.
* Returns null for modes without a preset (ACT, AUTO).
*/
resolvePreset(mode: Mode): CouncilPreset | null {
const preset = COUNCIL_PRESETS[mode as CouncilMode];
if (!preset) return null;
return { ...preset, specialists: [...preset.specialists] };
}

/**
* List all available council presets.
*/
listPresets(): CouncilPreset[] {
return Object.values(COUNCIL_PRESETS).map(p => ({
...p,
specialists: [...p.specialists],
}));
}
}
1 change: 1 addition & 0 deletions apps/mcp-server/src/agent/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './agent.module';
export * from './agent.service';
export * from './agent-stack.service';
export * from './council-preset.service';
export * from './agent.types';
export * from './agent-prompt.builder';
export * from './agent-stack.schema';
Expand Down
Loading