Skip to content

Commit 983c4f8

Browse files
committed
feat(mcp-server): make Discover→Design→Plan a real staged default (#1420)
Change default routing so clear prompts start at discover instead of skipping directly to plan. Users advance through stages by passing the planning_stage parameter (discover → design → plan). - resolveStage() now defaults to 'discover' for all non-hinted calls - stageHint override preserved: callers can still force any stage - Added stageProgression metadata (completedStages, currentStage, remainingStages) - Updated unit tests for new default behavior (28 pass) - Updated mode.handler integration tests (122 pass) - Full test suite: 6190 pass, 0 fail
1 parent f7f8302 commit 983c4f8

3 files changed

Lines changed: 138 additions & 32 deletions

File tree

apps/mcp-server/src/mcp/handlers/mode.handler.spec.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1817,7 +1817,7 @@ describe('ModeHandler', () => {
18171817
expect(parsed.planningStage.recommendedSkill).toBe('brainstorming');
18181818
});
18191819

1820-
it('includes planningStage with plan for clear PLAN prompt', async () => {
1820+
it('includes planningStage with discover for clear PLAN prompt (staged default)', async () => {
18211821
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
18221822
...mockParseModeResult,
18231823
mode: 'PLAN',
@@ -1831,11 +1831,16 @@ describe('ModeHandler', () => {
18311831
expect(result?.isError).toBeFalsy();
18321832
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
18331833
expect(parsed.planningStage).toBeDefined();
1834-
expect(parsed.planningStage.currentStage).toBe('plan');
1835-
expect(parsed.planningStage.nextStage).toBeUndefined();
1836-
expect(parsed.planningStage.stageTransitionHint).toBeUndefined();
1837-
expect(parsed.planningStage.recommendedAgent).toBe('technical-planner');
1838-
expect(parsed.planningStage.recommendedSkill).toBe('writing-plans');
1834+
expect(parsed.planningStage.currentStage).toBe('discover');
1835+
expect(parsed.planningStage.nextStage).toBe('design');
1836+
expect(parsed.planningStage.stageTransitionHint).toBeTruthy();
1837+
expect(parsed.planningStage.recommendedAgent).toBe('solution-architect');
1838+
expect(parsed.planningStage.recommendedSkill).toBe('brainstorming');
1839+
expect(parsed.planningStage.stageProgression).toEqual({
1840+
completedStages: [],
1841+
currentStage: 'discover',
1842+
remainingStages: ['design', 'plan'],
1843+
});
18391844
});
18401845

18411846
it('honors explicit planning_stage=design hint', async () => {
@@ -1924,7 +1929,7 @@ describe('ModeHandler', () => {
19241929
expect(parsed.planningStage.currentStage).toBe('discover');
19251930
});
19261931

1927-
it('routes budget-exhausted to plan stage', async () => {
1932+
it('routes budget-exhausted to discover stage (staged default)', async () => {
19281933
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
19291934
...mockParseModeResult,
19301935
mode: 'PLAN',
@@ -1938,8 +1943,8 @@ describe('ModeHandler', () => {
19381943

19391944
expect(result?.isError).toBeFalsy();
19401945
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
1941-
expect(parsed.planningStage.currentStage).toBe('plan');
1942-
expect(parsed.planningStage.recommendedAgent).toBe('technical-planner');
1946+
expect(parsed.planningStage.currentStage).toBe('discover');
1947+
expect(parsed.planningStage.recommendedAgent).toBe('solution-architect');
19431948
});
19441949
});
19451950

apps/mcp-server/src/mcp/handlers/planning-stage.spec.ts

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { describe, it, expect } from 'vitest';
2-
import { resolvePlanningStage, type PlanningStageMetadata } from './planning-stage';
2+
import {
3+
resolvePlanningStage,
4+
type PlanningStageMetadata,
5+
type PlanningStage,
6+
} from './planning-stage';
37
import type { ClarificationMetadata } from './clarification-gate';
48

59
// ---------------------------------------------------------------------------
@@ -54,16 +58,16 @@ describe('planning-stage', () => {
5458
expect(result.currentStage).toBe('discover');
5559
});
5660

57-
it('routes clear prompt directly to plan (skip discover + design)', () => {
61+
it('routes clear prompt to discover (staged default, no skip)', () => {
5862
const result = resolvePlanningStage(clearClarification());
5963

60-
expect(result.currentStage).toBe('plan');
64+
expect(result.currentStage).toBe('discover');
6165
});
6266

63-
it('routes budget-exhausted prompt to plan', () => {
67+
it('routes budget-exhausted prompt to discover (staged default)', () => {
6468
const result = resolvePlanningStage(budgetExhaustedClarification());
6569

66-
expect(result.currentStage).toBe('plan');
70+
expect(result.currentStage).toBe('discover');
6771
});
6872

6973
it('routes explicit stageHint=design to design', () => {
@@ -113,7 +117,9 @@ describe('planning-stage', () => {
113117
});
114118

115119
it('has a description for plan stage', () => {
116-
const result = resolvePlanningStage(clearClarification());
120+
const result = resolvePlanningStage(clearClarification(), {
121+
stageHint: 'plan',
122+
});
117123

118124
expect(result.stageDescription).toContain('Plan');
119125
expect(result.stageDescription).toContain('implementation');
@@ -140,7 +146,9 @@ describe('planning-stage', () => {
140146
});
141147

142148
it('plan → nextStage is undefined (terminal)', () => {
143-
const result = resolvePlanningStage(clearClarification());
149+
const result = resolvePlanningStage(clearClarification(), {
150+
stageHint: 'plan',
151+
});
144152

145153
expect(result.nextStage).toBeUndefined();
146154
});
@@ -168,7 +176,9 @@ describe('planning-stage', () => {
168176
});
169177

170178
it('has no transition hint for plan stage (terminal)', () => {
171-
const result = resolvePlanningStage(clearClarification());
179+
const result = resolvePlanningStage(clearClarification(), {
180+
stageHint: 'plan',
181+
});
172182

173183
expect(result.stageTransitionHint).toBeUndefined();
174184
});
@@ -194,7 +204,9 @@ describe('planning-stage', () => {
194204
});
195205

196206
it('recommends technical-planner for plan', () => {
197-
const result = resolvePlanningStage(clearClarification());
207+
const result = resolvePlanningStage(clearClarification(), {
208+
stageHint: 'plan',
209+
});
198210

199211
expect(result.recommendedAgent).toBe('technical-planner');
200212
});
@@ -220,26 +232,47 @@ describe('planning-stage', () => {
220232
});
221233

222234
it('recommends writing-plans skill for plan', () => {
223-
const result = resolvePlanningStage(clearClarification());
235+
const result = resolvePlanningStage(clearClarification(), {
236+
stageHint: 'plan',
237+
});
224238

225239
expect(result.recommendedSkill).toBe('writing-plans');
226240
});
227241
});
228242

229243
// ------------------------------------------------------------------
230-
// Backward compatibility
244+
// Staged default behavior
231245
// ------------------------------------------------------------------
232246

233-
describe('backward compatibility', () => {
234-
it('clear prompt produces planReady + plan stage with no intermediate steps', () => {
247+
describe('staged default behavior', () => {
248+
it('clear prompt starts at discover with design as next stage', () => {
235249
const clarification = clearClarification();
236250
const result = resolvePlanningStage(clarification);
237251

252+
expect(result.currentStage).toBe('discover');
253+
expect(result.nextStage).toBe('design');
254+
expect(result.stageTransitionHint).toBeTruthy();
255+
});
256+
257+
it('stageHint=plan still jumps directly to plan (caller override)', () => {
258+
const result = resolvePlanningStage(clearClarification(), {
259+
stageHint: 'plan',
260+
});
261+
238262
expect(result.currentStage).toBe('plan');
239263
expect(result.nextStage).toBeUndefined();
240264
expect(result.stageTransitionHint).toBeUndefined();
241265
});
242266

267+
it('stageHint=design advances to design (caller override)', () => {
268+
const result = resolvePlanningStage(clearClarification(), {
269+
stageHint: 'design',
270+
});
271+
272+
expect(result.currentStage).toBe('design');
273+
expect(result.nextStage).toBe('plan');
274+
});
275+
243276
it('all fields are present in the return type', () => {
244277
const result: PlanningStageMetadata = resolvePlanningStage(ambiguousClarification());
245278

@@ -251,5 +284,45 @@ describe('planning-stage', () => {
251284
expect(result).toHaveProperty('recommendedSkill');
252285
});
253286
});
287+
288+
// ------------------------------------------------------------------
289+
// Stage progression metadata
290+
// ------------------------------------------------------------------
291+
292+
describe('stageProgression', () => {
293+
it('discover stage shows no completed, discover current, design+plan remaining', () => {
294+
const result = resolvePlanningStage(ambiguousClarification());
295+
296+
expect(result.stageProgression).toEqual({
297+
completedStages: [],
298+
currentStage: 'discover',
299+
remainingStages: ['design', 'plan'],
300+
});
301+
});
302+
303+
it('design stage shows discover completed, design current, plan remaining', () => {
304+
const result = resolvePlanningStage(clearClarification(), {
305+
stageHint: 'design',
306+
});
307+
308+
expect(result.stageProgression).toEqual({
309+
completedStages: ['discover'],
310+
currentStage: 'design',
311+
remainingStages: ['plan'],
312+
});
313+
});
314+
315+
it('plan stage shows discover+design completed, plan current, no remaining', () => {
316+
const result = resolvePlanningStage(clearClarification(), {
317+
stageHint: 'plan',
318+
});
319+
320+
expect(result.stageProgression).toEqual({
321+
completedStages: ['discover', 'design'],
322+
currentStage: 'plan',
323+
remainingStages: [],
324+
});
325+
});
326+
});
254327
});
255328
});

apps/mcp-server/src/mcp/handlers/planning-stage.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ import type { ClarificationMetadata } from './clarification-gate';
2525
/** The three stages of the planning flow. */
2626
export type PlanningStage = 'discover' | 'design' | 'plan';
2727

28+
/** Progression tracking across the three planning stages. */
29+
export interface StageProgression {
30+
/** Stages already completed before the current one. */
31+
completedStages: PlanningStage[];
32+
/** The stage currently active. */
33+
currentStage: PlanningStage;
34+
/** Stages remaining after the current one. */
35+
remainingStages: PlanningStage[];
36+
}
37+
2838
/** Metadata emitted alongside the clarification fields in the PLAN response. */
2939
export interface PlanningStageMetadata {
3040
/** Current planning stage. */
@@ -39,6 +49,8 @@ export interface PlanningStageMetadata {
3949
recommendedAgent?: string;
4050
/** Recommended supporting skill for this stage. */
4151
recommendedSkill?: string;
52+
/** Progression metadata showing completed / current / remaining stages. */
53+
stageProgression?: StageProgression;
4254
}
4355

4456
/** Options to override automatic stage resolution. */
@@ -90,9 +102,12 @@ const STAGE_SKILLS: Record<PlanningStage, string | undefined> = {
90102
* Routing rules (evaluated in order):
91103
* 1. If `stageHint` is provided → use it directly (caller knows best).
92104
* 2. If `clarification.clarificationNeeded === true` → `discover`.
93-
* 3. If `clarification.planReady === true` and no stageHint → `plan`
94-
* (clear prompt — skip discover and design).
95-
* 4. Otherwise → `design` (clarification resolved, approach not yet confirmed).
105+
* 3. Default → `discover` (staged default — always start at discover).
106+
*
107+
* The user advances through stages by passing `planning_stage` parameter:
108+
* First call (no hint) → discover
109+
* User confirms direction → passes `planning_stage: "design"` → design
110+
* User confirms approach → passes `planning_stage: "plan"` → plan
96111
*/
97112
export function resolvePlanningStage(
98113
clarification: ClarificationMetadata,
@@ -109,6 +124,7 @@ export function resolvePlanningStage(
109124
}),
110125
recommendedAgent: STAGE_AGENTS[stage],
111126
...(STAGE_SKILLS[stage] && { recommendedSkill: STAGE_SKILLS[stage] }),
127+
stageProgression: buildStageProgression(stage),
112128
};
113129
}
114130

@@ -130,15 +146,27 @@ function resolveStage(
130146
return 'discover';
131147
}
132148

133-
// 3. Clear / plan-ready → plan (skip discover+design)
134-
if (clarification.planReady) {
135-
return 'plan';
136-
}
137-
138-
// 4. Fallback: clarification resolved but approach not confirmed → design
139-
return 'design';
149+
// 3. Staged default — always start at discover regardless of planReady.
150+
// The user advances through stages via the planning_stage parameter.
151+
return 'discover';
140152
}
141153

142154
function getNextStage(stage: 'discover' | 'design'): 'design' | 'plan' {
143155
return stage === 'discover' ? 'design' : 'plan';
144156
}
157+
158+
/** Canonical ordered list of all planning stages. */
159+
const ALL_STAGES: readonly PlanningStage[] = ['discover', 'design', 'plan'];
160+
161+
/**
162+
* Build stage progression metadata showing which stages are completed,
163+
* which is current, and which remain.
164+
*/
165+
function buildStageProgression(currentStage: PlanningStage): StageProgression {
166+
const currentIndex = ALL_STAGES.indexOf(currentStage);
167+
return {
168+
completedStages: ALL_STAGES.slice(0, currentIndex) as PlanningStage[],
169+
currentStage,
170+
remainingStages: ALL_STAGES.slice(currentIndex + 1) as PlanningStage[],
171+
};
172+
}

0 commit comments

Comments
 (0)