diff --git a/apps/mcp-server/src/keyword/keyword.service.ts b/apps/mcp-server/src/keyword/keyword.service.ts index e34b2bbb..1af7c051 100644 --- a/apps/mcp-server/src/keyword/keyword.service.ts +++ b/apps/mcp-server/src/keyword/keyword.service.ts @@ -37,6 +37,7 @@ import { isTaskmaestroAvailable } from './taskmaestro-detector'; import { type ClientType } from '../shared/client-type'; import { getDiffFiles, analyzeDiffFiles, type DiffAnalysisResult } from './diff-analyzer'; import { matchStack, type StackMatchInput, type StackMatchResult } from '../agent/stack-matcher'; +import { resolvePlanningContract } from '../mcp/handlers/planning-contract'; /** * Options for parseMode method @@ -992,6 +993,12 @@ export class KeywordService { }; } + // Attach planning contract regardless of verbosity level + const planningContract = resolvePlanningContract(mode, result.delegates_to); + if (planningContract) { + result.included_agent.planningContract = [...planningContract]; + } + this.logger.log(`Auto-included agent: ${agentPrompt.displayName} (verbosity: ${verbosity})`); } } diff --git a/apps/mcp-server/src/keyword/keyword.types.ts b/apps/mcp-server/src/keyword/keyword.types.ts index d299338f..a6329906 100644 --- a/apps/mcp-server/src/keyword/keyword.types.ts +++ b/apps/mcp-server/src/keyword/keyword.types.ts @@ -388,6 +388,8 @@ export interface IncludedAgent { systemPrompt: string; /** Agent's areas of expertise */ expertise: string[]; + /** Core planning behavior rules that survive all verbosity levels. */ + planningContract?: string[]; } /** Source of Primary Agent selection */ diff --git a/apps/mcp-server/src/mcp/handlers/planning-contract.spec.ts b/apps/mcp-server/src/mcp/handlers/planning-contract.spec.ts new file mode 100644 index 00000000..46fb26f2 --- /dev/null +++ b/apps/mcp-server/src/mcp/handlers/planning-contract.spec.ts @@ -0,0 +1,77 @@ +import { resolvePlanningContract, PLANNING_CONTRACT } from './planning-contract'; + +describe('planning-contract', () => { + describe('PLANNING_CONTRACT', () => { + it('should be a readonly array with expected length', () => { + expect(PLANNING_CONTRACT).toHaveLength(5); + }); + + it('should contain question-first guidance rules', () => { + expect(PLANNING_CONTRACT[0]).toContain('clarifying question'); + expect(PLANNING_CONTRACT[1]).toContain('Wait for user confirmation'); + }); + }); + + describe('resolvePlanningContract', () => { + it('should return contract for PLAN mode with technical-planner', () => { + const result = resolvePlanningContract('PLAN', 'technical-planner'); + expect(result).toBe(PLANNING_CONTRACT); + }); + + it('should return contract for PLAN mode with solution-architect', () => { + const result = resolvePlanningContract('PLAN', 'solution-architect'); + expect(result).toBe(PLANNING_CONTRACT); + }); + + it('should return contract for PLAN mode with plan-mode', () => { + const result = resolvePlanningContract('PLAN', 'plan-mode'); + expect(result).toBe(PLANNING_CONTRACT); + }); + + it('should return contract for AUTO mode with auto-mode', () => { + const result = resolvePlanningContract('AUTO', 'auto-mode'); + expect(result).toBe(PLANNING_CONTRACT); + }); + + it('should return contract for AUTO mode with technical-planner', () => { + const result = resolvePlanningContract('AUTO', 'technical-planner'); + expect(result).toBe(PLANNING_CONTRACT); + }); + + it('should return undefined for ACT mode', () => { + const result = resolvePlanningContract('ACT', 'technical-planner'); + expect(result).toBeUndefined(); + }); + + it('should return undefined for EVAL mode', () => { + const result = resolvePlanningContract('EVAL', 'technical-planner'); + expect(result).toBeUndefined(); + }); + + it('should return undefined for non-planning agents in PLAN mode', () => { + const result = resolvePlanningContract('PLAN', 'code-reviewer'); + expect(result).toBeUndefined(); + }); + + it('should return undefined for non-planning agents in AUTO mode', () => { + const result = resolvePlanningContract('AUTO', 'frontend-developer'); + expect(result).toBeUndefined(); + }); + + it('should handle case-insensitive mode matching', () => { + expect(resolvePlanningContract('plan', 'technical-planner')).toBe(PLANNING_CONTRACT); + expect(resolvePlanningContract('Plan', 'technical-planner')).toBe(PLANNING_CONTRACT); + expect(resolvePlanningContract('auto', 'auto-mode')).toBe(PLANNING_CONTRACT); + }); + + it('should return undefined when agentId is undefined', () => { + const result = resolvePlanningContract('PLAN'); + expect(result).toBeUndefined(); + }); + + it('should return undefined when agentId is undefined in AUTO mode', () => { + const result = resolvePlanningContract('AUTO'); + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/apps/mcp-server/src/mcp/handlers/planning-contract.ts b/apps/mcp-server/src/mcp/handlers/planning-contract.ts new file mode 100644 index 00000000..00893276 --- /dev/null +++ b/apps/mcp-server/src/mcp/handlers/planning-contract.ts @@ -0,0 +1,39 @@ +/** + * Minimum planning behavior contract that must survive standard verbosity. + * These rules define the core question-first planning workflow. + */ +export const PLANNING_CONTRACT: readonly string[] = [ + 'Ask one clarifying question at a time — do not batch questions.', + 'Wait for user confirmation before advancing to the next planning stage.', + 'Use the recommended skill for the current stage (brainstorming for discover, writing-plans for plan).', + 'Present 2-3 alternative approaches with trade-offs before settling on a direction.', + 'Break implementation into bite-sized tasks (2-5 minutes each).', +]; + +/** Agent IDs that qualify for the planning contract. */ +const PLANNING_AGENT_IDS = new Set([ + 'technical-planner', + 'solution-architect', + 'plan-mode', + 'auto-mode', +]); + +/** Modes that qualify for the planning contract. */ +const PLANNING_MODES = new Set(['PLAN', 'AUTO']); + +/** + * Determine whether the planning contract should be included. + * Returns the contract array if applicable, undefined otherwise. + */ +export function resolvePlanningContract( + mode: string, + agentId?: string, +): readonly string[] | undefined { + if (!PLANNING_MODES.has(mode.toUpperCase())) { + return undefined; + } + if (agentId && PLANNING_AGENT_IDS.has(agentId)) { + return PLANNING_CONTRACT; + } + return undefined; +}