From 453cd9f6bca23b0d5f5ea096772d420aa808fbb4 Mon Sep 17 00:00:00 2001 From: JeremyDev87 Date: Sat, 4 Apr 2026 18:13:20 +0900 Subject: [PATCH 1/2] feat(mcp): emit council preset and consensus data from parse_mode (#1300) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrate CouncilPresetService into ModeHandler so PLAN/EVAL mode responses include deterministic council participant data. Add councilPreset and councilSummary optional fields to ParseModeResult for backward-compatible API extension. - Extend ParseModeResult with councilPreset (CouncilPreset) and councilSummary (CouncilSummary) optional fields - Inject CouncilPresetService into ModeHandler and resolve preset for PLAN/EVAL modes (null → undefined for ACT/AUTO) - Add 6 handler-level tests covering PLAN, EVAL, ACT, AUTO modes and serialization round-trip - Update all ModeHandler constructor call sites in test files Closes #1300 --- apps/mcp-server/src/keyword/keyword.types.ts | 16 +++ .../abstract-handler.integration.spec.ts | 2 + .../src/mcp/handlers/mode.handler.spec.ts | 107 ++++++++++++++++++ .../src/mcp/handlers/mode.handler.ts | 8 ++ apps/mcp-server/src/mcp/mcp.service.spec.ts | 2 + 5 files changed, 135 insertions(+) diff --git a/apps/mcp-server/src/keyword/keyword.types.ts b/apps/mcp-server/src/keyword/keyword.types.ts index 5cd20f83..b15fe28c 100644 --- a/apps/mcp-server/src/keyword/keyword.types.ts +++ b/apps/mcp-server/src/keyword/keyword.types.ts @@ -1,4 +1,6 @@ import type { DiffAnalysisResult } from './diff-analyzer'; +import type { CouncilPreset } from '../agent/council-preset.service'; +import type { CouncilSummary } from '../collaboration/council-summary.types'; export const KEYWORDS = ['PLAN', 'ACT', 'EVAL', 'AUTO'] as const; @@ -507,6 +509,20 @@ export interface ParseModeResult { * Present when staged or unstaged changes exist in the working directory. */ diffAnalysis?: DiffAnalysisResult; + /** + * @apiProperty External API - do not rename. + * Deterministic council preset for the current mode. + * Present in PLAN and EVAL modes — lists the primary agent and specialist reviewers + * that form the council for that mode. Absent for ACT/AUTO modes. + */ + councilPreset?: CouncilPreset; + /** + * @apiProperty External API - do not rename. + * Structured consensus summary from specialist council outputs. + * Present only when specialist results have been collected and aggregated. + * Clients should treat this as read-only diagnostic data. + */ + councilSummary?: CouncilSummary; } /** diff --git a/apps/mcp-server/src/mcp/handlers/abstract-handler.integration.spec.ts b/apps/mcp-server/src/mcp/handlers/abstract-handler.integration.spec.ts index 584a1f0a..44da9660 100644 --- a/apps/mcp-server/src/mcp/handlers/abstract-handler.integration.spec.ts +++ b/apps/mcp-server/src/mcp/handlers/abstract-handler.integration.spec.ts @@ -19,6 +19,7 @@ import { ModelResolverService } from '../../model/model-resolver.service'; import { StateService } from '../../state/state.service'; import { ContextDocumentService } from '../../context/context-document.service'; import { DiagnosticLogService } from '../../diagnostic/diagnostic-log.service'; +import { CouncilPresetService } from '../../agent/council-preset.service'; import type { ImpactEventService } from '../../impact'; import type { RuleEventCollector } from '../../rules/rule-event-collector'; @@ -179,6 +180,7 @@ describe('Handler Security Integration', () => { mockContextDocService, mockDiagnosticLogService, mockAgentServiceForMode as AgentService, + new CouncilPresetService(), mockImpactEventService, mockRuleEventCollector, ); diff --git a/apps/mcp-server/src/mcp/handlers/mode.handler.spec.ts b/apps/mcp-server/src/mcp/handlers/mode.handler.spec.ts index 989fa50e..885ea774 100644 --- a/apps/mcp-server/src/mcp/handlers/mode.handler.spec.ts +++ b/apps/mcp-server/src/mcp/handlers/mode.handler.spec.ts @@ -8,6 +8,7 @@ import { StateService } from '../../state/state.service'; import { ContextDocumentService } from '../../context/context-document.service'; import { DiagnosticLogService } from '../../diagnostic/diagnostic-log.service'; import type { AgentService } from '../../agent/agent.service'; +import { CouncilPresetService } from '../../agent/council-preset.service'; import type { ImpactEventService } from '../../impact'; import type { RuleEventCollector } from '../../rules/rule-event-collector'; @@ -21,6 +22,7 @@ describe('ModeHandler', () => { let mockContextDocService: ContextDocumentService; let mockDiagnosticLogService: DiagnosticLogService; let mockAgentService: Partial; + let mockCouncilPresetService: CouncilPresetService; let mockImpactEventService: Partial; let mockRuleEventCollector: Partial; @@ -129,6 +131,8 @@ describe('ModeHandler', () => { }), }; + mockCouncilPresetService = new CouncilPresetService(); + mockImpactEventService = { logEvent: vi.fn(), }; @@ -144,6 +148,7 @@ describe('ModeHandler', () => { mockContextDocService, mockDiagnosticLogService, mockAgentService as AgentService, + mockCouncilPresetService, mockImpactEventService as ImpactEventService, mockRuleEventCollector as RuleEventCollector, ); @@ -1251,4 +1256,106 @@ describe('ModeHandler', () => { expect(result?.isError).toBeFalsy(); }); }); + + describe('council preset integration', () => { + it('should include councilPreset in PLAN mode response', async () => { + const result = await handler.handle('parse_mode', { + prompt: 'PLAN design auth feature', + }); + + expect(result?.isError).toBeFalsy(); + const parsed = JSON.parse((result?.content[0] as { text: string }).text); + expect(parsed.councilPreset).toBeDefined(); + expect(parsed.councilPreset.mode).toBe('PLAN'); + expect(parsed.councilPreset.primary).toBe('technical-planner'); + expect(parsed.councilPreset.specialists).toEqual( + expect.arrayContaining([ + 'architecture-specialist', + 'test-strategy-specialist', + 'code-quality-specialist', + 'security-specialist', + ]), + ); + }); + + it('should include councilPreset in EVAL mode response', async () => { + mockKeywordService.parseMode = vi.fn().mockResolvedValue({ + ...mockParseModeResult, + mode: 'EVAL', + originalPrompt: 'evaluate implementation', + }); + + const result = await handler.handle('parse_mode', { + prompt: 'EVAL evaluate implementation', + }); + + expect(result?.isError).toBeFalsy(); + const parsed = JSON.parse((result?.content[0] as { text: string }).text); + expect(parsed.councilPreset).toBeDefined(); + expect(parsed.councilPreset.mode).toBe('EVAL'); + expect(parsed.councilPreset.primary).toBe('code-reviewer'); + expect(parsed.councilPreset.specialists).toEqual( + expect.arrayContaining([ + 'security-specialist', + 'performance-specialist', + 'accessibility-specialist', + ]), + ); + }); + + it('should NOT include councilPreset in ACT mode response', async () => { + mockKeywordService.parseMode = vi.fn().mockResolvedValue({ + ...mockParseModeResult, + mode: 'ACT', + originalPrompt: 'implement feature', + }); + + const result = await handler.handle('parse_mode', { + prompt: 'ACT implement feature', + }); + + expect(result?.isError).toBeFalsy(); + const parsed = JSON.parse((result?.content[0] as { text: string }).text); + expect(parsed.councilPreset).toBeUndefined(); + }); + + it('should NOT include councilPreset in AUTO mode response', async () => { + mockKeywordService.parseMode = vi.fn().mockResolvedValue({ + ...mockParseModeResult, + mode: 'AUTO', + originalPrompt: 'implement dashboard', + }); + + const result = await handler.handle('parse_mode', { + prompt: 'AUTO implement dashboard', + }); + + expect(result?.isError).toBeFalsy(); + const parsed = JSON.parse((result?.content[0] as { text: string }).text); + expect(parsed.councilPreset).toBeUndefined(); + }); + + it('should not include councilSummary when no specialist results exist', async () => { + const result = await handler.handle('parse_mode', { + prompt: 'PLAN design feature', + }); + + expect(result?.isError).toBeFalsy(); + const parsed = JSON.parse((result?.content[0] as { text: string }).text); + expect(parsed.councilSummary).toBeUndefined(); + }); + + it('councilPreset should be serializable JSON', async () => { + const result = await handler.handle('parse_mode', { + prompt: 'PLAN design feature', + }); + + expect(result?.isError).toBeFalsy(); + const text = (result?.content[0] as { text: string }).text; + // Round-trip serialization test + const parsed = JSON.parse(text); + const reserialized = JSON.stringify(parsed.councilPreset); + expect(JSON.parse(reserialized)).toEqual(parsed.councilPreset); + }); + }); }); diff --git a/apps/mcp-server/src/mcp/handlers/mode.handler.ts b/apps/mcp-server/src/mcp/handlers/mode.handler.ts index 663d3e8b..63e19feb 100644 --- a/apps/mcp-server/src/mcp/handlers/mode.handler.ts +++ b/apps/mcp-server/src/mcp/handlers/mode.handler.ts @@ -29,6 +29,7 @@ import { import { buildVisualData, type AgentVisualInput } from '../../keyword/visual-data.builder'; import { isValidVerbosity } from '../../shared/verbosity.types'; import { AgentService } from '../../agent/agent.service'; +import { CouncilPresetService } from '../../agent/council-preset.service'; import { ImpactEventService } from '../../impact'; import { RuleEventCollector } from '../../rules/rule-event-collector'; @@ -104,6 +105,7 @@ export class ModeHandler extends AbstractHandler { private readonly contextDocService: ContextDocumentService, private readonly diagnosticLogService: DiagnosticLogService, private readonly agentService: AgentService, + private readonly councilPresetService: CouncilPresetService, private readonly impactEventService: ImpactEventService, private readonly ruleEventCollector: RuleEventCollector, ) { @@ -276,6 +278,10 @@ export class ModeHandler extends AbstractHandler { settings?.ai?.agentDiscussion, ); + // Resolve council preset for PLAN/EVAL modes + const councilPreset = + this.councilPresetService.resolvePreset(result.mode as Mode) ?? undefined; + // Build visual data for agent visualization const visual = await this.buildVisual( result.mode as Mode, @@ -299,6 +305,8 @@ export class ModeHandler extends AbstractHandler { ...(agentDiscussion && { agentDiscussion }), // Include visual data for agent visualization ...(visual && { visual }), + // Include council preset for PLAN/EVAL modes + ...(councilPreset && { councilPreset }), // Include context document info (mandatory) ...contextResult, // Include project root warning when auto-detected and config missing diff --git a/apps/mcp-server/src/mcp/mcp.service.spec.ts b/apps/mcp-server/src/mcp/mcp.service.spec.ts index 52350dd4..e69754e5 100644 --- a/apps/mcp-server/src/mcp/mcp.service.spec.ts +++ b/apps/mcp-server/src/mcp/mcp.service.spec.ts @@ -10,6 +10,7 @@ import { SkillRecommendationService } from '../skill/skill-recommendation.servic import type { RecommendSkillsResult, ListSkillsResult } from '../skill/skill-recommendation.types'; import { LanguageService } from '../shared/language.service'; import { AgentService } from '../agent/agent.service'; +import { CouncilPresetService } from '../agent/council-preset.service'; import type { AgentSystemPrompt, ParallelAgentSet } from '../agent/agent.types'; import { ChecklistService } from '../checklist/checklist.service'; import { ContextService } from '../context/context.service'; @@ -492,6 +493,7 @@ function createMcpServiceWithHandlers( services.contextDocService as ContextDocumentService, services.diagnosticLogService as DiagnosticLogService, services.agentService as AgentService, + new CouncilPresetService(), services.impactEventService as ImpactEventService, services.ruleEventCollector as RuleEventCollector, ), From ffea4241f72373aac47c9fdd46cd8582e8035400 Mon Sep 17 00:00:00 2001 From: JeremyDev87 Date: Sat, 4 Apr 2026 18:27:19 +0900 Subject: [PATCH 2/2] fix(mcp): break circular dependency between keyword.types and council-preset.service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract CouncilPreset interface and CouncilMode type to a dedicated council-preset.types.ts file that has no imports from keyword module. Both keyword.types.ts and council-preset.service.ts now import from the shared types file, eliminating the cycle. - keyword.types.ts → council-preset.types.ts (no cycle) - council-preset.service.ts → council-preset.types.ts (no cycle) - council-preset.service.ts re-exports CouncilPreset for API compat --- .../src/agent/council-preset.service.ts | 15 +++------------ .../mcp-server/src/agent/council-preset.types.ts | 16 ++++++++++++++++ apps/mcp-server/src/keyword/keyword.types.ts | 2 +- 3 files changed, 20 insertions(+), 13 deletions(-) create mode 100644 apps/mcp-server/src/agent/council-preset.types.ts diff --git a/apps/mcp-server/src/agent/council-preset.service.ts b/apps/mcp-server/src/agent/council-preset.service.ts index b35c7990..1e451d80 100644 --- a/apps/mcp-server/src/agent/council-preset.service.ts +++ b/apps/mcp-server/src/agent/council-preset.service.ts @@ -1,16 +1,7 @@ import { Injectable } from '@nestjs/common'; -import type { Mode } from '../keyword/keyword.types'; +import type { CouncilMode, CouncilPreset } from './council-preset.types'; -/** - * A deterministic council preset: a primary agent plus specialist reviewers - */ -export interface CouncilPreset { - mode: 'PLAN' | 'EVAL'; - primary: string; - specialists: string[]; -} - -type CouncilMode = Extract; +export type { CouncilPreset } from './council-preset.types'; const COUNCIL_PRESETS: Record = { PLAN: { @@ -36,7 +27,7 @@ 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 { + resolvePreset(mode: string): CouncilPreset | null { const preset = COUNCIL_PRESETS[mode as CouncilMode]; if (!preset) return null; return { ...preset, specialists: [...preset.specialists] }; diff --git a/apps/mcp-server/src/agent/council-preset.types.ts b/apps/mcp-server/src/agent/council-preset.types.ts new file mode 100644 index 00000000..661dd664 --- /dev/null +++ b/apps/mcp-server/src/agent/council-preset.types.ts @@ -0,0 +1,16 @@ +/** + * Council Preset Types — extracted to break circular dependency + * between keyword.types and council-preset.service. + */ + +/** Modes that support council presets */ +export type CouncilMode = 'PLAN' | 'EVAL'; + +/** + * A deterministic council preset: a primary agent plus specialist reviewers. + */ +export interface CouncilPreset { + mode: CouncilMode; + primary: string; + specialists: string[]; +} diff --git a/apps/mcp-server/src/keyword/keyword.types.ts b/apps/mcp-server/src/keyword/keyword.types.ts index b15fe28c..7c8ce333 100644 --- a/apps/mcp-server/src/keyword/keyword.types.ts +++ b/apps/mcp-server/src/keyword/keyword.types.ts @@ -1,5 +1,5 @@ import type { DiffAnalysisResult } from './diff-analyzer'; -import type { CouncilPreset } from '../agent/council-preset.service'; +import type { CouncilPreset } from '../agent/council-preset.types'; import type { CouncilSummary } from '../collaboration/council-summary.types'; export const KEYWORDS = ['PLAN', 'ACT', 'EVAL', 'AUTO'] as const;