Skip to content

Commit 896383e

Browse files
committed
feat(mcp): integrate review_pr into parse_mode EVAL dispatch
- Add ReviewContext type to keyword.types.ts - Add buildReviewContext() to mode.handler.ts for EVAL-only PR detection - Extract PR number via regex (PR #N, PR N, pull request #N) - Extract optional issue number (issue #N) - Return reviewContext with hint for review_pr tool call - Add 7 tests: PR detected, no hash, PR+issue, pull request, no PR, non-EVAL, malformed Closes #1411
1 parent 231a623 commit 896383e

3 files changed

Lines changed: 187 additions & 0 deletions

File tree

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,21 @@ export interface VisualData {
671671
/** Re-export DiffAnalysisResult for consumers */
672672
export type { DiffAnalysisResult, DiffAgentScore } from './diff-analyzer';
673673

674+
/**
675+
* Review context detected from EVAL mode prompts containing PR references.
676+
* Included in parse_mode response to guide the reviewing agent to call review_pr.
677+
*/
678+
export interface ReviewContext {
679+
/** Whether a PR review context was detected in the prompt */
680+
detected: boolean;
681+
/** Extracted PR number from the prompt */
682+
pr_number: number;
683+
/** Optional linked issue number extracted from the prompt */
684+
issue_number?: number;
685+
/** Hint for the AI agent to call review_pr tool */
686+
hint: string;
687+
}
688+
674689
export interface ModeConfig {
675690
description: string;
676691
instructions: string;

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

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2036,4 +2036,133 @@ describe('ModeHandler', () => {
20362036
expect(JSON.parse(reserialized)).toEqual(parsed.councilScene);
20372037
});
20382038
});
2039+
2040+
describe('reviewContext (#1411)', () => {
2041+
it('should include reviewContext when EVAL prompt contains PR #N', async () => {
2042+
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
2043+
...mockParseModeResult,
2044+
mode: 'EVAL',
2045+
originalPrompt: 'EVAL: review PR #42',
2046+
});
2047+
2048+
const result = await handler.handle('parse_mode', {
2049+
prompt: 'EVAL: review PR #42',
2050+
});
2051+
2052+
expect(result?.isError).toBeFalsy();
2053+
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
2054+
expect(parsed.reviewContext).toEqual({
2055+
detected: true,
2056+
pr_number: 42,
2057+
hint: 'Call review_pr({ pr_number: 42 }) to get structured review data including diff, checklists, and specialist recommendations.',
2058+
});
2059+
});
2060+
2061+
it('should include reviewContext when EVAL prompt contains PR N (no hash)', async () => {
2062+
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
2063+
...mockParseModeResult,
2064+
mode: 'EVAL',
2065+
originalPrompt: 'EVAL: review PR 100',
2066+
});
2067+
2068+
const result = await handler.handle('parse_mode', {
2069+
prompt: 'EVAL: review PR 100',
2070+
});
2071+
2072+
expect(result?.isError).toBeFalsy();
2073+
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
2074+
expect(parsed.reviewContext).toEqual({
2075+
detected: true,
2076+
pr_number: 100,
2077+
hint: 'Call review_pr({ pr_number: 100 }) to get structured review data including diff, checklists, and specialist recommendations.',
2078+
});
2079+
});
2080+
2081+
it('should include reviewContext with issue_number when prompt contains both PR and issue', async () => {
2082+
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
2083+
...mockParseModeResult,
2084+
mode: 'EVAL',
2085+
originalPrompt: 'EVAL: review PR #42 issue #1364',
2086+
});
2087+
2088+
const result = await handler.handle('parse_mode', {
2089+
prompt: 'EVAL: review PR #42 issue #1364',
2090+
});
2091+
2092+
expect(result?.isError).toBeFalsy();
2093+
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
2094+
expect(parsed.reviewContext).toEqual({
2095+
detected: true,
2096+
pr_number: 42,
2097+
issue_number: 1364,
2098+
hint: 'Call review_pr({ pr_number: 42, issue_number: 1364 }) to get structured review data including diff, checklists, and specialist recommendations.',
2099+
});
2100+
});
2101+
2102+
it('should include reviewContext when prompt contains "pull request"', async () => {
2103+
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
2104+
...mockParseModeResult,
2105+
mode: 'EVAL',
2106+
originalPrompt: 'EVAL: review pull request #55',
2107+
});
2108+
2109+
const result = await handler.handle('parse_mode', {
2110+
prompt: 'EVAL: review pull request #55',
2111+
});
2112+
2113+
expect(result?.isError).toBeFalsy();
2114+
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
2115+
expect(parsed.reviewContext).toBeDefined();
2116+
expect(parsed.reviewContext.detected).toBe(true);
2117+
expect(parsed.reviewContext.pr_number).toBe(55);
2118+
});
2119+
2120+
it('should NOT include reviewContext when EVAL prompt has no PR reference', async () => {
2121+
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
2122+
...mockParseModeResult,
2123+
mode: 'EVAL',
2124+
originalPrompt: 'EVAL: evaluate implementation quality',
2125+
});
2126+
2127+
const result = await handler.handle('parse_mode', {
2128+
prompt: 'EVAL: evaluate implementation quality',
2129+
});
2130+
2131+
expect(result?.isError).toBeFalsy();
2132+
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
2133+
expect(parsed.reviewContext).toBeUndefined();
2134+
});
2135+
2136+
it('should NOT include reviewContext for non-EVAL modes even with PR reference', async () => {
2137+
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
2138+
...mockParseModeResult,
2139+
mode: 'PLAN',
2140+
originalPrompt: 'PLAN: design PR #42 review feature',
2141+
});
2142+
2143+
const result = await handler.handle('parse_mode', {
2144+
prompt: 'PLAN: design PR #42 review feature',
2145+
});
2146+
2147+
expect(result?.isError).toBeFalsy();
2148+
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
2149+
expect(parsed.reviewContext).toBeUndefined();
2150+
});
2151+
2152+
it('should handle malformed PR reference gracefully', async () => {
2153+
mockKeywordService.parseMode = vi.fn().mockResolvedValue({
2154+
...mockParseModeResult,
2155+
mode: 'EVAL',
2156+
originalPrompt: 'EVAL: review PR abc',
2157+
});
2158+
2159+
const result = await handler.handle('parse_mode', {
2160+
prompt: 'EVAL: review PR abc',
2161+
});
2162+
2163+
expect(result?.isError).toBeFalsy();
2164+
const parsed = JSON.parse((result?.content[0] as { text: string }).text);
2165+
expect(parsed.reviewContext).toBeUndefined();
2166+
});
2167+
});
20392168
});

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
type DispatchStrength,
2626
type IncludedAgent,
2727
type ParallelAgentRecommendation,
28+
type ReviewContext,
2829
} from '../../keyword/keyword.types';
2930
import { buildVisualData, type AgentVisualInput } from '../../keyword/visual-data.builder';
3031
import { isValidVerbosity } from '../../shared/verbosity.types';
@@ -321,6 +322,9 @@ export class ModeHandler extends AbstractHandler {
321322
settings?.ai?.agentDiscussion,
322323
);
323324

