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
15 changes: 15 additions & 0 deletions apps/mcp-server/src/keyword/keyword.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,21 @@ export interface VisualData {
/** Re-export DiffAnalysisResult for consumers */
export type { DiffAnalysisResult, DiffAgentScore } from './diff-analyzer';

/**
* Review context detected from EVAL mode prompts containing PR references.
* Included in parse_mode response to guide the reviewing agent to call review_pr.
*/
export interface ReviewContext {
/** Whether a PR review context was detected in the prompt */
detected: boolean;
/** Extracted PR number from the prompt */
pr_number: number;
/** Optional linked issue number extracted from the prompt */
issue_number?: number;
/** Hint for the AI agent to call review_pr tool */
hint: string;
}

export interface ModeConfig {
description: string;
instructions: string;
Expand Down
129 changes: 129 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 @@ -2036,4 +2036,133 @@ describe('ModeHandler', () => {
expect(JSON.parse(reserialized)).toEqual(parsed.councilScene);
});
});

describe('reviewContext (#1411)', () => {
it('should include reviewContext when EVAL prompt contains PR #N', async () => {
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
...mockParseModeResult,
mode: 'EVAL',
originalPrompt: 'EVAL: review PR #42',
});

const result = await handler.handle('parse_mode', {
prompt: 'EVAL: review PR #42',
});

expect(result?.isError).toBeFalsy();
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
expect(parsed.reviewContext).toEqual({
detected: true,
pr_number: 42,
hint: 'Call review_pr({ pr_number: 42 }) to get structured review data including diff, checklists, and specialist recommendations.',
});
});

it('should include reviewContext when EVAL prompt contains PR N (no hash)', async () => {
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
...mockParseModeResult,
mode: 'EVAL',
originalPrompt: 'EVAL: review PR 100',
});

const result = await handler.handle('parse_mode', {
prompt: 'EVAL: review PR 100',
});

expect(result?.isError).toBeFalsy();
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
expect(parsed.reviewContext).toEqual({
detected: true,
pr_number: 100,
hint: 'Call review_pr({ pr_number: 100 }) to get structured review data including diff, checklists, and specialist recommendations.',
});
});

it('should include reviewContext with issue_number when prompt contains both PR and issue', async () => {
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
...mockParseModeResult,
mode: 'EVAL',
originalPrompt: 'EVAL: review PR #42 issue #1364',
});

const result = await handler.handle('parse_mode', {
prompt: 'EVAL: review PR #42 issue #1364',
});

expect(result?.isError).toBeFalsy();
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
expect(parsed.reviewContext).toEqual({
detected: true,
pr_number: 42,
issue_number: 1364,
hint: 'Call review_pr({ pr_number: 42, issue_number: 1364 }) to get structured review data including diff, checklists, and specialist recommendations.',
});
});

it('should include reviewContext when prompt contains "pull request"', async () => {
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
...mockParseModeResult,
mode: 'EVAL',
originalPrompt: 'EVAL: review pull request #55',
});

const result = await handler.handle('parse_mode', {
prompt: 'EVAL: review pull request #55',
});

expect(result?.isError).toBeFalsy();
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
expect(parsed.reviewContext).toBeDefined();
expect(parsed.reviewContext.detected).toBe(true);
expect(parsed.reviewContext.pr_number).toBe(55);
});

it('should NOT include reviewContext when EVAL prompt has no PR reference', async () => {
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
...mockParseModeResult,
mode: 'EVAL',
originalPrompt: 'EVAL: evaluate implementation quality',
});

const result = await handler.handle('parse_mode', {
prompt: 'EVAL: evaluate implementation quality',
});

expect(result?.isError).toBeFalsy();
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
expect(parsed.reviewContext).toBeUndefined();
});

it('should NOT include reviewContext for non-EVAL modes even with PR reference', async () => {
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
...mockParseModeResult,
mode: 'PLAN',
originalPrompt: 'PLAN: design PR #42 review feature',
});

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

expect(result?.isError).toBeFalsy();
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
expect(parsed.reviewContext).toBeUndefined();
});

it('should handle malformed PR reference gracefully', async () => {
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
...mockParseModeResult,
mode: 'EVAL',
originalPrompt: 'EVAL: review PR abc',
});

const result = await handler.handle('parse_mode', {
prompt: 'EVAL: review PR abc',
});

expect(result?.isError).toBeFalsy();
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
expect(parsed.reviewContext).toBeUndefined();
});
});
});
43 changes: 43 additions & 0 deletions apps/mcp-server/src/mcp/handlers/mode.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
type DispatchStrength,
type IncludedAgent,
type ParallelAgentRecommendation,
type ReviewContext,
} from '../../keyword/keyword.types';
import { buildVisualData, type AgentVisualInput } from '../../keyword/visual-data.builder';
import { isValidVerbosity } from '../../shared/verbosity.types';
Expand Down Expand Up @@ -321,6 +322,9 @@ export class ModeHandler extends AbstractHandler {
settings?.ai?.agentDiscussion,
);

// Build review context for EVAL mode when PR reference detected (#1411)
const reviewContext = this.buildReviewContext(result.mode as Mode, result.originalPrompt);

// Resolve council preset for PLAN/EVAL modes
const councilPreset =
this.councilPresetService.resolvePreset(result.mode as Mode) ?? undefined;
Expand Down Expand Up @@ -387,6 +391,8 @@ export class ModeHandler extends AbstractHandler {
...(planReviewGate && { planReviewGate }),
// Include agent discussion config for EVAL mode
...(agentDiscussion && { agentDiscussion }),
// Include review context for EVAL mode PR reviews (#1411)
...(reviewContext && { reviewContext }),
// Include visual data for agent visualization
...(visual && { visual }),
// Include council preset for PLAN/EVAL modes
Expand Down Expand Up @@ -735,6 +741,43 @@ export class ModeHandler extends AbstractHandler {
};
}

/**
* Build review context for EVAL mode when the prompt contains a PR reference (#1411).
* Extracts PR number and optional issue number to guide the reviewing agent.
* Returns undefined for non-EVAL modes or when no PR reference is detected.
*/
private buildReviewContext(mode: Mode, originalPrompt: string): ReviewContext | undefined {
if (mode !== 'EVAL') {
return undefined;
}

// Detect PR reference: "PR #N", "PR N", or "pull request #N"
const prMatch = originalPrompt.match(/(?:PR\s*#?(\d+)|pull\s*request\s*#?(\d+))/i);
if (!prMatch) {
return undefined;
}

const prNumber = parseInt(prMatch[1] ?? prMatch[2], 10);
if (isNaN(prNumber)) {
return undefined;
}

// Detect optional issue reference: "issue #N" or "#N" after PR reference
const issueMatch = originalPrompt.match(/issue\s*#?(\d+)/i);
const issueNumber = issueMatch ? parseInt(issueMatch[1], 10) : undefined;

const hintParams = issueNumber
? `pr_number: ${prNumber}, issue_number: ${issueNumber}`
: `pr_number: ${prNumber}`;

return {
detected: true,
pr_number: prNumber,
...(issueNumber && { issue_number: issueNumber }),
hint: `Call review_pr({ ${hintParams} }) to get structured review data including diff, checklists, and specialist recommendations.`,
};
}

/**
* Build visual data for agent visualization in response.
* Loads visual fields from agent profiles and builds banner/face/collaboration data.
Expand Down
Loading