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
7 changes: 7 additions & 0 deletions apps/mcp-server/src/keyword/keyword.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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})`);
}
}
Expand Down
2 changes: 2 additions & 0 deletions apps/mcp-server/src/keyword/keyword.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
77 changes: 77 additions & 0 deletions apps/mcp-server/src/mcp/handlers/planning-contract.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
39 changes: 39 additions & 0 deletions apps/mcp-server/src/mcp/handlers/planning-contract.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading