From 896383edb4a123a4c3a3ca931f328f1a6c048a8b Mon Sep 17 00:00:00 2001 From: JeremyDev87 Date: Tue, 7 Apr 2026 01:51:14 +0900 Subject: [PATCH] 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 --- apps/mcp-server/src/keyword/keyword.types.ts | 15 ++ .../src/mcp/handlers/mode.handler.spec.ts | 129 ++++++++++++++++++ .../src/mcp/handlers/mode.handler.ts | 43 ++++++ 3 files changed, 187 insertions(+) diff --git a/apps/mcp-server/src/keyword/keyword.types.ts b/apps/mcp-server/src/keyword/keyword.types.ts index 47714333..d299338f 100644 --- a/apps/mcp-server/src/keyword/keyword.types.ts +++ b/apps/mcp-server/src/keyword/keyword.types.ts @@ -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; diff --git a/apps/mcp-server/src/mcp/handlers/mode.handler.spec.ts b/apps/mcp-server/src/mcp/handlers/mode.handler.spec.ts index 416027a1..f644726c 100644 --- a/apps/mcp-server/src/mcp/handlers/mode.handler.spec.ts +++ b/apps/mcp-server/src/mcp/handlers/mode.handler.spec.ts @@ -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(); + }); + }); }); diff --git a/apps/mcp-server/src/mcp/handlers/mode.handler.ts b/apps/mcp-server/src/mcp/handlers/mode.handler.ts index 944a772c..06ba7462 100644 --- a/apps/mcp-server/src/mcp/handlers/mode.handler.ts +++ b/apps/mcp-server/src/mcp/handlers/mode.handler.ts @@ -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'; @@ -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; @@ -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 @@ -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.