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 +2 -0
- package/dist/config.js +6 -3
- package/dist/context/bundler.d.ts +17 -1
- package/dist/context/bundler.js +65 -13
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +1 -0
- package/dist/context/pr.d.ts +51 -0
- package/dist/context/pr.js +141 -0
- package/dist/output/writer.d.ts +7 -0
- package/dist/providers/base.d.ts +1 -4
- package/dist/providers/base.js +16 -20
- package/dist/providers/gemini.js +1 -1
- package/dist/providers/openai.js +1 -1
- package/dist/server.js +4 -0
- package/dist/tools/review.d.ts +13 -0
- package/dist/tools/review.js +29 -30
- package/dist/utils/tokens.d.ts +3 -2
- package/dist/utils/tokens.js +4 -2
- package/package.json +1 -1
- package/templates/second-opinion.md +37 -2
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(
|
|
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. **
|
|
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
|
package/dist/context/bundler.js
CHANGED
|
@@ -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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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)",
|
package/dist/context/index.d.ts
CHANGED
package/dist/context/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/output/writer.d.ts
CHANGED
|
@@ -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
|
package/dist/providers/base.d.ts
CHANGED
|
@@ -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
|
|
22
|
+
export declare function getSystemPrompt(hasTask: boolean): string;
|
|
26
23
|
/**
|
|
27
24
|
* Build the full prompt for the LLM
|
|
28
25
|
*/
|
package/dist/providers/base.js
CHANGED
|
@@ -1,31 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
6
|
+
const VERIFICATION_REQUIREMENTS = `
|
|
5
7
|
|
|
6
|
-
## Important:
|
|
8
|
+
## Important: Verification Requirements
|
|
7
9
|
|
|
8
|
-
|
|
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.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
4.
|
|
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
|
|
21
|
-
|
|
22
|
-
? "You are
|
|
23
|
-
: "You are
|
|
24
|
-
|
|
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
|
package/dist/providers/gemini.js
CHANGED
|
@@ -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
|
|
13
|
+
const systemInstruction = getSystemPrompt(!!request.task);
|
|
14
14
|
const model = this.client.getGenerativeModel({
|
|
15
15
|
model: this.model,
|
|
16
16
|
systemInstruction,
|
package/dist/providers/openai.js
CHANGED
|
@@ -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
|
|
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)",
|
package/dist/tools/review.d.ts
CHANGED
|
@@ -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";
|
package/dist/tools/review.js
CHANGED
|
@@ -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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
137
|
-
|
|
131
|
+
redactions: redactionStats.totalCount > 0
|
|
132
|
+
? { totalCount: redactionStats.totalCount, types: redactionStats.types }
|
|
133
|
+
: undefined,
|
|
134
|
+
prContext: prMetadata
|
|
138
135
|
? {
|
|
139
|
-
|
|
140
|
-
|
|
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.
|
|
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
|
-
//
|
|
202
|
+
// 9. Derive session name if not provided
|
|
205
203
|
const sessionName = input.sessionName ||
|
|
206
204
|
deriveSessionName(bundle.conversationContext, "code-review");
|
|
207
|
-
//
|
|
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
|
-
//
|
|
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
|
package/dist/utils/tokens.d.ts
CHANGED
|
@@ -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.
|
|
20
|
-
readonly
|
|
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;
|
package/dist/utils/tokens.js
CHANGED
|
@@ -18,8 +18,9 @@ export function estimateTokens(text) {
|
|
|
18
18
|
*/
|
|
19
19
|
export const BUDGET_ALLOCATION = {
|
|
20
20
|
explicit: 0.15,
|
|
21
|
-
session: 0.
|
|
22
|
-
|
|
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
|
@@ -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,
|
|
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
|
|
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
|