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
1 change: 1 addition & 0 deletions RESULT.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"status":"success","issue":"#840","pr_number":879,"pr_url":"https://github.com/JeremyDev87/codingbuddy/pull/879","timestamp":"2026-03-22T21:15:00.000Z","cost":null,"error":null}
24 changes: 24 additions & 0 deletions TASK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Task: Issue #840 - wire plan-reviewer as automatic gate after PLAN completion

## Objective
Modify `apps/mcp-server/src/mcp/handlers/mode.handler.ts` to automatically recommend plan-reviewer dispatch after PLAN completion.

## Requirements
- When parse_mode returns PLAN mode response, include plan-reviewer in specialist recommendations
- Add `planReviewGate` field to PLAN response: { enabled: true, agent: "plan-reviewer", dispatch: "recommend" }
- Configurable via project config (can be disabled)
- TDD: write tests FIRST in mode.handler.spec.ts
- DO NOT modify agent.handler.ts or context-document.handler.ts

## Read First
- `apps/mcp-server/src/mcp/handlers/mode.handler.ts` (current PLAN response)
- `apps/mcp-server/src/mcp/handlers/mode.handler.spec.ts` (existing tests)
- `packages/rules/.ai-rules/agents/plan-reviewer.json` (agent definition)

## Methodology — MANDATORY
Use codingbuddy PLAN→ACT→EVAL. Use /ship to create PR closing #840. Write RESULT.json after completion.

## RESULT.json Protocol
```json
{"status":"success|failure|error","issue":"#840","pr_number":null,"pr_url":null,"timestamp":"<ISO>","cost":null,"error":null}
```
13 changes: 13 additions & 0 deletions apps/mcp-server/src/config/config.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,19 @@ const AIConfigSchema = z.object({
* ```
*/
maxIncludedSkills: z.number().int().min(0).max(10).optional(),
/**
* Enable/disable automatic plan-reviewer gate after PLAN completion.
* When enabled, parse_mode PLAN response includes a planReviewGate recommendation.
* Default: true (enabled)
*
* @example
* ```javascript
* ai: {
* planReviewGate: false, // disable plan review gate
* }
* ```
*/
planReviewGate: z.boolean().optional(),
});

const AutoConfigSchema = z.object({
Expand Down
99 changes: 99 additions & 0 deletions apps/mcp-server/src/mcp/handlers/mode.handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1014,4 +1014,103 @@ describe('ModeHandler', () => {
expect(parsed.deepThinkingInstructions).toBeUndefined();
});
});

describe('planReviewGate in PLAN mode', () => {
it('should include planReviewGate in PLAN mode response by default', async () => {
const result = await handler.handle('parse_mode', {
prompt: 'PLAN design auth feature',
});

expect(result?.isError).toBeFalsy();
const parsed = JSON.parse(result!.content[0].text as string);
expect(parsed.planReviewGate).toBeDefined();
expect(parsed.planReviewGate).toEqual({
enabled: true,
agent: 'plan-reviewer',
dispatch: 'recommend',
});
});

it('should include planReviewGate 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].text as string);
expect(parsed.planReviewGate).toBeDefined();
expect(parsed.planReviewGate.enabled).toBe(true);
expect(parsed.planReviewGate.agent).toBe('plan-reviewer');
});

it('should NOT include planReviewGate in ACT mode', 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].text as string);
expect(parsed.planReviewGate).toBeUndefined();
});

it('should NOT include planReviewGate in EVAL mode', 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].text as string);
expect(parsed.planReviewGate).toBeUndefined();
});

it('should disable planReviewGate when config ai.planReviewGate is false', async () => {
(mockConfigService.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
ai: { planReviewGate: false },
});

const result = await handler.handle('parse_mode', {
prompt: 'PLAN design feature',
});

expect(result?.isError).toBeFalsy();
const parsed = JSON.parse(result!.content[0].text as string);
expect(parsed.planReviewGate).toBeDefined();
expect(parsed.planReviewGate.enabled).toBe(false);
});

it('should enable planReviewGate when config ai.planReviewGate is true', async () => {
(mockConfigService.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
ai: { planReviewGate: true },
});

const result = await handler.handle('parse_mode', {
prompt: 'PLAN design feature',
});

expect(result?.isError).toBeFalsy();
const parsed = JSON.parse(result!.content[0].text as string);
expect(parsed.planReviewGate).toBeDefined();
expect(parsed.planReviewGate.enabled).toBe(true);
expect(parsed.planReviewGate.agent).toBe('plan-reviewer');
expect(parsed.planReviewGate.dispatch).toBe('recommend');
});
});
});
40 changes: 39 additions & 1 deletion apps/mcp-server/src/mcp/handlers/mode.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ interface DeepThinkingInstructions {
detailLevel: string;
}

/** Plan review gate recommendation included in PLAN/AUTO mode responses */
interface PlanReviewGate {
/** Whether the plan review gate is enabled */
enabled: boolean;
/** Agent name for plan review */
agent: string;
/** Dispatch strength for the gate */
dispatch: string;
}

/** Result type for context document handling */
interface ContextResult {
/** Path to the context file */
Expand Down Expand Up @@ -215,9 +225,11 @@ export class ModeHandler extends AbstractHandler {
// Persist state for context recovery after compaction
await this.persistModeState(result.mode);

// Load settings once for dispatch strength and plan review gate
const settings = await this.configService.getSettings();

// Enrich parallelAgentsRecommendation with dispatch strength
if (result.parallelAgentsRecommendation) {
const settings = await this.configService.getSettings();
const configDispatch = settings.ai?.dispatchStrength as DispatchStrength | undefined;
result.parallelAgentsRecommendation.dispatch =
configDispatch ?? MODE_DISPATCH_DEFAULTS[result.mode as Mode] ?? 'recommend';
Expand All @@ -235,6 +247,12 @@ export class ModeHandler extends AbstractHandler {
// Build deep thinking instructions for PLAN/AUTO modes
const deepThinkingInstructions = this.buildDeepThinkingInstructions(result.mode as Mode);

// Build plan review gate for PLAN/AUTO modes
const planReviewGate = this.buildPlanReviewGate(
result.mode as Mode,
settings?.ai?.planReviewGate,
);

return createJsonResponse({
...result,
language,
Expand All @@ -244,6 +262,8 @@ export class ModeHandler extends AbstractHandler {
...(dispatchReady && { dispatchReady }),
// Include deep thinking instructions for PLAN/AUTO modes
...(deepThinkingInstructions && { deepThinkingInstructions }),
// Include plan review gate for PLAN/AUTO modes
...(planReviewGate && { planReviewGate }),
// Include context document info (mandatory)
...contextResult,
// Include project root warning when auto-detected and config missing
Expand Down Expand Up @@ -443,6 +463,24 @@ export class ModeHandler extends AbstractHandler {
};
}

/**
* Build plan review gate for PLAN/AUTO modes.
* Returns undefined for ACT/EVAL modes (not applicable).
*/
private buildPlanReviewGate(mode: Mode, configValue?: boolean): PlanReviewGate | undefined {
if (mode !== 'PLAN' && mode !== 'AUTO') {
return undefined;
}

const enabled = configValue !== false;

return {
enabled,
agent: 'plan-reviewer',
dispatch: 'recommend',
};
}

/**
* Persist mode state for context recovery after compaction
*/
Expand Down
Loading