Skip to content

Commit 8d16bb3

Browse files
committed
feat(mcp-server): preserve planning agent question-first guidance at standard verbosity (#1419)
Add planningContract field to IncludedAgent that survives all verbosity levels. Planning agents (technical-planner, solution-architect, plan-mode, auto-mode) in PLAN/AUTO modes receive core question-first rules regardless of whether systemPrompt is truncated to a summary. - Create pure planning-contract.ts module with resolvePlanningContract() - Add planningContract optional field to IncludedAgent interface - Wire into addIncludedAgentToResult after both full and standard paths - 14 unit tests covering mode/agent filtering and edge cases
1 parent 4508a04 commit 8d16bb3

4 files changed

Lines changed: 128 additions & 0 deletions

File tree

apps/mcp-server/src/keyword/keyword.service.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { isTaskmaestroAvailable } from './taskmaestro-detector';
3737
import { type ClientType } from '../shared/client-type';
3838
import { getDiffFiles, analyzeDiffFiles, type DiffAnalysisResult } from './diff-analyzer';
3939
import { matchStack, type StackMatchInput, type StackMatchResult } from '../agent/stack-matcher';
40+
import { resolvePlanningContract } from '../mcp/handlers/planning-contract';
4041

4142
/**
4243
* Options for parseMode method
@@ -992,6 +993,15 @@ export class KeywordService {
992993
};
993994
}
994995

996+
// Attach planning contract regardless of verbosity level
997+
const planningContract = resolvePlanningContract(
998+
mode,
999+
result.delegates_to,
1000+
);
1001+
if (planningContract) {
1002+
result.included_agent.planningContract = [...planningContract];
1003+
}
1004+
9951005
this.logger.log(`Auto-included agent: ${agentPrompt.displayName} (verbosity: ${verbosity})`);
9961006
}
9971007
}

apps/mcp-server/src/keyword/keyword.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,8 @@ export interface IncludedAgent {
388388
systemPrompt: string;
389389
/** Agent's areas of expertise */
390390
expertise: string[];
391+
/** Core planning behavior rules that survive all verbosity levels. */
392+
planningContract?: string[];
391393
}
392394

393395
/** Source of Primary Agent selection */
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { resolvePlanningContract, PLANNING_CONTRACT } from './planning-contract';
2+
3+
describe('planning-contract', () => {
4+
describe('PLANNING_CONTRACT', () => {
5+
it('should be a readonly array with expected length', () => {
6+
expect(PLANNING_CONTRACT).toHaveLength(5);
7+
});
8+
9+
it('should contain question-first guidance rules', () => {
10+
expect(PLANNING_CONTRACT[0]).toContain('clarifying question');
11+
expect(PLANNING_CONTRACT[1]).toContain('Wait for user confirmation');
12+
});
13+
});
14+
15+
describe('resolvePlanningContract', () => {
16+
it('should return contract for PLAN mode with technical-planner', () => {
17+
const result = resolvePlanningContract('PLAN', 'technical-planner');
18+
expect(result).toBe(PLANNING_CONTRACT);
19+
});
20+
21+
it('should return contract for PLAN mode with solution-architect', () => {
22+
const result = resolvePlanningContract('PLAN', 'solution-architect');
23+
expect(result).toBe(PLANNING_CONTRACT);
24+
});
25+
26+
it('should return contract for PLAN mode with plan-mode', () => {
27+
const result = resolvePlanningContract('PLAN', 'plan-mode');
28+
expect(result).toBe(PLANNING_CONTRACT);
29+
});
30+
31+
it('should return contract for AUTO mode with auto-mode', () => {
32+
const result = resolvePlanningContract('AUTO', 'auto-mode');
33+
expect(result).toBe(PLANNING_CONTRACT);
34+
});
35+
36+
it('should return contract for AUTO mode with technical-planner', () => {
37+
const result = resolvePlanningContract('AUTO', 'technical-planner');
38+
expect(result).toBe(PLANNING_CONTRACT);
39+
});
40+
41+
it('should return undefined for ACT mode', () => {
42+
const result = resolvePlanningContract('ACT', 'technical-planner');
43+
expect(result).toBeUndefined();
44+
});
45+
46+
it('should return undefined for EVAL mode', () => {
47+
const result = resolvePlanningContract('EVAL', 'technical-planner');
48+
expect(result).toBeUndefined();
49+
});
50+
51+
it('should return undefined for non-planning agents in PLAN mode', () => {
52+
const result = resolvePlanningContract('PLAN', 'code-reviewer');
53+
expect(result).toBeUndefined();
54+
});
55+
56+
it('should return undefined for non-planning agents in AUTO mode', () => {
57+
const result = resolvePlanningContract('AUTO', 'frontend-developer');
58+
expect(result).toBeUndefined();
59+
});
60+
61+
it('should handle case-insensitive mode matching', () => {
62+
expect(resolvePlanningContract('plan', 'technical-planner')).toBe(PLANNING_CONTRACT);
63+
expect(resolvePlanningContract('Plan', 'technical-planner')).toBe(PLANNING_CONTRACT);
64+
expect(resolvePlanningContract('auto', 'auto-mode')).toBe(PLANNING_CONTRACT);
65+
});
66+
67+
it('should return undefined when agentId is undefined', () => {
68+
const result = resolvePlanningContract('PLAN');
69+
expect(result).toBeUndefined();
70+
});
71+
72+
it('should return undefined when agentId is undefined in AUTO mode', () => {
73+
const result = resolvePlanningContract('AUTO');
74+
expect(result).toBeUndefined();
75+
});
76+
});
77+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Minimum planning behavior contract that must survive standard verbosity.
3+
* These rules define the core question-first planning workflow.
4+
*/
5+
export const PLANNING_CONTRACT: readonly string[] = [
6+
'Ask one clarifying question at a time — do not batch questions.',
7+
'Wait for user confirmation before advancing to the next planning stage.',
8+
'Use the recommended skill for the current stage (brainstorming for discover, writing-plans for plan).',
9+
'Present 2-3 alternative approaches with trade-offs before settling on a direction.',
10+
'Break implementation into bite-sized tasks (2-5 minutes each).',
11+
];
12+
13+
/** Agent IDs that qualify for the planning contract. */
14+
const PLANNING_AGENT_IDS = new Set([
15+
'technical-planner',
16+
'solution-architect',
17+
'plan-mode',
18+
'auto-mode',
19+
]);
20+
21+
/** Modes that qualify for the planning contract. */
22+
const PLANNING_MODES = new Set(['PLAN', 'AUTO']);
23+
24+
/**
25+
* Determine whether the planning contract should be included.
26+
* Returns the contract array if applicable, undefined otherwise.
27+
*/
28+
export function resolvePlanningContract(
29+
mode: string,
30+
agentId?: string,
31+
): readonly string[] | undefined {
32+
if (!PLANNING_MODES.has(mode.toUpperCase())) {
33+
return undefined;
34+
}
35+
if (agentId && PLANNING_AGENT_IDS.has(agentId)) {
36+
return PLANNING_CONTRACT;
37+
}
38+
return undefined;
39+
}

0 commit comments

Comments
 (0)