From 5ff48d823622ecec9ab3414a987d61dc06aa0bbe Mon Sep 17 00:00:00 2001 From: JeremyDev87 Date: Sat, 4 Apr 2026 10:49:26 +0900 Subject: [PATCH] feat(council): add deterministic specialist presets for PLAN and EVAL (#1262) Add CouncilPresetService with deterministic council presets: - PLAN: technical-planner + architecture, test-strategy, code-quality, security - EVAL: code-reviewer + security, performance, accessibility Includes 13 unit tests covering preset resolution, determinism, and catalog validation. --- apps/mcp-server/src/agent/agent.module.ts | 5 +- .../src/agent/council-preset.service.spec.ts | 115 ++++++++++++++++++ .../src/agent/council-preset.service.ts | 54 ++++++++ apps/mcp-server/src/agent/index.ts | 1 + 4 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 apps/mcp-server/src/agent/council-preset.service.spec.ts create mode 100644 apps/mcp-server/src/agent/council-preset.service.ts diff --git a/apps/mcp-server/src/agent/agent.module.ts b/apps/mcp-server/src/agent/agent.module.ts index 3e217005..d32ba4bb 100644 --- a/apps/mcp-server/src/agent/agent.module.ts +++ b/apps/mcp-server/src/agent/agent.module.ts @@ -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 {} diff --git a/apps/mcp-server/src/agent/council-preset.service.spec.ts b/apps/mcp-server/src/agent/council-preset.service.spec.ts new file mode 100644 index 00000000..f8ed75b8 --- /dev/null +++ b/apps/mcp-server/src/agent/council-preset.service.spec.ts @@ -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']); + }); + }); +}); diff --git a/apps/mcp-server/src/agent/council-preset.service.ts b/apps/mcp-server/src/agent/council-preset.service.ts new file mode 100644 index 00000000..b35c7990 --- /dev/null +++ b/apps/mcp-server/src/agent/council-preset.service.ts @@ -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; + +const COUNCIL_PRESETS: Record = { + 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], + })); + } +} diff --git a/apps/mcp-server/src/agent/index.ts b/apps/mcp-server/src/agent/index.ts index cff76da9..c91331cb 100644 --- a/apps/mcp-server/src/agent/index.ts +++ b/apps/mcp-server/src/agent/index.ts @@ -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';