325+
// Build review context for EVAL mode when PR reference detected (#1411)
326+
const reviewContext = this.buildReviewContext(result.mode as Mode, result.originalPrompt);
327+
324328
// Resolve council preset for PLAN/EVAL modes
325329
const councilPreset =
326330
this.councilPresetService.resolvePreset(result.mode as Mode) ?? undefined;
@@ -387,6 +391,8 @@ export class ModeHandler extends AbstractHandler {
387391
...(planReviewGate && { planReviewGate }),
388392
// Include agent discussion config for EVAL mode
389393
...(agentDiscussion && { agentDiscussion }),
394+
// Include review context for EVAL mode PR reviews (#1411)
395+
...(reviewContext && { reviewContext }),
390396
// Include visual data for agent visualization
391397
...(visual && { visual }),
392398
// Include council preset for PLAN/EVAL modes
@@ -735,6 +741,43 @@ export class ModeHandler extends AbstractHandler {
735741
};
736742
}
737743

744+
/**
745+
* Build review context for EVAL mode when the prompt contains a PR reference (#1411).
746+
* Extracts PR number and optional issue number to guide the reviewing agent.
747+
* Returns undefined for non-EVAL modes or when no PR reference is detected.
748+
*/
749+
private buildReviewContext(mode: Mode, originalPrompt: string): ReviewContext | undefined {
750+
if (mode !== 'EVAL') {
751+
return undefined;
752+
}
753+
754+
// Detect PR reference: "PR #N", "PR N", or "pull request #N"
755+
const prMatch = originalPrompt.match(/(?:PR\s*#?(\d+)|pull\s*request\s*#?(\d+))/i);
756+
if (!prMatch) {
757+
return undefined;
758+
}
759+
760+
const prNumber = parseInt(prMatch[1] ?? prMatch[2], 10);
761+
if (isNaN(prNumber)) {
762+
return undefined;
763+
}
764+
765+
// Detect optional issue reference: "issue #N" or "#N" after PR reference
766+
const issueMatch = originalPrompt.match(/issue\s*#?(\d+)/i);
767+
const issueNumber = issueMatch ? parseInt(issueMatch[1], 10) : undefined;
768+
769+
const hintParams = issueNumber
770+
? `pr_number: ${prNumber}, issue_number: ${issueNumber}`
771+
: `pr_number: ${prNumber}`;
772+
773+
return {
774+
detected: true,
775+
pr_number: prNumber,
776+
...(issueNumber && { issue_number: issueNumber }),
777+
hint: `Call review_pr({ ${hintParams} }) to get structured review data including diff, checklists, and specialist recommendations.`,
778+
};
779+
}
780+
738781
/**
739782
* Build visual data for agent visualization in response.
740783
* Loads visual fields from agent profiles and builds banner/face/collaboration data.

0 commit comments

Comments
 (0)