second-opinion-mcp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,6 +33,7 @@ Second Opinion reads your Claude Code session to understand what you're working
33
33
  - **Dependents** — Files that import your modified code
34
34
  - **Tests** — Test files related to your changes
35
35
  - **Types** — TypeScript/JSDoc type definitions
36
+ - **Pull request** — PR metadata, comments, reviews, and changed files (requires `gh` CLI)
36
37
 
37
38
  ### Custom Tasks
38
39
 
@@ -377,6 +378,7 @@ When calling the MCP tool directly:
377
378
  - Node.js 18+
378
379
  - Claude Code CLI
379
380
  - At least one API key (Gemini or OpenAI)
381
+ - [GitHub CLI (`gh`)](https://cli.github.com/) — optional, required for PR context detection
380
382
 
381
383
  ## License
382
384
 
package/dist/config.js CHANGED
@@ -8,7 +8,7 @@ export const ConfigSchema = z.object({
8
8
  defaultProvider: z.enum(["gemini", "openai"]).default("gemini"),
9
9
  geminiModel: z.string().default("gemini-3-flash-preview"),
10
10
  openaiModel: z.string().default("gpt-5.2"),
11
- maxContextTokens: z.number().default(100000),
11
+ maxContextTokens: z.number().default(200000),
12
12
  reviewsDir: z.string().default("second-opinions"),
13
13
  /** Default temperature for LLM generation (0-1) */
14
14
  temperature: z.number().min(0).max(1).default(0.3),
@@ -74,13 +74,14 @@ export function loadReviewInstructions(projectPath) {
74
74
  // Default instructions
75
75
  return `# Code Review Instructions
76
76
 
77
- You are a code reviewer providing a second opinion on code changes.
77
+ You are a senior code reviewer providing a second opinion on code changes. Look beyond the immediate diff.
78
78
 
79
79
  ## Your Role
80
80
  - Review the code changes objectively
81
81
  - Identify potential issues, bugs, or improvements
82
82
  - Be constructive and specific in your feedback
83
83
  - Consider security, performance, and maintainability
84
+ - Ask "what would have to be true for this problem not to exist?" — flag when complexity is a symptom of an upstream design choice
84
85
 
85
86
  ## Review Focus
86
87
  - Security vulnerabilities and best practices
@@ -88,12 +89,14 @@ You are a code reviewer providing a second opinion on code changes.
88
89
  - Code clarity and maintainability
89
90
  - Error handling and edge cases
90
91
  - Testing coverage
92
+ - Upstream/downstream design opportunities: would a different API contract, tighter type, or removed abstraction simplify this code?
91
93
 
92
94
  ## Output Format
93
95
  Structure your review with:
94
96
  1. **Summary** (2-3 sentences overview)
95
97
  2. **Critical Issues** (if any - things that must be fixed)
96
98
  3. **Suggestions** (improvements that would be nice)
97
- 4. **What's Done Well** (positive feedback)
99
+ 4. **Upstream/Downstream Opportunities** (changes outside the diff that could improve the design — label each Safe / Worth Investigating / Bold)
100
+ 5. **What's Done Well** (positive feedback)
98
101
  `;
99
102
  }
@@ -13,11 +13,12 @@ export interface BundleOptions {
13
13
  includeFiles?: string[];
14
14
  allowExternalFiles?: boolean;
15
15
  maxTokens?: number;
16
+ prNumber?: number;
16
17
  }
17
18
  export interface FileEntry {
18
19
  path: string;
19
20
  content: string;
20
- category: "session" | "git" | "dependency" | "dependent" | "test" | "type" | "explicit";
21
+ category: "session" | "pr" | "git" | "dependency" | "dependent" | "test" | "type" | "explicit";
21
22
  tokenEstimate: number;
22
23
  }
23
24
  export interface OmittedFile {
@@ -36,11 +37,21 @@ export interface BudgetWarning {
36
37
  }
37
38
  export interface ContextBundle {
38
39
  conversationContext: string;
40
+ /** Formatted PR metadata markdown (title, body, comments, reviews) */
41
+ prContext?: string;
42
+ /** Raw PR metadata for structured access (egress summary, etc.) */
43
+ prMetadata?: {
44
+ number: number;
45
+ url: string;
46
+ commentsCount: number;
47
+ reviewsCount: number;
48
+ };
39
49
  files: FileEntry[];
40
50
  omittedFiles: OmittedFile[];
41
51
  totalTokens: number;
42
52
  categories: {
43
53
  session: number;
54
+ pr: number;
44
55
  git: number;
45
56
  dependency: number;
46
57
  dependent: number;
@@ -55,6 +66,11 @@ export interface ContextBundle {
55
66
  };
56
67
  /** Warnings about high-priority files being omitted due to budget */
57
68
  budgetWarnings: BudgetWarning[];
69
+ /** Set when PR detection failed for a non-trivial reason (not "no PR found") */
70
+ prDetectionFailure?: {
71
+ reason: string;
72
+ message: string;
73
+ };
58
74
  }
59
75
  /**
60
76
  * Collect and bundle all context for review
@@ -3,6 +3,7 @@ import * as path from "path";
3
3
  import * as os from "os";
4
4
  import { parseSession, findLatestSession, formatConversationContext, } from "./session.js";
5
5
  import { getAllModifiedFiles } from "./git.js";
6
+ import { detectPR, formatPRMetadata } from "./pr.js";
6
7
  import { getDependenciesForFiles, getDependentsForFiles, isWithinProject, } from "./imports.js";
7
8
  import { findTestFilesForFiles } from "./tests.js";
8
9
  import { findTypeFilesForFiles } from "./types.js";
@@ -189,7 +190,7 @@ function readFileEntry(filePath, category, existingContent) {
189
190
  * Collect and bundle all context for review
190
191
  */
191
192
  export async function bundleContext(options) {
192
- const { projectPath, sessionId, includeConversation = true, includeDependencies = true, includeDependents = true, includeTests = true, includeTypes = true, includeFiles = [], allowExternalFiles = false, maxTokens = 100000, } = options;
193
+ const { projectPath, sessionId, includeConversation = true, includeDependencies = true, includeDependents = true, includeTests = true, includeTypes = true, includeFiles = [], allowExternalFiles = false, maxTokens = 100000, prNumber, } = options;
193
194
  const bundle = {
194
195
  conversationContext: "",
195
196
  files: [],
@@ -197,6 +198,7 @@ export async function bundleContext(options) {
197
198
  totalTokens: 0,
198
199
  categories: {
199
200
  session: 0,
201
+ pr: 0,
200
202
  git: 0,
201
203
  dependency: 0,
202
204
  dependent: 0,
@@ -210,11 +212,8 @@ export async function bundleContext(options) {
210
212
  },
211
213
  budgetWarnings: [],
212
214
  };
213
- // Track files we've already added
214
215
  const addedFiles = new Set();
215
- // Track redaction types across all files
216
216
  const allRedactedTypes = new Set();
217
- // Helper to accumulate redaction stats
218
217
  const accumulateRedactionStats = (result) => {
219
218
  bundle.redactionStats.totalCount += result.redactionCount;
220
219
  for (const type of result.redactedTypes) {
@@ -236,15 +235,10 @@ export async function bundleContext(options) {
236
235
  remainingBudget -= conversationTokens;
237
236
  }
238
237
  // Calculate base budget for each category (used for spillover calculation)
239
- const baseBudgets = {
240
- explicit: Math.floor(remainingBudget * BUDGET_ALLOCATION.explicit),
241
- session: Math.floor(remainingBudget * BUDGET_ALLOCATION.session),
242
- git: Math.floor(remainingBudget * BUDGET_ALLOCATION.git),
243
- dependency: Math.floor(remainingBudget * BUDGET_ALLOCATION.dependency),
244
- dependent: Math.floor(remainingBudget * BUDGET_ALLOCATION.dependent),
245
- test: Math.floor(remainingBudget * BUDGET_ALLOCATION.test),
246
- type: Math.floor(remainingBudget * BUDGET_ALLOCATION.type),
247
- };
238
+ const baseBudgets = Object.fromEntries(Object.entries(BUDGET_ALLOCATION).map(([cat, pct]) => [
239
+ cat,
240
+ Math.floor(remainingBudget * pct),
241
+ ]));
248
242
  // Track spillover budget from underutilized categories
249
243
  let spilloverBudget = 0;
250
244
  // Track omitted files per category for budget warnings
@@ -363,6 +357,57 @@ export async function bundleContext(options) {
363
357
  const sessionBudget = baseBudgets.session + Math.floor(spilloverBudget * 0.5);
364
358
  const sessionUsed = addFilesWithBudget(sessionFiles, "session", sessionBudget);
365
359
  getBudgetWithSpillover("session", sessionUsed);
360
+ // 2b. PR context (auto-detected or explicit prNumber)
361
+ const prDetection = detectPR(projectPath, prNumber);
362
+ if (prDetection.ok) {
363
+ const prContext = prDetection.pr;
364
+ // Format PR metadata and subtract from remaining budget
365
+ const prMetadata = formatPRMetadata(prContext);
366
+ const prMetadataTokens = estimateTokens(prMetadata);
367
+ bundle.prContext = prMetadata;
368
+ bundle.prMetadata = {
369
+ number: prContext.number,
370
+ url: prContext.url,
371
+ commentsCount: prContext.comments.length,
372
+ reviewsCount: prContext.reviews.length,
373
+ };
374
+ bundle.totalTokens += prMetadataTokens;
375
+ // Read PR changed files (with path validation)
376
+ const prFiles = [];
377
+ for (const filePath of prContext.changedFiles) {
378
+ if (addedFiles.has(filePath))
379
+ continue;
380
+ const normalizedPath = path.normalize(filePath);
381
+ if (isSensitivePath(normalizedPath)) {
382
+ bundle.omittedFiles.push({ path: filePath, category: "pr", tokenEstimate: 0, reason: "sensitive_path" });
383
+ continue;
384
+ }
385
+ if (!isWithinProject(normalizedPath, projectPath)) {
386
+ bundle.omittedFiles.push({ path: filePath, category: "pr", tokenEstimate: 0, reason: "outside_project" });
387
+ continue;
388
+ }
389
+ const result = readFileEntry(filePath, "pr");
390
+ if (result.entry) {
391
+ prFiles.push(result.entry);
392
+ accumulateRedactionStats(result);
393
+ modifiedFiles.push(filePath);
394
+ }
395
+ }
396
+ // Sort by token count (smaller files first to fit more)
397
+ prFiles.sort((a, b) => a.tokenEstimate - b.tokenEstimate);
398
+ // Process PR files with spillover
399
+ const prBudget = baseBudgets.pr + Math.floor(spilloverBudget * 0.5);
400
+ const prUsed = addFilesWithBudget(prFiles, "pr", prBudget);
401
+ getBudgetWithSpillover("pr", prUsed);
402
+ }
403
+ else {
404
+ // Surface failure reason (except no_pr_found which is normal)
405
+ if (prDetection.reason !== "no_pr_found") {
406
+ bundle.prDetectionFailure = { reason: prDetection.reason, message: prDetection.message };
407
+ }
408
+ // No PR — spillover the full pr budget to later categories
409
+ getBudgetWithSpillover("pr", 0);
410
+ }
366
411
  // 3. Git changes not in session
367
412
  const gitChangedFiles = getAllModifiedFiles(projectPath);
368
413
  const gitFiles = [];
@@ -506,10 +551,16 @@ export function formatBundleAsMarkdown(bundle, projectPath) {
506
551
  lines.push(bundle.conversationContext);
507
552
  lines.push("---\n");
508
553
  }
554
+ // PR context (metadata: title, body, comments, reviews)
555
+ if (bundle.prContext) {
556
+ lines.push(bundle.prContext);
557
+ lines.push("---\n");
558
+ }
509
559
  // Group files by category
510
560
  const categories = {
511
561
  explicit: [],
512
562
  session: [],
563
+ pr: [],
513
564
  git: [],
514
565
  dependency: [],
515
566
  dependent: [],
@@ -522,6 +573,7 @@ export function formatBundleAsMarkdown(bundle, projectPath) {
522
573
  const categoryLabels = {
523
574
  explicit: "Explicitly Included Files",
524
575
  session: "Modified Files (from Claude session)",
576
+ pr: "Pull Request Changed Files",
525
577
  git: "Additional Git Changes",
526
578
  dependency: "Dependencies (files imported by modified code)",
527
579
  dependent: "Dependents (files that import modified code)",
@@ -1,5 +1,6 @@
1
1
  export * from "./session.js";
2
2
  export * from "./git.js";
3
+ export * from "./pr.js";
3
4
  export * from "./imports.js";
4
5
  export * from "./tests.js";
5
6
  export * from "./types.js";
@@ -1,5 +1,6 @@
1
1
  export * from "./session.js";
2
2
  export * from "./git.js";
3
+ export * from "./pr.js";
3
4
  export * from "./imports.js";
4
5
  export * from "./tests.js";
5
6
  export * from "./types.js";
@@ -0,0 +1,51 @@
1
+ export interface PRComment {
2
+ author: string;
3
+ body: string;
4
+ createdAt: string;
5
+ }
6
+ export interface PRReview {
7
+ author: string;
8
+ body: string;
9
+ state: string;
10
+ createdAt: string;
11
+ }
12
+ export interface PRContext {
13
+ number: number;
14
+ title: string;
15
+ body: string;
16
+ url: string;
17
+ state: string;
18
+ baseBranch: string;
19
+ headBranch: string;
20
+ labels: string[];
21
+ comments: PRComment[];
22
+ reviews: PRReview[];
23
+ changedFiles: string[];
24
+ }
25
+ export type PRDetectionResult = {
26
+ ok: true;
27
+ pr: PRContext;
28
+ } | {
29
+ ok: false;
30
+ reason: "gh_not_installed" | "gh_command_failed" | "no_pr_found" | "parse_error";
31
+ message: string;
32
+ };
33
+ /**
34
+ * Check if the GitHub CLI (gh) is installed and accessible
35
+ */
36
+ export declare function isGhAvailable(): boolean;
37
+ /**
38
+ * Detect a PR associated with the current branch or a specific PR number.
39
+ * Returns a discriminated union with failure reason on error.
40
+ */
41
+ export declare function detectPR(projectPath: string, prNumber?: number): PRDetectionResult;
42
+ /**
43
+ * Get changed files between the PR base branch and HEAD via git diff.
44
+ * Fallback/complement to the files field from gh pr view.
45
+ */
46
+ export declare function getPRChangedFiles(projectPath: string, baseBranch: string): string[];
47
+ /**
48
+ * Format PR metadata (title, body, comments, reviews) as markdown.
49
+ * All text content is run through redactSecrets before inclusion.
50
+ */
51
+ export declare function formatPRMetadata(pr: PRContext): string;
@@ -0,0 +1,141 @@
1
+ import { spawnSync } from "child_process";
2
+ import * as path from "path";
3
+ import { redactSecrets } from "../security/redactor.js";
4
+ const SPAWN_OPTS = {
5
+ encoding: "utf-8",
6
+ stdio: ["pipe", "pipe", "pipe"],
7
+ };
8
+ /**
9
+ * Check if the GitHub CLI (gh) is installed and accessible
10
+ */
11
+ export function isGhAvailable() {
12
+ return spawnSync("gh", ["--version"], SPAWN_OPTS).status === 0;
13
+ }
14
+ /**
15
+ * Detect a PR associated with the current branch or a specific PR number.
16
+ * Returns a discriminated union with failure reason on error.
17
+ */
18
+ export function detectPR(projectPath, prNumber) {
19
+ if (!isGhAvailable()) {
20
+ return {
21
+ ok: false,
22
+ reason: "gh_not_installed",
23
+ message: "GitHub CLI (gh) is not installed. Install from https://cli.github.com/ for PR context detection.",
24
+ };
25
+ }
26
+ try {
27
+ const fields = "number,title,body,url,state,baseRefName,headRefName,labels,comments,reviews,files";
28
+ const args = prNumber
29
+ ? ["pr", "view", String(prNumber), "--json", fields]
30
+ : ["pr", "view", "--json", fields];
31
+ const result = spawnSync("gh", args, { cwd: projectPath, ...SPAWN_OPTS });
32
+ if (result.error || result.status !== 0) {
33
+ const stderr = (result.stderr || "").trim();
34
+ // "no pull requests found" is a normal condition, not an error
35
+ if (stderr.includes("no pull requests found") || stderr.includes("Could not resolve")) {
36
+ return { ok: false, reason: "no_pr_found", message: "No pull request found for the current branch." };
37
+ }
38
+ return {
39
+ ok: false,
40
+ reason: "gh_command_failed",
41
+ message: `gh pr view failed: ${stderr.slice(0, 200)}`,
42
+ };
43
+ }
44
+ const data = JSON.parse(result.stdout);
45
+ const pr = {
46
+ number: data.number,
47
+ title: data.title || "",
48
+ body: data.body || "",
49
+ url: data.url || "",
50
+ state: data.state || "",
51
+ baseBranch: data.baseRefName || "",
52
+ headBranch: data.headRefName || "",
53
+ labels: (data.labels || []).map((l) => l.name || ""),
54
+ comments: (data.comments || []).map((c) => ({
55
+ author: c.author?.login || "unknown",
56
+ body: c.body || "",
57
+ createdAt: c.createdAt || "",
58
+ })),
59
+ reviews: (data.reviews || []).map((r) => ({
60
+ author: r.author?.login || "unknown",
61
+ body: r.body || "",
62
+ state: r.state || "COMMENTED",
63
+ createdAt: r.createdAt || "",
64
+ })),
65
+ changedFiles: (data.files || [])
66
+ .map((f) => f.path)
67
+ .filter(Boolean)
68
+ .map((p) => path.join(projectPath, p)),
69
+ };
70
+ // Fallback: if gh returned no files but we have a base branch, use git diff
71
+ if (pr.changedFiles.length === 0 && pr.baseBranch) {
72
+ pr.changedFiles = getPRChangedFiles(projectPath, pr.baseBranch);
73
+ }
74
+ return { ok: true, pr };
75
+ }
76
+ catch {
77
+ return { ok: false, reason: "parse_error", message: "Failed to parse gh pr view output." };
78
+ }
79
+ }
80
+ /**
81
+ * Get changed files between the PR base branch and HEAD via git diff.
82
+ * Fallback/complement to the files field from gh pr view.
83
+ */
84
+ export function getPRChangedFiles(projectPath, baseBranch) {
85
+ try {
86
+ const result = spawnSync("git", ["diff", `${baseBranch}...HEAD`, "--name-only"], { cwd: projectPath, ...SPAWN_OPTS });
87
+ if (result.error || result.status !== 0) {
88
+ return [];
89
+ }
90
+ return result.stdout
91
+ .split("\n")
92
+ .filter(Boolean)
93
+ .map((f) => path.join(projectPath, f));
94
+ }
95
+ catch {
96
+ return [];
97
+ }
98
+ }
99
+ /**
100
+ * Format PR metadata (title, body, comments, reviews) as markdown.
101
+ * All text content is run through redactSecrets before inclusion.
102
+ */
103
+ export function formatPRMetadata(pr) {
104
+ const lines = [];
105
+ lines.push(`## Pull Request #${pr.number}\n`);
106
+ lines.push(`**${redactSecrets(pr.title).content}**\n`);
107
+ lines.push(`- **URL:** ${pr.url}`);
108
+ lines.push(`- **State:** ${pr.state}`);
109
+ lines.push(`- **Base:** ${pr.baseBranch} ← **Head:** ${pr.headBranch}`);
110
+ if (pr.labels.length > 0) {
111
+ lines.push(`- **Labels:** ${pr.labels.join(", ")}`);
112
+ }
113
+ lines.push("");
114
+ // PR body
115
+ if (pr.body) {
116
+ lines.push("### Description\n");
117
+ lines.push(redactSecrets(pr.body).content);
118
+ lines.push("");
119
+ }
120
+ // Discussion comments
121
+ if (pr.comments.length > 0) {
122
+ lines.push("### Discussion Comments\n");
123
+ for (const comment of pr.comments) {
124
+ lines.push(`**${comment.author}** (${comment.createdAt}):`);
125
+ lines.push(redactSecrets(comment.body).content);
126
+ lines.push("");
127
+ }
128
+ }
129
+ // Reviews
130
+ if (pr.reviews.length > 0) {
131
+ lines.push("### Reviews\n");
132
+ for (const review of pr.reviews) {
133
+ lines.push(`**${review.author}** — ${review.state} (${review.createdAt}):`);
134
+ if (review.body) {
135
+ lines.push(redactSecrets(review.body).content);
136
+ }
137
+ lines.push("");
138
+ }
139
+ }
140
+ return lines.join("\n");
141
+ }
@@ -23,6 +23,13 @@ export interface EgressSummary {
23
23
  totalCount: number;
24
24
  types: string[];
25
25
  };
26
+ /** PR context metadata when reviewing a pull request */
27
+ prContext?: {
28
+ prNumber: number;
29
+ prUrl: string;
30
+ commentsIncluded: number;
31
+ reviewsIncluded: number;
32
+ };
26
33
  }
27
34
  /**
28
35
  * Write a review/response to a markdown file
@@ -6,8 +6,6 @@ export interface ReviewRequest {
6
6
  customPrompt?: string;
7
7
  /** Temperature for LLM generation (0-1). Lower = more focused, higher = more creative. */
8
8
  temperature?: number;
9
- /** Whether files were omitted from context due to budget constraints */
10
- hasOmittedFiles?: boolean;
11
9
  }
12
10
  export interface ReviewResponse {
13
11
  review: string;
@@ -20,9 +18,8 @@ export interface ReviewProvider {
20
18
  }
21
19
  /**
22
20
  * Get the system prompt based on whether a custom task is provided
23
- * and whether files were omitted from context
24
21
  */
25
- export declare function getSystemPrompt(hasTask: boolean, hasOmittedFiles?: boolean): string;
22
+ export declare function getSystemPrompt(hasTask: boolean): string;
26
23
  /**
27
24
  * Build the full prompt for the LLM
28
25
  */
@@ -1,31 +1,27 @@
1
1
  /**
2
- * Calibration text for when files were omitted due to budget constraints
2
+ * Verification instructions appended to every system prompt to prevent hallucinated claims.
3
+ * Always included regardless of whether files were omitted — models can fabricate issues
4
+ * even when the relevant code is in context.
3
5
  */
4
- const CONTEXT_CALIBRATION = `
6
+ const VERIFICATION_REQUIREMENTS = `
5
7
 
6
- ## Important: Context Limitations
8
+ ## Important: Verification Requirements
7
9
 
8
- This review is based on a subset of the codebase. Some files were omitted due to token limits.
9
-
10
- When reviewing:
10
+ When reviewing, you MUST verify claims against the provided code:
11
11
  1. Only report issues you can VERIFY in the provided code
12
- 2. If you suspect an issue but cannot see the relevant implementation, mark it as:
13
- "⚠️ UNVERIFIED: [description] - relevant code not in context"
14
- 3. Do NOT assume missing code is actually missing from the codebase
15
- 4. Check the "Omitted Files" section before flagging missing implementations`;
12
+ 2. For Critical Issues, QUOTE the specific code that demonstrates the problem
13
+ 3. If you suspect an issue but cannot find confirming code, mark it as:
14
+ "⚠️ UNVERIFIED: [description] - could not locate confirming code"
15
+ 4. Do NOT assume code is missing or broken without evidence
16
+ 5. Search the full provided context before claiming something doesn't exist`;
16
17
  /**
17
18
  * Get the system prompt based on whether a custom task is provided
18
- * and whether files were omitted from context
19
19
  */
20
- export function getSystemPrompt(hasTask, hasOmittedFiles) {
21
- let prompt = hasTask
22
- ? "You are an expert software engineer. Complete the requested task thoroughly and provide clear, actionable output."
23
- : "You are an expert software engineer performing a code review. Be thorough, constructive, and actionable.";
24
- // Add calibration when files were omitted
25
- if (hasOmittedFiles) {
26
- prompt += CONTEXT_CALIBRATION;
27
- }
28
- return prompt;
20
+ export function getSystemPrompt(hasTask) {
21
+ const base = hasTask
22
+ ? "You are a senior software engineer. Complete the requested task thoroughly and provide clear, actionable output. When relevant, consider whether changes upstream or downstream of the immediate scope would produce a better outcome."
23
+ : "You are a senior software engineer performing a code review. You have seen systems like this evolve over years. Be thorough, constructive, and actionable. Look beyond the immediate diff — consider whether the right change might be above or below the code under review.";
24
+ return base + VERIFICATION_REQUIREMENTS;
29
25
  }
30
26
  /**
31
27
  * Build the full prompt for the LLM
@@ -10,7 +10,7 @@ export class GeminiProvider {
10
10
  }
11
11
  async review(request) {
12
12
  const prompt = buildReviewPrompt(request);
13
- const systemInstruction = getSystemPrompt(!!request.task, request.hasOmittedFiles);
13
+ const systemInstruction = getSystemPrompt(!!request.task);
14
14
  const model = this.client.getGenerativeModel({
15
15
  model: this.model,
16
16
  systemInstruction,
@@ -10,7 +10,7 @@ export class OpenAIProvider {
10
10
  }
11
11
  async review(request) {
12
12
  const prompt = buildReviewPrompt(request);
13
- const systemPrompt = getSystemPrompt(!!request.task, request.hasOmittedFiles);
13
+ const systemPrompt = getSystemPrompt(!!request.task);
14
14
  // Use provided temperature or default to 0.3 for focused output
15
15
  const temperature = request.temperature ?? 0.3;
16
16
  const response = await this.client.chat.completions.create({
package/dist/server.js CHANGED
@@ -77,6 +77,10 @@ The reviewer sees the same context Claude had, plus related code for full unders
77
77
  default: 100000,
78
78
  description: "Maximum tokens for context",
79
79
  },
80
+ prNumber: {
81
+ type: "number",
82
+ description: "PR number to review. Auto-detects from current branch if omitted.",
83
+ },
80
84
  sessionName: {
81
85
  type: "string",
82
86
  description: "Name for this review (used in output filename)",
@@ -14,6 +14,7 @@ export declare const SecondOpinionInputSchema: z.ZodObject<{
14
14
  includeTests: z.ZodDefault<z.ZodBoolean>;
15
15
  includeTypes: z.ZodDefault<z.ZodBoolean>;
16
16
  maxTokens: z.ZodDefault<z.ZodNumber>;
17
+ prNumber: z.ZodOptional<z.ZodNumber>;
17
18
  temperature: z.ZodOptional<z.ZodNumber>;
18
19
  sessionName: z.ZodOptional<z.ZodString>;
19
20
  customPrompt: z.ZodOptional<z.ZodString>;
@@ -33,6 +34,7 @@ export declare const SecondOpinionInputSchema: z.ZodObject<{
33
34
  temperature?: number | undefined;
34
35
  sessionId?: string | undefined;
35
36
  includeFiles?: string[] | undefined;
37
+ prNumber?: number | undefined;
36
38
  task?: string | undefined;
37
39
  sessionName?: string | undefined;
38
40
  customPrompt?: string | undefined;
@@ -50,6 +52,7 @@ export declare const SecondOpinionInputSchema: z.ZodObject<{
50
52
  includeTypes?: boolean | undefined;
51
53
  includeFiles?: string[] | undefined;
52
54
  maxTokens?: number | undefined;
55
+ prNumber?: number | undefined;
53
56
  task?: string | undefined;
54
57
  sessionName?: string | undefined;
55
58
  customPrompt?: string | undefined;
@@ -67,6 +70,11 @@ export interface SecondOpinionDryRunOutput {
67
70
  budgetWarnings: BudgetWarning[];
68
71
  /** Human-readable message about the dry run status */
69
72
  message: string;
73
+ /** Present when PR detection failed (e.g. gh not installed) */
74
+ prDetectionFailure?: {
75
+ reason: string;
76
+ message: string;
77
+ };
70
78
  }
71
79
  export interface SecondOpinionOutput {
72
80
  dryRun?: false;
@@ -80,6 +88,11 @@ export interface SecondOpinionOutput {
80
88
  filesReviewed: number;
81
89
  contextTokens: number;
82
90
  summary: EgressSummary;
91
+ /** Present when PR detection failed (e.g. gh not installed) */
92
+ prDetectionFailure?: {
93
+ reason: string;
94
+ message: string;
95
+ };
83
96
  }
84
97
  export declare function executeReview(input: SecondOpinionInput): Promise<SecondOpinionOutput | SecondOpinionDryRunOutput>;
85
98
  export { resetRateLimiter } from "../security/rate-limiter.js";
@@ -79,6 +79,11 @@ export const SecondOpinionInputSchema = z.object({
79
79
  .number()
80
80
  .default(100000)
81
81
  .describe("Maximum tokens for context"),
82
+ // PR options
83
+ prNumber: z
84
+ .number()
85
+ .optional()
86
+ .describe("PR number to review. Auto-detects from current branch if omitted."),
82
87
  // LLM options
83
88
  temperature: z
84
89
  .number()
@@ -108,36 +113,30 @@ export const SecondOpinionInputSchema = z.object({
108
113
  * Build egress summary from bundle, categorizing files as project vs external
109
114
  */
110
115
  function buildEgressSummary(bundle, projectPath, provider) {
111
- const projectFilePaths = [];
112
- const externalFilePaths = [];
113
- for (const file of bundle.files) {
114
- if (isWithinProject(file.path, projectPath)) {
115
- projectFilePaths.push(file.path);
116
- }
117
- else {
118
- externalFilePaths.push(file.path);
119
- }
120
- }
121
- // Get unique parent directories of external files
122
- const externalLocations = [
123
- ...new Set(externalFilePaths.map((p) => path.dirname(p))),
124
- ];
116
+ const projectFilePaths = bundle.files
117
+ .filter((f) => isWithinProject(f.path, projectPath))
118
+ .map((f) => f.path);
119
+ const externalFilePaths = bundle.files
120
+ .filter((f) => !isWithinProject(f.path, projectPath))
121
+ .map((f) => f.path);
122
+ const { redactionStats, prMetadata } = bundle;
125
123
  return {
126
124
  projectFilesSent: projectFilePaths.length,
127
125
  projectFilePaths,
128
126
  externalFilesSent: externalFilePaths.length,
129
127
  externalFilePaths,
130
- externalLocations,
131
- blockedFiles: bundle.omittedFiles.map((f) => ({
132
- path: f.path,
133
- reason: f.reason,
134
- })),
128
+ externalLocations: [...new Set(externalFilePaths.map((p) => path.dirname(p)))],
129
+ blockedFiles: bundle.omittedFiles.map((f) => ({ path: f.path, reason: f.reason })),
135
130
  provider,
136
- // Include redaction stats if any secrets were found
137
- redactions: bundle.redactionStats.totalCount > 0
131
+ redactions: redactionStats.totalCount > 0
132
+ ? { totalCount: redactionStats.totalCount, types: redactionStats.types }
133
+ : undefined,
134
+ prContext: prMetadata
138
135
  ? {
139
- totalCount: bundle.redactionStats.totalCount,
140
- types: bundle.redactionStats.types,
136
+ prNumber: prMetadata.number,
137
+ prUrl: prMetadata.url,
138
+ commentsIncluded: prMetadata.commentsCount,
139
+ reviewsIncluded: prMetadata.reviewsCount,
141
140
  }
142
141
  : undefined,
143
142
  };
@@ -158,6 +157,7 @@ export async function executeReview(input) {
158
157
  includeTests: input.includeTests,
159
158
  includeTypes: input.includeTypes,
160
159
  maxTokens: input.maxTokens,
160
+ prNumber: input.prNumber,
161
161
  });
162
162
  // Build egress summary (used for both dry run and actual execution)
163
163
  const summary = buildEgressSummary(bundle, input.projectPath, input.provider);
@@ -173,6 +173,7 @@ export async function executeReview(input) {
173
173
  message: hasWarnings
174
174
  ? `⚠️ ${bundle.budgetWarnings.length} budget warning(s) - some important files will be omitted`
175
175
  : "Ready to send",
176
+ prDetectionFailure: bundle.prDetectionFailure,
176
177
  };
177
178
  }
178
179
  // 3. Check rate limit before calling external API
@@ -188,9 +189,7 @@ export async function executeReview(input) {
188
189
  const instructions = loadReviewInstructions(input.projectPath);
189
190
  // 6. Determine temperature (input > config > default)
190
191
  const temperature = input.temperature ?? config.temperature;
191
- // 7. Check if files were omitted due to budget (for system prompt calibration)
192
- const hasOmittedFiles = bundle.omittedFiles.some((f) => f.reason === "budget_exceeded");
193
- // 8. Create provider and execute task
192
+ // 7. Create provider and execute task
194
193
  const provider = createProvider(input.provider, config);
195
194
  const response = await provider.review({
196
195
  instructions,
@@ -199,12 +198,11 @@ export async function executeReview(input) {
199
198
  focusAreas: input.focusAreas,
200
199
  customPrompt: input.customPrompt,
201
200
  temperature,
202
- hasOmittedFiles,
203
201
  });
204
- // 6. Derive session name if not provided
202
+ // 9. Derive session name if not provided
205
203
  const sessionName = input.sessionName ||
206
204
  deriveSessionName(bundle.conversationContext, "code-review");
207
- // 7. Write the output files
205
+ // 10. Write the output files
208
206
  const timestamp = new Date().toISOString();
209
207
  const metadata = {
210
208
  sessionName,
@@ -216,7 +214,7 @@ export async function executeReview(input) {
216
214
  task: input.task,
217
215
  };
218
216
  const reviewFile = writeReview(input.projectPath, config.reviewsDir, metadata, response.review);
219
- // 8. Write egress manifest for audit trail
217
+ // 11. Write egress manifest for audit trail
220
218
  const egressManifestFile = writeEgressManifest(input.projectPath, config.reviewsDir, metadata, summary);
221
219
  return {
222
220
  dryRun: false,
@@ -230,6 +228,7 @@ export async function executeReview(input) {
230
228
  filesReviewed: bundle.files.length,
231
229
  contextTokens: bundle.totalTokens,
232
230
  summary,
231
+ prDetectionFailure: bundle.prDetectionFailure,
233
232
  };
234
233
  }
235
234
  // Re-export for testing
@@ -16,8 +16,9 @@ export declare function estimateTokens(text: string): number;
16
16
  */
17
17
  export declare const BUDGET_ALLOCATION: {
18
18
  readonly explicit: 0.15;
19
- readonly session: 0.3;
20
- readonly git: 0.1;
19
+ readonly session: 0.2;
20
+ readonly pr: 0.15;
21
+ readonly git: 0.05;
21
22
  readonly dependency: 0.15;
22
23
  readonly dependent: 0.15;
23
24
  readonly test: 0.1;
@@ -18,8 +18,9 @@ export function estimateTokens(text) {
18
18
  */
19
19
  export const BUDGET_ALLOCATION = {
20
20
  explicit: 0.15,
21
- session: 0.3,
22
- git: 0.1,
21
+ session: 0.2,
22
+ pr: 0.15,
23
+ git: 0.05,
23
24
  dependency: 0.15,
24
25
  dependent: 0.15,
25
26
  test: 0.1,
@@ -33,6 +34,7 @@ export const BUDGET_ALLOCATION = {
33
34
  export const CATEGORY_PRIORITY_ORDER = [
34
35
  "explicit", // User explicitly requested - highest priority
35
36
  "session", // Claude worked on these - critical context
37
+ "pr", // PR changed files
36
38
  "git", // Other git changes
37
39
  "dependency", // Files imported by modified code
38
40
  "dependent", // Files that import modified code
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "second-opinion-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for getting code reviews from Gemini/GPT in Claude Code",
5
5
  "keywords": [
6
6
  "mcp",
@@ -5,7 +5,7 @@ You are a code reviewer providing a second opinion on code changes made during a
5
5
  ## Your Role
6
6
 
7
7
  - Review the code changes objectively and thoroughly
8
- - Identify potential issues, bugs, security vulnerabilities, or improvements
8
+ - Identify potential issues, bugs, security vulnerabilities, and improvements
9
9
  - Be constructive and specific in your feedback
10
10
  - Consider the conversation context to understand what was requested
11
11
 
@@ -18,6 +18,34 @@ You are a code reviewer providing a second opinion on code changes made during a
18
18
  5. **Error Handling**: Are errors handled appropriately?
19
19
  6. **Edge Cases**: Are edge cases considered?
20
20
 
21
+ ## Beyond the Diff
22
+
23
+ Don't just evaluate the code as presented — consider whether the best fix lives somewhere else entirely.
24
+
25
+ ### Think Upstream
26
+
27
+ Ask: **"What would have to be true for this problem not to exist?"**
28
+
29
+ Often the complexity you're reviewing is a symptom of a design choice made earlier. For example, if code is littered with null checks, the real issue might be an upstream API that returns `null` instead of a `Result` type. Flag these when you see them.
30
+
31
+ ### Think Downstream
32
+
33
+ Ask: **"What assumptions does this change bake in, and who inherits them?"**
34
+
35
+ Changes at boundaries (APIs, shared types, configuration) ripple outward. If a simpler contract, a tighter type, or a collapsed abstraction at this layer would save downstream consumers from defensive code, say so.
36
+
37
+ ### Permission to Be Bold
38
+
39
+ You have explicit permission to:
40
+ - Suggest breaking changes (with migration paths)
41
+ - Question whether a requirement should exist at all
42
+ - Propose removing code rather than improving it
43
+
44
+ Label the confidence level of bold suggestions:
45
+ - **Safe** — Low risk, clearly beneficial
46
+ - **Worth Investigating** — Promising but needs validation
47
+ - **Bold** — High-impact but requires careful consideration
48
+
21
49
  ## Output Format
22
50
 
23
51
  Structure your review as follows:
@@ -28,6 +56,7 @@ Structure your review as follows:
28
56
  ### Critical Issues
29
57
  Issues that should be fixed before merging (if any):
30
58
  - Issue description
59
+ - **Evidence**: Quote the specific code (file:line) that demonstrates this issue
31
60
  - Why it matters
32
61
  - Suggested fix
33
62
 
@@ -40,6 +69,12 @@ Improvements that would be nice to have:
40
69
  Things that are unclear or might need clarification:
41
70
  - Question about intent or implementation
42
71
 
72
+ ### Upstream/Downstream Opportunities
73
+ Changes outside the immediate diff that could improve the overall design:
74
+ - **What/Where**: What change, and where in the stack
75
+ - **Why**: How it simplifies or strengthens the current code
76
+ - **Risk Level**: Safe / Worth Investigating / Bold
77
+
43
78
  ### What's Done Well
44
79
  Positive aspects of the implementation:
45
80
  - Good practices observed
@@ -47,7 +82,7 @@ Positive aspects of the implementation:
47
82
 
48
83
  ## Guidelines
49
84
 
50
- - Be specific: Reference file names and line numbers when possible
85
+ - Be specific: Reference file names and line numbers. For Critical Issues, quote the relevant code
51
86
  - Be constructive: Don't just point out problems, suggest solutions
52
87
  - Be proportionate: Don't nitpick minor style issues if there are bigger concerns
53
88
  - Consider context: The conversation shows what was asked for - review against those requirements